Explorar el Código

feat:增加测试服tou模版API,添加模版用法skill

Jerry Liang hace 2 meses
padre
commit
3659034834

+ 204 - 0
service/cmd/mcp/.kiro/skills/tou-template-scheduling/SKILL.md

@@ -0,0 +1,204 @@
+---
+name: tou-template-scheduling
+description: 编写、校验并保存 FranklinWH 的 TOU(分时电价)调度模板,用于 EMS 调度配置。当用户需要构建、编辑、理解或上传 TOU 电价模板时使用——包括时段策略、光伏/负载优先级、充放电功率限制、省/市选择等。涵盖 updateOrSaveIotTouTemplate 请求体以及 MCP 工具 service_tou_template_*。
+---
+
+# FranklinWH TOU 调度模板
+
+用于构建并保存 EMS 调度(TOU)模板。一个模板 = 基础信息 + 一个或多个「季节/日类型」策略;每个策略 = 覆盖一整天的时段列表。它对应云端 `updateOrSaveIotTouTemplate` 接口和设备侧 355/357 调度协议。
+
+参考资料(完整协议与字段语义):
+`/home/jerry-remote/web/FranklinWH-Wiki/client/docs/Software/ScriptingForSchedulingConfiguration.md`
+
+字段权威定义(Go DTO):`service/dto/fwhTestTouTemplateDTO.go`
+
+---
+
+## MCP 工具
+
+所有工具都接受可选参数 `env`:`pro`(生产)或 `test`(测试)。
+
+| 工具                          | 用途                                         | env 默认值           |
+| ----------------------------- | -------------------------------------------- | -------------------- |
+| `service_tou_template_list`   | 查询模板列表(按类型/状态/关键字筛选)       | `pro`                |
+| `service_tou_template_detail` | 按`id` 查询单个模板详情                      | `pro`                |
+| `service_tou_template_save`   | 新增/更新模板,返回 templateId**【写操作】** | `test`(当前仅支持) |
+| `service_tou_template_cites`  | 查询某国家下的省/市列表                      | `test`(当前仅支持) |
+
+save 与 cites 目前仅支持 `test`。读操作默认走 `pro`,需要读测试环境时传 `env:"test"`。
+
+典型流程:`cites`(拿 countryId/provinceId)→ 构建 payload → `save` → `detail` 校验。
+
+---
+
+## 请求体结构(`service_tou_template_save` 的 `payload`)
+
+顶层结构(`FwhTestUpdateOrSaveTouTemplatePayload`):
+
+```json
+{
+  "template": {
+    /* 模板主体,见下表 */
+  },
+  "strategyList": [
+    {
+      /* 策略,见下表 */
+    }
+  ],
+  "lang": "ZH_CN"
+}
+```
+
+- 所有字段都是 JSON **字符串**。数值型字段以「字符串形式的数字」传递;**未设置的字段传空串 `""`**(不要传 `null`)。
+- `template.templateId`:**创建模板时不需要此字段(省略);更新模板时必须带此字段并填入模板 ID**。读取(`detail`/`list` 返回)时该字段必定存在。
+- `template.sourceType`:**必填**。当前**仅允许写入系统模板 `"0"`**(`1`API模板 / `2`用户推荐 暂不支持写入,`save` 会拒绝)。
+
+### 1. `template`(模板主体 · `FwhTestTouTemplateInfo`)
+
+| 字段                 | 类型              | 含义 / 取值                                                                                             |
+| -------------------- | ----------------- | ------------------------------------------------------------------------------------------------------- |
+| `countryId`          | string(数值)      | 国家 ID                                                                                                 |
+| `provinceId`         | string(数值)      | 州/省 ID                                                                                                |
+| `derSchdule`         | string            | DER 调度配置                                                                                            |
+| `eleCompanyFullName` | string            | 电力公司全称                                                                                            |
+| `electricCompany`    | string            | 电力公司(简称)                                                                                        |
+| `gridType`           | string            | 电网类型 / 套餐类型                                                                                     |
+| `electricityType`    | string(数值)      | 电价类型:`1`分时电价 `2`阶梯电价 `3`固定电价 `4`分时&阶梯电价 `5`BB套餐 `6`Peak Demand                 |
+| `name`               | string            | 模板/策略表名称                                                                                         |
+| `mookRunCount`       | string(数值)      | 模式执行次数(loop):`0`不限次数 `1`执行一次                                                             |
+| `priority`           | string(数值)      | 模式优先级,默认`0`;`0` 最低,数字越大优先级越高                                                       |
+| `env`                | string(数值·位域) | 模式使用条件,默认`0`;bit0~1 光伏;bit2~3 馈网;bit4~5 电网充电                                        |
+| `templateId`         | string(数值)      | 模板 ID。**创建时不需要(省略此字段);更新时必须带上并填 ID**。读取(detail/list)返回时该字段必定存在 |
+| `sourceType`         | string(数值·必填) | 模板类型。**必填,当前仅允许 `0`系统模板**(`1`API模板 `2`用户推荐 暂不支持写入)                       |
+
+### 2. `strategyList[]`(策略 · `FwhTestTouTemplateStrategy`)
+
+一个策略 = 一组「季节/日类型」+ 覆盖一整天的时段列表。
+
+| 字段           | 类型         | 含义 / 取值                                                      |
+| -------------- | ------------ | ---------------------------------------------------------------- |
+| `ccociateType` | string(数值) | 关联类型编码,如`1`                                              |
+| `ccociateSn`   | string       | 关联设备 SN(无则空串)                                          |
+| `detailVoList` | array        | 时段明细表,**所有时段必须覆盖一整天**(见下表)                 |
+| `seasonName`   | string       | 季节名,用于标识策略范围,可自定义,如`summer`、`All months`     |
+| `month`        | string       | 逗号分隔的月份,如`1,2,3,4,5,6,7,8,9,10,11,12`                   |
+| `dayName`      | string       | 日期类型可读名称,如`Every day`、`Weekdays`(与 `dayType` 对应) |
+| `dayType`      | string(数值) | 日期类型编码:`1`工作日 `2`节假日 `3`每天(可读名见 `dayName`)  |
+
+### 3. `detailVoList[]`(时段明细 · `FwhTestTouTemplateDetailVo`)
+
+同一策略内的所有时段必须铺满一整天:每个 `endHourTime` 等于下一段的 `startHourTime`,第一段是 `00:00`,最后一段是 `24:00`。值均为字符串,`""` 表示未设置(勿传 `null`)。
+
+| 字段               | 类型              | 含义 / 取值                                                                                                                                                       |
+| ------------------ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `startHourTime`    | string            | 开始时间,格式`HH:mm`,如 `06:30`                                                                                                                                 |
+| `endHourTime`      | string            | 结束时间,格式`HH:mm`;需与下一段 `startHourTime` 衔接,所有时段覆盖一整天                                                                                        |
+| `waveType`         | string(数值·flag) | 峰平谷定义:`0`谷/off-peak `1`平/mid-peak `2`峰/on-peak `3`尖峰/super-on-peak `4`低谷/super-off-peak                                                              |
+| `name`             | string            | 时段名,一般为`supper-off-peak`、`off-peak`、`mid-peak`、`on-peak`、`supper-on-peak` 等                                                                           |
+| `briefDescribe`    | string            | 时段描述,如`Self-consumption`                                                                                                                                    |
+| `solarPriority`    | string            | **光伏供电优先级**,格式 `"市电,负载,FHP"`,如 `"1,3,2"`;优先级 `0/1/2/3`,可相同(表示按比例/功率分配),`0`=不涉及,数字越大优先级越高                           |
+| `loadPriority`     | string            | **负载取电优先级**,格式 `"市电,光伏,FHP"`,如 `"1,3,2"`;优先级 `0/1/2/3`,可相同(按比例/功率分配),`0`=不使用此源,数字越大优先级越高                           |
+| `gridDischargeMax` | string(kW,0.1)    | 电池最大允许馈网功率;可选,默认`0` 不能馈网;`0`不可馈网,`-1`不限制                                                                                             |
+| `gridChargeMax`    | string(kW,0.1)    | 电网允许最大充电功率;可选,默认`0` 不允许;`0`不允许,`-1`不限制                                                                                                 |
+| `chargePower`      | string(kW,0.1)    | 固定电池充电功率,按单台功率计算;可选,默认`-1` 未设置                                                                                                           |
+| `dischargePower`   | string(kW,0.1)    | 固定电池放电功率,按单台功率计算;可选,默认`-1` 未设置                                                                                                           |
+| `chargeMax`        | string(kW,0.1)    | 电池最大允许充电功率,按单台功率计算;`-1`无限制,默认 `-1`                                                                                                       |
+| `dischargeMax`     | string(kW,0.1)    | 电池最大允许放电功率,按单台功率计算;`-1`无限制,默认 `-1`                                                                                                       |
+| `gcaoMax`          | string(kW,0.1)    | GCAO(grid charge after an outage):停电后补电至预留 SOC 的单台市电取电功率;可选;`0`不允许,`-1`不限制;默认未设置(`-3`);使能后未设值则为 `5kw*N`(用 `-2` 标识) |
+| `gridFeedMax`      | string(kW,0.1)    | 电网允许最大馈网功率;可选,默认`-2`;`0`不允许,`-1`不限制,`-2`未设置                                                                                           |
+| `gridMax`          | string(kW,0.1)    | 电网允许最大取电功率;可选,默认`-1` 不限制;`0`不允许,`-1`不限制                                                                                                |
+| `minDischargeSoc`  | string(%,整数)    | 放电截止 SOC;达到此 SOC 后电池不放电,可选,默认`0%`;若 `reserved_soc` 大于此值则以 `reserved_soc` 为准                                                         |
+| `maxChargeSoc`     | string(%,整数)    | 充电截止 SOC;可选,默认`100%`                                                                                                                                    |
+| `heatEnable`       | string(数值)      | 充电加热使能:`0`未设置(默认) `1`不使能 `2`使能                                                                                                                   |
+| `powerOffApower`   | string(数值)      | 主动关闭 APower;可选,`0`不控制(默认) `1`关机 `2`开机(预留)                                                                                                      |
+| `rampTime`         | string(秒)        | 爬坡时间,`0~300`;默认 `0`,BB套餐默认 `300`                                                                                                                     |
+| `offGrid`          | string(数值)      | 主动断开市电继电器:`0`不控制(默认) `1`断开 `2`闭合(预留)                                                                                                         |
+| `solarCutoff`      | string(数值)      | 主动断开光伏继电器:`0`不控制(默认) `1`断开 `2`闭合(预留)                                                                                                         |
+
+**要点**:
+
+- `solarPriority` / `loadPriority` 是 3 值字符串,顺序 `[市电, 负载或光伏, FHP/aPower]`;`0`=不参与,数字越大优先级越高,相同非 0 值=按比例分配。
+- 功率字段单位 **kW、精度 0.1**,按单台 aPower 计(总功率 = 值 × 台数 N):`gridChargeMax`、`gridDischargeMax`、`gridFeedMax`、`gridMax`、`chargePower`、`dischargePower`、`chargeMax`、`dischargeMax`、`gcaoMax`。常见哨兵值:`-1`不限制、`0`不允许/不可、`-2`未设置(仅 `gridFeedMax`/`gcaoMax`)、`-3`未设置(仅 `gcaoMax`)。
+
+### 完整 detailVoList 单段示例
+
+```json
+{
+  "startHourTime": "00:00",
+  "endHourTime": "11:00",
+  "waveType": "1",
+  "name": "mid-peak",
+  "briefDescribe": "Self-consumption",
+  "solarPriority": "1,3,2",
+  "loadPriority": "1,3,2",
+  "gridChargeMax": "",
+  "gridDischargeMax": "",
+  "gridFeedMax": "",
+  "gridMax": "",
+  "chargePower": "",
+  "dischargePower": "",
+  "chargeMax": "",
+  "dischargeMax": "",
+  "gcaoMax": "",
+  "minDischargeSoc": "",
+  "maxChargeSoc": "",
+  "heatEnable": "",
+  "rampTime": "",
+  "offGrid": "",
+  "solarCutoff": "0",
+  "powerOffApower": ""
+}
+```
+
+### selectCitesList 返回结构(`service_tou_template_cites`)
+
+`FwhTestCitesListData`:`countryList` / `provinceList` / `cityList` 三个数组。各项字段(注意此接口返回中 `id` 为**数值**,非字符串):
+
+| 结构                  | 字段                                                                 |
+| --------------------- | -------------------------------------------------------------------- |
+| `FwhTestCountryInfo`  | `id`(int), `nameEn`, `nameZh`, `latitude`, `longitude`               |
+| `FwhTestProvinceInfo` | `id`(int), `nameEn`, `nameZh`, `latitude`, `longitude`, `cityList[]` |
+| `FwhTestCityInfo`     | `id`(int), `nameEn`, `nameZh`, `latitude`, `longitude`               |
+
+---
+
+## 优先级 → 调度策略 速查
+
+常见 `solarPriority`/`loadPriority` 组合及其含义(来自 `templateTable.vue`):
+
+| 光伏优先级 | 负载优先级 | 策略                                                          |
+| ---------- | ---------- | ------------------------------------------------------------- |
+| `1,2,0`    | `1,3,2`    | A                                                             |
+| `1,2,0`    | `1,2,0`    | B —— 电池不参与调度(光伏优先供载,剩余馈网,市电补充)       |
+| `1,3,2`    | `1,2,0`    | C —— 光伏剩余充电(光伏优先供载,剩余给电池充电,电池不放电) |
+| `1,3,2`    | `1,3,2`    | D —— 自发自用(标准,最常用)                                 |
+| `1,2,3`    | `1,2,0`    | E/G —— 光伏优先充电                                           |
+| `1,2,0`    | `1,2,3`    | F/H —— 电池优先供载                                           |
+
+标准模板(来自 wiki《常规调度配置》):
+
+- **自发自用**:`solarPriority:"1,3,2"`,`loadPriority:"1,3,2"`,describe `"Self-consumption"`。
+- **仅备电**:`gridChargeMax:"-1"`(不限充),`chargePower:"8"`(约 8kW 满充),describe `"Emergency backup"`。
+- **强制放电/馈网收益**:`gridDischargeMax:"-1"`(不限馈网),`dischargePower:"10"`(约 10kW 满放)。
+
+---
+
+## 规则
+
+1. **时间覆盖**:每个策略的 `detailVoList` 必须覆盖 00:00→24:00,无空档、无重叠;`endHourTime` == 下一段 `startHourTime`;时间格式 `HH:mm`。
+2. **未设置传 ""**:明细字段绝不传 `null`,用空串;数字也以字符串形式传。
+3. **功率单位是 kW**(步进 0.1),按单台 aPower 计。不要对这些模板字段套用设备侧的 W 或 `5kw*N/-2` 约定。
+4. **优先级是 3 值字符串** `"a,b,c"`,顺序为 `[市电, 负载/光伏, aPower]`。
+5. **新增 vs 更新**:`template.templateId` 留空为新增;填入 ID 为更新。
+6. **写操作走 `test`**:`service_tou_template_save` 仅支持 `env:"test"`。保存前先与用户确认(会修改服务器数据)。保存后用 `service_tou_template_detail`(带 `env:"test"`)校验。
+7. 执行写操作前,需要和用户确认最终写入或更新的信息,确认后才能调用 `service_tou_template_save` 创建或更新
+8. 不确定某国家的 `provinceId` 时,先调用 `service_tou_template_cites` 传 `country_id`。
+
+---
+
+## 示例流程:构建一个标准自发自用模板
+
+1. `service_tou_template_cites { "country_id": 3 }` → 选出 `provinceId`。
+2. 构建一个策略(dayType 3 = 每天),用策略 D(`solarPriority:"1,3,2"`,`loadPriority:"1,3,2"`)铺一整天单一时段,或拆成 off/mid/on-peak 多个时段。
+3. `service_tou_template_save { "env":"test", "payload": "<json>" }` → 记下返回的 `templateId`。
+4. `service_tou_template_detail { "env":"test", "id": <templateId> }` 确认结果。

+ 99 - 2
service/cmd/mcp/tools/service/tou_template.go

@@ -2,11 +2,13 @@ package service
 
 
 import (
 import (
 	"context"
 	"context"
+	"encoding/json"
 
 
 	"github.com/mark3labs/mcp-go/mcp"
 	"github.com/mark3labs/mcp-go/mcp"
 	"github.com/mark3labs/mcp-go/server"
 	"github.com/mark3labs/mcp-go/server"
 
 
 	"fwh-toolbox-server/cmd/mcp/tools/helper"
 	"fwh-toolbox-server/cmd/mcp/tools/helper"
+	"fwh-toolbox-server/dto"
 	"fwh-toolbox-server/services"
 	"fwh-toolbox-server/services"
 )
 )
 
 
@@ -14,6 +16,7 @@ func registerTouTemplateTools(s *server.MCPServer) {
 	s.AddTool(
 	s.AddTool(
 		mcp.NewTool("service_tou_template_list",
 		mcp.NewTool("service_tou_template_list",
 			mcp.WithDescription("查询 TOU 电价模版列表,支持按类型、状态筛选和关键字搜索"),
 			mcp.WithDescription("查询 TOU 电价模版列表,支持按类型、状态筛选和关键字搜索"),
+			mcp.WithString("env", mcp.Description("目标服务器环境:pro-生产(默认) test-测试")),
 			mcp.WithNumber("page", mcp.Description("页码,从1开始,默认1")),
 			mcp.WithNumber("page", mcp.Description("页码,从1开始,默认1")),
 			mcp.WithNumber("size", mcp.Description("每页数量,默认10")),
 			mcp.WithNumber("size", mcp.Description("每页数量,默认10")),
 			mcp.WithNumber("type", mcp.Description("模版类型:0-系统模板 1-API模板 2-用户推荐")),
 			mcp.WithNumber("type", mcp.Description("模版类型:0-系统模板 1-API模板 2-用户推荐")),
@@ -26,10 +29,41 @@ func registerTouTemplateTools(s *server.MCPServer) {
 	s.AddTool(
 	s.AddTool(
 		mcp.NewTool("service_tou_template_detail",
 		mcp.NewTool("service_tou_template_detail",
 			mcp.WithDescription("查询指定 TOU 电价模版的详细信息"),
 			mcp.WithDescription("查询指定 TOU 电价模版的详细信息"),
+			mcp.WithString("env", mcp.Description("目标服务器环境:pro-生产(默认) test-测试")),
 			mcp.WithNumber("id", mcp.Required(), mcp.Description("模版 ID")),
 			mcp.WithNumber("id", mcp.Required(), mcp.Description("模版 ID")),
 		),
 		),
 		handleTouTemplateDetail,
 		handleTouTemplateDetail,
 	)
 	)
+
+	s.AddTool(
+		mcp.NewTool("service_tou_template_save",
+			mcp.WithDescription("新增或更新 TOU 电价模版(updateOrSaveIotTouTemplate),返回模版 ID [写操作]。"+
+				"template.templateId 有值=更新,空=新增。当前仅支持测试环境(test)。"),
+			mcp.WithString("env", mcp.Description("目标服务器环境:test-测试(默认,当前仅支持)")),
+			mcp.WithString("payload", mcp.Required(),
+				mcp.Description("模版请求体 JSON,结构:{\"template\":{...},\"strategyList\":[{...}],\"lang\":\"ZH_CN\"}。"+
+					"template 含 countryId、provinceId、electricityType、name、mookRunCount、priority、env、templateId(更新时填)、sourceType 等;"+
+					"strategyList[].detailVoList[] 为时段明细,含 startHourTime/endHourTime(HH:mm)、waveType、solarPriority/loadPriority(\"市电,负载,FHP\")、各功率(kW)、SOC(%) 等,未设置字段传空串。")),
+		),
+		handleTouTemplateSave,
+	)
+
+	s.AddTool(
+		mcp.NewTool("service_tou_template_cites",
+			mcp.WithDescription("查询国家/省/市列表(selectCitesList)。不传 country_id 和 type 时查询国家信息;"+
+				"查询某国家下的省/市(用于填写模版的 countryId/provinceId)时需传 country_id,且 TOU 模版场景 type 必须为 1。当前仅支持测试环境(test)。"),
+			mcp.WithString("env", mcp.Description("目标服务器环境:test-测试(默认,当前仅支持)")),
+			mcp.WithNumber("country_id", mcp.Description("国家 ID。不传则查询国家信息")),
+			mcp.WithNumber("type", mcp.Description("查询类型,与页面相关;TOU 模版查询省/市时必须为 1。不传则不带该参数(配合不传 country_id 查国家)")),
+		),
+		handleTouTemplateCites,
+	)
+}
+
+// isTestEnv 判断请求指定的目标环境是否为测试环境(test)。
+// 默认返回 defaultTest 指定的默认值。
+func isTestEnv(req mcp.CallToolRequest, defaultEnv string) bool {
+	return req.GetString("env", defaultEnv) == "test"
 }
 }
 
 
 func handleTouTemplateList(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 func handleTouTemplateList(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
@@ -49,7 +83,13 @@ func handleTouTemplateList(ctx context.Context, req mcp.CallToolRequest) (*mcp.C
 		status = &s
 		status = &s
 	}
 	}
 
 
-	data, err := services.GetTouTemplateProList(page, size, tempType, status, search)
+	var data *dto.ResPage[interface{}]
+	var err error
+	if isTestEnv(req, "pro") {
+		data, err = services.FwhTestGetTouTemplateProList(page, size, tempType, status, search)
+	} else {
+		data, err = services.GetTouTemplateProList(page, size, tempType, status, search)
+	}
 	if err != nil {
 	if err != nil {
 		return helper.ServiceError("查询TOU模版列表", err), nil
 		return helper.ServiceError("查询TOU模版列表", err), nil
 	}
 	}
@@ -63,10 +103,67 @@ func handleTouTemplateDetail(ctx context.Context, req mcp.CallToolRequest) (*mcp
 		return helper.ParamError("id"), nil
 		return helper.ParamError("id"), nil
 	}
 	}
 
 
-	data, err := services.GetTouTemplateProDetailInfo(uint(id))
+	var data *interface{}
+	if isTestEnv(req, "pro") {
+		data, err = services.FwhTestGetTouTemplateProDetailInfo(uint(id))
+	} else {
+		data, err = services.GetTouTemplateProDetailInfo(uint(id))
+	}
 	if err != nil {
 	if err != nil {
 		return helper.ServiceError("查询TOU模版详情", err), nil
 		return helper.ServiceError("查询TOU模版详情", err), nil
 	}
 	}
 
 
 	return helper.JSONResultOrFileAuto(data), nil
 	return helper.JSONResultOrFileAuto(data), nil
 }
 }
+
+func handleTouTemplateSave(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	if !isTestEnv(req, "test") {
+		return helper.ParamInvalidError("env", "保存TOU模版当前仅支持测试环境(test)"), nil
+	}
+
+	payloadStr, err := req.RequireString("payload")
+	if err != nil {
+		return helper.ParamError("payload"), nil
+	}
+
+	var payload dto.FwhTestUpdateOrSaveTouTemplatePayload
+	if err := json.Unmarshal([]byte(payloadStr), &payload); err != nil {
+		return helper.ParamInvalidError("payload", "需要有效的JSON格式"), nil
+	}
+
+	templateId, err := services.FwhTestSaveTouTemplate(payload)
+	if err != nil {
+		return helper.ServiceError("保存TOU模版", err), nil
+	}
+
+	return helper.JSONResultOrFileAuto(map[string]interface{}{
+		"templateId": templateId,
+		"message":    "TOU模版保存成功",
+	}), nil
+}
+
+func handleTouTemplateCites(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	if !isTestEnv(req, "test") {
+		return helper.ParamInvalidError("env", "查询省/市列表当前仅支持测试环境(test)"), nil
+	}
+
+	// country_id、type 均为可选:都不传时查询国家信息。
+	var countryId *int
+	if v, err := req.RequireFloat("country_id"); err == nil {
+		c := int(v)
+		countryId = &c
+	}
+
+	var cityType *int
+	if v, err := req.RequireFloat("type"); err == nil {
+		t := int(v)
+		cityType = &t
+	}
+
+	data, err := services.FwhTestGetTouTemplateCitesList(countryId, cityType)
+	if err != nil {
+		return helper.ServiceError("查询省/市列表", err), nil
+	}
+
+	return helper.JSONResultOrFileAuto(data), nil
+}

+ 104 - 0
service/dto/fwhTestTouTemplateDTO.go

@@ -0,0 +1,104 @@
+package dto
+
+/************ updateOrSaveIotTouTemplate(新增/更新模板) ************/
+
+// FwhTestTouTemplateDetailVo 时段明细
+// 说明:字段统一用 string,未设置传空串 "";有值时按下方各字段说明的数值格式填写(与前端 HAR 行为一致)。
+type FwhTestTouTemplateDetailVo struct {
+	StartHourTime    string `json:"startHourTime"`    // 开始时间 格式 HH:mm,如 06:30
+	EndHourTime      string `json:"endHourTime"`      // 结束时间 格式 HH:mm,需与下一个 startHourTime 重叠,所有时段需覆盖一整天
+	WaveType         string `json:"waveType"`         // 峰平谷定义(flag) 数值:0-谷 1-平 2-峰 3-尖峰 4-低谷
+	Name             string `json:"name"`             // 时段名 一般为 supper-off-peak、off-peak、mid-peak、on-peak、supper-on-peak 等
+	BriefDescribe    string `json:"briefDescribe"`    // 时段描述
+	SolarPriority    string `json:"solarPriority"`    // 光伏供电优先级 格式 "市电,负载,FHP",如 "1,3,2";优先级 0/1/2/3,可相同(表示按比例/功率分配),0-不涉及,数字越大优先级越高
+	GridDischargeMax string `json:"gridDischargeMax"` // 电池最大允许馈网功率 数值(单位kW,精度0.1);可选,默认0不能馈网;0-不可馈网,-1-不限制
+	GridChargeMax    string `json:"gridChargeMax"`    // 电网允许最大充电功率 数值(单位kW,精度0.1);可选,默认0不允许;0-不允许,-1-不限制
+	ChargePower      string `json:"chargePower"`      // 固定电池充电功率 数值(单位kW,精度0.1),按单台功率计算;可选,默认-1未设置
+	DischargePower   string `json:"dischargePower"`   // 固定电池放电功率 数值(单位kW,精度0.1),按单台功率计算;可选,默认-1未设置
+	ChargeMax        string `json:"chargeMax"`        // 电池最大允许充电功率 数值(单位kW,精度0.1),按单台功率计算;-1-无限制,默认-1
+	DischargeMax     string `json:"dischargeMax"`     // 电池最大允许放电功率 数值(单位kW,精度0.1),按单台功率计算;-1-无限制,默认-1
+	GcaoMax          string `json:"gcaoMax"`          // GCAO(grid charge after an outage) 停电后补电至预留SOC的单台市电取电功率 可选;0-不允许,-1-不限制;默认未设置(-3);使能后未设值则为 5kw*N(用-2标识)
+	LoadPriority     string `json:"loadPriority"`     // 负载取电优先级 格式 "市电,光伏,FHP",如 "1,3,2";优先级 0/1/2/3,可相同(表示按比例/功率分配),0-不使用此源,数字越大优先级越高
+	GridFeedMax      string `json:"gridFeedMax"`      // 电网允许最大馈网功率 数值(单位kW,精度0.1);可选,默认-2;0-不允许,-1-不限制,-2-未设置
+	GridMax          string `json:"gridMax"`          // 电网允许最大取电功率 数值(单位kW,精度0.1);可选,默认-1不限制;0-不允许,-1-不限制
+	MinDischargeSoc  string `json:"minDischargeSoc"`  // 放电截止SOC 数值(单位%,整数);达到此SOC后电池不放电,可选,默认0%;若 reserved_soc 大于此值则以 reserved_soc 为准
+	MaxChargeSoc     string `json:"maxChargeSoc"`     // 充电截止SOC 数值(单位%,整数);可选,默认100%
+	HeatEnable       string `json:"heatEnable"`       // 充电加热使能 数值:0-未设置(默认) 1-不使能 2-使能
+	PowerOffApower   string `json:"powerOffApower"`   // 主动关闭APower 可选,0-不控制(默认) 1-关机 2-开机(预留)
+	RampTime         string `json:"rampTime"`         // 爬坡时间 数值(单位秒,0~300);默认0,BB套餐默认300
+	OffGrid          string `json:"offGrid"`          // 主动断开市电继电器 数值:0-不控制(默认) 1-断开 2-闭合(预留)
+	SolarCutoff      string `json:"solarCutoff"`      // 主动断开光伏继电器 数值:0-不控制(默认) 1-断开 2-闭合(预留)
+}
+
+// FwhTestTouTemplateStrategy 策略(季节/日类型 + 时段明细列表)
+type FwhTestTouTemplateStrategy struct {
+	CcociateType string                       `json:"ccociateType"` // 关联类型 数值编码,如 1
+	CcociateSn   string                       `json:"ccociateSn"`   // 关联设备SN
+	DetailVoList []FwhTestTouTemplateDetailVo `json:"detailVoList"` // 时段策略表,所有配置的时间需覆盖一整天
+	SeasonName   string                       `json:"seasonName"`   // 季节名 用于标识策略范围,可自定义,如 summer、All months
+	Month        string                       `json:"month"`        // 逗号分隔的月份,如 1,2,3...
+	DayName      string                       `json:"dayName"`      // 日期类型的可读名称,如 Every day、Weekdays(与 dayType 对应)
+	DayType      string                       `json:"dayType"`      // 日期类型 数值编码(非自定义字符串),如 3-Every day;可读名见 dayName
+}
+
+// FwhTestTouTemplateInfo 模板主体
+// templateId:创建模板时省略此字段(nil);更新模板时必须带此字段并填入模板ID。读取(detail/list)返回时该字段必定存在。
+type FwhTestTouTemplateInfo struct {
+	CountryId          string  `json:"countryId"`            // 国家ID 数值
+	ProvinceId         string  `json:"provinceId"`           // 州/省ID 数值
+	DerSchdule         string  `json:"derSchdule"`           // DER 调度配置
+	EleCompanyFullName string  `json:"eleCompanyFullName"`   // 电力公司全称
+	ElectricCompany    string  `json:"electricCompany"`      // 电力公司
+	GridType           string  `json:"gridType"`             // 电网类型
+	ElectricityType    string  `json:"electricityType"`      // 电价类型 数值:1-分时电价 2-阶梯电价 3-固定电价 4-分时&阶梯电价 5-BB套餐 6-Peak Demand
+	Name               string  `json:"name"`                 // 模板/策略表名称
+	MookRunCount       string  `json:"mookRunCount"`         // 模式执行次数(loop) 数值:0-不限次数 1-执行一次
+	Priority           string  `json:"priority"`             // 模式优先级 数值,默认0,0最低,数字越大优先级越高
+	Env                string  `json:"env"`                  // 模式使用条件 数值(位域),默认0;bit0~1 光伏;bit2~3 馈网;bit4~5 电网充电
+	TemplateId         *string `json:"templateId,omitempty"` // 模板ID 数值;创建时省略(nil),更新时必填。读取返回时必定存在
+	SourceType         string  `json:"sourceType"`           // 模板类型 数值,必填;当前仅允许 0-系统模板(1-API模板 2-用户推荐 暂不支持写入)
+}
+
+// FwhTestUpdateOrSaveTouTemplatePayload 新增/更新模板请求体(JSON)
+type FwhTestUpdateOrSaveTouTemplatePayload struct {
+	Template     FwhTestTouTemplateInfo       `json:"template"`
+	StrategyList []FwhTestTouTemplateStrategy `json:"strategyList"`
+	Lang         string                       `json:"lang"`
+}
+
+/************ selectCitesList(查询国家下的省/市列表) ************/
+
+// FwhTestCityInfo 城市
+type FwhTestCityInfo struct {
+	Id        int    `json:"id"`
+	NameEn    string `json:"nameEn"`
+	NameZh    string `json:"nameZh"`
+	Latitude  string `json:"latitude"`
+	Longitude string `json:"longitude"`
+}
+
+// FwhTestProvinceInfo 省/州(可能包含 cityList)
+type FwhTestProvinceInfo struct {
+	Id        int               `json:"id"`
+	NameEn    string            `json:"nameEn"`
+	NameZh    string            `json:"nameZh"`
+	Latitude  string            `json:"latitude"`
+	Longitude string            `json:"longitude"`
+	CityList  []FwhTestCityInfo `json:"cityList"`
+}
+
+// FwhTestCountryInfo 国家
+type FwhTestCountryInfo struct {
+	Id        int    `json:"id"`
+	NameEn    string `json:"nameEn"`
+	NameZh    string `json:"nameZh"`
+	Latitude  string `json:"latitude"`
+	Longitude string `json:"longitude"`
+}
+
+// FwhTestCitesListData selectCitesList 返回结构
+type FwhTestCitesListData struct {
+	CountryList  []FwhTestCountryInfo  `json:"countryList"`
+	ProvinceList []FwhTestProvinceInfo `json:"provinceList"`
+	CityList     []FwhTestCityInfo     `json:"cityList"`
+}

+ 72 - 0
service/repositories/api/fwh_test/fwh_touTemplate.go

@@ -2,6 +2,7 @@ package fwh_test
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"fwh-toolbox-server/domain"
 	"fwh-toolbox-server/dto"
 	"fwh-toolbox-server/dto"
 	"net/url"
 	"net/url"
 
 
@@ -69,3 +70,74 @@ func QueryTouTemplateDetailInfo(id uint) (*interface{}, error) {
 	}
 	}
 	return response.Result, nil
 	return response.Result, nil
 }
 }
+
+/************ updateOrSaveIotTouTemplate(新增/更新模板) ************/
+
+// updateOrSaveIotTouTemplate 新增/更新TOU模板(JSON body)
+func updateOrSaveIotTouTemplate(payload dto.FwhTestUpdateOrSaveTouTemplatePayload) (*domain.FwhResponse[int64], error) {
+	if payload.Lang == "" {
+		payload.Lang = "ZH_CN"
+	}
+
+	// struct 会被 httpClient 自动序列化为 application/json
+	response, err := FwhHttpPostUnmarshal[int64](getFullEnergyUrl("/hes-gateway/manage/updateOrSaveIotTouTemplate"), payload)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// UpdateOrSaveTouTemplate 新增/更新TOU模板,返回模板ID
+func UpdateOrSaveTouTemplate(payload dto.FwhTestUpdateOrSaveTouTemplatePayload) (int64, error) {
+	res, err := updateOrSaveIotTouTemplate(payload)
+	if err != nil {
+		return 0, err
+	}
+
+	if res.Code != 200 {
+		return 0, fmt.Errorf("[UpdateOrSaveTouTemplate] 操作失败 code: %d, message: %s", res.Code, res.Message)
+	}
+
+	var templateId int64
+	if res.Result != nil {
+		templateId = *res.Result
+	}
+	return templateId, nil
+}
+
+/************ selectCitesList(查询国家下的省/市列表) ************/
+
+// selectCitesList 查询国家/省/市列表
+// countryId 为 nil:不传 countryId,用于查询国家信息。
+// cityType 为 nil:不传 type。TOU 模板页面查询城市/省时必须传 1。
+func selectCitesList(countryId *int, cityType *int) (*domain.FwhResponse[dto.FwhTestCitesListData], error) {
+	params := url.Values{}
+	if countryId != nil {
+		params.Add("countryId", fmt.Sprintf("%d", *countryId))
+	}
+	if cityType != nil {
+		params.Add("type", fmt.Sprintf("%d", *cityType))
+	}
+	params.Add("lang", "ZH_CN")
+
+	response, err := FwhHttpGetUnmarshal[dto.FwhTestCitesListData](getFullEnergyUrl("/hes-gateway/manage/selectCitesList"), params)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// SelectCitesList 查询国家/省/市列表
+// countryId 为 nil 且 cityType 为 nil 时,查询国家信息。
+func SelectCitesList(countryId *int, cityType *int) (*dto.FwhTestCitesListData, error) {
+	res, err := selectCitesList(countryId, cityType)
+	if err != nil {
+		return nil, err
+	}
+
+	if res.Code != 200 {
+		return nil, fmt.Errorf("[SelectCitesList] 查询失败 code: %d, message: %s", res.Code, res.Message)
+	}
+
+	return res.Result, nil
+}

+ 2 - 0
service/repositories/repositories.go

@@ -135,6 +135,8 @@ var FwhTestQueryAHubUpdateLog = fwh_test.QueryAHubUpdateLog                 // 
 // fwh_touTemplate
 // fwh_touTemplate
 var FwhTestQueryTouTemplateInfoList = fwh_test.QueryTouTemplateInfoList // 查询模板列表
 var FwhTestQueryTouTemplateInfoList = fwh_test.QueryTouTemplateInfoList // 查询模板列表
 var FwhTestQueryTouTemplateDetailInfo = fwh_test.QueryTouTemplateDetailInfo
 var FwhTestQueryTouTemplateDetailInfo = fwh_test.QueryTouTemplateDetailInfo
+var FwhTestUpdateOrSaveTouTemplate = fwh_test.UpdateOrSaveTouTemplate // 新增/更新模板
+var FwhTestSelectCitesList = fwh_test.SelectCitesList                 // 查询国家下的省/市列表
 
 
 // SmartHub
 // SmartHub
 var QuerySmartHubBatchSimCardInfo = smartHub.QueryBatchSimCardInfo // 批量查询SIM卡信息
 var QuerySmartHubBatchSimCardInfo = smartHub.QueryBatchSimCardInfo // 批量查询SIM卡信息

+ 27 - 0
service/services/despatchConfigServices.go

@@ -5,6 +5,8 @@ import (
 	"fwh-toolbox-server/repositories"
 	"fwh-toolbox-server/repositories"
 )
 )
 
 
+// ============ fwh_pro(生产环境)============
+
 // 查询模版列表
 // 查询模版列表
 func GetTouTemplateProList(pageNum uint, pageSize uint, tempType *int, status *int, search string) (*dto.ResPage[interface{}], error) {
 func GetTouTemplateProList(pageNum uint, pageSize uint, tempType *int, status *int, search string) (*dto.ResPage[interface{}], error) {
 	return repositories.QueryTouTemplateInfoList(search, pageSize, pageNum, tempType, status)
 	return repositories.QueryTouTemplateInfoList(search, pageSize, pageNum, tempType, status)
@@ -14,3 +16,28 @@ func GetTouTemplateProList(pageNum uint, pageSize uint, tempType *int, status *i
 func GetTouTemplateProDetailInfo(id uint) (*interface{}, error) {
 func GetTouTemplateProDetailInfo(id uint) (*interface{}, error) {
 	return repositories.QueryTouTemplateDetailInfo(id)
 	return repositories.QueryTouTemplateDetailInfo(id)
 }
 }
+
+// ============ fwh_test(测试环境)============
+
+// FwhTestGetTouTemplateProList 查询模版列表
+func FwhTestGetTouTemplateProList(pageNum uint, pageSize uint, tempType *int, status *int, search string) (*dto.ResPage[interface{}], error) {
+	return repositories.FwhTestQueryTouTemplateInfoList(search, pageSize, pageNum, tempType, status)
+}
+
+// FwhTestGetTouTemplateProDetailInfo 查询模版信息
+func FwhTestGetTouTemplateProDetailInfo(id uint) (*interface{}, error) {
+	return repositories.FwhTestQueryTouTemplateDetailInfo(id)
+}
+
+// FwhTestSaveTouTemplate 新增/更新 TOU 模版(updateOrSaveIotTouTemplate)
+// payload.Template.TemplateId 有值表示更新,为空表示新增。返回模版 ID。
+func FwhTestSaveTouTemplate(payload dto.FwhTestUpdateOrSaveTouTemplatePayload) (int64, error) {
+	return repositories.FwhTestUpdateOrSaveTouTemplate(payload)
+}
+
+// FwhTestGetTouTemplateCitesList 查询国家/省/市列表
+// countryId 与 cityType 均为 nil:查询国家信息。
+// TOU 模板页面查询城市/省时 cityType 必须为 1。
+func FwhTestGetTouTemplateCitesList(countryId *int, cityType *int) (*dto.FwhTestCitesListData, error) {
+	return repositories.FwhTestSelectCitesList(countryId, cityType)
+}