功能: 服务端生成完整报告并保护单任务

- 使用 libxlsxwriter 常量内存导出 Excel\n- 增加全局任务占用提示与原始数据持久化\n- 保留运行时任务数据并更新接口文档
This commit is contained in:
cloud
2026-08-05 16:47:31 +08:00
parent 07274797af
commit 99ed321d24
2292 changed files with 134317 additions and 126 deletions
+8
View File
@@ -36,6 +36,13 @@ endif()
find_package(Drogon CONFIG REQUIRED)
find_package(Threads REQUIRED)
# libxlsxwriter 使用常量内存模式把大型工作簿逐行落盘,避免报告导出占满服务进程内存。
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
add_subdirectory(${REPO_ROOT}/third_party/libxlsxwriter_repo
${CMAKE_CURRENT_BINARY_DIR}/libxlsxwriter)
file(GLOB_RECURSE SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.h"
@@ -54,6 +61,7 @@ target_include_directories(wind_server PRIVATE
target_link_libraries(wind_server PRIVATE
Drogon::Drogon
Threads::Threads
xlsxwriter
)
# 运行期 .so 解析:RPATH 指向 Drogon install 的 libs 目录
+460 -18
View File
@@ -9,6 +9,7 @@
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <mutex>
#include <optional>
#include <random>
#include <set>
@@ -17,6 +18,11 @@
#include <unordered_map>
#include <vector>
#include <xlsxwriter/format.h>
#include <xlsxwriter/workbook.h>
#include <xlsxwriter/worksheet.h>
#include <drogon/utils/Utilities.h>
using json = nlohmann::json;
namespace fs = std::filesystem;
@@ -25,6 +31,9 @@ namespace {
constexpr int kErrorInvalidRequest = 1001;
constexpr int kErrorJobNotFound = 1002;
constexpr int kErrorServer = 1003;
constexpr int kErrorJobBusy = 1004;
constexpr auto kUploadIdleTimeout = std::chrono::minutes(30);
constexpr auto kCompletedJobRetention = std::chrono::hours(24);
constexpr const char* kDefaultSchemeId = "scheme_one";
constexpr const char* kSchemeTwoId = "scheme_two";
constexpr const char* kSchemeOneName = "方案一";
@@ -65,15 +74,17 @@ struct RemovedPoint {
struct CalculationOptions {
std::string scheme_id = kDefaultSchemeId;
double rated_power = 4800.0;
double rated_wind_speed = 18.0;
double rated_wind_speed = 14.0;
double power_step = 5.0;
double cleaning_wind_speed_step = 0.25;
double curve_wind_speed_step = 0.5;
double wind_speed_change_threshold = 1.0;
double iqr_lower_multiplier = 1.8;
double iqr_lower_multiplier = 1.2;
double iqr_upper_multiplier = 2.0;
double minimum_generator_speed = 1.0;
double generator_speed_k = 0.9;
double rotor_radius = 78.0;
double gearbox_ratio = 162.0;
double grid_connected_speed = 0.0;
double rated_generator_speed = 0.0;
bool rated_power_provided = false;
@@ -97,6 +108,17 @@ struct SchemeInfo {
std::string id;
std::string name;
std::string description;
double scheme_one_rated_power = 4800.0;
double scheme_one_rated_wind_speed = 14.0;
double scheme_one_power_step = 5.0;
double scheme_one_cleaning_wind_speed_step = 0.25;
double scheme_one_wind_speed_change_threshold = 1.0;
double scheme_one_iqr_lower_multiplier = 1.2;
double scheme_one_iqr_upper_multiplier = 2.0;
double scheme_one_minimum_generator_speed = 1.0;
double scheme_one_generator_speed_k = 0.9;
double scheme_one_rotor_radius = 78.0;
double scheme_one_gearbox_ratio = 162.0;
double grid_connected_speed = 1030.0;
double rated_generator_speed = 1755.0;
double rated_power = 2000.0;
@@ -137,6 +159,123 @@ fs::path JobRowsPath(const std::string& job_id) {
return JobDir(job_id) / "rows.jsonl";
}
fs::path JobRawRowsPath(const std::string& job_id) {
return JobDir(job_id) / "raw_rows.jsonl";
}
fs::path JobResultPath(const std::string& job_id) {
return JobDir(job_id) / "result.json";
}
std::mutex g_task_mutex;
std::string g_active_job_id;
std::chrono::steady_clock::time_point g_active_since;
bool g_active_is_upload = false;
bool IsTaskBusyFor(const std::string& job_id) {
std::lock_guard<std::mutex> lock(g_task_mutex);
return !g_active_job_id.empty() && g_active_job_id != job_id;
}
bool AcquireTask(const std::string& job_id, bool upload) {
std::lock_guard<std::mutex> lock(g_task_mutex);
if (!g_active_job_id.empty() && g_active_job_id != job_id) return false;
g_active_job_id = job_id;
g_active_is_upload = upload;
g_active_since = std::chrono::steady_clock::now();
return true;
}
void TouchTask(const std::string& job_id) {
std::lock_guard<std::mutex> lock(g_task_mutex);
if (g_active_job_id == job_id) g_active_since = std::chrono::steady_clock::now();
}
void ReleaseTask(const std::string& job_id) {
std::lock_guard<std::mutex> lock(g_task_mutex);
if (g_active_job_id == job_id) {
g_active_job_id.clear();
g_active_is_upload = false;
}
}
class TaskReleaseGuard {
public:
explicit TaskReleaseGuard(std::string job_id) : job_id_(std::move(job_id)) {}
~TaskReleaseGuard() { ReleaseTask(job_id_); }
private:
std::string job_id_;
};
void ExpireIdleUploadTask() {
std::string expired;
{
std::lock_guard<std::mutex> lock(g_task_mutex);
if (g_active_is_upload && !g_active_job_id.empty() &&
std::chrono::steady_clock::now() - g_active_since > kUploadIdleTimeout) {
expired = g_active_job_id;
g_active_job_id.clear();
g_active_is_upload = false;
}
}
if (!expired.empty()) {
std::error_code ignored;
fs::remove_all(JobDir(expired), ignored);
}
}
void CleanupExpiredCompletedJobs() {
std::error_code error;
if (!fs::exists(JobsRoot(), error)) return;
const auto now = fs::file_time_type::clock::now();
for (const auto& entry : fs::directory_iterator(JobsRoot(), error)) {
if (error || !entry.is_directory()) continue;
const auto job_id = entry.path().filename().string();
if (IsTaskBusyFor(job_id) || !fs::exists(entry.path() / "result.json")) continue;
const auto modified = fs::last_write_time(entry.path(), error);
if (!error && now - modified > kCompletedJobRetention) fs::remove_all(entry.path(), error);
error.clear();
}
}
std::string ExcelColumnName(size_t index) {
std::string name;
for (size_t value = index + 1; value > 0; value = (value - 1) / 26) {
name.insert(name.begin(), static_cast<char>('A' + (value - 1) % 26));
}
return name;
}
void WriteJsonCell(lxw_worksheet* sheet, lxw_row_t row, lxw_col_t column,
const json& value, lxw_format* format = nullptr) {
if (value.is_number()) {
worksheet_write_number(sheet, row, column, value.get<double>(), format);
} else if (value.is_boolean()) {
worksheet_write_boolean(sheet, row, column, value.get<bool>(), format);
} else if (!value.is_null()) {
const auto text = value.is_string() ? value.get<std::string>() : value.dump();
worksheet_write_string(sheet, row, column, text.c_str(), format);
}
}
std::string JsonText(const json& value) {
if (value.is_string()) return value.get<std::string>();
if (value.is_number_integer()) return std::to_string(value.get<long long>());
if (value.is_number_unsigned()) return std::to_string(value.get<unsigned long long>());
if (value.is_number_float()) {
std::ostringstream output;
output << value.get<double>();
return output.str();
}
return value.is_null() ? "" : value.dump();
}
std::string FileNameForFan(const std::string& fan_id) {
std::string output;
for (const auto ch : fan_id) output += std::isalnum(static_cast<unsigned char>(ch)) ? ch : '_';
return output.empty() ? "wind_turbine" : output;
}
fs::path SchemeConfigPath() {
return fs::path("data") / "wind_schemes.json";
}
@@ -347,10 +486,16 @@ std::string NormalizeSchemeId(const std::string& scheme_id) {
}
std::vector<SchemeInfo> DefaultSchemes() {
return {
{kDefaultSchemeId, kSchemeOneName, kSchemeOneDefaultDescription},
{kSchemeTwoId, kSchemeTwoName, kSchemeTwoDefaultDescription, 1030.0, 1755.0, 2000.0},
};
SchemeInfo scheme_one;
scheme_one.id = kDefaultSchemeId;
scheme_one.name = kSchemeOneName;
scheme_one.description = kSchemeOneDefaultDescription;
SchemeInfo scheme_two;
scheme_two.id = kSchemeTwoId;
scheme_two.name = kSchemeTwoName;
scheme_two.description = kSchemeTwoDefaultDescription;
return {scheme_one, scheme_two};
}
std::optional<SchemeInfo> FindScheme(const std::vector<SchemeInfo>& schemes,
@@ -369,7 +514,21 @@ json SchemeToJson(const SchemeInfo& scheme) {
data["id"] = scheme.id;
data["name"] = scheme.name;
data["description"] = scheme.description;
if (scheme.id == kSchemeTwoId) {
if (scheme.id == kDefaultSchemeId) {
data["parameters"] = {
{"rated_power", scheme.scheme_one_rated_power},
{"rated_wind_speed", scheme.scheme_one_rated_wind_speed},
{"power_step", scheme.scheme_one_power_step},
{"cleaning_wind_speed_step", scheme.scheme_one_cleaning_wind_speed_step},
{"wind_speed_change_threshold", scheme.scheme_one_wind_speed_change_threshold},
{"iqr_lower_multiplier", scheme.scheme_one_iqr_lower_multiplier},
{"iqr_upper_multiplier", scheme.scheme_one_iqr_upper_multiplier},
{"minimum_generator_speed", scheme.scheme_one_minimum_generator_speed},
{"generator_speed_k", scheme.scheme_one_generator_speed_k},
{"rotor_radius", scheme.scheme_one_rotor_radius},
{"gearbox_ratio", scheme.scheme_one_gearbox_ratio},
};
} else if (scheme.id == kSchemeTwoId) {
data["parameters"]["grid_connected_speed"] = scheme.grid_connected_speed;
data["parameters"]["rated_generator_speed"] = scheme.rated_generator_speed;
data["parameters"]["rated_power"] = scheme.rated_power;
@@ -401,10 +560,37 @@ std::vector<SchemeInfo> LoadSchemes() {
if (description.has_value()) {
scheme.description = description.value();
}
if (scheme.id == kSchemeTwoId &&
item.contains("parameters") &&
item["parameters"].is_object()) {
const auto& params = item["parameters"];
if (!item.contains("parameters") || !item["parameters"].is_object()) {
continue;
}
const auto& params = item["parameters"];
if (scheme.id == kDefaultSchemeId) {
const auto load_positive = [&](const std::string& field, double& target) {
if (const auto value = GetNumberField(params, field);
value.has_value() && std::isfinite(value.value()) && value.value() > 0.0) {
target = value.value();
}
};
const auto load_non_negative = [&](const std::string& field, double& target) {
if (const auto value = GetNumberField(params, field);
value.has_value() && std::isfinite(value.value()) && value.value() >= 0.0) {
target = value.value();
}
};
load_positive("rated_power", scheme.scheme_one_rated_power);
load_positive("rated_wind_speed", scheme.scheme_one_rated_wind_speed);
load_positive("power_step", scheme.scheme_one_power_step);
load_positive("cleaning_wind_speed_step", scheme.scheme_one_cleaning_wind_speed_step);
load_non_negative("wind_speed_change_threshold",
scheme.scheme_one_wind_speed_change_threshold);
load_non_negative("iqr_lower_multiplier", scheme.scheme_one_iqr_lower_multiplier);
load_non_negative("iqr_upper_multiplier", scheme.scheme_one_iqr_upper_multiplier);
load_non_negative("minimum_generator_speed",
scheme.scheme_one_minimum_generator_speed);
load_non_negative("generator_speed_k", scheme.scheme_one_generator_speed_k);
load_positive("rotor_radius", scheme.scheme_one_rotor_radius);
load_positive("gearbox_ratio", scheme.scheme_one_gearbox_ratio);
} else if (scheme.id == kSchemeTwoId) {
if (const auto value = GetNumberField(params, "grid_connected_speed");
value.has_value() && value.value() > 0.0) {
scheme.grid_connected_speed = value.value();
@@ -624,6 +810,14 @@ CalculationOptions ParseOptions(const json& body) {
value.has_value() && value.value() >= 0.0) {
options.generator_speed_k = value.value();
}
if (const auto value = GetDoubleField(opt, "rotor_radius");
value.has_value() && value.value() > 0.0) {
options.rotor_radius = value.value();
}
if (const auto value = GetDoubleField(opt, "gearbox_ratio");
value.has_value() && value.value() > 0.0) {
options.gearbox_ratio = value.value();
}
if (const auto value = GetDoubleField(opt, "grid_connected_speed");
value.has_value() && value.value() > 0.0) {
options.grid_connected_speed = value.value();
@@ -1161,10 +1355,43 @@ void WindPowerController::SaveSchemeDescription(
return;
}
std::optional<json> scheme_one_parameters;
std::optional<double> grid_connected_speed;
std::optional<double> rated_generator_speed;
std::optional<double> rated_power;
if (normalized_id == kSchemeTwoId && body.value().contains("parameters")) {
if (normalized_id == kDefaultSchemeId) {
if (!body.value().contains("parameters") || !body.value()["parameters"].is_object()) {
SendError(callback, kErrorInvalidRequest, "方案一参数格式错误");
return;
}
const auto& params = body.value()["parameters"];
const auto valid_positive = [&](const std::string& field) {
const auto value = GetDoubleField(params, field);
return value.has_value() && std::isfinite(value.value()) && value.value() > 0.0;
};
const auto valid_non_negative = [&](const std::string& field) {
const auto value = GetDoubleField(params, field);
return value.has_value() && std::isfinite(value.value()) && value.value() >= 0.0;
};
if (!valid_positive("rated_power") || !valid_positive("rated_wind_speed") ||
!valid_positive("power_step") || !valid_positive("cleaning_wind_speed_step") ||
!valid_positive("rotor_radius") || !valid_positive("gearbox_ratio") ||
!valid_non_negative("wind_speed_change_threshold") ||
!valid_non_negative("iqr_lower_multiplier") ||
!valid_non_negative("iqr_upper_multiplier") ||
!valid_non_negative("minimum_generator_speed") ||
!valid_non_negative("generator_speed_k")) {
SendError(callback, kErrorInvalidRequest, "方案一参数必须为合法数值");
return;
}
if (GetDoubleField(params, "cleaning_wind_speed_step").value() > 2.0 ||
GetDoubleField(params, "iqr_lower_multiplier").value() > 10.0 ||
GetDoubleField(params, "iqr_upper_multiplier").value() > 10.0) {
SendError(callback, kErrorInvalidRequest, "方案一参数超出允许范围");
return;
}
scheme_one_parameters = params;
} else if (normalized_id == kSchemeTwoId && body.value().contains("parameters")) {
if (!body.value()["parameters"].is_object()) {
SendError(callback, kErrorInvalidRequest, "方案参数格式错误");
return;
@@ -1186,7 +1413,26 @@ void WindPowerController::SaveSchemeDescription(
for (auto& scheme : schemes) {
if (scheme.id == normalized_id) {
scheme.description = description.value();
if (scheme.id == kSchemeTwoId) {
if (scheme.id == kDefaultSchemeId) {
const auto& params = scheme_one_parameters.value();
scheme.scheme_one_rated_power = GetDoubleField(params, "rated_power").value();
scheme.scheme_one_rated_wind_speed = GetDoubleField(params, "rated_wind_speed").value();
scheme.scheme_one_power_step = GetDoubleField(params, "power_step").value();
scheme.scheme_one_cleaning_wind_speed_step =
GetDoubleField(params, "cleaning_wind_speed_step").value();
scheme.scheme_one_wind_speed_change_threshold =
GetDoubleField(params, "wind_speed_change_threshold").value();
scheme.scheme_one_iqr_lower_multiplier =
GetDoubleField(params, "iqr_lower_multiplier").value();
scheme.scheme_one_iqr_upper_multiplier =
GetDoubleField(params, "iqr_upper_multiplier").value();
scheme.scheme_one_minimum_generator_speed =
GetDoubleField(params, "minimum_generator_speed").value();
scheme.scheme_one_generator_speed_k =
GetDoubleField(params, "generator_speed_k").value();
scheme.scheme_one_rotor_radius = GetDoubleField(params, "rotor_radius").value();
scheme.scheme_one_gearbox_ratio = GetDoubleField(params, "gearbox_ratio").value();
} else if (scheme.id == kSchemeTwoId) {
if (grid_connected_speed.has_value()) {
scheme.grid_connected_speed = grid_connected_speed.value();
}
@@ -1268,9 +1514,16 @@ void WindPowerController::StartJob(
return;
}
ExpireIdleUploadTask();
CleanupExpiredCompletedJobs();
const auto job_id = GenerateJobId();
if (!AcquireTask(job_id, true)) {
SendError(callback, kErrorJobBusy, "服务器正在处理数据,请等待当前任务完成");
return;
}
try {
fs::create_directories(JobsRoot());
const auto job_id = GenerateJobId();
fs::create_directories(JobDir(job_id));
std::ofstream meta(JobDir(job_id) / "metadata.json", std::ios::trunc);
@@ -1279,11 +1532,14 @@ void WindPowerController::StartJob(
std::ofstream rows(JobRowsPath(job_id), std::ios::trunc);
rows.close();
std::ofstream raw_rows(JobRawRowsPath(job_id), std::ios::trunc);
raw_rows.close();
json data;
data["job_id"] = job_id;
SendSuccess(callback, data);
} catch (const std::exception&) {
ReleaseTask(job_id);
SendError(callback, kErrorServer, "创建计算任务失败");
}
}
@@ -1308,9 +1564,13 @@ void WindPowerController::UploadChunk(
SendError(callback, kErrorInvalidRequest, "缺少 rows 参数");
return;
}
if (IsTaskBusyFor(job_id.value())) {
SendError(callback, kErrorJobBusy, "服务器正在处理数据,请等待当前任务完成");
return;
}
try {
std::ofstream out(JobRowsPath(job_id.value()), std::ios::app);
std::ofstream raw_out(JobRawRowsPath(job_id.value()), std::ios::app);
int accepted = 0;
for (const auto& row : (*body)["rows"]) {
if (!row.is_object()) {
@@ -1320,6 +1580,13 @@ void WindPowerController::UploadChunk(
++accepted;
}
out.close();
if (body->contains("raw_rows") && (*body)["raw_rows"].is_array()) {
for (const auto& row : (*body)["raw_rows"]) {
if (row.is_object()) raw_out << row.dump() << '\n';
}
}
raw_out.close();
TouchTask(job_id.value());
json data;
data["accepted_rows"] = accepted;
@@ -1345,6 +1612,11 @@ void WindPowerController::FinishJob(
SendError(callback, kErrorJobNotFound, "计算任务不存在");
return;
}
if (IsTaskBusyFor(job_id.value())) {
SendError(callback, kErrorJobBusy, "服务器正在处理数据,请等待当前任务完成");
return;
}
TaskReleaseGuard finish_guard(job_id.value());
const CalculationOptions options = ParseOptions(*body);
if (IsSchemeTwo(options) &&
@@ -1526,7 +1798,8 @@ void WindPowerController::FinishJob(
limit_power_count += removed;
for (auto& row : fan_rows) {
row.tip_speed_ratio = row.generator_speed * 3.14 * 162.0 * 78.0 * 30.0 /
row.tip_speed_ratio = row.generator_speed * 3.14 * options.gearbox_ratio *
options.rotor_radius * 30.0 /
row.wind_speed;
}
@@ -1700,14 +1973,182 @@ void WindPowerController::FinishJob(
data["estimated_params"] = estimated_params;
try {
fs::remove_all(JobDir(job_id.value()));
std::ofstream result_file(JobResultPath(job_id.value()), std::ios::trunc);
result_file << data.dump();
} catch (const std::exception&) {
// 任务结果已经生成,临时文件清理失败不影响本次响应。
SendError(callback, kErrorServer, "保存计算结果失败");
return;
}
SendSuccess(callback, data);
}
void WindPowerController::ExportReport(
const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback,
const std::string& job_id) {
if (!IsSafeJobId(job_id) || !fs::exists(JobResultPath(job_id))) {
SendError(callback, kErrorJobNotFound, "计算任务不存在或结果已过期");
return;
}
std::string error;
const auto body = ParseBody(req, error);
if (!body.has_value()) {
SendError(callback, kErrorInvalidRequest, error);
return;
}
const auto fan_id = GetStringField(*body, "fan_id");
if (!fan_id.has_value() || !body->contains("effective_rows") ||
!(*body)["effective_rows"].is_array() || !body->contains("report_rows") ||
!(*body)["report_rows"].is_array()) {
SendError(callback, kErrorInvalidRequest, "报告参数不完整");
return;
}
if (!AcquireTask(job_id, false)) {
SendError(callback, kErrorJobBusy, "服务器正在处理数据,请等待当前任务完成");
return;
}
TaskReleaseGuard report_guard(job_id);
try {
json metadata;
json result;
{ std::ifstream input(JobDir(job_id) / "metadata.json"); input >> metadata; }
{ std::ifstream input(JobResultPath(job_id)); input >> result; }
const auto headers = metadata.value("raw_headers", json::array());
const auto mapping = metadata.value("mapping", json::object());
const auto fan_header = mapping.value("fan_id", "");
const auto wind_header = mapping.value("wind_speed", "");
if (!headers.is_array() || fan_header.empty() || wind_header.empty()) {
SendError(callback, kErrorServer, "任务未保存原始数据列,无法导出完整报告");
return;
}
size_t fan_column = headers.size();
size_t wind_column = headers.size();
for (size_t i = 0; i < headers.size(); ++i) {
const auto header = headers[i].is_string() ? headers[i].get<std::string>() : "";
if (header == fan_header) fan_column = i;
if (header == wind_header) wind_column = i;
}
if (fan_column == headers.size() || wind_column == headers.size()) {
SendError(callback, kErrorServer, "原始数据缺少风机编号或风速列");
return;
}
const auto report_path = JobDir(job_id) / ("report_" + FileNameForFan(fan_id.value()) + ".xlsx");
lxw_workbook_options options{};
const auto temp_dir = JobDir(job_id).string();
options.constant_memory = LXW_TRUE;
options.use_zip64 = LXW_TRUE;
options.tmpdir = const_cast<char*>(temp_dir.c_str());
lxw_workbook* workbook = workbook_new_opt(report_path.string().c_str(), &options);
if (!workbook) throw std::runtime_error("无法创建 Excel 工作簿");
lxw_format* header_format = workbook_add_format(workbook);
format_set_bold(header_format);
format_set_bg_color(header_format, 0xE2E8F0);
format_set_align(header_format, LXW_ALIGN_CENTER);
lxw_format* number_format = workbook_add_format(workbook);
format_set_num_format(number_format, "0.0000");
lxw_worksheet* detail = workbook_add_worksheet(workbook, "筛选后的数据");
worksheet_freeze_panes(detail, 1, 0);
const bool include_pitch = result.value("scheme", json::object()).value("id", "") == kSchemeTwoId;
const std::vector<std::string> detail_headers = include_pitch
? std::vector<std::string>{"风机编号", "采样时间", "平均功率", "平均转速", "平均风速", "3个叶片变桨角平均值"}
: std::vector<std::string>{"风机编号", "采样时间", "平均功率", "平均转速", "平均风速"};
for (size_t i = 0; i < detail_headers.size(); ++i) {
worksheet_write_string(detail, 0, i, detail_headers[i].c_str(), header_format);
worksheet_set_column(detail, i, i, i == 1 ? 22 : 16, nullptr);
}
lxw_row_t detail_row = 1;
for (const auto& point : (*body)["effective_rows"]) {
if (!point.is_object()) continue;
worksheet_write_string(detail, detail_row, 0, point.value("fan_id", fan_id.value()).c_str(), nullptr);
worksheet_write_string(detail, detail_row, 1, point.value("time", "").c_str(), nullptr);
worksheet_write_number(detail, detail_row, 2, point.value("active_power", 0.0), number_format);
worksheet_write_number(detail, detail_row, 3, point.value("generator_speed", 0.0), number_format);
worksheet_write_number(detail, detail_row, 4, point.value("wind_speed", 0.0), number_format);
if (include_pitch) worksheet_write_number(detail, detail_row, 5, point.value("pitch_angle_average", 0.0), number_format);
++detail_row;
}
worksheet_autofilter(detail, 0, 0, std::max<lxw_row_t>(1, detail_row - 1), detail_headers.size() - 1);
lxw_worksheet* raw = workbook_add_worksheet(workbook, "筛选前的数据");
worksheet_freeze_panes(raw, 1, 0);
for (size_t i = 0; i < headers.size(); ++i) {
const auto header = headers[i].is_string() ? headers[i].get<std::string>() : "";
worksheet_write_string(raw, 0, i, header.c_str(), header_format);
worksheet_set_column(raw, i, i, 16, nullptr);
}
std::set<std::string> source_files;
lxw_row_t raw_row = 1;
std::ifstream raw_input(JobRawRowsPath(job_id));
std::string line;
while (std::getline(raw_input, line)) {
const auto source = json::parse(line, nullptr, false);
if (source.is_discarded() || !source.contains("values") || !source["values"].is_array()) continue;
const auto& values = source["values"];
if (fan_column >= values.size() || JsonText(values[fan_column]) != fan_id.value()) continue;
source_files.insert(source.value("file_name", ""));
for (size_t column = 0; column < values.size(); ++column) WriteJsonCell(raw, raw_row, column, values[column]);
++raw_row;
}
if (source_files.size() != 1) {
workbook_close(workbook);
SendError(callback, kErrorInvalidRequest, "完整报告要求当前风机对应唯一一份上传 Excel 文件");
return;
}
worksheet_autofilter(raw, 0, 0, std::max<lxw_row_t>(1, raw_row - 1), headers.size() - 1);
lxw_worksheet* curve = workbook_add_worksheet(workbook, "功率曲线计算表");
worksheet_freeze_panes(curve, 1, 0);
const std::vector<std::string> curve_headers = {"序号", "风速 (m/s)", "风频时间 (h)", "计算功率 (kW)", "保证功率 (kW)", "计算发电量 (kWh)", "理论发电量 (kWh)", "", "K值"};
for (size_t i = 0; i < curve_headers.size(); ++i) {
worksheet_write_string(curve, 0, i, curve_headers[i].c_str(), header_format);
worksheet_set_column(curve, i, i, i == 0 ? 9 : 17, nullptr);
}
const auto raw_wind = ExcelColumnName(wind_column);
lxw_row_t curve_row = 1;
for (const auto& point : (*body)["report_rows"]) {
const auto excel_row = curve_row + 1;
worksheet_write_number(curve, curve_row, 0, excel_row - 1, nullptr);
worksheet_write_number(curve, curve_row, 1, point.value("wind_speed", 0.0), number_format);
const auto frequency = "=(COUNTIFS('筛选前的数据'!" + raw_wind + ":" + raw_wind + ",\">=\"&B" + std::to_string(excel_row) + "-0.5,'筛选前的数据'!" + raw_wind + ":" + raw_wind + ",\"<\"&B" + std::to_string(excel_row) + "+0.5)/COUNT('筛选前的数据'!" + raw_wind + ":" + raw_wind + "))*8760";
worksheet_write_formula(curve, curve_row, 2, frequency.c_str(), number_format);
const auto actual = "=IFERROR(AVERAGEIFS('筛选后的数据'!$C:$C,'筛选后的数据'!$E:$E,\">=\"&B" + std::to_string(excel_row) + "-0.5,'筛选后的数据'!$E:$E,\"<\"&B" + std::to_string(excel_row) + "+0.5),0)";
worksheet_write_formula(curve, curve_row, 3, actual.c_str(), number_format);
worksheet_write_number(curve, curve_row, 4, point.value("design_power", 0.0), number_format);
const auto generated = "=IFERROR(ROUND(C" + std::to_string(excel_row) + "*D" + std::to_string(excel_row) + "/1000,4),0)";
const auto theoretical = "=ROUND(C" + std::to_string(excel_row) + "*E" + std::to_string(excel_row) + "/1000,4)";
worksheet_write_formula(curve, curve_row, 5, generated.c_str(), number_format);
worksheet_write_formula(curve, curve_row, 6, theoretical.c_str(), number_format);
++curve_row;
}
const auto last_row = std::max<lxw_row_t>(2, curve_row);
const auto k_formula = "=IFERROR(SUM(F2:F" + std::to_string(last_row) + ")/SUM(G2:G" + std::to_string(last_row) + "),0)";
worksheet_write_formula(curve, 1, 8, k_formula.c_str(), number_format);
worksheet_autofilter(curve, 0, 0, std::max<lxw_row_t>(1, curve_row - 1), 6);
if (body->contains("chart_image") && (*body)["chart_image"].is_string()) {
auto image_data = (*body)["chart_image"].get<std::string>();
const auto comma = image_data.find(',');
if (comma != std::string::npos) image_data = image_data.substr(comma + 1);
const auto image_path = JobDir(job_id) / "report_chart.png";
std::ofstream image(image_path, std::ios::binary | std::ios::trunc);
image << drogon::utils::base64Decode(image_data);
image.close();
worksheet_insert_image(curve, 3, 8, image_path.string().c_str());
}
if (workbook_close(workbook) != LXW_NO_ERROR) throw std::runtime_error("写入 Excel 文件失败");
callback(HttpResponse::newFileResponse(report_path.string(),
"完整功率曲线报告_" + FileNameForFan(fan_id.value()) + ".xlsx",
CT_CUSTOM,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
} catch (const std::exception&) {
SendError(callback, kErrorServer, "完整报告生成失败,请稍后重试");
}
}
void WindPowerController::DeleteJob(
const HttpRequestPtr&,
std::function<void(const HttpResponsePtr&)>&& callback,
@@ -1719,6 +2160,7 @@ void WindPowerController::DeleteJob(
try {
fs::remove_all(JobDir(job_id));
ReleaseTask(job_id);
SendSuccess(callback);
} catch (const std::exception&) {
SendError(callback, kErrorServer, "清理任务失败");
@@ -14,6 +14,7 @@ public:
ADD_METHOD_TO(WindPowerController::StartJob, "/api/wind/jobs/start", Post);
ADD_METHOD_TO(WindPowerController::UploadChunk, "/api/wind/jobs/chunk", Post);
ADD_METHOD_TO(WindPowerController::FinishJob, "/api/wind/jobs/finish", Post);
ADD_METHOD_TO(WindPowerController::ExportReport, "/api/wind/jobs/{1}/report", Post);
ADD_METHOD_TO(WindPowerController::DeleteJob, "/api/wind/jobs/{1}", Delete);
ADD_METHOD_TO(WindPowerController::GetSchemes, "/api/wind/schemes", Get);
ADD_METHOD_TO(WindPowerController::SaveSchemeDescription,
@@ -38,6 +39,9 @@ public:
std::function<void(const HttpResponsePtr&)>&& callback);
void FinishJob(const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback);
void ExportReport(const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback,
const std::string& job_id);
void DeleteJob(const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback,
const std::string& job_id);