#include "WindPowerController.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 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 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 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 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 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('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(), format); } else if (value.is_boolean()) { worksheet_write_boolean(sheet, row, column, value.get(), format); } else if (!value.is_null()) { const auto text = value.is_string() ? value.get() : 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(); if (value.is_number_integer()) return std::to_string(value.get()); if (value.is_number_unsigned()) return std::to_string(value.get()); if (value.is_number_float()) { std::ostringstream output; output << value.get(); 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(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 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 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 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()); } if (body[field].is_number_integer()) { return std::to_string(body[field].get()); } if (body[field].is_number_float()) { std::ostringstream oss; oss << body[field].get(); return oss.str(); } return std::nullopt; } std::optional GetNumberField(const json& body, const std::string& field) { if (!body.contains(field) || !body[field].is_number()) { return std::nullopt; } return body[field].get(); } bool IsHexColor(const json& value) { if (!value.is_string()) { return false; } const auto color = value.get(); 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& allowed) { return value.is_string() && std::find(allowed.begin(), allowed.end(), value.get()) != allowed.end(); } bool ValidateChartOptions(const json& options, std::string& error) { if (!options.is_object()) { error = "图表参数必须是对象"; return false; } const std::array 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().size() > 200) { error = "图表文字参数无效"; return false; } } const std::array 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 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()) && options[field].get() >= minimum && options[field].get() <= 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()); 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 line_styles = {"solid", "dashed", "dotted"}; const std::vector 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 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() / 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{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(out); } catch (const std::exception&) { return false; } } std::string NormalizeSchemeId(const std::string& scheme_id) { return scheme_id == kSchemeTwoId ? kSchemeTwoId : kDefaultSchemeId; } std::vector 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 FindScheme(const std::vector& 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 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& 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 GetDoubleField(const json& body, const std::string& field) { if (!body.contains(field)) { return std::nullopt; } if (body[field].is_number()) { return body[field].get(); } if (body[field].is_string()) { try { size_t parsed = 0; const auto value = std::stod(Trim(body[field].get()), &parsed); if (parsed == Trim(body[field].get()).size()) { return value; } } catch (const std::exception&) { return std::nullopt; } } return std::nullopt; } std::optional 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& counters) { json data = json::object(); for (const auto& item : counters) { data[item.first] = item.second; } return data; } double Quantile(std::vector values, double q) { if (values.empty()) { return 0.0; } std::sort(values.begin(), values.end()); const double pos = (static_cast(values.size()) - 1.0) * q; const auto low = static_cast(std::floor(pos)); const auto high = static_cast(std::ceil(pos)); if (low == high) { return values[low]; } const double weight = pos - static_cast(low); return values[low] * (1.0 - weight) + values[high] * weight; } double Mean(const std::vector& values) { if (values.empty()) { return 0.0; } double sum = 0.0; for (double value : values) { sum += value; } return sum / static_cast(values.size()); } double StdDev(const std::vector& 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(values.size())); } double MedianAbsoluteDeviation(const std::vector& values, double median) { if (values.empty()) { return 0.0; } std::vector 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& counters, const std::string& reason) { counters[reason] += 1; } void ComputeWindSpeed600sAverage(std::vector& rows) { std::deque 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(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 FilterLimitPower(const std::vector& rows, const CalculationOptions& options, const EstimatedParams* transition_params, int& removed_count, std::vector& 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 remove_indexes; for (double interval = min_power; interval < options.rated_power; interval += options.power_step) { std::unordered_map> 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 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 FilterSchemeTwoGridSpeed(const std::vector& rows, const CalculationOptions& options, int& removed_count, std::vector& removed_points) { removed_count = 0; if (!IsSchemeTwo(options) || rows.empty()) { return rows; } std::vector 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 FilterSchemeTwoPitchRules( const std::vector& rows, const CalculationOptions& options, int& low_power_pitch_wind_count, int& low_speed_pitch_count, int& low_power_pitch_count, std::vector& 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 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 std::vector FilterByWindBinIqr(const std::vector& rows, const CalculationOptions& options, ValueGetter value_getter, int& removed_count, std::vector& 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 result; result.reserve(rows.size()); for (double interval = min_wind; interval <= max_wind; interval += options.cleaning_wind_speed_step) { std::vector interval_rows; std::vector 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 BuildMedianCurveBins(const std::vector& 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 bins; for (double start = curve_min; start < curve_max; start += wind_speed_step) { const double end = start + wind_speed_step; std::vector 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& 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& 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 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 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 FilterHighWindLowPower(const std::vector& rows, const EstimatedParams& params, int& removed_count, std::vector& 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 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 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 FilterCurveResidualOutliers(const std::vector& rows, const CalculationOptions& options, const EstimatedParams& params, int& removed_count, std::vector& 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 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 interval_rows; std::vector 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 FilterStrictRatedPlateau(const std::vector& rows, const EstimatedParams& params, int& removed_count, std::vector& 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 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 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&& 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&& 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 scheme_one_parameters; std::optional grid_connected_speed; std::optional rated_generator_speed; std::optional rated_power; std::optional 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&& 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&& 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&& 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&& 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&& 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 parsed_rows; std::unordered_map 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 deduped_rows; std::set 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> 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 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 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(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 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()); 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&& 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() : ""; 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(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 detail_headers = include_pitch ? std::vector{"风机编号", "采样时间", "平均功率", "平均转速", "平均风速", "3个叶片变桨角平均值"} : std::vector{"风机编号", "采样时间", "平均功率", "平均转速", "平均风速"}; 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(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() : ""; worksheet_write_string(raw, 0, i, header.c_str(), header_format); worksheet_set_column(raw, i, i, 16, nullptr); } std::set 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(1, raw_row - 1), headers.size() - 1); lxw_worksheet* curve = workbook_add_worksheet(workbook, "功率曲线计算表"); worksheet_freeze_panes(curve, 1, 0); const std::vector 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(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(1, curve_row - 1), 6); if (body->contains("chart_image") && (*body)["chart_image"].is_string()) { auto image_data = (*body)["chart_image"].get(); 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&& 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, "清理任务失败"); } }