Files
wind_power_cal/backend/src/controllers/WindPowerController.cpp
T
cloud 0e28826073 修复: 升级框架并完善报告导出
- 升级 Drogon 和 Trantor,修复畸形请求导致的连接计数泄漏\n- 增加第三方框架版本校验与自动重建\n- 完善完整报告导出和接口文档
2026-08-10 09:50:09 +08:00

2224 lines
87 KiB
C++

#include "WindPowerController.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <ctime>
#include <deque>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <mutex>
#include <optional>
#include <random>
#include <set>
#include <sstream>
#include <string>
#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;
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 = "方案一";
constexpr const char* kSchemeTwoName = "方案二";
constexpr const char* kSchemeOneDefaultDescription = "通用方案";
constexpr const char* kSchemeTwoDefaultDescription =
"桨角筛选方案,使用三支叶片角度平均值、600s 平均风速和手填转速/功率参数";
struct RawRow {
std::string time;
std::string fan_id;
double wind_speed = 0.0;
double active_power = 0.0;
double generator_speed = 0.0;
};
struct ValidRow {
std::string time;
std::time_t timestamp = 0;
std::string date;
std::string fan_id;
double wind_speed = 0.0;
double active_power = 0.0;
double generator_speed = 0.0;
double blade_pitch_1 = 0.0;
double blade_pitch_2 = 0.0;
double blade_pitch_3 = 0.0;
double pitch_angle_average = 0.0;
double wind_speed_600s_average = 0.0;
double tip_speed_ratio = 0.0;
};
struct RemovedPoint {
ValidRow row;
std::string reason;
};
struct CalculationOptions {
std::string scheme_id = kDefaultSchemeId;
double rated_power = 4800.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 report_wind_speed_interval = 0.25;
double wind_speed_change_threshold = 1.0;
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;
bool grid_connected_speed_provided = false;
bool rated_generator_speed_provided = false;
};
struct EstimatedParams {
double rated_power = 0.0;
double rated_wind_speed = 0.0;
std::string source = "fallback";
};
struct CurveBin {
double wind_speed = 0.0;
double median_power = 0.0;
size_t sample_count = 0;
};
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 scheme_one_report_wind_speed_interval = 0.25;
double grid_connected_speed = 1030.0;
double rated_generator_speed = 1755.0;
double rated_power = 2000.0;
double scheme_two_report_wind_speed_interval = 0.25;
};
constexpr double kRatedCornerWindBefore = 0.5;
constexpr double kRatedCornerWindAfter = 1.2;
constexpr double kRatedCornerPowerLowerRatio = 0.88;
constexpr double kRatedCornerPowerUpperRatio = 1.03;
std::string Trim(const std::string& value) {
const auto begin = value.find_first_not_of(" \t\r\n");
if (begin == std::string::npos) {
return "";
}
const auto end = value.find_last_not_of(" \t\r\n");
return value.substr(begin, end - begin + 1);
}
bool IsSafeJobId(const std::string& job_id) {
if (job_id.empty() || job_id.size() > 80) {
return false;
}
return std::all_of(job_id.begin(), job_id.end(), [](unsigned char ch) {
return std::isalnum(ch) || ch == '_' || ch == '-';
});
}
fs::path JobsRoot() {
return fs::path("uploads") / "wind_jobs";
}
fs::path JobDir(const std::string& job_id) {
return JobsRoot() / job_id;
}
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";
}
fs::path ChartOptionsConfigPath() {
return fs::path("data") / "wind_chart_options.json";
}
std::string GenerateJobId() {
const auto now = std::chrono::system_clock::now().time_since_epoch().count();
std::random_device rd;
std::mt19937 rng(rd());
std::uniform_int_distribution<int> dist(0, 15);
std::ostringstream oss;
oss << "job_" << now << "_";
for (int i = 0; i < 8; ++i) {
oss << std::hex << dist(rng);
}
return oss.str();
}
std::optional<json> ParseBody(const HttpRequestPtr& req, std::string& error) {
try {
if (req->getBody().empty()) {
error = "请求体不能为空";
return std::nullopt;
}
return json::parse(req->getBody());
} catch (const std::exception&) {
error = "请求体不是合法 JSON";
return std::nullopt;
}
}
std::optional<std::string> GetStringField(const json& body, const std::string& field) {
if (!body.contains(field)) {
return std::nullopt;
}
if (body[field].is_string()) {
return Trim(body[field].get<std::string>());
}
if (body[field].is_number_integer()) {
return std::to_string(body[field].get<long long>());
}
if (body[field].is_number_float()) {
std::ostringstream oss;
oss << body[field].get<double>();
return oss.str();
}
return std::nullopt;
}
std::optional<double> GetNumberField(const json& body, const std::string& field) {
if (!body.contains(field) || !body[field].is_number()) {
return std::nullopt;
}
return body[field].get<double>();
}
bool IsHexColor(const json& value) {
if (!value.is_string()) {
return false;
}
const auto color = value.get<std::string>();
if (color.size() != 7 || color.front() != '#') {
return false;
}
return std::all_of(color.begin() + 1, color.end(), [](unsigned char ch) {
return std::isxdigit(ch);
});
}
bool IsOneOf(const json& value, const std::vector<std::string>& allowed) {
return value.is_string() && std::find(allowed.begin(), allowed.end(), value.get<std::string>()) != allowed.end();
}
bool ValidateChartOptions(const json& options, std::string& error) {
if (!options.is_object()) {
error = "图表参数必须是对象";
return false;
}
const std::array<std::string, 3> text_fields = {"title", "x_axis_label", "y_axis_label"};
for (const auto& field : text_fields) {
if (!options.contains(field) || !options[field].is_string() || options[field].get<std::string>().size() > 200) {
error = "图表文字参数无效";
return false;
}
}
const std::array<std::string, 4> color_fields = {
"scatter_color", "text_color", "actual_color", "design_color",
};
for (const auto& field : color_fields) {
if (!options.contains(field) || !IsHexColor(options[field])) {
error = "图表颜色参数无效";
return false;
}
}
const std::array<std::string, 5> bool_fields = {
"show_grid", "show_legend", "show_scatter", "show_filtered", "actual_show_markers",
};
for (const auto& field : bool_fields) {
if (!options.contains(field) || !options[field].is_boolean()) {
error = "图表开关参数无效";
return false;
}
}
if (!options.contains("design_show_markers") || !options["design_show_markers"].is_boolean()) {
error = "图表开关参数无效";
return false;
}
const auto validate_number = [&](const std::string& field, double minimum, double maximum) {
return options.contains(field) && options[field].is_number() &&
std::isfinite(options[field].get<double>()) &&
options[field].get<double>() >= minimum && options[field].get<double>() <= maximum;
};
if (!validate_number("scatter_size", 1.0, 8.0) ||
!validate_number("scatter_opacity", 0.0, 100.0) ||
!validate_number("text_scale", 0.75, 1.5) ||
!validate_number("actual_marker_size", 2.0, 12.0) ||
!validate_number("design_marker_size", 2.0, 12.0)) {
error = "图表数值参数无效";
return false;
}
for (const auto& field : {"x_tick_interval", "y_tick_interval"}) {
if (!options.contains(field) || !options[field].is_string()) {
error = "图表刻度参数无效";
return false;
}
const auto value = Trim(options[field].get<std::string>());
if (!value.empty()) {
try {
size_t parsed = 0;
const double number = std::stod(value, &parsed);
if (parsed != value.size() || !std::isfinite(number) || number <= 0.0) {
error = "图表刻度参数无效";
return false;
}
} catch (const std::exception&) {
error = "图表刻度参数无效";
return false;
}
}
}
const std::vector<std::string> line_styles = {"solid", "dashed", "dotted"};
const std::vector<std::string> marker_shapes = {"circle", "square", "diamond", "triangle", "cross", "x"};
if (!options.contains("actual_line_style") || !IsOneOf(options["actual_line_style"], line_styles) ||
!options.contains("design_line_style") || !IsOneOf(options["design_line_style"], line_styles) ||
!options.contains("actual_marker_shape") || !IsOneOf(options["actual_marker_shape"], marker_shapes) ||
!options.contains("design_marker_shape") || !IsOneOf(options["design_marker_shape"], marker_shapes)) {
error = "图表线型或标记参数无效";
return false;
}
return true;
}
std::optional<json> LoadChartOptions() {
const auto config_path = ChartOptionsConfigPath();
if (!fs::exists(config_path)) {
return std::nullopt;
}
try {
std::ifstream in(config_path);
auto options = json::parse(in);
// Compatible with configurations saved before filtered-point visibility became persistent.
if (!options.contains("show_filtered")) {
options["show_filtered"] = false;
}
// Migrate the earlier single pixel-size setting to the shared text multiplier.
if (!options.contains("text_scale")) {
double scale = 1.0;
if (options.contains("text_size") && options["text_size"].is_number()) {
scale = options["text_size"].get<double>() / 18.0;
}
options["text_scale"] = std::clamp(scale, 0.75, 1.5);
}
options.erase("text_size");
std::string error;
if (ValidateChartOptions(options, error)) {
return std::optional<json>{options};
}
} catch (const std::exception&) {
// Invalid optional chart configuration falls back to browser defaults.
}
return std::nullopt;
}
bool SaveChartOptionsToFile(const json& options) {
try {
const auto config_path = ChartOptionsConfigPath();
fs::create_directories(config_path.parent_path());
std::ofstream out(config_path);
out << options.dump(2);
return static_cast<bool>(out);
} catch (const std::exception&) {
return false;
}
}
std::string NormalizeSchemeId(const std::string& scheme_id) {
return scheme_id == kSchemeTwoId ? kSchemeTwoId : kDefaultSchemeId;
}
std::vector<SchemeInfo> DefaultSchemes() {
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,
const std::string& scheme_id) {
const std::string normalized_id = NormalizeSchemeId(scheme_id);
for (const auto& scheme : schemes) {
if (scheme.id == normalized_id) {
return scheme;
}
}
return std::nullopt;
}
json SchemeToJson(const SchemeInfo& scheme) {
json data;
data["id"] = scheme.id;
data["name"] = scheme.name;
data["description"] = scheme.description;
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},
{"report_wind_speed_interval", scheme.scheme_one_report_wind_speed_interval},
};
} 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;
data["parameters"]["report_wind_speed_interval"] =
scheme.scheme_two_report_wind_speed_interval;
}
return data;
}
std::vector<SchemeInfo> LoadSchemes() {
auto schemes = DefaultSchemes();
const auto config_path = SchemeConfigPath();
if (!fs::exists(config_path)) {
return schemes;
}
try {
std::ifstream in(config_path);
const auto saved = json::parse(in);
if (!saved.contains("schemes") || !saved["schemes"].is_array()) {
return schemes;
}
for (auto& scheme : schemes) {
for (const auto& item : saved["schemes"]) {
const auto saved_id = GetStringField(item, "id");
if (!saved_id.has_value() || saved_id.value() != scheme.id) {
continue;
}
const auto description = GetStringField(item, "description");
if (description.has_value()) {
scheme.description = description.value();
}
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);
load_positive("report_wind_speed_interval",
scheme.scheme_one_report_wind_speed_interval);
} 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();
}
if (const auto value = GetNumberField(params, "rated_generator_speed");
value.has_value() && value.value() > 0.0) {
scheme.rated_generator_speed = value.value();
}
if (const auto value = GetNumberField(params, "rated_power");
value.has_value() && value.value() > 0.0) {
scheme.rated_power = value.value();
}
if (const auto value = GetNumberField(params, "report_wind_speed_interval");
value.has_value() && value.value() > 0.0 && value.value() <= 2.0) {
scheme.scheme_two_report_wind_speed_interval = value.value();
}
}
}
}
} catch (const std::exception&) {
return schemes;
}
return schemes;
}
std::string LoadDefaultSchemeId() {
const auto config_path = SchemeConfigPath();
if (!fs::exists(config_path)) {
return kDefaultSchemeId;
}
try {
std::ifstream in(config_path);
const auto saved = json::parse(in);
const auto default_scheme_id = GetStringField(saved, "default_scheme_id");
if (default_scheme_id.has_value() &&
default_scheme_id.value() == NormalizeSchemeId(default_scheme_id.value())) {
return default_scheme_id.value();
}
} catch (const std::exception&) {
// A malformed optional config must not block the built-in default scheme.
}
return kDefaultSchemeId;
}
bool SaveSchemes(const std::vector<SchemeInfo>& schemes,
const std::string& default_scheme_id) {
try {
const auto config_path = SchemeConfigPath();
fs::create_directories(config_path.parent_path());
json data;
data["default_scheme_id"] = NormalizeSchemeId(default_scheme_id);
data["schemes"] = json::array();
for (const auto& scheme : schemes) {
data["schemes"].push_back(SchemeToJson(scheme));
}
std::ofstream out(config_path);
out << data.dump(2);
return true;
} catch (const std::exception&) {
return false;
}
}
std::optional<double> GetDoubleField(const json& body, const std::string& field) {
if (!body.contains(field)) {
return std::nullopt;
}
if (body[field].is_number()) {
return body[field].get<double>();
}
if (body[field].is_string()) {
try {
size_t parsed = 0;
const auto value = std::stod(Trim(body[field].get<std::string>()), &parsed);
if (parsed == Trim(body[field].get<std::string>()).size()) {
return value;
}
} catch (const std::exception&) {
return std::nullopt;
}
}
return std::nullopt;
}
std::optional<std::time_t> ParseTime(std::string value) {
value = Trim(value);
if (value.empty()) {
return std::nullopt;
}
std::replace(value.begin(), value.end(), 'T', ' ');
if (!value.empty() && value.back() == 'Z') {
value.pop_back();
}
const auto dot_pos = value.find('.');
if (dot_pos != std::string::npos) {
value = value.substr(0, dot_pos);
}
if (value.size() == 10) {
value += " 00:00:00";
}
std::tm tm = {};
std::istringstream iss(value);
iss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
if (iss.fail()) {
return std::nullopt;
}
tm.tm_isdst = -1;
return std::mktime(&tm);
}
json CounterJson(const std::unordered_map<std::string, int>& counters) {
json data = json::object();
for (const auto& item : counters) {
data[item.first] = item.second;
}
return data;
}
double Quantile(std::vector<double> values, double q) {
if (values.empty()) {
return 0.0;
}
std::sort(values.begin(), values.end());
const double pos = (static_cast<double>(values.size()) - 1.0) * q;
const auto low = static_cast<size_t>(std::floor(pos));
const auto high = static_cast<size_t>(std::ceil(pos));
if (low == high) {
return values[low];
}
const double weight = pos - static_cast<double>(low);
return values[low] * (1.0 - weight) + values[high] * weight;
}
double Mean(const std::vector<double>& values) {
if (values.empty()) {
return 0.0;
}
double sum = 0.0;
for (double value : values) {
sum += value;
}
return sum / static_cast<double>(values.size());
}
double StdDev(const std::vector<double>& values, double mean) {
if (values.size() < 2) {
return 0.0;
}
double sum = 0.0;
for (double value : values) {
const double diff = value - mean;
sum += diff * diff;
}
return std::sqrt(sum / static_cast<double>(values.size()));
}
double MedianAbsoluteDeviation(const std::vector<double>& values, double median) {
if (values.empty()) {
return 0.0;
}
std::vector<double> deviations;
deviations.reserve(values.size());
for (double value : values) {
deviations.push_back(std::abs(value - median));
}
return Quantile(deviations, 0.5);
}
CalculationOptions ParseOptions(const json& body) {
CalculationOptions options;
if (!body.contains("options") || !body["options"].is_object()) {
return options;
}
const auto& opt = body["options"];
if (const auto value = GetStringField(opt, "scheme_id"); value.has_value()) {
options.scheme_id = NormalizeSchemeId(value.value());
}
if (const auto value = GetDoubleField(opt, "rated_power");
value.has_value() && value.value() > 0.0) {
options.rated_power = value.value();
options.rated_power_provided = true;
}
if (const auto value = GetDoubleField(opt, "rated_wind_speed");
value.has_value() && value.value() > 0.0) {
options.rated_wind_speed = value.value();
}
if (const auto value = GetDoubleField(opt, "power_step");
value.has_value() && value.value() > 0.0) {
options.power_step = value.value();
}
if (const auto value = GetDoubleField(opt, "cleaning_wind_speed_step");
value.has_value() && value.value() > 0.0 && value.value() <= 2.0) {
options.cleaning_wind_speed_step = value.value();
}
if (const auto value = GetDoubleField(opt, "curve_wind_speed_step");
value.has_value() && value.value() > 0.0 && value.value() <= 2.0) {
options.curve_wind_speed_step = value.value();
}
if (const auto value = GetDoubleField(opt, "report_wind_speed_interval");
value.has_value() && value.value() > 0.0 && value.value() <= 2.0) {
options.report_wind_speed_interval = value.value();
}
if (const auto value = GetDoubleField(opt, "wind_speed_change_threshold");
value.has_value() && value.value() >= 0.0) {
options.wind_speed_change_threshold = value.value();
}
if (const auto value = GetDoubleField(opt, "iqr_lower_multiplier");
value.has_value() && value.value() >= 0.0 && value.value() <= 10.0) {
options.iqr_lower_multiplier = value.value();
}
if (const auto value = GetDoubleField(opt, "iqr_upper_multiplier");
value.has_value() && value.value() >= 0.0 && value.value() <= 10.0) {
options.iqr_upper_multiplier = value.value();
}
if (const auto value = GetDoubleField(opt, "minimum_generator_speed");
value.has_value() && value.value() >= 0.0) {
options.minimum_generator_speed = value.value();
}
if (const auto value = GetDoubleField(opt, "generator_speed_k");
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();
options.grid_connected_speed_provided = true;
}
if (const auto value = GetDoubleField(opt, "rated_generator_speed");
value.has_value() && value.value() > 0.0) {
options.rated_generator_speed = value.value();
options.rated_generator_speed_provided = true;
}
return options;
}
bool IsSchemeTwo(const CalculationOptions& options) {
return options.scheme_id == kSchemeTwoId;
}
void AddInvalid(std::unordered_map<std::string, int>& counters, const std::string& reason) {
counters[reason] += 1;
}
void ComputeWindSpeed600sAverage(std::vector<ValidRow>& rows) {
std::deque<ValidRow> window;
double wind_speed_sum = 0.0;
for (auto& row : rows) {
while (!window.empty() &&
std::difftime(row.timestamp, window.front().timestamp) > 600.0) {
wind_speed_sum -= window.front().wind_speed;
window.pop_front();
}
window.push_back(row);
wind_speed_sum += row.wind_speed;
row.wind_speed_600s_average = wind_speed_sum / static_cast<double>(window.size());
}
}
bool IsRatedTransitionCorner(const ValidRow& row, const EstimatedParams& params) {
if (params.rated_power <= 0.0 || params.rated_wind_speed <= 0.0) {
return false;
}
return row.wind_speed >= params.rated_wind_speed - kRatedCornerWindBefore &&
row.wind_speed <= params.rated_wind_speed + kRatedCornerWindAfter &&
row.active_power >= params.rated_power * kRatedCornerPowerLowerRatio &&
row.active_power <= params.rated_power * kRatedCornerPowerUpperRatio;
}
std::vector<ValidRow> FilterLimitPower(const std::vector<ValidRow>& rows,
const CalculationOptions& options,
const EstimatedParams* transition_params,
int& removed_count,
std::vector<RemovedPoint>& removed_points) {
removed_count = 0;
if (rows.empty()) {
return {};
}
double min_power = rows.front().active_power;
for (const auto& row : rows) {
min_power = std::min(min_power, row.active_power);
}
std::set<size_t> remove_indexes;
for (double interval = min_power; interval < options.rated_power; interval += options.power_step) {
std::unordered_map<std::string, std::vector<size_t>> grouped_by_date;
for (size_t i = 0; i < rows.size(); ++i) {
const auto& row = rows[i];
if (row.active_power >= interval &&
row.active_power < interval + options.power_step) {
grouped_by_date[row.date].push_back(i);
}
}
for (const auto& group : grouped_by_date) {
if (group.second.empty()) {
continue;
}
double min_wind = rows[group.second.front()].wind_speed;
double max_wind = min_wind;
for (size_t index : group.second) {
min_wind = std::min(min_wind, rows[index].wind_speed);
max_wind = std::max(max_wind, rows[index].wind_speed);
}
if (max_wind - min_wind > options.wind_speed_change_threshold) {
remove_indexes.insert(group.second.begin(), group.second.end());
}
}
}
std::vector<ValidRow> result;
result.reserve(rows.size());
for (size_t i = 0; i < rows.size(); ++i) {
const bool protect_transition =
transition_params != nullptr && IsRatedTransitionCorner(rows[i], *transition_params);
if (remove_indexes.count(i) == 0 || protect_transition) {
result.push_back(rows[i]);
} else {
removed_points.push_back(RemovedPoint{rows[i], "limit_power"});
++removed_count;
}
}
return result;
}
std::vector<ValidRow> FilterSchemeTwoGridSpeed(const std::vector<ValidRow>& rows,
const CalculationOptions& options,
int& removed_count,
std::vector<RemovedPoint>& removed_points) {
removed_count = 0;
if (!IsSchemeTwo(options) || rows.empty()) {
return rows;
}
std::vector<ValidRow> result;
result.reserve(rows.size());
for (const auto& row : rows) {
if (row.generator_speed <= options.grid_connected_speed) {
++removed_count;
removed_points.push_back(RemovedPoint{row, "scheme_two_grid_speed_low"});
} else {
result.push_back(row);
}
}
return result;
}
std::vector<ValidRow> FilterSchemeTwoPitchRules(
const std::vector<ValidRow>& rows,
const CalculationOptions& options,
int& low_power_pitch_wind_count,
int& low_speed_pitch_count,
int& low_power_pitch_count,
std::vector<RemovedPoint>& removed_points) {
low_power_pitch_wind_count = 0;
low_speed_pitch_count = 0;
low_power_pitch_count = 0;
if (!IsSchemeTwo(options) || rows.empty()) {
return rows;
}
std::vector<ValidRow> result;
result.reserve(rows.size());
for (const auto& row : rows) {
if (row.active_power < options.rated_power &&
row.pitch_angle_average > 5.0 &&
row.wind_speed_600s_average < 15.0) {
++low_power_pitch_wind_count;
removed_points.push_back(RemovedPoint{row, "scheme_two_low_power_pitch_wind"});
continue;
}
if (row.generator_speed < options.rated_generator_speed &&
row.pitch_angle_average > 5.0) {
++low_speed_pitch_count;
removed_points.push_back(RemovedPoint{row, "scheme_two_low_speed_pitch"});
continue;
}
if (row.active_power < 1980.0 && row.pitch_angle_average > 2.0) {
++low_power_pitch_count;
removed_points.push_back(RemovedPoint{row, "scheme_two_low_power_pitch"});
continue;
}
result.push_back(row);
}
return result;
}
template <typename ValueGetter>
std::vector<ValidRow> FilterByWindBinIqr(const std::vector<ValidRow>& rows,
const CalculationOptions& options,
ValueGetter value_getter,
int& removed_count,
std::vector<RemovedPoint>& removed_points,
const std::string& reason,
const EstimatedParams* transition_params = nullptr) {
removed_count = 0;
if (rows.empty()) {
return {};
}
double min_wind = rows.front().wind_speed;
double max_wind = min_wind;
for (const auto& row : rows) {
min_wind = std::min(min_wind, row.wind_speed);
max_wind = std::max(max_wind, row.wind_speed);
}
std::vector<ValidRow> result;
result.reserve(rows.size());
for (double interval = min_wind; interval <= max_wind;
interval += options.cleaning_wind_speed_step) {
std::vector<ValidRow> interval_rows;
std::vector<double> values;
for (const auto& row : rows) {
if (row.wind_speed >= interval &&
row.wind_speed < interval + options.cleaning_wind_speed_step) {
interval_rows.push_back(row);
values.push_back(value_getter(row));
}
}
if (interval_rows.empty()) {
continue;
}
if (interval_rows.size() >= 4) {
const double q1 = Quantile(values, 0.25);
const double q3 = Quantile(values, 0.75);
const double iqr = q3 - q1;
const double lower = q1 - options.iqr_lower_multiplier * iqr;
const double upper = q3 + options.iqr_upper_multiplier * iqr;
for (const auto& row : interval_rows) {
const double value = value_getter(row);
if (value >= lower && value <= upper) {
result.push_back(row);
} else if (transition_params != nullptr &&
IsRatedTransitionCorner(row, *transition_params)) {
result.push_back(row);
} else {
++removed_count;
removed_points.push_back(RemovedPoint{row, reason});
}
}
} else {
result.insert(result.end(), interval_rows.begin(), interval_rows.end());
}
}
return result;
}
std::vector<CurveBin> BuildMedianCurveBins(const std::vector<ValidRow>& rows,
double wind_speed_step) {
if (rows.empty()) {
return {};
}
const double curve_min = 1.0 - wind_speed_step * 0.5;
const double curve_max = 25.0 + wind_speed_step * 0.5;
std::vector<CurveBin> bins;
for (double start = curve_min; start < curve_max; start += wind_speed_step) {
const double end = start + wind_speed_step;
std::vector<double> values;
for (const auto& row : rows) {
if (row.wind_speed > start && row.wind_speed <= end) {
values.push_back(row.active_power);
}
}
if (!values.empty()) {
CurveBin bin;
bin.wind_speed = (start + end) / 2.0;
bin.median_power = Quantile(values, 0.5);
bin.sample_count = values.size();
bins.push_back(bin);
}
}
return bins;
}
double InterpolateMedianPower(const std::vector<CurveBin>& bins, double wind_speed) {
if (bins.empty()) {
return 0.0;
}
if (wind_speed <= bins.front().wind_speed) {
return bins.front().median_power;
}
if (wind_speed >= bins.back().wind_speed) {
return bins.back().median_power;
}
for (size_t i = 1; i < bins.size(); ++i) {
if (wind_speed <= bins[i].wind_speed) {
const auto& left = bins[i - 1];
const auto& right = bins[i];
const double span = right.wind_speed - left.wind_speed;
if (span <= 0.0) {
return left.median_power;
}
const double ratio = (wind_speed - left.wind_speed) / span;
return left.median_power + (right.median_power - left.median_power) * ratio;
}
}
return bins.back().median_power;
}
EstimatedParams EstimateRatedParams(const std::vector<ValidRow>& rows,
const CalculationOptions& options) {
EstimatedParams params;
params.rated_power = options.rated_power;
params.rated_wind_speed = options.rated_wind_speed;
if (rows.size() < 20) {
return params;
}
std::vector<double> powers;
powers.reserve(rows.size());
for (const auto& row : rows) {
powers.push_back(row.active_power);
}
const double p90 = Quantile(powers, 0.90);
std::vector<double> platform_candidates;
for (double power : powers) {
if (power >= p90 * 0.85) {
platform_candidates.push_back(power);
}
}
if (platform_candidates.size() >= 10) {
params.rated_power = Quantile(platform_candidates, 0.5);
params.source = "auto";
}
const auto bins = BuildMedianCurveBins(rows, options.curve_wind_speed_step);
if (params.source == "auto") {
for (const auto& bin : bins) {
if (bin.sample_count >= 4 && bin.median_power >= params.rated_power * 0.95) {
params.rated_wind_speed = bin.wind_speed;
return params;
}
}
}
params.source = params.source == "auto" ? "auto_power_fallback_wind" : "fallback";
return params;
}
std::vector<ValidRow> FilterHighWindLowPower(const std::vector<ValidRow>& rows,
const EstimatedParams& params,
int& removed_count,
std::vector<RemovedPoint>& removed_points) {
removed_count = 0;
if (rows.empty() || params.rated_power <= 0.0 || params.rated_wind_speed <= 0.0) {
return rows;
}
const double high_wind_start = params.rated_wind_speed + 1.0;
std::vector<double> platform_powers;
for (const auto& row : rows) {
if (row.wind_speed >= high_wind_start) {
platform_powers.push_back(row.active_power);
}
}
if (platform_powers.size() < 8) {
return rows;
}
const double median = Quantile(platform_powers, 0.5);
const double mad_sigma = MedianAbsoluteDeviation(platform_powers, median) * 1.4826;
const double robust_margin = std::max(4.0 * mad_sigma, median * 0.08);
const double lower_limit = std::max(params.rated_power * 0.85, median - robust_margin);
std::vector<ValidRow> result;
result.reserve(rows.size());
for (const auto& row : rows) {
if (row.wind_speed >= high_wind_start &&
!IsRatedTransitionCorner(row, params) &&
row.active_power < lower_limit) {
++removed_count;
removed_points.push_back(RemovedPoint{row, "high_wind_low_power"});
} else {
result.push_back(row);
}
}
return result;
}
std::vector<ValidRow> FilterCurveResidualOutliers(const std::vector<ValidRow>& rows,
const CalculationOptions& options,
const EstimatedParams& params,
int& removed_count,
std::vector<RemovedPoint>& removed_points) {
removed_count = 0;
if (rows.size() < 20) {
return rows;
}
const auto median_curve = BuildMedianCurveBins(rows, options.curve_wind_speed_step);
if (median_curve.size() < 4) {
return rows;
}
std::vector<ValidRow> result;
result.reserve(rows.size());
double min_wind = rows.front().wind_speed;
double max_wind = min_wind;
for (const auto& row : rows) {
min_wind = std::min(min_wind, row.wind_speed);
max_wind = std::max(max_wind, row.wind_speed);
}
for (double start = min_wind - options.curve_wind_speed_step;
start <= max_wind;
start += options.curve_wind_speed_step) {
const double end = start + options.curve_wind_speed_step;
std::vector<ValidRow> interval_rows;
std::vector<double> residuals;
for (const auto& row : rows) {
if (row.wind_speed > start && row.wind_speed <= end) {
const double expected = InterpolateMedianPower(median_curve, row.wind_speed);
interval_rows.push_back(row);
residuals.push_back(row.active_power - expected);
}
}
if (interval_rows.empty()) {
continue;
}
if (interval_rows.size() < 8) {
result.insert(result.end(), interval_rows.begin(), interval_rows.end());
continue;
}
const double median = Quantile(residuals, 0.5);
const double mad_sigma = MedianAbsoluteDeviation(residuals, median) * 1.4826;
const double rated_power = params.rated_power > 0.0 ? params.rated_power : options.rated_power;
double transition_multiplier = 1.0;
if (params.rated_wind_speed > 0.0) {
const double interval_center = (start + end) / 2.0;
if (interval_center >= params.rated_wind_speed - 1.0 &&
interval_center <= params.rated_wind_speed + 1.5) {
transition_multiplier = 1.8;
}
}
const double lower_margin =
std::max(4.0 * mad_sigma, rated_power * 0.10) * transition_multiplier;
const double upper_margin =
std::max(4.0 * mad_sigma, rated_power * 0.16) * transition_multiplier;
const double lower = median - lower_margin;
const double upper = median + upper_margin;
for (size_t i = 0; i < interval_rows.size(); ++i) {
if (residuals[i] >= lower && residuals[i] <= upper) {
result.push_back(interval_rows[i]);
} else if (IsRatedTransitionCorner(interval_rows[i], params)) {
result.push_back(interval_rows[i]);
} else {
++removed_count;
removed_points.push_back(RemovedPoint{interval_rows[i], "curve_residual_outlier"});
}
}
}
return result;
}
std::vector<ValidRow> FilterStrictRatedPlateau(const std::vector<ValidRow>& rows,
const EstimatedParams& params,
int& removed_count,
std::vector<RemovedPoint>& removed_points) {
removed_count = 0;
if (rows.empty() || params.rated_power <= 0.0 || params.rated_wind_speed <= 0.0) {
return rows;
}
const double strict_start_wind_speed = params.rated_wind_speed + 1.5;
std::vector<double> plateau_powers;
for (const auto& row : rows) {
if (row.wind_speed >= strict_start_wind_speed) {
plateau_powers.push_back(row.active_power);
}
}
if (plateau_powers.size() < 6) {
return rows;
}
const double median = Quantile(plateau_powers, 0.5);
const double mad_sigma = MedianAbsoluteDeviation(plateau_powers, median) * 1.4826;
const double lower_limit = std::max(
params.rated_power * 0.92,
median - std::max(2.5 * mad_sigma, median * 0.04));
std::vector<ValidRow> result;
result.reserve(rows.size());
for (const auto& row : rows) {
if (row.wind_speed >= strict_start_wind_speed &&
!IsRatedTransitionCorner(row, params) &&
row.active_power < lower_limit) {
++removed_count;
removed_points.push_back(RemovedPoint{row, "rated_plateau_low_power"});
} else {
result.push_back(row);
}
}
return result;
}
std::string DatePart(const std::string& time_text) {
if (time_text.size() >= 10) {
return time_text.substr(0, 10);
}
return "";
}
} // namespace
void WindPowerController::GetSchemes(
const HttpRequestPtr&,
std::function<void(const HttpResponsePtr&)>&& callback) {
json data;
data["default_scheme_id"] = LoadDefaultSchemeId();
data["schemes"] = json::array();
for (const auto& scheme : LoadSchemes()) {
data["schemes"].push_back(SchemeToJson(scheme));
}
SendSuccess(callback, data);
}
void WindPowerController::SaveSchemeDescription(
const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback,
const std::string& scheme_id) {
const std::string normalized_id = NormalizeSchemeId(scheme_id);
if (scheme_id != normalized_id) {
SendError(callback, kErrorInvalidRequest, "方案不存在");
return;
}
std::string error;
const auto body = ParseBody(req, error);
if (!body.has_value()) {
SendError(callback, kErrorInvalidRequest, error);
return;
}
const auto description = GetStringField(body.value(), "description");
if (!description.has_value()) {
SendError(callback, kErrorInvalidRequest, "方案描述不能为空");
return;
}
if (description.value().size() > 1000) {
SendError(callback, kErrorInvalidRequest, "方案描述不能超过 1000 个字符");
return;
}
std::optional<json> scheme_one_parameters;
std::optional<double> grid_connected_speed;
std::optional<double> rated_generator_speed;
std::optional<double> rated_power;
std::optional<double> report_wind_speed_interval;
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_positive("report_wind_speed_interval") ||
!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, "report_wind_speed_interval").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;
}
const auto& params = body.value()["parameters"];
grid_connected_speed = GetDoubleField(params, "grid_connected_speed");
rated_generator_speed = GetDoubleField(params, "rated_generator_speed");
rated_power = GetDoubleField(params, "rated_power");
report_wind_speed_interval = GetDoubleField(params, "report_wind_speed_interval");
if (!grid_connected_speed.has_value() || grid_connected_speed.value() <= 0.0 ||
!rated_generator_speed.has_value() || rated_generator_speed.value() <= 0.0 ||
!rated_power.has_value() || rated_power.value() <= 0.0 ||
!report_wind_speed_interval.has_value() || report_wind_speed_interval.value() <= 0.0 ||
report_wind_speed_interval.value() > 2.0) {
SendError(callback, kErrorInvalidRequest, "方案二参数必须为正数");
return;
}
}
auto schemes = LoadSchemes();
bool updated = false;
for (auto& scheme : schemes) {
if (scheme.id == normalized_id) {
scheme.description = description.value();
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();
scheme.scheme_one_report_wind_speed_interval =
GetDoubleField(params, "report_wind_speed_interval").value();
} else if (scheme.id == kSchemeTwoId) {
if (grid_connected_speed.has_value()) {
scheme.grid_connected_speed = grid_connected_speed.value();
}
if (rated_generator_speed.has_value()) {
scheme.rated_generator_speed = rated_generator_speed.value();
}
if (rated_power.has_value()) {
scheme.rated_power = rated_power.value();
}
if (report_wind_speed_interval.has_value()) {
scheme.scheme_two_report_wind_speed_interval =
report_wind_speed_interval.value();
}
}
updated = true;
break;
}
}
if (!updated) {
SendError(callback, kErrorInvalidRequest, "方案不存在");
return;
}
if (!SaveSchemes(schemes, normalized_id)) {
SendError(callback, kErrorServer, "保存方案描述失败");
return;
}
const auto saved_scheme = FindScheme(schemes, normalized_id);
json data;
data["default_scheme_id"] = normalized_id;
data["scheme"] = SchemeToJson(saved_scheme.value());
SendSuccess(callback, data);
}
void WindPowerController::GetChartOptions(
const HttpRequestPtr&,
std::function<void(const HttpResponsePtr&)>&& callback) {
json data;
const auto options = LoadChartOptions();
data["configured"] = options.has_value();
if (options.has_value()) {
data["options"] = options.value();
}
SendSuccess(callback, data);
}
void WindPowerController::SaveChartOptions(
const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback) {
std::string error;
const auto options = ParseBody(req, error);
if (!options.has_value()) {
SendError(callback, kErrorInvalidRequest, error);
return;
}
if (!ValidateChartOptions(options.value(), error)) {
SendError(callback, kErrorInvalidRequest, error);
return;
}
if (!SaveChartOptionsToFile(options.value())) {
SendError(callback, kErrorServer, "保存图表参数失败");
return;
}
json data;
data["configured"] = true;
data["options"] = options.value();
SendSuccess(callback, data);
}
void WindPowerController::StartJob(
const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback) {
std::string error;
const auto body = ParseBody(req, error);
if (!body.has_value()) {
SendError(callback, kErrorInvalidRequest, error);
return;
}
if (!body->contains("files") || !(*body)["files"].is_array() ||
!body->contains("mapping") || !(*body)["mapping"].is_object()) {
SendError(callback, kErrorInvalidRequest, "缺少 files 或 mapping 参数");
return;
}
ExpireIdleUploadTask();
CleanupExpiredCompletedJobs();
const auto job_id = GenerateJobId();
if (!AcquireTask(job_id, true)) {
SendError(callback, kErrorJobBusy, "服务器正在处理数据,请等待当前任务完成");
return;
}
try {
fs::create_directories(JobsRoot());
fs::create_directories(JobDir(job_id));
std::ofstream meta(JobDir(job_id) / "metadata.json", std::ios::trunc);
meta << body->dump(2);
meta.close();
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, "创建计算任务失败");
}
}
void WindPowerController::UploadChunk(
const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback) {
std::string error;
const auto body = ParseBody(req, error);
if (!body.has_value()) {
SendError(callback, kErrorInvalidRequest, error);
return;
}
const auto job_id = GetStringField(*body, "job_id");
if (!job_id.has_value() || !IsSafeJobId(job_id.value()) ||
!fs::exists(JobDir(job_id.value()))) {
SendError(callback, kErrorJobNotFound, "计算任务不存在");
return;
}
if (!body->contains("rows") || !(*body)["rows"].is_array()) {
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()) {
continue;
}
out << row.dump() << '\n';
++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;
SendSuccess(callback, data);
} catch (const std::exception&) {
SendError(callback, kErrorServer, "保存分片数据失败");
}
}
void WindPowerController::FinishJob(
const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback) {
std::string error;
const auto body = ParseBody(req, error);
if (!body.has_value()) {
SendError(callback, kErrorInvalidRequest, error);
return;
}
const auto job_id = GetStringField(*body, "job_id");
if (!job_id.has_value() || !IsSafeJobId(job_id.value()) ||
!fs::exists(JobRowsPath(job_id.value()))) {
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 (body->contains("options") && (*body)["options"].is_object() &&
(*body)["options"].contains("report_wind_speed_interval")) {
const auto interval = GetDoubleField((*body)["options"], "report_wind_speed_interval");
if (!interval.has_value() || !std::isfinite(interval.value()) ||
interval.value() <= 0.0 || interval.value() > 2.0) {
SendError(callback, kErrorInvalidRequest,
"报告公式风速区间半宽必须大于 0 且不超过 2");
return;
}
}
if (IsSchemeTwo(options) &&
(!options.rated_power_provided ||
!options.grid_connected_speed_provided ||
!options.rated_generator_speed_provided)) {
SendError(callback, kErrorInvalidRequest, "方案二需要填写并网转速、额定转速和额定功率");
return;
}
std::vector<ValidRow> parsed_rows;
std::unordered_map<std::string, int> invalid_reasons;
int raw_rows = 0;
try {
std::ifstream in(JobRowsPath(job_id.value()));
std::string line;
while (std::getline(in, line)) {
if (Trim(line).empty()) {
continue;
}
++raw_rows;
json row;
try {
row = json::parse(line);
} catch (const std::exception&) {
AddInvalid(invalid_reasons, "invalid_json");
continue;
}
const auto time_text = GetStringField(row, "time");
const auto fan_id = GetStringField(row, "fan_id");
const auto wind_speed = GetDoubleField(row, "wind_speed");
const auto active_power = GetDoubleField(row, "active_power");
const auto generator_speed = GetDoubleField(row, "generator_speed");
const auto blade_pitch_1 = GetDoubleField(row, "blade_pitch_1");
const auto blade_pitch_2 = GetDoubleField(row, "blade_pitch_2");
const auto blade_pitch_3 = GetDoubleField(row, "blade_pitch_3");
if (!active_power.has_value() || !std::isfinite(active_power.value()) ||
active_power.value() <= 0.0) {
AddInvalid(invalid_reasons, "invalid_active_power");
continue;
}
if (!generator_speed.has_value() || !std::isfinite(generator_speed.value()) ||
generator_speed.value() <
options.generator_speed_k * options.minimum_generator_speed) {
AddInvalid(invalid_reasons, "invalid_generator_speed");
continue;
}
if (IsSchemeTwo(options) &&
(!blade_pitch_1.has_value() || !std::isfinite(blade_pitch_1.value()) ||
!blade_pitch_2.has_value() || !std::isfinite(blade_pitch_2.value()) ||
!blade_pitch_3.has_value() || !std::isfinite(blade_pitch_3.value()))) {
AddInvalid(invalid_reasons, "invalid_pitch_angle");
continue;
}
if (!time_text.has_value()) {
AddInvalid(invalid_reasons, "invalid_time");
continue;
}
const auto timestamp = ParseTime(time_text.value());
if (!timestamp.has_value()) {
AddInvalid(invalid_reasons, "invalid_time");
continue;
}
if (!fan_id.has_value() || fan_id.value().empty()) {
AddInvalid(invalid_reasons, "empty_fan_id");
continue;
}
if (!wind_speed.has_value() || !std::isfinite(wind_speed.value()) ||
wind_speed.value() <= 0.0) {
AddInvalid(invalid_reasons, "invalid_wind_speed");
continue;
}
ValidRow valid_row;
valid_row.time = time_text.value();
valid_row.timestamp = timestamp.value();
valid_row.date = DatePart(time_text.value());
valid_row.fan_id = fan_id.value();
valid_row.wind_speed = wind_speed.value();
valid_row.active_power = active_power.value();
valid_row.generator_speed = generator_speed.value();
if (IsSchemeTwo(options)) {
valid_row.blade_pitch_1 = blade_pitch_1.value();
valid_row.blade_pitch_2 = blade_pitch_2.value();
valid_row.blade_pitch_3 = blade_pitch_3.value();
valid_row.pitch_angle_average =
(valid_row.blade_pitch_1 +
valid_row.blade_pitch_2 +
valid_row.blade_pitch_3) / 3.0;
}
parsed_rows.push_back(valid_row);
}
} catch (const std::exception&) {
SendError(callback, kErrorServer, "读取任务数据失败");
return;
}
std::sort(parsed_rows.begin(), parsed_rows.end(), [](const ValidRow& left, const ValidRow& right) {
if (left.fan_id != right.fan_id) {
return left.fan_id < right.fan_id;
}
return left.timestamp < right.timestamp;
});
std::vector<ValidRow> deduped_rows;
std::set<std::string> seen_keys;
int duplicate_rows = 0;
for (const auto& row : parsed_rows) {
const auto key = row.fan_id + "|" + std::to_string(row.timestamp);
if (!seen_keys.insert(key).second) {
++duplicate_rows;
continue;
}
deduped_rows.push_back(row);
}
if (duplicate_rows > 0) {
AddInvalid(invalid_reasons, "duplicate_time");
}
invalid_reasons["duplicate_time"] = duplicate_rows;
std::unordered_map<std::string, std::vector<ValidRow>> rows_by_fan;
for (const auto& row : deduped_rows) {
rows_by_fan[row.fan_id].push_back(row);
}
json fans = json::array();
json curves = json::object();
json bins = json::object();
json scatter_points = json::object();
json filtered_points = json::object();
json estimated_params = json::object();
std::vector<std::string> fan_ids;
fan_ids.reserve(rows_by_fan.size());
for (const auto& item : rows_by_fan) {
fan_ids.push_back(item.first);
}
std::sort(fan_ids.begin(), fan_ids.end());
int limit_power_count = 0;
int tip_speed_ratio_outlier_count = 0;
int speed_power_outlier_count = 0;
int high_wind_low_power_count = 0;
int curve_residual_outlier_count = 0;
int rated_plateau_low_power_count = 0;
int scheme_two_grid_speed_low_count = 0;
int scheme_two_low_power_pitch_wind_count = 0;
int scheme_two_low_speed_pitch_count = 0;
int scheme_two_low_power_pitch_count = 0;
int cleaned_rows_count = 0;
for (const auto& fan_id : fan_ids) {
fans.push_back(fan_id);
auto fan_rows = rows_by_fan[fan_id];
std::vector<RemovedPoint> fan_removed_points;
if (IsSchemeTwo(options)) {
ComputeWindSpeed600sAverage(fan_rows);
}
int removed = 0;
fan_rows = FilterSchemeTwoGridSpeed(
fan_rows,
options,
removed,
fan_removed_points);
scheme_two_grid_speed_low_count += removed;
const auto initial_estimated_params = EstimateRatedParams(fan_rows, options);
fan_rows = FilterLimitPower(
fan_rows,
options,
&initial_estimated_params,
removed,
fan_removed_points);
limit_power_count += removed;
for (auto& row : fan_rows) {
row.tip_speed_ratio = row.generator_speed * 3.14 * options.gearbox_ratio *
options.rotor_radius * 30.0 /
row.wind_speed;
}
fan_rows = FilterByWindBinIqr(
fan_rows,
options,
[](const ValidRow& row) { return row.tip_speed_ratio; },
removed,
fan_removed_points,
"tip_speed_ratio_outlier");
tip_speed_ratio_outlier_count += removed;
fan_rows = FilterByWindBinIqr(
fan_rows,
options,
[](const ValidRow& row) { return row.active_power; },
removed,
fan_removed_points,
"speed_power_outlier",
&initial_estimated_params);
speed_power_outlier_count += removed;
const auto fan_estimated_params = EstimateRatedParams(fan_rows, options);
json params_json;
params_json["rated_power"] = fan_estimated_params.rated_power;
params_json["rated_wind_speed"] = fan_estimated_params.rated_wind_speed;
params_json["source"] = fan_estimated_params.source;
estimated_params[fan_id] = params_json;
fan_rows = FilterHighWindLowPower(
fan_rows,
fan_estimated_params,
removed,
fan_removed_points);
high_wind_low_power_count += removed;
fan_rows = FilterCurveResidualOutliers(
fan_rows,
options,
fan_estimated_params,
removed,
fan_removed_points);
curve_residual_outlier_count += removed;
fan_rows = FilterStrictRatedPlateau(
fan_rows,
fan_estimated_params,
removed,
fan_removed_points);
rated_plateau_low_power_count += removed;
int low_power_pitch_wind_removed = 0;
int low_speed_pitch_removed = 0;
int low_power_pitch_removed = 0;
fan_rows = FilterSchemeTwoPitchRules(
fan_rows,
options,
low_power_pitch_wind_removed,
low_speed_pitch_removed,
low_power_pitch_removed,
fan_removed_points);
scheme_two_low_power_pitch_wind_count += low_power_pitch_wind_removed;
scheme_two_low_speed_pitch_count += low_speed_pitch_removed;
scheme_two_low_power_pitch_count += low_power_pitch_removed;
cleaned_rows_count += static_cast<int>(fan_rows.size());
json fan_scatter = json::array();
for (const auto& row : fan_rows) {
json point;
point["fan_id"] = row.fan_id;
point["time"] = row.time;
point["wind_speed"] = row.wind_speed;
point["active_power"] = row.active_power;
point["generator_speed"] = row.generator_speed;
point["pitch_angle_average"] = row.pitch_angle_average;
fan_scatter.push_back(point);
}
scatter_points[fan_id] = fan_scatter;
json fan_filtered = json::array();
for (const auto& removed_point : fan_removed_points) {
json point;
point["fan_id"] = removed_point.row.fan_id;
point["time"] = removed_point.row.time;
point["wind_speed"] = removed_point.row.wind_speed;
point["active_power"] = removed_point.row.active_power;
point["generator_speed"] = removed_point.row.generator_speed;
point["pitch_angle_average"] = removed_point.row.pitch_angle_average;
point["reason"] = removed_point.reason;
fan_filtered.push_back(point);
}
filtered_points[fan_id] = fan_filtered;
json fan_curve = json::array();
json fan_bins_json = json::array();
const double curve_min = 1.0 - options.curve_wind_speed_step * 0.5;
const double curve_max = 25.0 + options.curve_wind_speed_step * 0.5;
for (double start = curve_min; start < curve_max; start += options.curve_wind_speed_step) {
const double end = start + options.curve_wind_speed_step;
std::vector<double> values;
for (const auto& row : fan_rows) {
if (row.wind_speed > start && row.wind_speed <= end) {
values.push_back(row.active_power);
}
}
if (!values.empty()) {
json point;
point["wind_speed_start"] = start;
point["wind_speed_end"] = end;
point["wind_speed"] = (start + end) / 2.0;
point["sample_count"] = values.size();
point["average_power"] = Mean(values);
point["median_power"] = Quantile(values, 0.5);
point["stddev_power"] = StdDev(values, point["average_power"].get<double>());
point["p25_power"] = Quantile(values, 0.25);
point["p75_power"] = Quantile(values, 0.75);
point["confidence"] = "脚本分箱";
fan_curve.push_back(point);
fan_bins_json.push_back(point);
}
}
curves[fan_id] = fan_curve;
bins[fan_id] = fan_bins_json;
}
invalid_reasons["limit_power"] = limit_power_count;
invalid_reasons["tip_speed_ratio_outlier"] = tip_speed_ratio_outlier_count;
invalid_reasons["speed_power_outlier"] = speed_power_outlier_count;
invalid_reasons["high_wind_low_power"] = high_wind_low_power_count;
invalid_reasons["curve_residual_outlier"] = curve_residual_outlier_count;
invalid_reasons["rated_plateau_low_power"] = rated_plateau_low_power_count;
invalid_reasons["scheme_two_grid_speed_low"] = scheme_two_grid_speed_low_count;
invalid_reasons["scheme_two_low_power_pitch_wind"] =
scheme_two_low_power_pitch_wind_count;
invalid_reasons["scheme_two_low_speed_pitch"] = scheme_two_low_speed_pitch_count;
invalid_reasons["scheme_two_low_power_pitch"] = scheme_two_low_power_pitch_count;
const int invalid_total = raw_rows - cleaned_rows_count;
json summary;
summary["raw_rows"] = raw_rows;
summary["valid_rows"] = std::max(0, cleaned_rows_count);
summary["invalid_rows"] = std::max(0, invalid_total);
summary["duplicate_rows"] = duplicate_rows;
summary["limit_power_rows"] = limit_power_count;
summary["tip_speed_ratio_outlier_rows"] = tip_speed_ratio_outlier_count;
summary["speed_power_outlier_rows"] = speed_power_outlier_count;
summary["high_wind_low_power_rows"] = high_wind_low_power_count;
summary["curve_residual_outlier_rows"] = curve_residual_outlier_count;
summary["rated_plateau_low_power_rows"] = rated_plateau_low_power_count;
summary["scheme_two_grid_speed_low_rows"] = scheme_two_grid_speed_low_count;
summary["scheme_two_low_power_pitch_wind_rows"] =
scheme_two_low_power_pitch_wind_count;
summary["scheme_two_low_speed_pitch_rows"] = scheme_two_low_speed_pitch_count;
summary["scheme_two_low_power_pitch_rows"] = scheme_two_low_power_pitch_count;
summary["invalid_reasons"] = CounterJson(invalid_reasons);
summary["fan_count"] = fans.size();
json data;
const auto selected_scheme = FindScheme(LoadSchemes(), options.scheme_id);
data["scheme"] = SchemeToJson(selected_scheme.value());
data["scheme"]["parameters"]["report_wind_speed_interval"] =
options.report_wind_speed_interval;
data["summary"] = summary;
data["fans"] = fans;
data["curves"] = curves;
data["bins"] = bins;
data["scatter_points"] = scatter_points;
data["filtered_points"] = filtered_points;
data["estimated_params"] = estimated_params;
try {
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);
const auto report_interval = result.value("scheme", json::object())
.value("parameters", json::object())
.value("report_wind_speed_interval", 0.25);
const auto interval_text = std::to_string(report_interval);
const auto report_row_count = (*body)["report_rows"].size();
const auto k_last_row = std::max<size_t>(2, report_row_count + 1);
const auto k_formula = "=IFERROR(SUM(F2:F" + std::to_string(k_last_row) +
")/SUM(G2:G" + std::to_string(k_last_row) + "),0)";
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) + "-" + interval_text + ",'筛选前的数据'!" + raw_wind + ":" + raw_wind + ",\"<\"&B" + std::to_string(excel_row) + "+" + interval_text + ")/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) + "-" + interval_text + ",'筛选后的数据'!$E:$E,\"<\"&B" + std::to_string(excel_row) + "+" + interval_text + "),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);
if (curve_row == 1) {
worksheet_write_formula(curve, curve_row, 8, k_formula.c_str(), number_format);
}
++curve_row;
}
if (report_row_count == 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();
lxw_image_options image_options{};
image_options.x_scale = 0.5;
image_options.y_scale = 0.5;
worksheet_insert_image_opt(curve, 3, 8, image_path.string().c_str(),
&image_options);
}
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,
const std::string& job_id) {
if (!IsSafeJobId(job_id)) {
SendError(callback, kErrorInvalidRequest, "任务编号非法");
return;
}
try {
fs::remove_all(JobDir(job_id));
ReleaseTask(job_id);
SendSuccess(callback);
} catch (const std::exception&) {
SendError(callback, kErrorServer, "清理任务失败");
}
}