功能: 完善风电功率方案计算

- 新增计算方案保存与方案二筛选参数
- 支持方案二桨角规则和有效数据导出
- 优化功率曲线绘制和设计功率展示
This commit is contained in:
cloud
2026-07-17 16:01:58 +08:00
parent 3cb7684039
commit d5d381759e
23 changed files with 918 additions and 50 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ description: 技能建设规范。用于为 wind_power_cal 创建/改写 .agents
## 规则 ## 规则
- 每个 skill 一个目录,内含 `SKILL.md` - 每个 skill 一个目录,内含 `SKILL.md`
- frontmatter`name`kebab-case)、`description`(一句话:**何时触发** + 做什么;用于检索匹配)。 - frontmatter`name`kebab-case)、`description`(一句话:**何时触发** + 做什么;用于检索匹配)。
- 内容贴合 **wind_power_cal 实际**C++ Drogon 后端(:8848)、React+Vite 前端、`package.sh / run.sh / deploy.sh``third_party` 源码编译 Drogon、响应信封 `{status,msg,data}` - 内容贴合 **wind_power_cal 实际**C++ Drogon 后端(:8080)、React+Vite 前端、`package.sh / run.sh / deploy.sh``third_party` 源码编译 Drogon、响应信封 `{status,msg,data}`
- **禁止**原封照搬 edge_collector 等其它项目的 skill——其 OTA / 协议采集 / 边缘主机 / collector / configurator / cloud_server 等内容在本仓库不存在,会误导。 - **禁止**原封照搬 edge_collector 等其它项目的 skill——其 OTA / 协议采集 / 边缘主机 / collector / configurator / cloud_server 等内容在本仓库不存在,会误导。
-`[[other-skill-name]]` 链接相关技能。 -`[[other-skill-name]]` 链接相关技能。
- 通用工程规范(代码风格、git 提交、shell、前端调试)与项目专属规范(`wind-*`)分开维护。 - 通用工程规范(代码风格、git 提交、shell、前端调试)与项目专属规范(`wind-*`)分开维护。
@@ -32,7 +32,7 @@ SendForbidden(callback); // 无权限(code 3, 403
- `main.cpp` 必须 `using namespace drogon;`,否则 `app()` / `HttpResponse` / `CT_TEXT_HTML` 未声明 - `main.cpp` 必须 `using namespace drogon;`,否则 `app()` / `HttpResponse` / `CT_TEXT_HTML` 未声明
## 配置 / SPA ## 配置 / SPA
- `backend/config/server_config.json`listener(默认 :8848)、CORS、`document_root ./web` - `backend/config/server_config.json`listener(默认 :8080)、CORS、`document_root ./web`
- SPA 回退:`app().setCustom404Page(HttpResponse::newFileResponse("./web/index.html","",CT_TEXT_HTML), false)` - SPA 回退:`app().setCustom404Page(HttpResponse::newFileResponse("./web/index.html","",CT_TEXT_HTML), false)`
## 现有 Controller ## 现有 Controller
+3 -3
View File
@@ -14,11 +14,11 @@ description: wind_power_cal 本地构建与运行规范。用于编译后端、
`--publish` 额外生成 `publish/wind_power-<ver>.tar.gz` `--publish` 额外生成 `publish/wind_power-<ver>.tar.gz`
## 运行 ## 运行
- 一键(后端托管 SPA):`./run.sh`,访问 http://localhost:8848/ - 一键(后端托管 SPA):`./run.sh`,访问 http://localhost:8080/
- 仅后端:`bash backend/run.sh`(先 build.sh 再启动,设 `LD_LIBRARY_PATH=libs` - 仅后端:`bash backend/run.sh`(先 build.sh 再启动,设 `LD_LIBRARY_PATH=libs`
- 开发(前后端分离热更): - 开发(前后端分离热更):
- 后端 `bash backend/run.sh`:8848 - 后端 `bash backend/run.sh`:8080
- 前端 `cd frontend/web_app && npm install && npm run dev`:5173,代理 `/api`→:8848 - 前端 `cd frontend/web_app && npm install && npm run dev`:5173,代理 `/api`→:8080
## 后端构建细节 ## 后端构建细节
- `backend/CMakeLists.txt``find_package(Drogon CONFIG REQUIRED)``file(GLOB_RECURSE src/*.cpp)`,链 `Drogon::Drogon`include `src` + `third_party` - `backend/CMakeLists.txt``find_package(Drogon CONFIG REQUIRED)``file(GLOB_RECURSE src/*.cpp)`,链 `Drogon::Drogon`include `src` + `third_party`
+3 -3
View File
@@ -1,6 +1,6 @@
--- ---
name: wind-deploy name: wind-deploy
description: wind_power_cal 远端部署规范。用于把服务部署到公网主机(默认 82.157.83.226)时,按 deploy.sh 流程操作并校验 :8848 description: wind_power_cal 远端部署规范。用于把服务部署到公网主机(默认 82.157.83.226)时,按 deploy.sh 流程操作并校验 :8080
--- ---
# 远端部署(deploy.sh # 远端部署(deploy.sh
@@ -14,7 +14,7 @@ description: wind_power_cal 远端部署规范。用于把服务部署到公网
- `User=ubuntu``ExecStart=~/wind_power/wind_server` - `User=ubuntu``ExecStart=~/wind_power/wind_server`
- `Environment=LD_LIBRARY_PATH=~/wind_power/libs` - `Environment=LD_LIBRARY_PATH=~/wind_power/libs`
- 日志 `logs/server.log``Restart=on-failure` - 日志 `logs/server.log``Restart=on-failure`
4. 重启 `wind_power`,校验 `localhost:8848` 4. 重启 `wind_power`,校验 `localhost:8080`
## 远端运行期依赖(最小化服务器常缺) ## 远端运行期依赖(最小化服务器常缺)
`sudo apt-get install -y libjsoncpp25 libc-ares2`deploy.sh 已内置)。 `sudo apt-get install -y libjsoncpp25 libc-ares2`deploy.sh 已内置)。
@@ -28,7 +28,7 @@ description: wind_power_cal 远端部署规范。用于把服务部署到公网
## 校验 ## 校验
``` ```
curl -s localhost:8848/api/system/health curl -s localhost:8080/api/system/health
# {"status":0,"msg":"success","data":{"status":"ok"}} # {"status":0,"msg":"success","data":{"status":"ok"}}
``` ```
查看服务:`ssh ubuntu@82.157.83.226 'systemctl status wind_power'` 查看服务:`ssh ubuntu@82.157.83.226 'systemctl status wind_power'`
@@ -26,7 +26,7 @@ description: wind_power_cal 前端 React+Vite 规范。用于前端页面/接口
- 后端经 `/api` 前缀;生产由后端托管 SPA`document_root ./web` - 后端经 `/api` 前缀;生产由后端托管 SPA`document_root ./web`
## 构建配置 ## 构建配置
`vite.config.js``base='/'`dev server :5173proxy `/api` → `http://localhost:8848`。 `vite.config.js``base='/'`dev server :5173proxy `/api` → `http://localhost:8080`。
## 开发 ## 开发
``` ```
@@ -32,8 +32,8 @@ wind_power_cal/
## 端口 ## 端口
| 服务 | 端口 | | 服务 | 端口 |
| --- | --- | | --- | --- |
| 后端 wind_server | 8848(监听地址见 server_config.json | | 后端 wind_server | 8080(监听地址见 server_config.json |
| 前端 dev | 5173vite proxy `/api` → :8848 | | 前端 dev | 5173vite proxy `/api` → :8080 |
## 脚本入口 ## 脚本入口
- `./run.sh` 一键打包+前台运行(后端托管 SPA) - `./run.sh` 一键打包+前台运行(后端托管 SPA)
+20 -1
View File
@@ -1,7 +1,26 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(grep -E \"node_modules|/build/|build_package|^runtime/|^publish/|third_party/drogon/install|\\\\.so$|\\\\.o$\")" "Bash(grep -E \"node_modules|/build/|build_package|^runtime/|^publish/|third_party/drogon/install|\\\\.so$|\\\\.o$\")",
"Bash(git commit *)",
"Bash(git push *)",
"Bash(echo \"=== exit: $? ===\")",
"Bash(sshpass -p 'i7568737i~' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 ubuntu@82.157.83.226 'echo \"=== nginx status ===\"; systemctl is-active nginx 2>/dev/null || service nginx status 2>/dev/null | head -3; echo; echo \"=== all listen ports in nginx config ===\"; sudo grep -rhoE \"listen[^;]*;\" /etc/nginx/ 2>/dev/null | sort | uniq -c')",
"Bash(sshpass -p 'i7568737i~' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 ubuntu@82.157.83.226 'echo \"=== nginx 监听的端口(去重)===\"; sudo nginx -T 2>/dev/null | grep -oE \"listen[[:space:]]+[^;]+\" | sed \"s/listen[[:space:]]*//\" | sed \"s/[[:space:]]*default_server//\" | sed \"s/[[:space:]]*ssl//\" | sort -u; echo; echo \"=== proxy_pass 转发目标(去重)===\"; sudo nginx -T 2>/dev/null | grep -oE \"proxy_pass[[:space:]]+[^;]+\" | sed \"s/proxy_pass[[:space:]]*//\" | sort -u')",
"Bash(sshpass -p i7568737i~ ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 ubuntu@82.157.83.226 ' *)",
"Bash(sshpass -p 'i7568737i~' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 ubuntu@82.157.83.226 'echo \"=== 服务器上 8080 / 8082 占用情况 ===\"; sudo ss -tlnp 2>/dev/null | grep -E \":8080|:8082|:8848|:3000\" ; echo; echo \"=== nginx 对外监听 8080 的 server 块 ===\"; sudo nginx -T 2>/dev/null | grep -B2 -A20 \"listen.*8080\"')",
"Bash(sshpass -p 'i7568737i~' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 ubuntu@82.157.83.226 'echo \"=== IPv4 网卡 ===\"; ip -4 addr show | grep -E \"inet \" | grep -v \"127.0.0.1\"; echo; echo \"=== IPv6 全局地址 ===\"; ip -6 addr show | grep -i \"scope global\" || echo \"\\(无全局 IPv6\\)\"; echo; echo \"=== 现有 listen 10.2.0.8:8081 server 块\\(作为方案A的参照\\) ===\"; sudo nginx -T 2>/dev/null | grep -B1 -A18 \"listen[[:space:]]+10.2.0.8:8081\"; echo; echo \"=== 确认 8080 当前 default_server 标记 ===\"; sudo nginx -T 2>/dev/null | grep -nE \"listen.*8080\"')",
"Bash(sshpass -p 'i7568737i~' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 ubuntu@82.157.83.226 'bash -s')",
"Bash(curl -s -m 5 -o /dev/null -w \"HTTP %{http_code} 耗时%{time_total}s\\\\n\" http://82.157.83.226:8080/api/system/health)",
"Bash(curl -s -m 5 http://82.157.83.226:8080/api/system/health)",
"Bash(echo \"health body: $\\(curl -s -m 5 http://82.157.83.226:8080/api/system/health\\)\")",
"Bash(sshpass -p 'i7568737i~' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 ubuntu@82.157.83.226 'sudo nginx -t && sudo systemctl reload nginx && echo \"[nginx] reloaded -> 10.2.0.8:8080 转发 127.0.0.1:8080 \\(中断开始\\)\"')",
"Bash(./deploy.sh)",
"Bash(curl -s -m5 http://82.157.83.226:8080/api/system/health)",
"Bash(curl -s -m5 http://82.157.83.226:8080/api/system/version)",
"Bash(curl -s -m5 -o /dev/null -w \"HTTP %{http_code} 类型%{content_type}\\\\n\" http://82.157.83.226:8080/)",
"Bash(curl -s -m5 -o /dev/null -w \"8848 HTTP %{http_code} \\(000=不通/拒绝, 符合预期\\)\\\\n\" http://82.157.83.226:8848/)",
"Bash(git restore *)"
] ]
} }
} }
+1 -1
View File
@@ -10,7 +10,7 @@ description: 技能建设规范。用于为 wind_power_cal 创建/改写 .agents
## 规则 ## 规则
- 每个 skill 一个目录,内含 `SKILL.md` - 每个 skill 一个目录,内含 `SKILL.md`
- frontmatter`name`kebab-case)、`description`(一句话:**何时触发** + 做什么;用于检索匹配)。 - frontmatter`name`kebab-case)、`description`(一句话:**何时触发** + 做什么;用于检索匹配)。
- 内容贴合 **wind_power_cal 实际**C++ Drogon 后端(:8848)、React+Vite 前端、`package.sh / run.sh / deploy.sh``third_party` 源码编译 Drogon、响应信封 `{status,msg,data}` - 内容贴合 **wind_power_cal 实际**C++ Drogon 后端(:8080)、React+Vite 前端、`package.sh / run.sh / deploy.sh``third_party` 源码编译 Drogon、响应信封 `{status,msg,data}`
- **禁止**原封照搬 edge_collector 等其它项目的 skill——其 OTA / 协议采集 / 边缘主机 / collector / configurator / cloud_server 等内容在本仓库不存在,会误导。 - **禁止**原封照搬 edge_collector 等其它项目的 skill——其 OTA / 协议采集 / 边缘主机 / collector / configurator / cloud_server 等内容在本仓库不存在,会误导。
-`[[other-skill-name]]` 链接相关技能。 -`[[other-skill-name]]` 链接相关技能。
- 通用工程规范(代码风格、git 提交、shell、前端调试)与项目专属规范(`wind-*`)分开维护。 - 通用工程规范(代码风格、git 提交、shell、前端调试)与项目专属规范(`wind-*`)分开维护。
@@ -32,7 +32,7 @@ SendForbidden(callback); // 无权限(code 3, 403
- `main.cpp` 必须 `using namespace drogon;`,否则 `app()` / `HttpResponse` / `CT_TEXT_HTML` 未声明 - `main.cpp` 必须 `using namespace drogon;`,否则 `app()` / `HttpResponse` / `CT_TEXT_HTML` 未声明
## 配置 / SPA ## 配置 / SPA
- `backend/config/server_config.json`listener(默认 :8848)、CORS、`document_root ./web` - `backend/config/server_config.json`listener(默认 :8080)、CORS、`document_root ./web`
- SPA 回退:`app().setCustom404Page(HttpResponse::newFileResponse("./web/index.html","",CT_TEXT_HTML), false)` - SPA 回退:`app().setCustom404Page(HttpResponse::newFileResponse("./web/index.html","",CT_TEXT_HTML), false)`
## 现有 Controller ## 现有 Controller
+3 -3
View File
@@ -14,11 +14,11 @@ description: wind_power_cal 本地构建与运行规范。用于编译后端、
`--publish` 额外生成 `publish/wind_power-<ver>.tar.gz` `--publish` 额外生成 `publish/wind_power-<ver>.tar.gz`
## 运行 ## 运行
- 一键(后端托管 SPA):`./run.sh`,访问 http://localhost:8848/ - 一键(后端托管 SPA):`./run.sh`,访问 http://localhost:8080/
- 仅后端:`bash backend/run.sh`(先 build.sh 再启动,设 `LD_LIBRARY_PATH=libs` - 仅后端:`bash backend/run.sh`(先 build.sh 再启动,设 `LD_LIBRARY_PATH=libs`
- 开发(前后端分离热更): - 开发(前后端分离热更):
- 后端 `bash backend/run.sh`:8848 - 后端 `bash backend/run.sh`:8080
- 前端 `cd frontend/web_app && npm install && npm run dev`:5173,代理 `/api`→:8848 - 前端 `cd frontend/web_app && npm install && npm run dev`:5173,代理 `/api`→:8080
## 后端构建细节 ## 后端构建细节
- `backend/CMakeLists.txt``find_package(Drogon CONFIG REQUIRED)``file(GLOB_RECURSE src/*.cpp)`,链 `Drogon::Drogon`include `src` + `third_party` - `backend/CMakeLists.txt``find_package(Drogon CONFIG REQUIRED)``file(GLOB_RECURSE src/*.cpp)`,链 `Drogon::Drogon`include `src` + `third_party`
+3 -3
View File
@@ -1,6 +1,6 @@
--- ---
name: wind-deploy name: wind-deploy
description: wind_power_cal 远端部署规范。用于把服务部署到公网主机(默认 82.157.83.226)时,按 deploy.sh 流程操作并校验 :8848 description: wind_power_cal 远端部署规范。用于把服务部署到公网主机(默认 82.157.83.226)时,按 deploy.sh 流程操作并校验 :8080
--- ---
# 远端部署(deploy.sh # 远端部署(deploy.sh
@@ -14,7 +14,7 @@ description: wind_power_cal 远端部署规范。用于把服务部署到公网
- `User=ubuntu``ExecStart=~/wind_power/wind_server` - `User=ubuntu``ExecStart=~/wind_power/wind_server`
- `Environment=LD_LIBRARY_PATH=~/wind_power/libs` - `Environment=LD_LIBRARY_PATH=~/wind_power/libs`
- 日志 `logs/server.log``Restart=on-failure` - 日志 `logs/server.log``Restart=on-failure`
4. 重启 `wind_power`,校验 `localhost:8848` 4. 重启 `wind_power`,校验 `localhost:8080`
## 远端运行期依赖(最小化服务器常缺) ## 远端运行期依赖(最小化服务器常缺)
`sudo apt-get install -y libjsoncpp25 libc-ares2`deploy.sh 已内置)。 `sudo apt-get install -y libjsoncpp25 libc-ares2`deploy.sh 已内置)。
@@ -28,7 +28,7 @@ description: wind_power_cal 远端部署规范。用于把服务部署到公网
## 校验 ## 校验
``` ```
curl -s localhost:8848/api/system/health curl -s localhost:8080/api/system/health
# {"status":0,"msg":"success","data":{"status":"ok"}} # {"status":0,"msg":"success","data":{"status":"ok"}}
``` ```
查看服务:`ssh ubuntu@82.157.83.226 'systemctl status wind_power'` 查看服务:`ssh ubuntu@82.157.83.226 'systemctl status wind_power'`
@@ -26,7 +26,7 @@ description: wind_power_cal 前端 React+Vite 规范。用于前端页面/接口
- 后端经 `/api` 前缀;生产由后端托管 SPA`document_root ./web` - 后端经 `/api` 前缀;生产由后端托管 SPA`document_root ./web`
## 构建配置 ## 构建配置
`vite.config.js``base='/'`dev server :5173proxy `/api` → `http://localhost:8848`。 `vite.config.js``base='/'`dev server :5173proxy `/api` → `http://localhost:8080`。
## 开发 ## 开发
``` ```
@@ -32,8 +32,8 @@ wind_power_cal/
## 端口 ## 端口
| 服务 | 端口 | | 服务 | 端口 |
| --- | --- | | --- | --- |
| 后端 wind_server | 8848(监听地址见 server_config.json | | 后端 wind_server | 8080(监听地址见 server_config.json |
| 前端 dev | 5173vite proxy `/api` → :8848 | | 前端 dev | 5173vite proxy `/api` → :8080 |
## 脚本入口 ## 脚本入口
- `./run.sh` 一键打包+前台运行(后端托管 SPA) - `./run.sh` 一键打包+前台运行(后端托管 SPA)
+6 -6
View File
@@ -24,7 +24,7 @@ wind_power_cal/
## 快速开始 ## 快速开始
### 1. 一键启动(默认监听 :8848 ### 1. 一键启动(默认监听 :8080
```bash ```bash
./run.sh ./run.sh
@@ -35,10 +35,10 @@ wind_power_cal/
验证: 验证:
```bash ```bash
curl -s localhost:8848/api/system/health curl -s localhost:8080/api/system/health
# {"data":{"status":"ok"},"msg":"success","status":0} # {"data":{"status":"ok"},"msg":"success","status":0}
curl -s localhost:8848/api/system/version curl -s localhost:8080/api/system/version
# {"data":{"name":"wind_power_cal","version":"0.1.0"},"msg":"success","status":0} # {"data":{"name":"wind_power_cal","version":"0.1.0"},"msg":"success","status":0}
``` ```
@@ -50,17 +50,17 @@ npm install
npm run dev npm run dev
``` ```
打开 http://localhost:5173 ,首页通过 Vite 代理 (`/api``localhost:8848`) 访问后端接口。 打开 http://localhost:5173 ,首页通过 Vite 代理 (`/api``localhost:8080`) 访问后端接口。
### 3. 生产联调 ### 3. 生产联调
统一使用根目录 `./run.sh`。脚本会构建前端并把 `dist/` 同步到 `runtime/wind_power/web/`,由后端托管 SPA;访问 http://localhost:8848/ 直接返回前端页面。 统一使用根目录 `./run.sh`。脚本会构建前端并把 `dist/` 同步到 `runtime/wind_power/web/`,由后端托管 SPA;访问 http://localhost:8080/ 直接返回前端页面。
## 端口 ## 端口
| 服务 | 端口 | | 服务 | 端口 |
| ------ | ----- | | ------ | ----- |
| 后端 | 8848 | | 后端 | 8080 |
| 前端 dev | 5173 | | 前端 dev | 5173 |
## 说明 ## 说明
+4 -4
View File
@@ -1,8 +1,8 @@
{ {
"listeners": [ "listeners": [
{ {
"address": "0.0.0.0", "address": "127.0.0.1",
"port": 8848, "port": 8080,
"https": false "https": false
} }
], ],
@@ -29,8 +29,8 @@
"enabled": true, "enabled": true,
"allow_origins": [ "allow_origins": [
"http://localhost:5173", "http://localhost:5173",
"http://localhost:8848", "http://localhost:8080",
"http://127.0.0.1:8848" "http://127.0.0.1:8080"
], ],
"allow_methods": [ "allow_methods": [
"GET", "GET",
@@ -4,6 +4,7 @@
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
#include <ctime> #include <ctime>
#include <deque>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iomanip> #include <iomanip>
@@ -23,6 +24,13 @@ namespace {
constexpr int kErrorInvalidRequest = 1001; constexpr int kErrorInvalidRequest = 1001;
constexpr int kErrorJobNotFound = 1002; constexpr int kErrorJobNotFound = 1002;
constexpr int kErrorServer = 1003; constexpr int kErrorServer = 1003;
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 { struct RawRow {
std::string time; std::string time;
@@ -40,6 +48,11 @@ struct ValidRow {
double wind_speed = 0.0; double wind_speed = 0.0;
double active_power = 0.0; double active_power = 0.0;
double generator_speed = 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; double tip_speed_ratio = 0.0;
}; };
@@ -49,6 +62,7 @@ struct RemovedPoint {
}; };
struct CalculationOptions { struct CalculationOptions {
std::string scheme_id = kDefaultSchemeId;
double rated_power = 4800.0; double rated_power = 4800.0;
double rated_wind_speed = 18.0; double rated_wind_speed = 18.0;
double power_step = 5.0; double power_step = 5.0;
@@ -59,6 +73,11 @@ struct CalculationOptions {
double iqr_upper_multiplier = 2.0; double iqr_upper_multiplier = 2.0;
double minimum_generator_speed = 1.0; double minimum_generator_speed = 1.0;
double generator_speed_k = 0.9; double generator_speed_k = 0.9;
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 { struct EstimatedParams {
@@ -73,6 +92,15 @@ struct CurveBin {
size_t sample_count = 0; size_t sample_count = 0;
}; };
struct SchemeInfo {
std::string id;
std::string name;
std::string description;
double grid_connected_speed = 1030.0;
double rated_generator_speed = 1755.0;
double rated_power = 2000.0;
};
std::string Trim(const std::string& value) { std::string Trim(const std::string& value) {
const auto begin = value.find_first_not_of(" \t\r\n"); const auto begin = value.find_first_not_of(" \t\r\n");
if (begin == std::string::npos) { if (begin == std::string::npos) {
@@ -103,6 +131,10 @@ fs::path JobRowsPath(const std::string& job_id) {
return JobDir(job_id) / "rows.jsonl"; return JobDir(job_id) / "rows.jsonl";
} }
fs::path SchemeConfigPath() {
return fs::path("data") / "wind_schemes.json";
}
std::string GenerateJobId() { std::string GenerateJobId() {
const auto now = std::chrono::system_clock::now().time_since_epoch().count(); const auto now = std::chrono::system_clock::now().time_since_epoch().count();
std::random_device rd; std::random_device rd;
@@ -148,6 +180,117 @@ std::optional<std::string> GetStringField(const json& body, const std::string& f
return std::nullopt; return std::nullopt;
} }
std::optional<double> GetNumberField(const json& body, const std::string& field) {
if (!body.contains(field) || !body[field].is_number()) {
return std::nullopt;
}
return body[field].get<double>();
}
std::string NormalizeSchemeId(const std::string& scheme_id) {
return scheme_id == kSchemeTwoId ? kSchemeTwoId : kDefaultSchemeId;
}
std::vector<SchemeInfo> DefaultSchemes() {
return {
{kDefaultSchemeId, kSchemeOneName, kSchemeOneDefaultDescription},
{kSchemeTwoId, kSchemeTwoName, kSchemeTwoDefaultDescription, 1030.0, 1755.0, 2000.0},
};
}
std::optional<SchemeInfo> FindScheme(const std::vector<SchemeInfo>& schemes,
const std::string& scheme_id) {
const std::string normalized_id = NormalizeSchemeId(scheme_id);
for (const auto& scheme : schemes) {
if (scheme.id == normalized_id) {
return scheme;
}
}
return std::nullopt;
}
json SchemeToJson(const SchemeInfo& scheme) {
json data;
data["id"] = scheme.id;
data["name"] = scheme.name;
data["description"] = scheme.description;
if (scheme.id == 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;
}
return data;
}
std::vector<SchemeInfo> LoadSchemes() {
auto schemes = DefaultSchemes();
const auto config_path = SchemeConfigPath();
if (!fs::exists(config_path)) {
return schemes;
}
try {
std::ifstream in(config_path);
const auto saved = json::parse(in);
if (!saved.contains("schemes") || !saved["schemes"].is_array()) {
return schemes;
}
for (auto& scheme : schemes) {
for (const auto& item : saved["schemes"]) {
const auto saved_id = GetStringField(item, "id");
if (!saved_id.has_value() || saved_id.value() != scheme.id) {
continue;
}
const auto description = GetStringField(item, "description");
if (description.has_value()) {
scheme.description = description.value();
}
if (scheme.id == kSchemeTwoId &&
item.contains("parameters") &&
item["parameters"].is_object()) {
const auto& params = item["parameters"];
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();
}
}
}
}
} catch (const std::exception&) {
return schemes;
}
return schemes;
}
bool SaveSchemes(const std::vector<SchemeInfo>& schemes) {
try {
const auto config_path = SchemeConfigPath();
fs::create_directories(config_path.parent_path());
json data;
data["default_scheme_id"] = kDefaultSchemeId;
data["schemes"] = json::array();
for (const auto& scheme : schemes) {
data["schemes"].push_back(SchemeToJson(scheme));
}
std::ofstream out(config_path);
out << data.dump(2);
return true;
} catch (const std::exception&) {
return false;
}
}
std::optional<double> GetDoubleField(const json& body, const std::string& field) { std::optional<double> GetDoubleField(const json& body, const std::string& field) {
if (!body.contains(field)) { if (!body.contains(field)) {
return std::nullopt; return std::nullopt;
@@ -261,9 +404,13 @@ CalculationOptions ParseOptions(const json& body) {
} }
const auto& opt = body["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"); if (const auto value = GetDoubleField(opt, "rated_power");
value.has_value() && value.value() > 0.0) { value.has_value() && value.value() > 0.0) {
options.rated_power = value.value(); options.rated_power = value.value();
options.rated_power_provided = true;
} }
if (const auto value = GetDoubleField(opt, "rated_wind_speed"); if (const auto value = GetDoubleField(opt, "rated_wind_speed");
value.has_value() && value.value() > 0.0) { value.has_value() && value.value() > 0.0) {
@@ -301,13 +448,43 @@ CalculationOptions ParseOptions(const json& body) {
value.has_value() && value.value() >= 0.0) { value.has_value() && value.value() >= 0.0) {
options.generator_speed_k = value.value(); options.generator_speed_k = 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; return options;
} }
bool IsSchemeTwo(const CalculationOptions& options) {
return options.scheme_id == kSchemeTwoId;
}
void AddInvalid(std::unordered_map<std::string, int>& counters, const std::string& reason) { void AddInvalid(std::unordered_map<std::string, int>& counters, const std::string& reason) {
counters[reason] += 1; counters[reason] += 1;
} }
void ComputeWindSpeed600sAverage(std::vector<ValidRow>& rows) {
std::deque<ValidRow> window;
double wind_speed_sum = 0.0;
for (auto& row : rows) {
while (!window.empty() &&
std::difftime(row.timestamp, window.front().timestamp) > 600.0) {
wind_speed_sum -= window.front().wind_speed;
window.pop_front();
}
window.push_back(row);
wind_speed_sum += row.wind_speed;
row.wind_speed_600s_average = wind_speed_sum / static_cast<double>(window.size());
}
}
std::vector<ValidRow> FilterLimitPower(const std::vector<ValidRow>& rows, std::vector<ValidRow> FilterLimitPower(const std::vector<ValidRow>& rows,
const CalculationOptions& options, const CalculationOptions& options,
int& removed_count, int& removed_count,
@@ -362,6 +539,68 @@ std::vector<ValidRow> FilterLimitPower(const std::vector<ValidRow>& rows,
return result; return result;
} }
std::vector<ValidRow> FilterSchemeTwoGridSpeed(const std::vector<ValidRow>& rows,
const CalculationOptions& options,
int& removed_count,
std::vector<RemovedPoint>& removed_points) {
removed_count = 0;
if (!IsSchemeTwo(options) || rows.empty()) {
return rows;
}
std::vector<ValidRow> result;
result.reserve(rows.size());
for (const auto& row : rows) {
if (row.generator_speed <= options.grid_connected_speed) {
++removed_count;
removed_points.push_back(RemovedPoint{row, "scheme_two_grid_speed_low"});
} else {
result.push_back(row);
}
}
return result;
}
std::vector<ValidRow> FilterSchemeTwoPitchRules(
const std::vector<ValidRow>& rows,
const CalculationOptions& options,
int& low_power_pitch_wind_count,
int& low_speed_pitch_count,
int& low_power_pitch_count,
std::vector<RemovedPoint>& removed_points) {
low_power_pitch_wind_count = 0;
low_speed_pitch_count = 0;
low_power_pitch_count = 0;
if (!IsSchemeTwo(options) || rows.empty()) {
return rows;
}
std::vector<ValidRow> result;
result.reserve(rows.size());
for (const auto& row : rows) {
if (row.active_power < options.rated_power &&
row.pitch_angle_average > 5.0 &&
row.wind_speed_600s_average < 15.0) {
++low_power_pitch_wind_count;
removed_points.push_back(RemovedPoint{row, "scheme_two_low_power_pitch_wind"});
continue;
}
if (row.generator_speed < options.rated_generator_speed &&
row.pitch_angle_average > 5.0) {
++low_speed_pitch_count;
removed_points.push_back(RemovedPoint{row, "scheme_two_low_speed_pitch"});
continue;
}
if (row.active_power < 1980.0 && row.pitch_angle_average > 2.0) {
++low_power_pitch_count;
removed_points.push_back(RemovedPoint{row, "scheme_two_low_power_pitch"});
continue;
}
result.push_back(row);
}
return result;
}
template <typename ValueGetter> template <typename ValueGetter>
std::vector<ValidRow> FilterByWindBinIqr(const std::vector<ValidRow>& rows, std::vector<ValidRow> FilterByWindBinIqr(const std::vector<ValidRow>& rows,
const CalculationOptions& options, const CalculationOptions& options,
@@ -674,6 +913,100 @@ std::string DatePart(const std::string& time_text) {
} // namespace } // namespace
void WindPowerController::GetSchemes(
const HttpRequestPtr&,
std::function<void(const HttpResponsePtr&)>&& callback) {
json data;
data["default_scheme_id"] = kDefaultSchemeId;
data["schemes"] = json::array();
for (const auto& scheme : LoadSchemes()) {
data["schemes"].push_back(SchemeToJson(scheme));
}
SendSuccess(callback, data);
}
void WindPowerController::SaveSchemeDescription(
const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback,
const std::string& scheme_id) {
const std::string normalized_id = NormalizeSchemeId(scheme_id);
if (scheme_id != normalized_id) {
SendError(callback, kErrorInvalidRequest, "方案不存在");
return;
}
std::string error;
const auto body = ParseBody(req, error);
if (!body.has_value()) {
SendError(callback, kErrorInvalidRequest, error);
return;
}
const auto description = GetStringField(body.value(), "description");
if (!description.has_value()) {
SendError(callback, kErrorInvalidRequest, "方案描述不能为空");
return;
}
if (description.value().size() > 1000) {
SendError(callback, kErrorInvalidRequest, "方案描述不能超过 1000 个字符");
return;
}
std::optional<double> grid_connected_speed;
std::optional<double> rated_generator_speed;
std::optional<double> rated_power;
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");
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) {
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 == 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();
}
}
updated = true;
break;
}
}
if (!updated) {
SendError(callback, kErrorInvalidRequest, "方案不存在");
return;
}
if (!SaveSchemes(schemes)) {
SendError(callback, kErrorServer, "保存方案描述失败");
return;
}
const auto saved_scheme = FindScheme(schemes, normalized_id);
json data;
data["scheme"] = SchemeToJson(saved_scheme.value());
SendSuccess(callback, data);
}
void WindPowerController::StartJob( void WindPowerController::StartJob(
const HttpRequestPtr& req, const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback) { std::function<void(const HttpResponsePtr&)>&& callback) {
@@ -769,6 +1102,14 @@ void WindPowerController::FinishJob(
} }
const CalculationOptions options = ParseOptions(*body); const CalculationOptions options = ParseOptions(*body);
if (IsSchemeTwo(options) &&
(!options.rated_power_provided ||
!options.grid_connected_speed_provided ||
!options.rated_generator_speed_provided)) {
SendError(callback, kErrorInvalidRequest, "方案二需要填写并网转速、额定转速和额定功率");
return;
}
std::vector<ValidRow> parsed_rows; std::vector<ValidRow> parsed_rows;
std::unordered_map<std::string, int> invalid_reasons; std::unordered_map<std::string, int> invalid_reasons;
int raw_rows = 0; int raw_rows = 0;
@@ -795,6 +1136,9 @@ void WindPowerController::FinishJob(
const auto wind_speed = GetDoubleField(row, "wind_speed"); const auto wind_speed = GetDoubleField(row, "wind_speed");
const auto active_power = GetDoubleField(row, "active_power"); const auto active_power = GetDoubleField(row, "active_power");
const auto generator_speed = GetDoubleField(row, "generator_speed"); 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()) || if (!active_power.has_value() || !std::isfinite(active_power.value()) ||
active_power.value() <= 0.0) { active_power.value() <= 0.0) {
@@ -807,6 +1151,13 @@ void WindPowerController::FinishJob(
AddInvalid(invalid_reasons, "invalid_generator_speed"); AddInvalid(invalid_reasons, "invalid_generator_speed");
continue; 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()) { if (!time_text.has_value()) {
AddInvalid(invalid_reasons, "invalid_time"); AddInvalid(invalid_reasons, "invalid_time");
continue; continue;
@@ -834,6 +1185,15 @@ void WindPowerController::FinishJob(
valid_row.wind_speed = wind_speed.value(); valid_row.wind_speed = wind_speed.value();
valid_row.active_power = active_power.value(); valid_row.active_power = active_power.value();
valid_row.generator_speed = generator_speed.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); parsed_rows.push_back(valid_row);
} }
} catch (const std::exception&) { } catch (const std::exception&) {
@@ -889,13 +1249,27 @@ void WindPowerController::FinishJob(
int high_wind_low_power_count = 0; int high_wind_low_power_count = 0;
int curve_residual_outlier_count = 0; int curve_residual_outlier_count = 0;
int rated_plateau_low_power_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; int cleaned_rows_count = 0;
for (const auto& fan_id : fan_ids) { for (const auto& fan_id : fan_ids) {
fans.push_back(fan_id); fans.push_back(fan_id);
auto fan_rows = rows_by_fan[fan_id]; auto fan_rows = rows_by_fan[fan_id];
std::vector<RemovedPoint> fan_removed_points; std::vector<RemovedPoint> fan_removed_points;
if (IsSchemeTwo(options)) {
ComputeWindSpeed600sAverage(fan_rows);
}
int removed = 0; int removed = 0;
fan_rows = FilterSchemeTwoGridSpeed(
fan_rows,
options,
removed,
fan_removed_points);
scheme_two_grid_speed_low_count += removed;
fan_rows = FilterLimitPower(fan_rows, options, removed, fan_removed_points); fan_rows = FilterLimitPower(fan_rows, options, removed, fan_removed_points);
limit_power_count += removed; limit_power_count += removed;
@@ -951,13 +1325,31 @@ void WindPowerController::FinishJob(
fan_removed_points); fan_removed_points);
rated_plateau_low_power_count += removed; rated_plateau_low_power_count += removed;
int low_power_pitch_wind_removed = 0;
int low_speed_pitch_removed = 0;
int low_power_pitch_removed = 0;
fan_rows = FilterSchemeTwoPitchRules(
fan_rows,
options,
low_power_pitch_wind_removed,
low_speed_pitch_removed,
low_power_pitch_removed,
fan_removed_points);
scheme_two_low_power_pitch_wind_count += low_power_pitch_wind_removed;
scheme_two_low_speed_pitch_count += low_speed_pitch_removed;
scheme_two_low_power_pitch_count += low_power_pitch_removed;
cleaned_rows_count += static_cast<int>(fan_rows.size()); cleaned_rows_count += static_cast<int>(fan_rows.size());
json fan_scatter = json::array(); json fan_scatter = json::array();
for (const auto& row : fan_rows) { for (const auto& row : fan_rows) {
json point; json point;
point["fan_id"] = row.fan_id;
point["time"] = row.time;
point["wind_speed"] = row.wind_speed; point["wind_speed"] = row.wind_speed;
point["active_power"] = row.active_power; 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); fan_scatter.push_back(point);
} }
scatter_points[fan_id] = fan_scatter; scatter_points[fan_id] = fan_scatter;
@@ -1012,6 +1404,11 @@ void WindPowerController::FinishJob(
invalid_reasons["high_wind_low_power"] = high_wind_low_power_count; invalid_reasons["high_wind_low_power"] = high_wind_low_power_count;
invalid_reasons["curve_residual_outlier"] = curve_residual_outlier_count; invalid_reasons["curve_residual_outlier"] = curve_residual_outlier_count;
invalid_reasons["rated_plateau_low_power"] = rated_plateau_low_power_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; const int invalid_total = raw_rows - cleaned_rows_count;
@@ -1026,10 +1423,17 @@ void WindPowerController::FinishJob(
summary["high_wind_low_power_rows"] = high_wind_low_power_count; summary["high_wind_low_power_rows"] = high_wind_low_power_count;
summary["curve_residual_outlier_rows"] = curve_residual_outlier_count; summary["curve_residual_outlier_rows"] = curve_residual_outlier_count;
summary["rated_plateau_low_power_rows"] = rated_plateau_low_power_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["invalid_reasons"] = CounterJson(invalid_reasons);
summary["fan_count"] = fans.size(); summary["fan_count"] = fans.size();
json data; json data;
const auto selected_scheme = FindScheme(LoadSchemes(), options.scheme_id);
data["scheme"] = SchemeToJson(selected_scheme.value());
data["summary"] = summary; data["summary"] = summary;
data["fans"] = fans; data["fans"] = fans;
data["curves"] = curves; data["curves"] = curves;
@@ -15,8 +15,17 @@ public:
ADD_METHOD_TO(WindPowerController::UploadChunk, "/api/wind/jobs/chunk", 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::FinishJob, "/api/wind/jobs/finish", Post);
ADD_METHOD_TO(WindPowerController::DeleteJob, "/api/wind/jobs/{1}", Delete); ADD_METHOD_TO(WindPowerController::DeleteJob, "/api/wind/jobs/{1}", Delete);
ADD_METHOD_TO(WindPowerController::GetSchemes, "/api/wind/schemes", Get);
ADD_METHOD_TO(WindPowerController::SaveSchemeDescription,
"/api/wind/schemes/{1}/description",
Post);
METHOD_LIST_END METHOD_LIST_END
void GetSchemes(const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback);
void SaveSchemeDescription(const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback,
const std::string& scheme_id);
void StartJob(const HttpRequestPtr& req, void StartJob(const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback); std::function<void(const HttpResponsePtr&)>&& callback);
void UploadChunk(const HttpRequestPtr& req, void UploadChunk(const HttpRequestPtr& req,
+6 -6
View File
@@ -6,7 +6,7 @@
# 1. 本地执行 package.sh 生成 runtime/wind_power/ # 1. 本地执行 package.sh 生成 runtime/wind_power/
# 2. rsync runtime/wind_power/ → ~/wind_power/ # 2. rsync runtime/wind_power/ → ~/wind_power/
# 3. 安装/更新 systemd 服务 wind_power.service # 3. 安装/更新 systemd 服务 wind_power.service
# 4. 重启 wind_power,并验证 8848 直连接口 # 4. 重启 wind_power,并验证 8080 直连接口
# #
# 不安装、不配置 nginx;不修改 gitea。 # 不安装、不配置 nginx;不修改 gitea。
# #
@@ -114,17 +114,17 @@ sudo systemctl restart "${SERVICE_NAME}"
echo "[remote] systemd ${SERVICE_NAME} 已安装并启动" echo "[remote] systemd ${SERVICE_NAME} 已安装并启动"
REMOTE REMOTE
step "5/5 校验 8848 直连服务..." step "5/5 校验 8080 直连服务..."
sleep 2 sleep 2
ssh_cmd "SERVICE_NAME='${SERVICE_NAME}'" 'bash -s' <<'REMOTE' | sed 's/^/[remote] /' ssh_cmd "SERVICE_NAME='${SERVICE_NAME}'" 'bash -s' <<'REMOTE' | sed 's/^/[remote] /'
set +e set +e
echo "service: $(systemctl is-active ${SERVICE_NAME})" echo "service: $(systemctl is-active ${SERVICE_NAME})"
echo "health: $(curl -s -m 5 localhost:8848/api/system/health)" echo "health: $(curl -s -m 5 localhost:8080/api/system/health)"
echo "version: $(curl -s -m 5 localhost:8848/api/system/version)" echo "version: $(curl -s -m 5 localhost:8080/api/system/version)"
echo "listen: $(ss -tlnp 2>/dev/null | grep ':8848' | head -1 | sed 's/.*users://')" echo "listen: $(ss -tlnp 2>/dev/null | grep ':8080' | head -1 | sed 's/.*users://')"
REMOTE REMOTE
echo "" echo ""
info "✅ 部署完成" info "✅ 部署完成"
echo -e " wind_power: ${GREEN}http://${TARGET#*@}:8848/${NC}" echo -e " wind_power: ${GREEN}http://${TARGET#*@}:8080/${NC}"
echo " 查看服务: ssh ${TARGET} 'systemctl status ${SERVICE_NAME}'" echo " 查看服务: ssh ${TARGET} 'systemctl status ${SERVICE_NAME}'"
+99 -1
View File
@@ -154,6 +154,102 @@
margin-bottom: 18px; margin-bottom: 18px;
} }
.schemePanel {
margin-bottom: 18px;
}
.schemeGrid {
display: grid;
grid-template-columns: minmax(240px, 0.38fr) minmax(0, 1fr);
gap: 14px;
align-items: start;
}
.schemeDescriptionControl {
display: grid;
grid-template-columns: 72px minmax(0, 1fr);
align-items: center;
gap: 10px;
}
.schemeDescriptionControl span {
color: #cbd5e1;
font-size: 14px;
}
.schemeDescriptionControl input {
width: 100%;
min-width: 0;
height: 36px;
border: 1px solid #343449;
border-radius: 7px;
background: #0f0f18;
color: #e5e7eb;
padding: 0 10px;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.schemeDescriptionControl input:focus {
border-color: #6366f1;
outline: none;
}
.schemeDescriptionControl input:disabled {
cursor: not-allowed;
opacity: 0.62;
}
.schemeParamGrid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-top: 14px;
}
.schemeParamGrid input {
min-width: 0;
width: 100%;
height: 36px;
border: 1px solid #343449;
border-radius: 7px;
background: #0f0f18;
color: #e5e7eb;
padding: 0 10px;
font-size: 14px;
font-variant-numeric: tabular-nums;
}
.schemeParamGrid input:focus {
border-color: #6366f1;
outline: none;
}
.schemeParamGrid input:disabled {
cursor: not-allowed;
opacity: 0.62;
}
.schemeParamGrid input::placeholder {
color: #5f6673;
}
.schemeFooter {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
margin-top: 14px;
}
.schemeFooter span {
min-height: 18px;
color: #86efac;
font-size: 13px;
}
.designActions { .designActions {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -571,7 +667,9 @@ td {
} }
.homeHeader, .homeHeader,
.workspace { .workspace,
.schemeGrid,
.schemeParamGrid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
+323 -6
View File
@@ -5,6 +5,8 @@ import * as XLSX from 'xlsx';
import { import {
deleteWindJob, deleteWindJob,
finishWindJob, finishWindJob,
getWindSchemes,
saveWindSchemeDescription,
startWindJob, startWindJob,
uploadWindChunk, uploadWindChunk,
} from '../utils/api'; } from '../utils/api';
@@ -15,6 +17,9 @@ const REQUIRED_FIELDS = [
{ key: 'wind_speed', label: '风速' }, { key: 'wind_speed', label: '风速' },
{ key: 'active_power', label: '有功功率' }, { key: 'active_power', label: '有功功率' },
{ key: 'generator_speed', label: '发电机转速' }, { key: 'generator_speed', label: '发电机转速' },
{ key: 'blade_pitch_1', label: '1#叶片角度' },
{ key: 'blade_pitch_2', label: '2#叶片角度' },
{ key: 'blade_pitch_3', label: '3#叶片角度' },
]; ];
const FIELD_HINTS = { const FIELD_HINTS = {
@@ -23,6 +28,9 @@ const FIELD_HINTS = {
wind_speed: ['风速', '平均风速', 'wind speed', 'windspeed'], wind_speed: ['风速', '平均风速', 'wind speed', 'windspeed'],
active_power: ['平均有功功率', '有功功率', 'active power', 'power'], active_power: ['平均有功功率', '有功功率', 'active power', 'power'],
generator_speed: ['发电机转速', '平均发电机转速', 'generator speed', 'rpm'], generator_speed: ['发电机转速', '平均发电机转速', 'generator speed', 'rpm'],
blade_pitch_1: ['1#叶片变桨角度', '1#叶片角度', '1叶片变桨角度', '1叶片角度', '1号叶片角度', '叶片1角度', '桨角1', '变桨角1', 'pitch 1', 'pitch angle 1', 'blade pitch 1'],
blade_pitch_2: ['2#叶片变桨角度', '2#叶片角度', '2叶片变桨角度', '2叶片角度', '2号叶片角度', '叶片2角度', '桨角2', '变桨角2', 'pitch 2', 'pitch angle 2', 'blade pitch 2'],
blade_pitch_3: ['3#叶片变桨角度', '3#叶片角度', '3叶片变桨角度', '3叶片角度', '3号叶片角度', '叶片3角度', '桨角3', '变桨角3', 'pitch 3', 'pitch angle 3', 'blade pitch 3'],
}; };
const FIELD_EXCLUDES = { const FIELD_EXCLUDES = {
@@ -41,6 +49,27 @@ const DESIGN_FIELD_HINTS = {
const CHUNK_SIZE = 4000; const CHUNK_SIZE = 4000;
const CHART_HEIGHT = 540; const CHART_HEIGHT = 540;
const DEFAULT_SCHEME_ID = 'scheme_one';
const SCHEME_TWO_ID = 'scheme_two';
const DEFAULT_SCHEMES = [
{ id: 'scheme_one', name: '方案一', description: '通用方案' },
{
id: SCHEME_TWO_ID,
name: '方案二',
description: '桨角筛选方案,使用三支叶片角度平均值、600s 平均风速和手填转速/功率参数',
parameters: {
grid_connected_speed: 1030,
rated_generator_speed: 1755,
rated_power: 2000,
},
},
];
const DEFAULT_SCHEME_TWO_PARAMS = {
grid_connected_speed: '1030',
rated_generator_speed: '1755',
rated_power: '2000',
};
function createDesignRows(points) { function createDesignRows(points) {
const source = points?.length ? points : [{ wind_speed: '', design_power: '' }]; const source = points?.length ? points : [{ wind_speed: '', design_power: '' }];
@@ -289,6 +318,9 @@ function buildStandardRows(files, mapping) {
wind_speed: normalizeNumber(getCell(row, localIndexes.wind_speed)), wind_speed: normalizeNumber(getCell(row, localIndexes.wind_speed)),
active_power: normalizeNumber(getCell(row, localIndexes.active_power)), active_power: normalizeNumber(getCell(row, localIndexes.active_power)),
generator_speed: normalizeNumber(getCell(row, localIndexes.generator_speed)), generator_speed: normalizeNumber(getCell(row, localIndexes.generator_speed)),
blade_pitch_1: normalizeNumber(getCell(row, localIndexes.blade_pitch_1)),
blade_pitch_2: normalizeNumber(getCell(row, localIndexes.blade_pitch_2)),
blade_pitch_3: normalizeNumber(getCell(row, localIndexes.blade_pitch_3)),
}); });
} }
} }
@@ -296,6 +328,69 @@ function buildStandardRows(files, mapping) {
return { rows, indexes }; return { rows, indexes };
} }
function formatTimestampForFile(date = new Date()) {
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}`
+ `${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
}
function sanitizeFileName(value) {
return String(value || '未选择风机').replace(/[\\/:*?"<>|]/g, '_');
}
function exportValidRowsToExcel(fanId, rows) {
const exportRows = (rows || []).map((row) => ({
风机编号: row.fan_id || fanId || '',
采样时间: row.time || '',
平均功率: row.active_power ?? '',
平均转速: row.generator_speed ?? '',
平均风速: row.wind_speed ?? '',
'3个叶片变桨角平均值': row.pitch_angle_average ?? '',
}));
const worksheet = XLSX.utils.json_to_sheet(exportRows);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, '有效数据');
XLSX.writeFile(
workbook,
`有效数据_${sanitizeFileName(fanId)}_${formatTimestampForFile()}.xlsx`,
);
}
function interpolateDesignPower(points, windSpeed) {
if (!points.length) return null;
if (windSpeed < points[0].wind_speed || windSpeed > points[points.length - 1].wind_speed) {
return null;
}
for (let index = 0; index < points.length; index += 1) {
const point = points[index];
if (Math.abs(point.wind_speed - windSpeed) < 0.000001) {
return point.design_power;
}
if (point.wind_speed > windSpeed && index > 0) {
const left = points[index - 1];
const right = point;
const span = right.wind_speed - left.wind_speed;
if (span <= 0) return left.design_power;
const ratio = (windSpeed - left.wind_speed) / span;
return left.design_power + (right.design_power - left.design_power) * ratio;
}
}
return points[points.length - 1].design_power;
}
function schemeParamsToState(parameters) {
return {
grid_connected_speed: String(
parameters?.grid_connected_speed ?? DEFAULT_SCHEME_TWO_PARAMS.grid_connected_speed,
),
rated_generator_speed: String(
parameters?.rated_generator_speed ?? DEFAULT_SCHEME_TWO_PARAMS.rated_generator_speed,
),
rated_power: String(parameters?.rated_power ?? DEFAULT_SCHEME_TWO_PARAMS.rated_power),
};
}
function SummaryCards({ summary }) { function SummaryCards({ summary }) {
if (!summary) return null; if (!summary) return null;
const cards = [ const cards = [
@@ -352,15 +447,12 @@ function PowerCurveChart({
const actualByWindSpeed = new Map( const actualByWindSpeed = new Map(
sortedCurve.map((point) => [point.wind_speed.toFixed(6), point.average_power]), sortedCurve.map((point) => [point.wind_speed.toFixed(6), point.average_power]),
); );
const designByWindSpeed = new Map(
sortedDesign.map((point) => [point.wind_speed.toFixed(6), point.design_power]),
);
const curveX = Array.from(new Set([ const curveX = Array.from(new Set([
...sortedCurve.map((point) => point.wind_speed), ...sortedCurve.map((point) => point.wind_speed),
...sortedDesign.map((point) => point.wind_speed), ...sortedDesign.map((point) => point.wind_speed),
])).sort((left, right) => left - right); ])).sort((left, right) => left - right);
const actualY = curveX.map((windSpeed) => actualByWindSpeed.get(windSpeed.toFixed(6)) ?? null); const actualY = curveX.map((windSpeed) => actualByWindSpeed.get(windSpeed.toFixed(6)) ?? null);
const designY = curveX.map((windSpeed) => designByWindSpeed.get(windSpeed.toFixed(6)) ?? null); const designY = curveX.map((windSpeed) => interpolateDesignPower(sortedDesign, windSpeed));
const maxScatterPower = validScatter.reduce( const maxScatterPower = validScatter.reduce(
(max, point) => Math.max(max, point.active_power), (max, point) => Math.max(max, point.active_power),
0, 0,
@@ -409,6 +501,7 @@ function PowerCurveChart({
stroke: '#ef4444', stroke: '#ef4444',
width: 2.5, width: 2.5,
paths: uPlot.paths.spline(), paths: uPlot.paths.spline(),
spanGaps: true,
points: { show: false }, points: { show: false },
}, },
{ {
@@ -416,6 +509,7 @@ function PowerCurveChart({
stroke: '#22c55e', stroke: '#22c55e',
width: 2.2, width: 2.2,
paths: uPlot.paths.spline(), paths: uPlot.paths.spline(),
spanGaps: true,
points: { show: false }, points: { show: false },
}, },
], ],
@@ -430,7 +524,7 @@ function PowerCurveChart({
const y = u.valToPos(point.active_power, 'y', true); const y = u.valToPos(point.active_power, 'y', true);
if (Number.isFinite(x) && Number.isFinite(y)) { if (Number.isFinite(x) && Number.isFinite(y)) {
ctx.beginPath(); ctx.beginPath();
ctx.arc(x, y, 1.6, 0, Math.PI * 2); ctx.arc(x, y, 2, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
} }
} }
@@ -441,7 +535,7 @@ function PowerCurveChart({
const y = u.valToPos(point.active_power, 'y', true); const y = u.valToPos(point.active_power, 'y', true);
if (Number.isFinite(x) && Number.isFinite(y)) { if (Number.isFinite(x) && Number.isFinite(y)) {
ctx.beginPath(); ctx.beginPath();
ctx.arc(x, y, 1.9, 0, Math.PI * 2); ctx.arc(x, y, 2, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
} }
} }
@@ -484,9 +578,17 @@ export default function HomePage() {
const [readingDesign, setReadingDesign] = useState(false); const [readingDesign, setReadingDesign] = useState(false);
const [designRows, setDesignRows] = useState(() => createDesignRows()); const [designRows, setDesignRows] = useState(() => createDesignRows());
const [designMeta, setDesignMeta] = useState(null); const [designMeta, setDesignMeta] = useState(null);
const [schemes, setSchemes] = useState(DEFAULT_SCHEMES);
const [selectedSchemeId, setSelectedSchemeId] = useState(DEFAULT_SCHEME_ID);
const [schemeDescription, setSchemeDescription] = useState(DEFAULT_SCHEMES[0].description);
const [savingScheme, setSavingScheme] = useState(false);
const [schemeMessage, setSchemeMessage] = useState('');
const [schemeTwoParams, setSchemeTwoParams] = useState(DEFAULT_SCHEME_TWO_PARAMS);
const headers = files[0]?.headers || []; const headers = files[0]?.headers || [];
const missingFields = REQUIRED_FIELDS.filter((field) => !mapping[field.key]); const missingFields = REQUIRED_FIELDS.filter((field) => !mapping[field.key]);
const selectedScheme = schemes.find((scheme) => scheme.id === selectedSchemeId) || schemes[0];
const isSchemeTwo = selectedSchemeId === SCHEME_TWO_ID;
const selectedPoints = selectedFan && result?.curves ? result.curves[selectedFan] || [] : []; const selectedPoints = selectedFan && result?.curves ? result.curves[selectedFan] || [] : [];
const selectedBins = selectedFan && result?.bins ? result.bins[selectedFan] || [] : []; const selectedBins = selectedFan && result?.bins ? result.bins[selectedFan] || [] : [];
const selectedScatter = selectedFan && result?.scatter_points const selectedScatter = selectedFan && result?.scatter_points
@@ -526,6 +628,95 @@ export default function HomePage() {
.sort((left, right) => left.wind_speed - right.wind_speed); .sort((left, right) => left.wind_speed - right.wind_speed);
}, [designRows]); }, [designRows]);
useEffect(() => {
let canceled = false;
async function loadSchemes() {
try {
const data = await getWindSchemes();
if (canceled) return;
const loadedSchemes = data.schemes?.length ? data.schemes : DEFAULT_SCHEMES;
const defaultSchemeId = data.default_scheme_id || DEFAULT_SCHEME_ID;
const defaultScheme = loadedSchemes.find((scheme) => scheme.id === defaultSchemeId)
|| loadedSchemes[0];
setSchemes(loadedSchemes);
setSelectedSchemeId(defaultScheme.id);
setSchemeDescription(defaultScheme.description || '');
const schemeTwo = loadedSchemes.find((scheme) => scheme.id === SCHEME_TWO_ID);
setSchemeTwoParams(schemeParamsToState(schemeTwo?.parameters));
} catch (err) {
if (!canceled) {
setSchemeMessage(err.message || '读取方案配置失败,已使用默认方案');
}
}
}
loadSchemes();
return () => {
canceled = true;
};
}, []);
function handleSchemeChange(value) {
const nextScheme = schemes.find((scheme) => scheme.id === value);
setSelectedSchemeId(value);
setSchemeDescription(nextScheme?.description || '');
if (value === SCHEME_TWO_ID) {
setSchemeTwoParams(schemeParamsToState(nextScheme?.parameters));
}
setSchemeMessage('');
}
function handleSchemeTwoParamChange(field, value) {
setSchemeTwoParams((prev) => ({
...prev,
[field]: value,
}));
}
async function handleSaveSchemeDescription() {
if (!selectedScheme) return;
setSavingScheme(true);
setError('');
setSchemeMessage('');
try {
const gridConnectedSpeed = normalizeNumber(schemeTwoParams.grid_connected_speed);
const ratedGeneratorSpeed = normalizeNumber(schemeTwoParams.rated_generator_speed);
const ratedPower = normalizeNumber(schemeTwoParams.rated_power);
if (selectedScheme.id === SCHEME_TWO_ID &&
(!Number.isFinite(gridConnectedSpeed) || gridConnectedSpeed <= 0 ||
!Number.isFinite(ratedGeneratorSpeed) || ratedGeneratorSpeed <= 0 ||
!Number.isFinite(ratedPower) || ratedPower <= 0)) {
throw new Error('方案二参数必须为正数');
}
const data = await saveWindSchemeDescription(selectedScheme.id, {
description: schemeDescription,
...(selectedScheme.id === SCHEME_TWO_ID ? {
parameters: {
grid_connected_speed: gridConnectedSpeed,
rated_generator_speed: ratedGeneratorSpeed,
rated_power: ratedPower,
},
} : {}),
});
const savedScheme = data.scheme;
setSchemes((prev) => prev.map((scheme) => (
scheme.id === savedScheme.id ? { ...scheme, ...savedScheme } : scheme
)));
setSchemeDescription(savedScheme.description || '');
if (savedScheme.id === SCHEME_TWO_ID) {
setSchemeTwoParams(schemeParamsToState(savedScheme.parameters));
}
setSchemeMessage('已保存');
} catch (err) {
setError(err.message || '保存失败');
} finally {
setSavingScheme(false);
}
}
async function handleFilesChange(event) { async function handleFilesChange(event) {
const selectedFiles = Array.from(event.target.files || []); const selectedFiles = Array.from(event.target.files || []);
if (!selectedFiles.length) return; if (!selectedFiles.length) return;
@@ -605,6 +796,16 @@ export default function HomePage() {
setError('请先上传文件并完成所有字段映射'); setError('请先上传文件并完成所有字段映射');
return; return;
} }
const gridConnectedSpeed = normalizeNumber(schemeTwoParams.grid_connected_speed);
const ratedGeneratorSpeed = normalizeNumber(schemeTwoParams.rated_generator_speed);
const ratedPower = normalizeNumber(schemeTwoParams.rated_power);
if (isSchemeTwo &&
(!Number.isFinite(gridConnectedSpeed) || gridConnectedSpeed <= 0 ||
!Number.isFinite(ratedGeneratorSpeed) || ratedGeneratorSpeed <= 0 ||
!Number.isFinite(ratedPower) || ratedPower <= 0)) {
setError('方案二需要填写并网转速、额定转速和额定功率,且必须为正数');
return;
}
let jobId = ''; let jobId = '';
setSubmitting(true); setSubmitting(true);
@@ -647,6 +848,12 @@ export default function HomePage() {
iqr_upper_multiplier: 2, iqr_upper_multiplier: 2,
minimum_generator_speed: 1, minimum_generator_speed: 1,
generator_speed_k: 0.9, generator_speed_k: 0.9,
scheme_id: selectedSchemeId,
...(isSchemeTwo ? {
grid_connected_speed: gridConnectedSpeed,
rated_generator_speed: ratedGeneratorSpeed,
rated_power: ratedPower,
} : {}),
}, },
}); });
setResult(calculation); setResult(calculation);
@@ -691,6 +898,102 @@ export default function HomePage() {
{error && <div className="alert error">{error}</div>} {error && <div className="alert error">{error}</div>}
{progress && <div className="alert info">{progress}</div>} {progress && <div className="alert info">{progress}</div>}
<section className="panel schemePanel">
<div className="panelHeader">
<div>
<h2>计算方案</h2>
<p className="panelHint">选择本次计算使用的清洗方案方案描述可保存为服务端共享配置</p>
</div>
<span>{selectedScheme?.name || '方案一'}</span>
</div>
<div className="schemeGrid">
<label className="fieldControl">
<span>方案选择</span>
<select
value={selectedSchemeId}
onChange={(event) => handleSchemeChange(event.target.value)}
disabled={submitting}
>
{schemes.map((scheme) => (
<option key={scheme.id} value={scheme.id}>{scheme.name}</option>
))}
</select>
</label>
<label className="schemeDescriptionControl">
<span>方案描述</span>
<input
type="text"
value={schemeDescription}
onChange={(event) => {
setSchemeDescription(event.target.value);
setSchemeMessage('');
}}
disabled={submitting || savingScheme}
/>
</label>
</div>
{isSchemeTwo && (
<div className="schemeParamGrid">
<label className="fieldControl">
<span>并网转速 (rpm)</span>
<input
type="number"
min="0"
step="0.01"
value={schemeTwoParams.grid_connected_speed}
onChange={(event) => handleSchemeTwoParamChange(
'grid_connected_speed',
event.target.value,
)}
disabled={submitting}
placeholder="请输入"
/>
</label>
<label className="fieldControl">
<span>额定转速 (rpm)</span>
<input
type="number"
min="0"
step="0.01"
value={schemeTwoParams.rated_generator_speed}
onChange={(event) => handleSchemeTwoParamChange(
'rated_generator_speed',
event.target.value,
)}
disabled={submitting}
placeholder="请输入"
/>
</label>
<label className="fieldControl">
<span>额定功率 (kW)</span>
<input
type="number"
min="0"
step="0.1"
value={schemeTwoParams.rated_power}
onChange={(event) => handleSchemeTwoParamChange(
'rated_power',
event.target.value,
)}
disabled={submitting}
placeholder="请输入"
/>
</label>
</div>
)}
<div className="schemeFooter">
<span>{schemeMessage}</span>
<button
className="secondaryButton"
type="button"
onClick={handleSaveSchemeDescription}
disabled={submitting || savingScheme}
>
{savingScheme ? '保存中' : '保存'}
</button>
</div>
</section>
<section className="panel designPowerPanel"> <section className="panel designPowerPanel">
<div className="panelHeader"> <div className="panelHeader">
<div> <div>
@@ -860,6 +1163,12 @@ export default function HomePage() {
<span>计算完成</span> <span>计算完成</span>
</div> </div>
<SummaryCards summary={result.summary} /> <SummaryCards summary={result.summary} />
{result.scheme && (
<div className="paramStrip">
<span>方案{result.scheme.name}</span>
<span>{result.scheme.description}</span>
</div>
)}
<div className="reasonList"> <div className="reasonList">
{Object.entries(result.summary?.invalid_reasons || {}).map(([reason, count]) => ( {Object.entries(result.summary?.invalid_reasons || {}).map(([reason, count]) => (
<span key={reason}>{reason}: {Number(count).toLocaleString()}</span> <span key={reason}>{reason}: {Number(count).toLocaleString()}</span>
@@ -883,6 +1192,14 @@ export default function HomePage() {
> >
{showFiltered ? '隐藏滤除点' : '显示滤除点'} {showFiltered ? '隐藏滤除点' : '显示滤除点'}
</button> </button>
<button
className="secondaryButton"
type="button"
onClick={() => exportValidRowsToExcel(selectedFan, selectedScatter)}
disabled={!selectedScatter.length}
>
导出有效数据
</button>
<select <select
className="fanSelect" className="fanSelect"
value={selectedFan} value={selectedFan}
+21
View File
@@ -54,6 +54,27 @@ export function getHealth() {
return request('/system/health'); return request('/system/health');
} }
/**
* 获取计算方案列表
* @returns {Promise<{default_scheme_id: string, schemes: Array<object>}>}
*/
export function getWindSchemes() {
return request('/wind/schemes');
}
/**
* 保存计算方案描述
* @param {string} schemeId
* @param {{description: string}} payload
* @returns {Promise<{scheme: object}>}
*/
export function saveWindSchemeDescription(schemeId, payload) {
return request(`/wind/schemes/${encodeURIComponent(schemeId)}/description`, {
method: 'POST',
body: JSON.stringify(payload),
});
}
/** /**
* 创建风功率计算任务 * 创建风功率计算任务
* @param {{files: Array<{file_name: string, row_count: number}>, mapping: object}} payload * @param {{files: Array<{file_name: string, row_count: number}>, mapping: object}} payload
+1 -1
View File
@@ -8,7 +8,7 @@ export default defineConfig({
port: 5173, port: 5173,
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:8848', target: 'http://localhost:8080',
changeOrigin: true, changeOrigin: true,
}, },
}, },
+3 -3
View File
@@ -6,8 +6,8 @@
# (后端同时提供 /api 与 SPA 静态资源,即“前后端一起跑”) # (后端同时提供 /api 与 SPA 静态资源,即“前后端一起跑”)
# #
# 开发模式前端热更新: # 开发模式前端热更新:
# ./run.sh # 后端托管生产前端,监听 :8848 # ./run.sh # 后端托管生产前端,监听 :8080
# cd frontend/web_app && npm run dev # 前端 dev :5173/api 代理到 :8848 # cd frontend/web_app && npm run dev # 前端 dev :5173/api 代理到 :8080
# #
# 参考: edge_collector/run_cloud.sh # 参考: edge_collector/run_cloud.sh
# ===================================================== # =====================================================
@@ -63,7 +63,7 @@ echo "========================================"
echo " wind_power 已启动" echo " wind_power 已启动"
echo " PID: ${SERVER_PID}" echo " PID: ${SERVER_PID}"
echo " 工作目录: ${APP_DIR}" echo " 工作目录: ${APP_DIR}"
echo " 访问: http://localhost:8848 (Ctrl+C 停止)" echo " 访问: http://localhost:8080 (Ctrl+C 停止)"
echo "========================================" echo "========================================"
# 4. 等待退出信号 # 4. 等待退出信号