复现已有算法
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
#include "SystemController.h"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
void SystemController::GetHealth(
|
||||
const HttpRequestPtr&,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
json data;
|
||||
data["status"] = "ok";
|
||||
SendSuccess(callback, data);
|
||||
}
|
||||
|
||||
void SystemController::GetVersion(
|
||||
const HttpRequestPtr&,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
json data;
|
||||
data["name"] = "wind_power_cal";
|
||||
data["version"] = "0.1.0";
|
||||
SendSuccess(callback, data);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef SYSTEMCONTROLLER_H
|
||||
#define SYSTEMCONTROLLER_H
|
||||
|
||||
#include <drogon/HttpController.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include "utils/ResponseUtil.h"
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class SystemController : public drogon::HttpController<SystemController> {
|
||||
public:
|
||||
METHOD_LIST_BEGIN
|
||||
ADD_METHOD_TO(SystemController::GetHealth, "/api/system/health", Get);
|
||||
ADD_METHOD_TO(SystemController::GetVersion, "/api/system/version", Get);
|
||||
METHOD_LIST_END
|
||||
|
||||
void GetHealth(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
void GetVersion(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,758 @@
|
||||
#include "WindPowerController.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
using json = nlohmann::json;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kErrorInvalidRequest = 1001;
|
||||
constexpr int kErrorJobNotFound = 1002;
|
||||
constexpr int kErrorServer = 1003;
|
||||
|
||||
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 tip_speed_ratio = 0.0;
|
||||
};
|
||||
|
||||
struct RemovedPoint {
|
||||
ValidRow row;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct CalculationOptions {
|
||||
double rated_power = 4800.0;
|
||||
double rated_wind_speed = 18.0;
|
||||
double power_step = 5.0;
|
||||
double cleaning_wind_speed_step = 0.25;
|
||||
double curve_wind_speed_step = 0.5;
|
||||
double wind_speed_change_threshold = 1.0;
|
||||
double iqr_lower_multiplier = 1.8;
|
||||
double iqr_upper_multiplier = 2.0;
|
||||
double minimum_generator_speed = 1.0;
|
||||
double generator_speed_k = 0.9;
|
||||
};
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
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> 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()));
|
||||
}
|
||||
|
||||
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 = GetDoubleField(opt, "rated_power");
|
||||
value.has_value() && value.value() > 0.0) {
|
||||
options.rated_power = value.value();
|
||||
}
|
||||
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, "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();
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
void AddInvalid(std::unordered_map<std::string, int>& counters, const std::string& reason) {
|
||||
counters[reason] += 1;
|
||||
}
|
||||
|
||||
std::vector<ValidRow> FilterLimitPower(const std::vector<ValidRow>& rows,
|
||||
const CalculationOptions& options,
|
||||
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) {
|
||||
if (remove_indexes.count(i) == 0) {
|
||||
result.push_back(rows[i]);
|
||||
} else {
|
||||
removed_points.push_back(RemovedPoint{rows[i], "limit_power"});
|
||||
}
|
||||
}
|
||||
removed_count = static_cast<int>(remove_indexes.size());
|
||||
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) {
|
||||
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 {
|
||||
++removed_count;
|
||||
removed_points.push_back(RemovedPoint{row, reason});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.insert(result.end(), interval_rows.begin(), interval_rows.end());
|
||||
}
|
||||
}
|
||||
|
||||
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::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;
|
||||
}
|
||||
|
||||
try {
|
||||
fs::create_directories(JobsRoot());
|
||||
const auto job_id = GenerateJobId();
|
||||
fs::create_directories(JobDir(job_id));
|
||||
|
||||
std::ofstream meta(JobDir(job_id) / "metadata.json", std::ios::trunc);
|
||||
meta << body->dump(2);
|
||||
meta.close();
|
||||
|
||||
std::ofstream rows(JobRowsPath(job_id), std::ios::trunc);
|
||||
rows.close();
|
||||
|
||||
json data;
|
||||
data["job_id"] = job_id;
|
||||
SendSuccess(callback, data);
|
||||
} catch (const std::exception&) {
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
std::ofstream out(JobRowsPath(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();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const CalculationOptions options = ParseOptions(*body);
|
||||
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");
|
||||
|
||||
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 (!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();
|
||||
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();
|
||||
|
||||
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 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;
|
||||
|
||||
int removed = 0;
|
||||
fan_rows = FilterLimitPower(fan_rows, options, removed, fan_removed_points);
|
||||
limit_power_count += removed;
|
||||
|
||||
for (auto& row : fan_rows) {
|
||||
row.tip_speed_ratio = row.generator_speed * 3.14 * 162.0 * 78.0 * 30.0 /
|
||||
row.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");
|
||||
speed_power_outlier_count += removed;
|
||||
cleaned_rows_count += static_cast<int>(fan_rows.size());
|
||||
|
||||
json fan_scatter = json::array();
|
||||
for (const auto& row : fan_rows) {
|
||||
json point;
|
||||
point["wind_speed"] = row.wind_speed;
|
||||
point["active_power"] = row.active_power;
|
||||
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["wind_speed"] = removed_point.row.wind_speed;
|
||||
point["active_power"] = removed_point.row.active_power;
|
||||
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;
|
||||
|
||||
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["invalid_reasons"] = CounterJson(invalid_reasons);
|
||||
summary["fan_count"] = fans.size();
|
||||
|
||||
json data;
|
||||
data["summary"] = summary;
|
||||
data["fans"] = fans;
|
||||
data["curves"] = curves;
|
||||
data["bins"] = bins;
|
||||
data["scatter_points"] = scatter_points;
|
||||
data["filtered_points"] = filtered_points;
|
||||
|
||||
try {
|
||||
fs::remove_all(JobDir(job_id.value()));
|
||||
} catch (const std::exception&) {
|
||||
// 任务结果已经生成,临时文件清理失败不影响本次响应。
|
||||
}
|
||||
|
||||
SendSuccess(callback, data);
|
||||
}
|
||||
|
||||
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));
|
||||
SendSuccess(callback);
|
||||
} catch (const std::exception&) {
|
||||
SendError(callback, kErrorServer, "清理任务失败");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef WINDPOWERCONTROLLER_H
|
||||
#define WINDPOWERCONTROLLER_H
|
||||
|
||||
#include <drogon/HttpController.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "utils/ResponseUtil.h"
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class WindPowerController : public drogon::HttpController<WindPowerController, false> {
|
||||
public:
|
||||
METHOD_LIST_BEGIN
|
||||
ADD_METHOD_TO(WindPowerController::StartJob, "/api/wind/jobs/start", Post);
|
||||
ADD_METHOD_TO(WindPowerController::UploadChunk, "/api/wind/jobs/chunk", Post);
|
||||
ADD_METHOD_TO(WindPowerController::FinishJob, "/api/wind/jobs/finish", Post);
|
||||
ADD_METHOD_TO(WindPowerController::DeleteJob, "/api/wind/jobs/{1}", Delete);
|
||||
METHOD_LIST_END
|
||||
|
||||
void StartJob(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
void UploadChunk(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
void FinishJob(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
void DeleteJob(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
const std::string& job_id);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
#include <drogon/drogon.h>
|
||||
#include <trantor/utils/Logger.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "controllers/WindPowerController.h"
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
int main() {
|
||||
// 抑制 Drogon/trantor 内部日志噪声
|
||||
trantor::Logger::setLogLevel(trantor::Logger::kFatal);
|
||||
|
||||
// 加载 Drogon 配置(监听端口 / CORS / 静态资源根目录)
|
||||
LOG_INFO << "Loading server configuration...";
|
||||
app().loadConfigFile("config/server_config.json");
|
||||
app().registerController(std::make_shared<WindPowerController>());
|
||||
|
||||
// SPA 前端路由回退:未匹配路径统一返回 index.html,交由前端路由处理
|
||||
app().setCustom404Page(
|
||||
HttpResponse::newFileResponse("./web/index.html", "", CT_TEXT_HTML),
|
||||
false);
|
||||
|
||||
LOG_INFO << "wind_server starting...";
|
||||
|
||||
// 阻塞运行,直到收到 SIGINT/SIGTERM
|
||||
app().run();
|
||||
|
||||
LOG_INFO << "wind_server stopped";
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
#ifndef HYPEREDGEX_RESPONSEUTIL_H
|
||||
#define HYPEREDGEX_RESPONSEUTIL_H
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
class ResponseUtil {
|
||||
public:
|
||||
// 生成成功响应,包含数据
|
||||
static nlohmann::json GenerateResponse(int status, const std::string& msg, const nlohmann::json& data) {
|
||||
nlohmann::json response;
|
||||
response["status"] = status;
|
||||
response["msg"] = msg;
|
||||
response["data"] = data;
|
||||
return response;
|
||||
}
|
||||
|
||||
// 生成成功响应,无数据
|
||||
static nlohmann::json GenerateResponse(int status, const std::string& msg) {
|
||||
nlohmann::json response;
|
||||
response["status"] = status;
|
||||
response["msg"] = msg;
|
||||
response["data"] = nullptr;
|
||||
return response;
|
||||
}
|
||||
|
||||
// 生成成功响应的便捷方法
|
||||
static nlohmann::json GenerateSuccessResponse(const nlohmann::json& data) {
|
||||
return GenerateResponse(0, "success", data);
|
||||
}
|
||||
|
||||
// 生成成功响应的便捷方法(无数据)
|
||||
static nlohmann::json GenerateSuccessResponse() {
|
||||
return GenerateResponse(0, "success");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成成功响应(原始 JSON 字符串版本)
|
||||
* 用于读配置文件原样返回前端的场景,
|
||||
* 绕过 nlohmann::json 的 parse/dump 避免丢失 key 顺序。
|
||||
*/
|
||||
static std::string GenerateSuccessResponseRaw(const std::string& rawJsonData) {
|
||||
return "{\"status\":0,\"msg\":\"success\",\"data\":" + rawJsonData + "}";
|
||||
}
|
||||
|
||||
// 生成错误响应的便捷方法
|
||||
static nlohmann::json GenerateErrorResponse(int status, const std::string& msg) {
|
||||
return GenerateResponse(status, msg, nullptr);
|
||||
}
|
||||
|
||||
ResponseUtil() = delete; // 禁止实例化
|
||||
};
|
||||
|
||||
// Drogon HTTP 响应便捷辅助
|
||||
#if __has_include(<drogon/HttpResponse.h>)
|
||||
|
||||
#include <drogon/HttpResponse.h>
|
||||
#include <functional>
|
||||
|
||||
using DrogonCallback = std::function<void(const drogon::HttpResponsePtr&)>;
|
||||
|
||||
inline void SendJson(DrogonCallback& cb, const nlohmann::json& body,
|
||||
drogon::HttpStatusCode code = drogon::k200OK) {
|
||||
auto resp = drogon::HttpResponse::newHttpResponse();
|
||||
resp->setStatusCode(code);
|
||||
resp->setBody(body.dump());
|
||||
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
|
||||
cb(resp);
|
||||
}
|
||||
|
||||
inline void SendSuccess(DrogonCallback& cb, const nlohmann::json& data) {
|
||||
SendJson(cb, ResponseUtil::GenerateSuccessResponse(data));
|
||||
}
|
||||
|
||||
inline void SendSuccess(DrogonCallback& cb) {
|
||||
SendJson(cb, ResponseUtil::GenerateSuccessResponse());
|
||||
}
|
||||
|
||||
inline void SendError(DrogonCallback& cb, int code, const std::string& msg,
|
||||
drogon::HttpStatusCode http = drogon::k200OK) {
|
||||
SendJson(cb, ResponseUtil::GenerateErrorResponse(code, msg), http);
|
||||
}
|
||||
|
||||
inline void SendForbidden(DrogonCallback& cb, const std::string& msg = "无权限执行此操作") {
|
||||
SendError(cb, 3, msg, drogon::k403Forbidden);
|
||||
}
|
||||
|
||||
#endif // __has_include drogon
|
||||
|
||||
#endif //HYPEREDGEX_RESPONSEUTIL_H
|
||||
Reference in New Issue
Block a user