功能: 服务端生成完整报告并保护单任务
- 使用 libxlsxwriter 常量内存导出 Excel\n- 增加全局任务占用提示与原始数据持久化\n- 保留运行时任务数据并更新接口文档
This commit is contained in:
@@ -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 目录
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user