Explorar o código

feat:新增故障录波设置\nfix:修复已知bug

Jerry Liang hai 4 meses
pai
achega
e6ec9a95f7
Modificáronse 36 ficheiros con 2606 adicións e 324 borrados
  1. 2 0
      package.json
  2. 56 32
      pnpm-lock.yaml
  3. 66 0
      service/controllers/despatchConfigControllers.go
  4. 26 0
      service/controllers/faultRecordControllers.go
  5. 38 0
      service/controllers/response.go
  6. 178 0
      service/domain/acceleronixDomain.go
  7. 29 18
      service/domain/smartHubDomain.go
  8. 17 0
      service/dto/faultRecordDTO.go
  9. 19 190
      service/repositories/api/acceleronix/simCardManage.go
  10. 52 0
      service/repositories/api/acceleronix/test/main.go
  11. 31 18
      service/repositories/api/fwh_pro/fwh_touTemplate.go
  12. 20 26
      service/repositories/api/smartHub/simCardManage.go
  13. 24 0
      service/repositories/api/smartHub/test/main.go
  14. 159 0
      service/repositories/mongo/simCardManageRepo.go
  15. 31 19
      service/repositories/mqtt/fwh_mqtt/fwh_mqtt_client.go
  16. 2 2
      service/repositories/mqtt/fwh_mqtt/fwh_mqtt_debug.go
  17. 118 19
      service/repositories/mqtt/fwh_mqtt/fwh_mqtt_fault_record.go
  18. 17 0
      service/repositories/repositories.go
  19. 20 0
      service/routes/despatchConfigRoutes.go
  20. 16 0
      service/routes/faultRecordRoutes.go
  21. 2 0
      service/routes/routes.go
  22. 9 0
      service/scheduler/scheduler.go
  23. 16 0
      service/services/despatchConfigServices.go
  24. 12 0
      service/services/faultRecordServices.go
  25. 2 0
      service/utils/httpClient/httpClient.go
  26. 66 0
      src/router/record.ts
  27. 39 0
      src/service/despatchConfig/index.ts
  28. 119 0
      src/service/despatchConfig/interface.ts
  29. 15 0
      src/service/faultRecord/index.ts
  30. 11 0
      src/service/faultRecord/interface.ts
  31. 17 0
      src/stores/user.ts
  32. 668 0
      src/views/despatch-config/component/templateTable.vue
  33. 417 0
      src/views/despatch-config/touTemplatePro.vue
  34. 24 0
      src/views/despatch-config/touTemplateTest.vue
  35. 3 0
      src/views/fault-record/read/index.vue
  36. 265 0
      src/views/fault-record/write/index.vue

+ 2 - 0
package.json

@@ -46,6 +46,7 @@
     "chroma-js": "^3.2.0",
     "dayjs": "^1.11.20",
     "es-toolkit": "^1.45.1",
+    "file-saver": "^2.0.5",
     "idb": "^8.0.3",
     "jwt-decode": "^4.0.0",
     "monaco-editor": "^0.55.1",
@@ -66,6 +67,7 @@
     "@playwright/test": "^1.58.2",
     "@tsconfig/node22": "^22.0.5",
     "@types/chroma-js": "^3.1.2",
+    "@types/file-saver": "^2.0.7",
     "@types/jsdom": "^28.0.1",
     "@types/node": "^25.5.0",
     "@types/streamsaver": "^2.0.5",

+ 56 - 32
pnpm-lock.yaml

@@ -83,6 +83,9 @@ importers:
       es-toolkit:
         specifier: ^1.45.1
         version: 1.45.1
+      file-saver:
+        specifier: ^2.0.5
+        version: 2.0.5
       idb:
         specifier: ^8.0.3
         version: 8.0.3
@@ -138,6 +141,9 @@ importers:
       '@types/chroma-js':
         specifier: ^3.1.2
         version: 3.1.2
+      '@types/file-saver':
+        specifier: ^2.0.7
+        version: 2.0.7
       '@types/jsdom':
         specifier: ^28.0.1
         version: 28.0.1
@@ -1755,6 +1761,9 @@ packages:
   '@types/estree@1.0.8':
     resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
 
+  '@types/file-saver@2.0.7':
+    resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
+
   '@types/geojson@7946.0.16':
     resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
 
@@ -1862,43 +1871,51 @@ packages:
     resolution: {integrity: sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260330.1':
-    resolution: {integrity: sha512-jX47OX4svh3/CzSDYd81+kELLP5r0+G7lFGiMAVUNpD+ZLmyh/nrkuuI643pGe8D0i9dTfg/aiZQq6d/00Feog==}
+  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260604.1':
+    resolution: {integrity: sha512-zs616um9UuaODLsNlCu5Aw95rFcTV4u3hVt090r6k0lVvTxfaJOv8HKA6BpIotcEYlZlMQowrMSYCCdedo7iyA==}
+    engines: {node: '>=16.20.0'}
     cpu: [arm64]
     os: [darwin]
 
-  '@typescript/native-preview-darwin-x64@7.0.0-dev.20260330.1':
-    resolution: {integrity: sha512-JysPTOMnyje+LkdB3LR+Emnl7hJCvMzUVRcOVJyzmxWOMFNCYPWNK3wDdwuoS6jE5Hii9vInt/xtn3pc+kgb+A==}
+  '@typescript/native-preview-darwin-x64@7.0.0-dev.20260604.1':
+    resolution: {integrity: sha512-pOdNAf2pwc9JBjo8gUsvLs5uqg7d0AhYXfE8/3zvPKBlIZG+mTcvEWW1hPawlWxXzf/vxnP4dgYwUHAMDghhKA==}
+    engines: {node: '>=16.20.0'}
     cpu: [x64]
     os: [darwin]
 
-  '@typescript/native-preview-linux-arm64@7.0.0-dev.20260330.1':
-    resolution: {integrity: sha512-9zlw4ENr6HAUNO+OiWuoQpWUGk4lkqb8NUyXDvTqtlgZGvjyePNVy8OW7KlPuPtCaQS3btV7w4h5X1wlTm2bFA==}
+  '@typescript/native-preview-linux-arm64@7.0.0-dev.20260604.1':
+    resolution: {integrity: sha512-GjIrt6YHP3bbOWBCCE08SlBSDf84Lnjn3Td822/lOX9nm6ODlA/HI7rtGh7KzS/fxehep2Vy4dXU4Il12X1s5A==}
+    engines: {node: '>=16.20.0'}
     cpu: [arm64]
     os: [linux]
 
-  '@typescript/native-preview-linux-arm@7.0.0-dev.20260330.1':
-    resolution: {integrity: sha512-rVwd9ZgjR8Le6ItUD/p59LCOYdZbi4DZYavXAMzPPpaJCnmdHwOcJFWc61vricsQljRjVBnunQknHfYABZDq9Q==}
+  '@typescript/native-preview-linux-arm@7.0.0-dev.20260604.1':
+    resolution: {integrity: sha512-IKaZL3i5HKmKqwb2IZEXW1j68fVg1HsvAaXkbrkOIG/J5Eyksu5tEnTzucrIY1oPdzgHT+y2HpDIYp2sGeHFvw==}
+    engines: {node: '>=16.20.0'}
     cpu: [arm]
     os: [linux]
 
-  '@typescript/native-preview-linux-x64@7.0.0-dev.20260330.1':
-    resolution: {integrity: sha512-7cB4e12e61xaXgJu6N9rMP+6qdkODVsW8zGjVS5a+cobBCsJSdMnlshfitlL9a+BYWe9HseMqPnLcsaE+t22CQ==}
+  '@typescript/native-preview-linux-x64@7.0.0-dev.20260604.1':
+    resolution: {integrity: sha512-twQ7XDjsmaHIevN1MjeRYIVLUPL0fBm8A0jg1FhGYPckhTxEBiHIpJf9E//eFidAdrEHjeTSBP1jJw3GNAensg==}
+    engines: {node: '>=16.20.0'}
     cpu: [x64]
     os: [linux]
 
-  '@typescript/native-preview-win32-arm64@7.0.0-dev.20260330.1':
-    resolution: {integrity: sha512-4wv/FX9F5oAQhzjqwL8KN4O5xgciBy5pT2yLP45xcQahxFUoPnQeF9Pz2x/jwXZSmuh8E1DQWX9evSVGA1MV2A==}
+  '@typescript/native-preview-win32-arm64@7.0.0-dev.20260604.1':
+    resolution: {integrity: sha512-QBRxaVT3SFiNfOhwYb/56ddpHWPMFdfiJ4zkFJIjaAXeZ/ssWNHM9lH7yR++GrE9VTsUZ4eVSZfOmQbzRERhgw==}
+    engines: {node: '>=16.20.0'}
     cpu: [arm64]
     os: [win32]
 
-  '@typescript/native-preview-win32-x64@7.0.0-dev.20260330.1':
-    resolution: {integrity: sha512-zKDOG/nq1yTx3cjfc19VFO+TYK2+03Wc17pGdD+X+dWTj15LZ2PKk3s4OtT2b8siTcAUn7zmwLsMCWPYO7xXQw==}
+  '@typescript/native-preview-win32-x64@7.0.0-dev.20260604.1':
+    resolution: {integrity: sha512-hR7YHoRpm88Q86gK6/IMayWUA+ROdHGzOPKK8EBp1xD/Cg2Bh5AXbut8HcyUDx/bcGQvnuht/XsW47bT3to9mg==}
+    engines: {node: '>=16.20.0'}
     cpu: [x64]
     os: [win32]
 
-  '@typescript/native-preview@7.0.0-dev.20260330.1':
-    resolution: {integrity: sha512-tyRn6/PAYpkN35bKz2RPfzV/QAACvle9EYlYyywn/p/qPI5yOBLka0zM/2dGKx+o5vHVQ1gr/iVrgW8w7Wqc8Q==}
+  '@typescript/native-preview@7.0.0-dev.20260604.1':
+    resolution: {integrity: sha512-A3/9yZTt2V5NlDURcVJ4mN2YjfeQTXCRyLuENKrNdGhO+y59mC/2UDr7UvpB3Li+83TRAuhDN8SBoM+7gkHdzQ==}
+    engines: {node: '>=16.20.0'}
     hasBin: true
 
   '@umami/node@0.4.0':
@@ -3280,6 +3297,9 @@ packages:
     resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
     engines: {node: '>=16.0.0'}
 
+  file-saver@2.0.5:
+    resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
+
   filelist@1.0.6:
     resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==}
 
@@ -6512,6 +6532,8 @@ snapshots:
 
   '@types/estree@1.0.8': {}
 
+  '@types/file-saver@2.0.7': {}
+
   '@types/geojson@7946.0.16': {}
 
   '@types/jsdom@28.0.1':
@@ -6657,36 +6679,36 @@ snapshots:
       '@typescript-eslint/types': 8.57.2
       eslint-visitor-keys: 5.0.1
 
-  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260330.1':
+  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260604.1':
     optional: true
 
-  '@typescript/native-preview-darwin-x64@7.0.0-dev.20260330.1':
+  '@typescript/native-preview-darwin-x64@7.0.0-dev.20260604.1':
     optional: true
 
-  '@typescript/native-preview-linux-arm64@7.0.0-dev.20260330.1':
+  '@typescript/native-preview-linux-arm64@7.0.0-dev.20260604.1':
     optional: true
 
-  '@typescript/native-preview-linux-arm@7.0.0-dev.20260330.1':
+  '@typescript/native-preview-linux-arm@7.0.0-dev.20260604.1':
     optional: true
 
-  '@typescript/native-preview-linux-x64@7.0.0-dev.20260330.1':
+  '@typescript/native-preview-linux-x64@7.0.0-dev.20260604.1':
     optional: true
 
-  '@typescript/native-preview-win32-arm64@7.0.0-dev.20260330.1':
+  '@typescript/native-preview-win32-arm64@7.0.0-dev.20260604.1':
     optional: true
 
-  '@typescript/native-preview-win32-x64@7.0.0-dev.20260330.1':
+  '@typescript/native-preview-win32-x64@7.0.0-dev.20260604.1':
     optional: true
 
-  '@typescript/native-preview@7.0.0-dev.20260330.1':
+  '@typescript/native-preview@7.0.0-dev.20260604.1':
     optionalDependencies:
-      '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260330.1
-      '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260330.1
-      '@typescript/native-preview-linux-arm': 7.0.0-dev.20260330.1
-      '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260330.1
-      '@typescript/native-preview-linux-x64': 7.0.0-dev.20260330.1
-      '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260330.1
-      '@typescript/native-preview-win32-x64': 7.0.0-dev.20260330.1
+      '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260604.1
+      '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260604.1
+      '@typescript/native-preview-linux-arm': 7.0.0-dev.20260604.1
+      '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260604.1
+      '@typescript/native-preview-linux-x64': 7.0.0-dev.20260604.1
+      '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260604.1
+      '@typescript/native-preview-win32-x64': 7.0.0-dev.20260604.1
 
   '@umami/node@0.4.0': {}
 
@@ -8369,6 +8391,8 @@ snapshots:
     dependencies:
       flat-cache: 4.0.1
 
+  file-saver@2.0.5: {}
+
   filelist@1.0.6:
     dependencies:
       minimatch: 5.1.9
@@ -10414,7 +10438,7 @@ snapshots:
 
   vue-vine-tsc@1.7.27(typescript@6.0.2):
     dependencies:
-      '@typescript/native-preview': 7.0.0-dev.20260330.1
+      '@typescript/native-preview': 7.0.0-dev.20260604.1
       '@volar/language-core': 2.4.20
       '@volar/typescript': 2.4.20
       '@vue-vine/language-service': 1.7.27(typescript@6.0.2)

+ 66 - 0
service/controllers/despatchConfigControllers.go

@@ -0,0 +1,66 @@
+package controllers
+
+import (
+	"fwh-toolbox-server/services"
+	"strconv"
+
+	"github.com/gofiber/fiber/v2"
+	"go.uber.org/zap"
+)
+
+// 查询模版列表
+// /despatchConfig/touTemplatePro/list
+func QueryTouTemplateProList(c *fiber.Ctx) error {
+	pageSize, _ := strconv.Atoi(c.Query("pageSize", "20"))
+	pageNum, _ := strconv.Atoi(c.Query("pageNum", "1"))
+	tempTypeStr := c.Query("tempType", "")
+	statusStr := c.Query("status", "")
+	search := c.Query("search", "")
+
+	if pageSize <= 0 || pageNum <= 0 {
+		return Fail(c, "Invalid request para")
+	}
+
+	var tempType *int = nil
+	var status *int = nil
+
+	if tempTypeStr != "" {
+		tempTypeNumber, err := strconv.Atoi(tempTypeStr)
+		if err == nil {
+			tempType = &tempTypeNumber
+		}
+	}
+	if statusStr != "" {
+		statusNumber, err := strconv.Atoi(statusStr)
+		if err == nil {
+			status = &statusNumber
+		}
+	}
+
+	zap.L().Info("QueryTouTemplateProList", zap.String("statusStr", statusStr), zap.String("tempTypeStr", tempTypeStr))
+	zap.L().Info("QueryTouTemplateProList", zap.Any("status", status), zap.Any("tempType", tempType))
+
+	listPage, err := services.GetTouTemplateProList(uint(pageNum), uint(pageSize), tempType, status, search)
+	if err != nil {
+		zap.L().Warn("FWH query failed.", zap.String("err", err.Error()))
+		return Error(c, "FWH query failed.")
+	}
+	return Success(c, listPage)
+}
+
+// 查询模版信息
+// /despatchConfig/touTemplatePro/info
+func QueryTouTemplateProDetailInfo(c *fiber.Ctx) error {
+	id, _ := strconv.Atoi(c.Query("id", "0"))
+
+	if id <= 0 {
+		return Fail(c, "Invalid request para")
+	}
+
+	detailInfo, err := services.GetTouTemplateProDetailInfo(uint(id))
+	if err != nil {
+		zap.L().Warn("FWH query failed.", zap.String("err", err.Error()))
+		return Error(c, "FWH query failed.")
+	}
+	return Success(c, detailInfo)
+}

+ 26 - 0
service/controllers/faultRecordControllers.go

@@ -0,0 +1,26 @@
+package controllers
+
+import (
+	"context"
+	"fwh-toolbox-server/dto"
+	"fwh-toolbox-server/services"
+
+	"github.com/gofiber/fiber/v2"
+	"go.uber.org/zap"
+)
+
+// 设置故障录波参数
+// /faultRecord/paramSetting
+func SetFaultRecordParam(c *fiber.Ctx) error {
+	var body dto.FaultRecordParamInfo
+	if err := c.BodyParser(&body); err != nil {
+		zap.L().Warn("Invalid request body", zap.String("err", err.Error()))
+		return Fail(c, "Invalid request body")
+	}
+
+	err := services.SetFaultRecordParam(context.Background(), body)
+	if err != nil {
+		return Error(c, err.Error())
+	}
+	return Success[any](c, nil, "Success")
+}

+ 38 - 0
service/controllers/response.go

@@ -1,6 +1,7 @@
 package controllers
 
 import (
+	"fwh-toolbox-server/dto"
 	"time"
 
 	"github.com/gofiber/fiber/v2"
@@ -17,6 +18,17 @@ type Response[T any] struct {
 	Timestamp int64  `json:"timestamp"`
 }
 
+type ResponsePage[T any] struct {
+	Code      int    `json:"code"`
+	Message   string `json:"message"`
+	Data      []T    `json:"data"`
+	PageSize  uint   `json:"pageSize"`
+	PageNum   uint   `json:"pageNum"`
+	Total     uint   `json:"total"`
+	TraceID   string `json:"traceId"`
+	Timestamp int64  `json:"timestamp"`
+}
+
 type ResponseNoData struct {
 	Code      int    `json:"code"`
 	Message   string `json:"message"`
@@ -47,6 +59,32 @@ func Success[T any](c *fiber.Ctx, data T, msg ...string) error {
 	})
 }
 
+// 成功返回 200
+func SuccessPage[T any](c *fiber.Ctx, data *dto.ResPage[T], msg ...string) error {
+	message := "success"
+	if len(msg) > 0 {
+		message = msg[0]
+	}
+
+	traceId := c.Get(traceIdHeader) // 从请求头取
+	if traceId == "" {
+		traceId = uuid.New().String() // 没有就生成
+		// 设置到响应头,让前端也能拿到
+		c.Set(traceIdHeader, traceId)
+	}
+
+	return c.Status(fiber.StatusOK).JSON(ResponsePage[T]{
+		Code:      0,
+		Message:   message,
+		Data:      data.Data,
+		PageSize:  data.PageSize,
+		PageNum:   data.PageNum,
+		Total:     data.Total,
+		TraceID:   traceId,
+		Timestamp: time.Now().UnixMilli(),
+	})
+}
+
 // 失败返回 400
 func Fail(c *fiber.Ctx, msg string) error {
 	traceId := c.Get(traceIdHeader) // 从请求头取

+ 178 - 0
service/domain/acceleronixDomain.go

@@ -0,0 +1,178 @@
+package domain
+
+// SIM卡状态
+type AcceleronixSimCardState int
+
+const (
+	SCS_PreActivation   AcceleronixSimCardState = iota + 2 // 2 预激活
+	SCS_Test                                               // 3 测试
+	SCS_Silence                                            // 4 沉默
+	SCS_Standby                                            // 5 待机
+	SCS_Activated                                          // 6 激活
+	SCS_Deactivated                                        // 7 停机
+	SCS_Pause                                              // 8 暂停
+	SCS_PreCancellation                                    // 9 预注销
+	SCS_Cancellation                                       // 10 注销
+)
+
+// 查询SIM列表 - 请求结构体
+type AcceleronixSimCardListRequestBody struct {
+	EnterpriseCode      *string                  `json:"enterpriseCode,omitempty"`      // 企业编码
+	EnterpriseDataPlan  *string                  `json:"enterpriseDataPlan,omitempty"`  // 企业资费计划名称
+	ExpirationTimeEnd   *string                  `json:"expirationTimeEnd,omitempty"`   // 支持到期时间区间段查询,这里指截止到期时间 格式:yyyy-MM-dd
+	ExpirationTimeStart *string                  `json:"expirationTimeStart,omitempty"` // 支持到期时间区间段查询,这里指起始到期时间 格式:yyyy-MM-dd
+	IccidEnd            *string                  `json:"iccidEnd,omitempty"`            // 支持ICCID号段查询,这里指ICCID截止号码
+	IccidStart          *string                  `json:"iccidStart,omitempty"`          // 支持ICCID号段查询,这里指ICCID起始号码
+	Label               *string                  `json:"label,omitempty"`               // 标签
+	PageNum             uint                     `json:"pageNum"`                       // 页码 默认值:1
+	PageSize            uint                     `json:"pageSize"`                      // 每页记录数 默认值:10 最大值:1000
+	SimState            *AcceleronixSimCardState `json:"simState,omitempty"`            // 卡状态 AcceleronixSimCardState
+	SimType             *string                  `json:"simType,omitempty"`             // 卡类型
+}
+
+// 查询SIM列表 - 响应
+type AcceleronixSimCardListResponse struct {
+	Code int `json:"code"` // 响应码
+	Data struct {
+		Current int64 `json:"current"` // 当前页码
+		List    []struct {
+			ActivationTime       string                  `json:"activationTime"`       // 激活时间 格式:yyyy-MM-ddd
+			Enterprise           string                  `json:"enterprise"`           // 归属企业
+			EnterpriseDataPlan   string                  `json:"enterpriseDataPlan"`   // 企业资费计划
+			ExpirationTime       string                  `json:"expirationTime"`       // 到期时间 格式:yyyy-MM-dd
+			Iccid                string                  `json:"iccid"`                // ICCID
+			Imei                 string                  `json:"imei"`                 // IMEI
+			Imsi                 string                  `json:"imsi"`                 // IMSI
+			InitialTime          string                  `json:"initialTime"`          // 开卡时间 格式:yyyy-MM-dd
+			Label                string                  `json:"label"`                // 标签,多个标签时逗号隔开
+			Msisdn               string                  `json:"msisdn"`               // MSISDN
+			NetworkType          string                  `json:"networkType"`          // 网络制式 1:ALL 2:Cat-M 3:LTE-M
+			SimId                string                  `json:"simId"`                // SIM ID
+			SimState             AcceleronixSimCardState `json:"simState"`             // 卡状态 AcceleronixSimCardState
+			SimStateChangeReason string                  `json:"simStateChangeReason"` // 卡状态变更原因
+			SimType              string                  `json:"simType"`              // 卡类型
+		} `json:"list"` // 数据集合
+		Pages int64 `json:"pages"` // 总页码
+		Size  int64 `json:"size"`  // 每页数量
+		Total int64 `json:"total"` // 总条数
+	} `json:"data"` // 响应体
+	Msg   string `json:"msg"` // 响应描述
+	ReqId string `json:"reqId"`
+}
+
+// 查询SIM卡详情 - 响应
+type AcceleronixSimCardInfoResponse struct {
+	Code int `json:"code"` // 响应码
+	Data struct {
+		ActivationTime          string                  `json:"activationTime"`          // 激活时间 格式:yyyy-MM-dd
+		Apn                     string                  `json:"apn"`                     // APN
+		CountryRegion           string                  `json:"countryRegion"`           // 所在国家/地区
+		Enterprise              string                  `json:"enterprise"`              // 归属企业
+		EnterpriseDataPlan      string                  `json:"enterpriseDataPlan"`      // 企业资费计划
+		EnterpriseDataPlanCode  string                  `json:"enterpriseDataPlanCode"`  // 计划编码
+		ExpirationTime          string                  `json:"expirationTime"`          // 到期时间 格式:yyyy-MM-dd
+		FirstActivationCdrTime  string                  `json:"firstActivationCdrTime"`  // 激活首次使用时间,格式yyyy-MM-dd
+		Iccid                   string                  `json:"iccid"`                   // ICCID
+		Imei                    string                  `json:"imei"`                    // IMEI
+		Imsi                    string                  `json:"imsi"`                    // IMSI
+		InitialTime             string                  `json:"initialTime"`             // 开卡时间 格式:yyyy-MM-dd
+		Label                   string                  `json:"label"`                   // 标签
+		LastSessionTime         string                  `json:"lastSessionTime"`         // 上次会话时间 格式:yyyy-MM-dd HH:mm:ss
+		Msisdn                  string                  `json:"msisdn"`                  // MSISDN
+		NetCommStatus           int32                   `json:"netCommStatus"`           // 网络通讯状态:0:关闭 1:开启
+		NetworkType             string                  `json:"networkType"`             // 网络制式
+		OperatorNetwork         string                  `json:"operatorNetwork"`         // 所在运营商网络
+		Rat                     string                  `json:"rat"`                     // RAT
+		SimId                   string                  `json:"simId"`                   // SIM ID
+		SimState                AcceleronixSimCardState `json:"simState"`                // 卡状态 AcceleronixSimCardState
+		SimStateChangeReason    string                  `json:"simStateChangeReason"`    // 卡状态变更原因
+		SimType                 string                  `json:"simType"`                 // 卡类型
+		UsedDataOfCurrentPeriod int64                   `json:"usedDataOfCurrentPeriod"` // 当前周期数据用量(B)
+	} `json:"data"` // 响应体
+	Msg   string `json:"msg"` // 响应描述
+	ReqId string `json:"reqId"`
+}
+
+// 批量查询SIM卡流量 - 请求结构体
+type AcceleronixBatchDataUsageRequestBody struct {
+	Iccids []string `json:"iccids"` // ICCID 最多支持100张卡批量操作
+	Month  string   `json:"month"`  // 统计月份,yyyyMM 日期格式化字符串
+}
+
+// 批量查询SIM卡流量 - 响应
+type AcceleronixSimCardDataUsageInfoResponse struct {
+	DataUsageDetails []struct {
+		DataAllowance          string `json:"dataAllowance"`          // 总流量 单位:MB
+		DataUsage              string `json:"dataUsage"`              // 已用流量 单位:MB
+		OrderName              string `json:"orderName"`              // 订购名
+		OutsideRegionDataUsage string `json:"outsideRegionDataUsage"` // 超区流量 单位:MB
+		Type                   int32  `json:"type"`                   // 流量类型 1:激活期资费套餐 2:测试期资费套餐 3:流量包
+	} `json:"dataUsageDetails"` // 流量详细
+	Iccid                  string `json:"iccid"`                  // ICCID
+	Month                  string `json:"month"`                  // 统计月份 格式:yyyyMM
+	OutsideRegionDataUsage string `json:"outsideRegionDataUsage"` // 超区流量 单位:MB
+	RemainingData          string `json:"remainingData"`          // 剩余流量(若无使用上限,剩余流量也为0) 单位:MB
+	TotalDataAllowance     string `json:"totalDataAllowance"`     // 总流量 单位:MB
+	TotalDataUsage         string `json:"totalDataUsage"`         // 已用流量 单位:MB
+}
+
+// 查询流量池列表 - 请求结构体
+type AcceleronixDataPoolListRequestBody struct {
+	EnterpriseCode *string `json:"enterpriseCode,omitempty"` // 企业编码。默认查询范围:本企业。可通过“子企业编码”查询子企业流量池
+	DataPoolId     *string `json:"dataPoolId"`               // 流量池ID
+	PageNum        uint    `json:"pageNum"`                  // 页码 默认值:1
+	PageSize       uint    `json:"pageSize"`                 // 每页记录数 默认值:10 最大值:1000
+}
+
+// 查询流量池列表 - 响应
+type AcceleronixDataPoolListResponse struct {
+	Code int `json:"code"` // 响应码
+	Data struct {
+		Current int64 `json:"current"` // 当前页码
+		List    []struct {
+			DataPlanAllowance   string  `json:"dataPlanAllowance"`   // 资费计划额度(MB)
+			DataPoolId          string  `json:"dataPoolId"`          // 流量池ID
+			DataPoolName        string  `json:"dataPoolName"`        // 流量池名称
+			DataUsage           float32 `json:"dataUsage"`           // 数据用量(MB)
+			EffectiveSims       string  `json:"effectiveSims"`       // 生效SIM数
+			EnterpriseName      string  `json:"enterpriseName"`      // 企业名称
+			Excessive           float32 `json:"excessive"`           // 超池用量(MB)
+			RemainingData       float32 `json:"remainingData"`       // 剩余用量(MB)
+			RemainingPercentage string  `json:"remainingPercentage"` // 剩余百分比(%)
+			TotalData           float32 `json:"totalData"`           // 池总量(MB)
+			TotalSims           string  `json:"totalSims"`           // 总SIM数
+			TotalVolume         float32 `json:"totalVolume"`         // 流量包总量(MB)
+			UsedSms             string  `json:"usedSms"`             // 短信用量(条)
+		} `json:"list"` // 数据集合
+		Pages int64 `json:"pages"` // 总页码
+		Size  int64 `json:"size"`  // 每页数量
+		Total int64 `json:"total"` // 总条数
+	} `json:"data"` // 响应体
+	Msg   string `json:"msg"` // 响应描述
+	ReqId string `json:"reqId"`
+}
+
+// 查询流量池SIM详情 - 响应
+type AcceleronixDataPoolSimInfoResponse struct {
+	Code int `json:"code"` // 响应码
+	Data struct {
+		Current int64 `json:"current"` // 当前页码
+		List    []struct {
+			DataSharingState        int32                   `json:"dataSharingState"`        // 数据共享状态 0:未生效 1:生效
+			DataUsage               float32                 `json:"dataUsage"`               // 当月数据用量(MB)
+			EffectiveTime           string                  `json:"effectiveTime"`           // 生效时间 格式:yyyy-MM-dd HH:mm:ss
+			EnterpriseDataPlan      string                  `json:"enterpriseDataPlan"`      // 企业资费计划
+			EnterpriseDataPlanCode  string                  `json:"enterpriseDataPlanCode"`  // 企业资费计划编码
+			EstimatedExpirationTime string                  `json:"estimatedExpirationTime"` // 预计失效时间 格式:yyyy-MM-dd HH:mm:ss
+			Iccid                   string                  `json:"iccid"`                   // ICCID
+			PoolInTime              string                  `json:"poolInTime"`              // 入池时间 格式:yyyy-MM-dd HH:mm:ss
+			SimState                AcceleronixSimCardState `json:"simState"`                // 卡状态 AcceleronixSimCardState
+			UsedSms                 float32                 `json:"usedSms"`                 // 当月短信用量(条)
+		} `json:"list"` // 数据集合
+		Pages int64 `json:"pages"` // 总页码
+		Size  int64 `json:"size"`  // 每页数量
+		Total int64 `json:"total"` // 总条数
+	} `json:"data"` // 响应体
+	Msg   string `json:"msg"` // 响应描述
+	ReqId string `json:"reqId"`
+}

+ 29 - 18
service/domain/smartHubDomain.go

@@ -6,14 +6,25 @@ import (
 	"go.uber.org/zap"
 )
 
+type SmartHubSimStatus string
+
+const (
+	SmartHubSimStatusTestReady     SmartHubSimStatus = "TEST_READY"     // 可测试
+	SmartHubSimStatusActivateReady SmartHubSimStatus = "ACTIVATE_READY" // 可激活
+	SmartHubSimStatusActivated     SmartHubSimStatus = "ACTIVATED"      // 已激活
+	SmartHubSimStatusDeactivated   SmartHubSimStatus = "DEACTIVATED"    // 已停用
+	SmartHubSimStatusInventory     SmartHubSimStatus = "INVENTORY"      // 库存
+	SmartHubSimStatusRetired       SmartHubSimStatus = "RETIRED"        // 失效
+)
+
 type SmartHubSimCardInfo struct {
-	GICCID             string `bson:"giccid" json:"giccid"`               // sim卡唯一标识
-	IMEI               string `bson:"imei" json:"imei"`                   // 国际移动用户识别码
-	IMSI               string `bson:"imsi" json:"imsi"`                   // 国际移动用户识别码
-	MSISDN             string `bson:"msisdn" json:"msisdn"`               // 移动用户号码
-	Status             string `bson:"status" json:"status"`               // SimStatus
-	SellTime           string `bson:"sellTime" json:"sellTime"`           // 销售时间
-	ActivatedTime      string `bson:"activatedTime" json:"activatedTime"` // 首次激活时间
+	GICCID             string            `bson:"giccid" json:"giccid"`               // sim卡唯一标识
+	IMEI               string            `bson:"imei" json:"imei"`                   // 国际移动用户识别码
+	IMSI               string            `bson:"imsi" json:"imsi"`                   // 国际移动用户识别码
+	MSISDN             string            `bson:"msisdn" json:"msisdn"`               // 移动用户号码
+	Status             SmartHubSimStatus `bson:"status" json:"status"`               // SmartHubSimStatus
+	SellTime           string            `bson:"sellTime" json:"sellTime"`           // 销售时间
+	ActivatedTime      string            `bson:"activatedTime" json:"activatedTime"` // 首次激活时间
 	CurrentCycleUsages struct {
 		Name       string  `bson:"name" json:"name"`             // 周期名称 exp.2018-10
 		DataUsage  float64 `bson:"dataUsage" json:"dataUsage"`   // 流量用量 KB
@@ -44,9 +55,9 @@ type SmartHubSimCardInfoItem struct {
 		IMSI   string `bson:"imsi" json:"imsi"`     // 国际移动用户识别码
 		MSISDN string `bson:"msisdn" json:"msisdn"` // 移动用户号码
 	} `bson:"deviceInfo" json:"deviceInfo"`
-	Status             string `bson:"status" json:"status"`               // SimStatus
-	SellTime           string `bson:"sellTime" json:"sellTime"`           // 销售时间
-	ActivatedTime      string `bson:"activatedTime" json:"activatedTime"` // 首次激活时间
+	Status             SmartHubSimStatus `bson:"status" json:"status"`               // SmartHubSimStatus
+	SellTime           string            `bson:"sellTime" json:"sellTime"`           // 销售时间
+	ActivatedTime      string            `bson:"activatedTime" json:"activatedTime"` // 首次激活时间
 	CurrentCycleUsages struct {
 		DataUsage  float64 `bson:"dataUsage" json:"dataUsage"`   // 流量用量 KB
 		SmsUsage   uint    `bson:"smsUsage" json:"smsUsage"`     // 短信用量 条
@@ -75,14 +86,14 @@ type SmartHubSimCardInfoList struct {
 }
 
 type SmartHubSimCardInfoStorage struct {
-	SN                 string `bson:"sn" json:"sn"`                       // 当前使用设备的SN
-	GICCID             string `bson:"_id" json:"giccid"`                  // sim卡唯一标识
-	IMEI               string `bson:"imei" json:"imei"`                   // 国际移动用户识别码
-	IMSI               string `bson:"imsi" json:"imsi"`                   // 国际移动用户识别码
-	MSISDN             string `bson:"msisdn" json:"msisdn"`               // 移动用户号码
-	Status             string `bson:"status" json:"status"`               // SimStatus
-	SellTime           string `bson:"sellTime" json:"sellTime"`           // 销售时间
-	ActivatedTime      string `bson:"activatedTime" json:"activatedTime"` // 首次激活时间
+	SN                 string            `bson:"sn" json:"sn"`                       // 当前使用设备的SN
+	GICCID             string            `bson:"_id" json:"giccid"`                  // sim卡唯一标识
+	IMEI               string            `bson:"imei" json:"imei"`                   // 国际移动用户识别码
+	IMSI               string            `bson:"imsi" json:"imsi"`                   // 国际移动用户识别码
+	MSISDN             string            `bson:"msisdn" json:"msisdn"`               // 移动用户号码
+	Status             SmartHubSimStatus `bson:"status" json:"status"`               // SmartHubSimStatus
+	SellTime           string            `bson:"sellTime" json:"sellTime"`           // 销售时间
+	ActivatedTime      string            `bson:"activatedTime" json:"activatedTime"` // 首次激活时间
 	CurrentCycleUsages struct {
 		Name       string  `bson:"name" json:"name"`             // 周期名称 exp.2018-10
 		DataUsage  float64 `bson:"dataUsage" json:"dataUsage"`   // 流量用量 KB

+ 17 - 0
service/dto/faultRecordDTO.go

@@ -0,0 +1,17 @@
+package dto
+
+type FaultRecordSubParamItem struct {
+	Id  int    `json:"id"`
+	Val string `json:"val"`
+}
+
+type FaultRecordSubParam struct {
+	Type     string                    `json:"type"` // 'pe' or 'agate'
+	Sn       string                    `json:"sn"`   // aGate/aPower SN
+	ParaList []FaultRecordSubParamItem `json:"paraList"`
+}
+
+type FaultRecordParamInfo struct {
+	Sn   string                `json:"sn"` // aGate SN
+	List []FaultRecordSubParam `json:"list"`
+}

+ 19 - 190
service/repositories/api/acceleronix/simCardManage.go

@@ -2,190 +2,14 @@ package acceleronix
 
 import (
 	"encoding/json"
+	"fwh-toolbox-server/domain"
 
 	"go.uber.org/zap"
 )
 
-// SIM卡状态
-type AcceleronixSimCardState int
-
-const (
-	SCS_PreActivation   AcceleronixSimCardState = iota + 2 // 2 预激活
-	SCS_Test                                               // 3 测试
-	SCS_Silence                                            // 4 沉默
-	SCS_Standby                                            // 5 待机
-	SCS_Activated                                          // 6 激活
-	SCS_Deactivated                                        // 7 停机
-	SCS_Pause                                              // 8 暂停
-	SCS_PreCancellation                                    // 9 预注销
-	SCS_Cancellation                                       // 10 注销
-)
-
-// 查询SIM列表 - 请求结构体
-type AcceleronixSimCardListRequestBody struct {
-	EnterpriseCode      *string                  `json:"enterpriseCode,omitempty"`      // 企业编码
-	EnterpriseDataPlan  *string                  `json:"enterpriseDataPlan,omitempty"`  // 企业资费计划名称
-	ExpirationTimeEnd   *string                  `json:"expirationTimeEnd,omitempty"`   // 支持到期时间区间段查询,这里指截止到期时间 格式:yyyy-MM-dd
-	ExpirationTimeStart *string                  `json:"expirationTimeStart,omitempty"` // 支持到期时间区间段查询,这里指起始到期时间 格式:yyyy-MM-dd
-	IccidEnd            *string                  `json:"iccidEnd,omitempty"`            // 支持ICCID号段查询,这里指ICCID截止号码
-	IccidStart          *string                  `json:"iccidStart,omitempty"`          // 支持ICCID号段查询,这里指ICCID起始号码
-	Label               *string                  `json:"label,omitempty"`               // 标签
-	PageNum             uint                     `json:"pageNum"`                       // 页码 默认值:1
-	PageSize            uint                     `json:"pageSize"`                      // 每页记录数 默认值:10 最大值:1000
-	SimState            *AcceleronixSimCardState `json:"simState,omitempty"`            // 卡状态 AcceleronixSimCardState
-	SimType             *string                  `json:"simType,omitempty"`             // 卡类型
-}
-
-// 查询SIM列表 - 响应
-type AcceleronixSimCardListResponse struct {
-	Code int `json:"code"` // 响应码
-	Data struct {
-		Current int64 `json:"current"` // 当前页码
-		List    []struct {
-			ActivationTime       string                  `json:"activationTime"`       // 激活时间 格式:yyyy-MM-ddd
-			Enterprise           string                  `json:"enterprise"`           // 归属企业
-			EnterpriseDataPlan   string                  `json:"enterpriseDataPlan"`   // 企业资费计划
-			ExpirationTime       string                  `json:"expirationTime"`       // 到期时间 格式:yyyy-MM-dd
-			Iccid                string                  `json:"iccid"`                // ICCID
-			Imei                 string                  `json:"imei"`                 // IMEI
-			Imsi                 string                  `json:"imsi"`                 // IMSI
-			InitialTime          string                  `json:"initialTime"`          // 开卡时间 格式:yyyy-MM-dd
-			Label                string                  `json:"label"`                // 标签,多个标签时逗号隔开
-			Msisdn               string                  `json:"msisdn"`               // MSISDN
-			NetworkType          string                  `json:"networkType"`          // 网络制式 1:ALL 2:Cat-M 3:LTE-M
-			SimId                string                  `json:"simId"`                // SIM ID
-			SimState             AcceleronixSimCardState `json:"simState"`             // 卡状态 AcceleronixSimCardState
-			SimStateChangeReason string                  `json:"simStateChangeReason"` // 卡状态变更原因
-			SimType              string                  `json:"simType"`              // 卡类型
-		} `json:"list"` // 数据集合
-		Pages int64 `json:"pages"` // 总页码
-		Size  int64 `json:"size"`  // 每页数量
-		Total int64 `json:"total"` // 总条数
-	} `json:"data"` // 响应体
-	Msg   string `json:"msg"` // 响应描述
-	ReqId string `json:"reqId"`
-}
-
-// 查询SIM卡详情 - 响应
-type AcceleronixSimCardInfoResponse struct {
-	Code int `json:"code"` // 响应码
-	Data struct {
-		ActivationTime          string                  `json:"activationTime"`          // 激活时间 格式:yyyy-MM-dd
-		Apn                     string                  `json:"apn"`                     // APN
-		CountryRegion           string                  `json:"countryRegion"`           // 所在国家/地区
-		Enterprise              string                  `json:"enterprise"`              // 归属企业
-		EnterpriseDataPlan      string                  `json:"enterpriseDataPlan"`      // 企业资费计划
-		EnterpriseDataPlanCode  string                  `json:"enterpriseDataPlanCode"`  // 计划编码
-		ExpirationTime          string                  `json:"expirationTime"`          // 到期时间 格式:yyyy-MM-dd
-		FirstActivationCdrTime  string                  `json:"firstActivationCdrTime"`  // 激活首次使用时间,格式yyyy-MM-dd
-		Iccid                   string                  `json:"iccid"`                   // ICCID
-		Imei                    string                  `json:"imei"`                    // IMEI
-		Imsi                    string                  `json:"imsi"`                    // IMSI
-		InitialTime             string                  `json:"initialTime"`             // 开卡时间 格式:yyyy-MM-dd
-		Label                   string                  `json:"label"`                   // 标签
-		LastSessionTime         string                  `json:"lastSessionTime"`         // 上次会话时间 格式:yyyy-MM-dd HH:mm:ss
-		Msisdn                  string                  `json:"msisdn"`                  // MSISDN
-		NetCommStatus           int32                   `json:"netCommStatus"`           // 网络通讯状态:0:关闭 1:开启
-		NetworkType             string                  `json:"networkType"`             // 网络制式
-		OperatorNetwork         string                  `json:"operatorNetwork"`         // 所在运营商网络
-		Rat                     string                  `json:"rat"`                     // RAT
-		SimId                   string                  `json:"simId"`                   // SIM ID
-		SimState                AcceleronixSimCardState `json:"simState"`                // 卡状态 AcceleronixSimCardState
-		SimStateChangeReason    string                  `json:"simStateChangeReason"`    // 卡状态变更原因
-		SimType                 string                  `json:"simType"`                 // 卡类型
-		UsedDataOfCurrentPeriod int64                   `json:"usedDataOfCurrentPeriod"` // 当前周期数据用量(B)
-	} `json:"data"` // 响应体
-	Msg   string `json:"msg"` // 响应描述
-	ReqId string `json:"reqId"`
-}
-
-// 批量查询SIM卡流量 - 请求结构体
-type AcceleronixBatchDataUsageRequestBody struct {
-	Iccids []string `json:"iccids"` // ICCID 最多支持100张卡批量操作
-	Month  string   `json:"month"`  // 统计月份,yyyyMM 日期格式化字符串
-}
-
-// 批量查询SIM卡流量 - 响应
-type AcceleronixSimCardDataUsageInfoResponse struct {
-	DataUsageDetails []struct {
-		DataAllowance          string `json:"dataAllowance"`          // 总流量 单位:MB
-		DataUsage              string `json:"dataUsage"`              // 已用流量 单位:MB
-		OrderName              string `json:"orderName"`              // 订购名
-		OutsideRegionDataUsage string `json:"outsideRegionDataUsage"` // 超区流量 单位:MB
-		Type                   int32  `json:"type"`                   // 流量类型 1:激活期资费套餐 2:测试期资费套餐 3:流量包
-	} `json:"dataUsageDetails"` // 流量详细
-	Iccid                  string `json:"iccid"`                  // ICCID
-	Month                  string `json:"month"`                  // 统计月份 格式:yyyyMM
-	OutsideRegionDataUsage string `json:"outsideRegionDataUsage"` // 超区流量 单位:MB
-	RemainingData          string `json:"remainingData"`          // 剩余流量(若无使用上限,剩余流量也为0) 单位:MB
-	TotalDataAllowance     string `json:"totalDataAllowance"`     // 总流量 单位:MB
-	TotalDataUsage         string `json:"totalDataUsage"`         // 已用流量 单位:MB
-}
-
-// 查询流量池列表 - 请求结构体
-type AcceleronixDataPoolListRequestBody struct {
-	EnterpriseCode *string `json:"enterpriseCode,omitempty"` // 企业编码。默认查询范围:本企业。可通过“子企业编码”查询子企业流量池
-	DataPoolId     *string `json:"dataPoolId"`               // 流量池ID
-	PageNum        uint    `json:"pageNum"`                  // 页码 默认值:1
-	PageSize       uint    `json:"pageSize"`                 // 每页记录数 默认值:10 最大值:1000
-}
-
-// 查询流量池列表 - 响应
-type AcceleronixDataPoolListResponse struct {
-	Code int `json:"code"` // 响应码
-	Data struct {
-		Current int64 `json:"current"` // 当前页码
-		List    []struct {
-			DataPlanAllowance   string  `json:"dataPlanAllowance"`   // 资费计划额度(MB)
-			DataPoolId          string  `json:"dataPoolId"`          // 流量池ID
-			DataPoolName        string  `json:"dataPoolName"`        // 流量池名称
-			DataUsage           float32 `json:"dataUsage"`           // 数据用量(MB)
-			EffectiveSims       int64   `json:"effectiveSims"`       // 生效SIM数
-			EnterpriseName      string  `json:"enterpriseName"`      // 企业名称
-			Excessive           float32 `json:"excessive"`           // 超池用量(MB)
-			RemainingData       float32 `json:"remainingData"`       // 剩余用量(MB)
-			RemainingPercentage float32 `json:"remainingPercentage"` // 剩余百分比(%)
-			TotalData           float32 `json:"totalData"`           // 池总量(MB)
-			TotalSims           int64   `json:"totalSims"`           // 总SIM数
-			TotalVolume         float32 `json:"totalVolume"`         // 流量包总量(MB)
-			UsedSms             int64   `json:"usedSms"`             // 短信用量(条)
-		} `json:"list"` // 数据集合
-		Pages int64 `json:"pages"` // 总页码
-		Size  int64 `json:"size"`  // 每页数量
-		Total int64 `json:"total"` // 总条数
-	} `json:"data"` // 响应体
-	Msg   string `json:"msg"` // 响应描述
-	ReqId string `json:"reqId"`
-}
-
-// 查询流量池SIM详情 - 响应
-type AcceleronixDataPoolSimInfoResponse struct {
-	Code int `json:"code"` // 响应码
-	Data struct {
-		Current int64 `json:"current"` // 当前页码
-		List    []struct {
-			DataSharingState        int32                   `json:"dataSharingState"`        // 数据共享状态 0:未生效 1:生效
-			DataUsage               float32                 `json:"dataUsage"`               // 当月数据用量(MB)
-			EffectiveTime           string                  `json:"effectiveTime"`           // 生效时间 格式:yyyy-MM-dd HH:mm:ss
-			EnterpriseDataPlan      string                  `json:"enterpriseDataPlan"`      // 企业资费计划
-			EnterpriseDataPlanCode  string                  `json:"enterpriseDataPlanCode"`  // 企业资费计划编码
-			EstimatedExpirationTime string                  `json:"estimatedExpirationTime"` // 预计失效时间 格式:yyyy-MM-dd HH:mm:ss
-			Iccid                   string                  `json:"iccid"`                   // ICCID
-			PoolInTime              string                  `json:"poolInTime"`              // 入池时间 格式:yyyy-MM-dd HH:mm:ss
-			SimState                AcceleronixSimCardState `json:"simState"`                // 卡状态 AcceleronixSimCardState
-			UsedSms                 float32                 `json:"usedSms"`                 // 当月短信用量(条)
-		} `json:"list"` // 数据集合
-		Pages int64 `json:"pages"` // 总页码
-		Size  int64 `json:"size"`  // 每页数量
-		Total int64 `json:"total"` // 总条数
-	} `json:"data"` // 响应体
-	Msg   string `json:"msg"` // 响应描述
-	ReqId string `json:"reqId"`
-}
-
 // 查询SIM卡列表
-func QuerySimCardList(page uint, size uint) (*AcceleronixSimCardListResponse, error) {
-	res, err := acceleronixHttpPost("/sim/page", AcceleronixSimCardListRequestBody{
+func QuerySimCardList(page uint, size uint) (*domain.AcceleronixSimCardListResponse, error) {
+	res, err := acceleronixHttpPost("/sim/page", domain.AcceleronixSimCardListRequestBody{
 		PageNum:  page,
 		PageSize: size,
 	})
@@ -194,8 +18,9 @@ func QuerySimCardList(page uint, size uint) (*AcceleronixSimCardListResponse, er
 		return nil, err
 	}
 
-	var resList AcceleronixSimCardListResponse
+	var resList domain.AcceleronixSimCardListResponse
 	if err := json.Unmarshal(res.Body, &resList); err != nil {
+		zap.L().Error("QuerySimCardList Unmarshal failed.", zap.String("err", err.Error()), zap.String("body", string(res.Body)))
 		return nil, err
 	}
 
@@ -203,7 +28,7 @@ func QuerySimCardList(page uint, size uint) (*AcceleronixSimCardListResponse, er
 }
 
 // 查询SIM卡详情
-func QuerySimCardInfo(iccid string) (*AcceleronixSimCardInfoResponse, error) {
+func QuerySimCardInfo(iccid string) (*domain.AcceleronixSimCardInfoResponse, error) {
 	res, err := acceleronixHttpPost("/sim/detail", map[string]interface{}{
 		"iccid": iccid,
 	})
@@ -212,8 +37,9 @@ func QuerySimCardInfo(iccid string) (*AcceleronixSimCardInfoResponse, error) {
 		return nil, err
 	}
 
-	var resInfo AcceleronixSimCardInfoResponse
+	var resInfo domain.AcceleronixSimCardInfoResponse
 	if err := json.Unmarshal(res.Body, &resInfo); err != nil {
+		zap.L().Error("QuerySimCardInfo Unmarshal failed.", zap.String("err", err.Error()), zap.String("body", string(res.Body)))
 		return nil, err
 	}
 
@@ -222,8 +48,8 @@ func QuerySimCardInfo(iccid string) (*AcceleronixSimCardInfoResponse, error) {
 
 // 批量获取SIM卡数据用量信息
 // month 格式YYYYMM exp.202301
-func QueryBatchSimCardDataInfo(iccidList []string, month string) ([]AcceleronixSimCardDataUsageInfoResponse, error) {
-	res, err := acceleronixHttpPost("/sim/batchQueryMonthData", AcceleronixBatchDataUsageRequestBody{
+func QueryBatchSimCardDataInfo(iccidList []string, month string) ([]domain.AcceleronixSimCardDataUsageInfoResponse, error) {
+	res, err := acceleronixHttpPost("/sim/batchQueryMonthData", domain.AcceleronixBatchDataUsageRequestBody{
 		Iccids: iccidList,
 		Month:  month,
 	})
@@ -232,8 +58,9 @@ func QueryBatchSimCardDataInfo(iccidList []string, month string) ([]AcceleronixS
 		return nil, err
 	}
 
-	var resList []AcceleronixSimCardDataUsageInfoResponse
+	var resList []domain.AcceleronixSimCardDataUsageInfoResponse
 	if err := json.Unmarshal(res.Body, &resList); err != nil {
+		zap.L().Error("QueryBatchSimCardDataInfo Unmarshal failed.", zap.String("err", err.Error()), zap.String("body", string(res.Body)))
 		return nil, err
 	}
 
@@ -241,8 +68,8 @@ func QueryBatchSimCardDataInfo(iccidList []string, month string) ([]AcceleronixS
 }
 
 // 查询流量池列表
-func QueryDataPoolList(page uint, size uint) (*AcceleronixDataPoolListResponse, error) {
-	res, err := acceleronixHttpPost("/dataPool/page", AcceleronixDataPoolListRequestBody{
+func QueryDataPoolList(page uint, size uint) (*domain.AcceleronixDataPoolListResponse, error) {
+	res, err := acceleronixHttpPost("/dataPool/page", domain.AcceleronixDataPoolListRequestBody{
 		PageNum:  page,
 		PageSize: size,
 	})
@@ -251,8 +78,9 @@ func QueryDataPoolList(page uint, size uint) (*AcceleronixDataPoolListResponse,
 		return nil, err
 	}
 
-	var resList AcceleronixDataPoolListResponse
+	var resList domain.AcceleronixDataPoolListResponse
 	if err := json.Unmarshal(res.Body, &resList); err != nil {
+		zap.L().Error("QueryDataPoolList Unmarshal failed.", zap.String("err", err.Error()), zap.String("body", string(res.Body)))
 		return nil, err
 	}
 
@@ -260,7 +88,7 @@ func QueryDataPoolList(page uint, size uint) (*AcceleronixDataPoolListResponse,
 }
 
 // 查询流量池SIM详情
-func QueryDataPoolSimInfo(dataPoolId string, page uint, size uint) (*AcceleronixDataPoolSimInfoResponse, error) {
+func QueryDataPoolSimInfo(dataPoolId string, page uint, size uint) (*domain.AcceleronixDataPoolSimInfoResponse, error) {
 	res, err := acceleronixHttpPost("/dataPool/detail", map[string]interface{}{
 		"dataPoolId": dataPoolId,
 		"pageNum":    page,
@@ -271,8 +99,9 @@ func QueryDataPoolSimInfo(dataPoolId string, page uint, size uint) (*Acceleronix
 		return nil, err
 	}
 
-	var resInfo AcceleronixDataPoolSimInfoResponse
+	var resInfo domain.AcceleronixDataPoolSimInfoResponse
 	if err := json.Unmarshal(res.Body, &resInfo); err != nil {
+		zap.L().Error("QueryDataPoolSimInfo Unmarshal failed.", zap.String("err", err.Error()), zap.String("body", string(res.Body)))
 		return nil, err
 	}
 

+ 52 - 0
service/repositories/api/acceleronix/test/main.go

@@ -0,0 +1,52 @@
+package main
+
+import (
+	"fmt"
+	"fwh-toolbox-server/repositories/api/acceleronix"
+)
+
+func main() {
+	// 读取SIM卡列表
+	list, err := acceleronix.QuerySimCardList(1, 10)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println("SIM卡列表前10项:")
+	fmt.Println(list)
+
+	// 查询SIM卡详情
+	info, err := acceleronix.QuerySimCardInfo(list.Data.List[0].Iccid)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println("查询SIM卡详情:")
+	fmt.Println(info)
+
+	// 批量获取SIM卡数据用量信息
+	simIdList := []string{}
+	for _, simInfo := range list.Data.List {
+		simIdList = append(simIdList, simInfo.Iccid)
+	}
+	usageInfo, err := acceleronix.QueryBatchSimCardDataInfo(simIdList, "202606")
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println("批量获取SIM卡数据用量信息:")
+	fmt.Println(usageInfo)
+
+	// 查询流量池列表
+	poolList, err := acceleronix.QueryDataPoolList(1, 10)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println("查询流量池列表前10项:")
+	fmt.Println(poolList)
+
+	// 查询流量池SIM详情
+	poolSimInfo, err := acceleronix.QueryDataPoolSimInfo(poolList.Data.List[0].DataPoolId, 1, 10)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println("查询流量池SIM详情前10项:")
+	fmt.Println(poolSimInfo)
+}

+ 31 - 18
service/repositories/api/fwh_pro/fwh_touTemplate.go

@@ -2,8 +2,10 @@ package fwh_pro
 
 import (
 	"fmt"
-	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/dto"
 	"net/url"
+
+	"go.uber.org/zap"
 )
 
 type SelectTouTemplateListPayload struct {
@@ -20,31 +22,42 @@ type SelectTouTemplateListPayload struct {
 }
 
 // 查询模板列表
-func QueryTouTemplateInfoList(search string, pageSize uint, pageNum uint) (*domain.FwhResponse[[]interface{}], error) {
-	// 要发送的 JSON
-	payload := SelectTouTemplateListPayload{
-		Search:   search,
-		PageSize: pageSize,
-		PageNum:  pageNum,
-		Lang:     "ZH_CN",
-	}
-
+func QueryTouTemplateInfoList(search string, pageSize uint, pageNum uint, sourceType *int, status *int) (*dto.ResPage[interface{}], error) {
 	// 要发送的表单数据
 	form := url.Values{}
-	form.Set("search", payload.Search)
-	form.Set("pageSize", fmt.Sprintf("%d", payload.PageSize))
-	form.Set("pageNum", fmt.Sprintf("%d", payload.PageNum))
-	form.Set("lang", payload.Lang)
+	form.Set("search", search)
+	if sourceType != nil {
+		form.Set("sourceType", fmt.Sprintf("%d", *sourceType))
+	}
+	if status != nil {
+		form.Set("status", fmt.Sprintf("%d", *status))
+	}
+	form.Set("pageSize", fmt.Sprintf("%d", pageSize))
+	form.Set("pageNum", fmt.Sprintf("%d", pageNum))
+	form.Set("lang", "ZH_CN")
+	zap.L().Info("QueryTouTemplateInfoList form", zap.Any("form", form))
 
-	response, err := FwhHttpPostUnmarshal[[]interface{}](getFullEnergyUrl("/hes-gateway/manage/selectIotTouTemplateList"), form)
+	response, err := FwhHttpPostUnmarshalPage[[]interface{}](getFullEnergyUrl("/hes-gateway/manage/selectIotTouTemplateList"), form)
 	if err != nil {
 		return nil, err
 	}
-	return response, nil
+
+	var data []interface{}
+	if response.Result != nil {
+		data = *response.Result
+	}
+	res := dto.ResPage[interface{}]{
+		Data:     data,
+		PageSize: pageSize,
+		PageNum:  pageNum,
+		Total:    uint(response.Total),
+	}
+
+	return &res, nil
 }
 
 // 查询模板列表
-func QueryTouTemplateDetailInfo(id uint) (*domain.FwhResponse[interface{}], error) {
+func QueryTouTemplateDetailInfo(id uint) (*interface{}, error) {
 	// 要发送的表单数据
 	form := url.Values{}
 	form.Set("id", fmt.Sprintf("%d", id))
@@ -54,5 +67,5 @@ func QueryTouTemplateDetailInfo(id uint) (*domain.FwhResponse[interface{}], erro
 	if err != nil {
 		return nil, err
 	}
-	return response, nil
+	return response.Result, nil
 }

+ 20 - 26
service/repositories/api/smartHub/simCardManage.go

@@ -2,6 +2,7 @@ package smartHub
 
 import (
 	"encoding/json"
+	"fmt"
 	"fwh-toolbox-server/domain"
 	"net/url"
 	"strconv"
@@ -9,32 +10,6 @@ import (
 	"go.uber.org/zap"
 )
 
-type SimStatus string
-
-const (
-	SimStatusTestReady     SimStatus = "TEST_READY"     // 可测试
-	SimStatusActivateReady SimStatus = "ACTIVATE_READY" // 可激活
-	SimStatusActivated     SimStatus = "ACTIVATED"      // 已激活
-	SimStatusDeactivated   SimStatus = "DEACTIVATED"    // 已停用
-	SimStatusInventory     SimStatus = "INVENTORY"      // 库存
-	SimStatusRetired       SimStatus = "RETIRED"        // 失效
-)
-
-// 获取SIM卡详细信息
-func QuerySimCardInfo(giccid string) (*domain.SmartHubSimCardInfo, error) {
-	res, err := smartHttpGet("/global-sims/"+giccid, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	var info domain.SmartHubSimCardInfo
-	if err := json.Unmarshal(res.Body, &info); err != nil {
-		return nil, err
-	}
-
-	return &info, nil
-}
-
 // 批量获取SIM卡详细信息
 func QueryBatchSimCardInfo(page uint, size uint) (*domain.SmartHubSimCardInfoList, error) {
 	// 构造 query 参数
@@ -48,6 +23,8 @@ func QueryBatchSimCardInfo(page uint, size uint) (*domain.SmartHubSimCardInfoLis
 	}
 
 	var infoList domain.SmartHubSimCardInfoList
+	fmt.Println(res.Header)
+	fmt.Println(string(res.Body))
 	if err := json.Unmarshal(res.Body, &infoList); err != nil {
 		zap.L().Error("QueryBatchSimCardInfo Unmarshal failed.", zap.String("body", string(res.Body)), zap.String("err", err.Error()))
 		return nil, err
@@ -55,3 +32,20 @@ func QueryBatchSimCardInfo(page uint, size uint) (*domain.SmartHubSimCardInfoLis
 
 	return &infoList, nil
 }
+
+// 获取SIM卡详细信息
+func QuerySimCardInfo(giccid string) (*domain.SmartHubSimCardInfo, error) {
+	res, err := smartHttpGet("/global-sims/"+giccid, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var info domain.SmartHubSimCardInfo
+	fmt.Println(string(res.Body))
+	if err := json.Unmarshal(res.Body, &info); err != nil {
+		zap.L().Error("QuerySimCardInfo Unmarshal failed.", zap.String("err", err.Error()), zap.String("body", string(res.Body)))
+		return nil, err
+	}
+
+	return &info, nil
+}

+ 24 - 0
service/repositories/api/smartHub/test/main.go

@@ -0,0 +1,24 @@
+package main
+
+import (
+	"fmt"
+	"fwh-toolbox-server/repositories/api/smartHub"
+)
+
+func main() {
+	// 批量获取SIM卡详细信息
+	infoList, err := smartHub.QueryBatchSimCardInfo(1, 50)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println("批量获取SIM卡详细信息前10项:")
+	fmt.Println(infoList)
+
+	// 获取SIM卡详细信息
+	info, err := smartHub.QuerySimCardInfo(infoList.Data[0].GICCID)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println("获取SIM卡详细信息:")
+	fmt.Println(info)
+}

+ 159 - 0
service/repositories/mongo/simCardManageRepo.go

@@ -0,0 +1,159 @@
+package mongo
+
+import (
+	"context"
+	"fwh-toolbox-server/database"
+	"fwh-toolbox-server/dto"
+	"time"
+
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+	CollectionName_SimCardManage      = "SimCardManage"
+	CollectionName_SimCardMonthlyData = "SimCardMonthlyData"
+)
+
+type SimStatus string
+
+const (
+	SmartHubSimStatusNotActivated SimStatus = "NOT_ACTIVATED" // 未激活
+	SmartHubSimStatusActivated    SimStatus = "ACTIVATED"     // 已激活
+	SmartHubSimStatusDeactivated  SimStatus = "DEACTIVATED"   // 已停用
+	SmartHubSimStatusRetired      SimStatus = "RETIRED"       // 失效
+)
+
+type SimCardInfo struct {
+	GICCID         string    `bson:"_id" json:"giccid"`                    // 只取前19位不取检验码
+	IMEI           string    `bson:"imei" json:"imei"`                     // 国际移动用户识别码
+	IMSI           string    `bson:"imsi" json:"imsi"`                     // 国际移动用户识别码
+	MSISDN         string    `bson:"msisdn" json:"msisdn"`                 // 移动用户号码
+	Status         SimStatus `bson:"status" json:"status"`                 // 卡状态
+	ExpirationTime string    `bson:"expirationTime" json:"expirationTime"` // 到期时间(后付费卡没有到期时间) 格式:yyyy-MM-dd
+	Month          string    `bson:"month" json:"month"`                   // 统计月份 格式:yyyy-MM
+	AllDataUsage   float64   `bson:"allDataUsage" json:"allDataUsage"`     // 月数据用量(KB)
+	UpdateTime     time.Time `bson:"updateTime" json:"updateTime"`         // UTC时间
+}
+
+type SimCardMonthlyData struct {
+	SimId        string                       `bson:"_id" json:"simId"`                 // GICCID
+	UpdateTime   time.Time                    `bson:"updateTime" json:"updateTime"`     // UTC时间
+	Month        string                       `bson:"month" json:"month"`               // 统计月份 格式:yyyy-MM
+	AllDataUsage float64                      `bson:"allDataUsage" json:"allDataUsage"` // 月数据用量(KB)
+	DailyUsage   map[string]SimCardDailyUsage `bson:"dailyUsage" json:"dailyUsage"`     // 日用量数据信息
+}
+
+type SimCardDailyUsage struct {
+	UpdateTime time.Time `bson:"updateTime" json:"updateTime"` // UTC时间
+	Day        string    `bson:"day" json:"day"`               // 统计日期 格式:yyyy-MM-DD
+	DataUsage  float64   `bson:"dataUsage" json:"dataUsage"`   // 数据用量(KB)
+}
+
+// 查询SIM卡信息(批量)
+func GetSimCardBatchInfo(page, size uint) (*dto.ResPage[SimCardInfo], error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_SimCardManage)
+
+	findOptions := options.Find().
+		SetSkip(int64((page - 1) * size)).
+		SetLimit(int64(size))
+
+	cursor, err := collection.Find(ctx, bson.M{}, findOptions)
+	if err != nil {
+		return nil, err
+	}
+	defer cursor.Close(ctx)
+
+	var infoList []SimCardInfo
+	for cursor.Next(ctx) {
+		var info SimCardInfo
+		if err := cursor.Decode(&info); err != nil {
+			return nil, err
+		}
+		infoList = append(infoList, info)
+	}
+
+	// 查询总数
+	total, err := collection.CountDocuments(ctx, bson.M{})
+	if err != nil {
+		return nil, err
+	}
+
+	res := dto.ResPage[SimCardInfo]{
+		Data:     infoList,
+		PageSize: size,
+		PageNum:  page,
+		Total:    uint(total),
+	}
+
+	return &res, nil
+}
+
+// 查询SIM卡信息(单卡)
+func GetSimCardInfo(id string) (*SimCardInfo, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_SimCardManage)
+
+	var info SimCardInfo
+	err := collection.FindOne(ctx, bson.M{"_id": id}).Decode(&info)
+	if err != nil {
+		return nil, err
+	}
+
+	return &info, nil
+}
+
+// 查询SIM卡月数据(单卡)
+func GetSimCardMonthlyData(month string, id string) (*SimCardMonthlyData, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_SimCardMonthlyData)
+
+	var info SimCardMonthlyData
+	err := collection.FindOne(ctx, bson.M{"_id": id}).Decode(&info)
+	if err != nil {
+		return nil, err
+	}
+
+	return &info, nil
+}
+
+// 插入SIM卡基本信息数据
+func UpdateSimCardInfo(info SimCardInfo) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_SimCardManage)
+
+	filter := bson.M{"_id": info.GICCID} // 按ID匹配
+
+	_, err := collection.ReplaceOne(ctx, filter, info, options.Replace().SetUpsert(true))
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// 插入SIM卡月流量数据
+func UpdateSimCardMonthlyData(data SimCardMonthlyData) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_SimCardMonthlyData)
+
+	filter := bson.M{"_id": data.SimId} // 按ID匹配
+
+	_, err := collection.ReplaceOne(ctx, filter, data, options.Replace().SetUpsert(true))
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 31 - 19
service/repositories/mqtt/fwh_mqtt/fwh_mqtt_client.go

@@ -19,6 +19,9 @@ const (
 	fwhClientIdPrefix  = "fwh_xlog_mqtt_client_"
 	sleepTimeout       = 5 * time.Minute
 	sendMessageTimeout = 1 * time.Minute
+
+	topicPrefixS2C = "fwh/s2c/"
+	topicPrefixC2S = "fwh/c2s/"
 )
 
 type FwhMqttResult struct {
@@ -40,8 +43,8 @@ type FwhMqttMsg struct {
 type MqttMsg struct {
 	Topic      string // 发送 Topic
 	Msg        FwhMqttMsg
-	SubTopic   *string // 响应 Topic
-	SubCmdType *uint
+	ResTopic   *string // 响应 Topic
+	ResCmdType *uint
 	ResultCh   chan FwhMqttResult
 	Ctx        context.Context
 }
@@ -107,13 +110,15 @@ func (c *FwhMqttClient) fwhMqttWorker() {
 				break
 			}
 
-			if err := c.Client.Connect(); err != nil {
-				zap.L().Error("FWH MQTT connect failed.", zap.String("msg", fmt.Sprintf("%+v", task)))
-				task.ResultCh <- FwhMqttResult{
-					Err:  fmt.Errorf("FWH MQTT connect failed."),
-					Data: nil,
+			if !c.Client.IsConnected() {
+				if err := c.Client.Connect(); err != nil {
+					zap.L().Error("FWH MQTT connect failed.", zap.String("msg", fmt.Sprintf("%+v", task)), zap.String("err", err.Error()))
+					task.ResultCh <- FwhMqttResult{
+						Err:  fmt.Errorf("FWH MQTT connect failed."),
+						Data: nil,
+					}
+					break
 				}
-				break
 			}
 
 			// 订阅响应消息 - 请求响应无法一一对应,不支持并发请求,改成简单的队列请求
@@ -157,9 +162,9 @@ func (c *FwhMqttClient) fwhMqttWorker() {
 			// }
 
 			var resultCh chan FwhMqttResult
-			if task.SubTopic != nil {
+			if task.ResTopic != nil {
 				resultCh = make(chan FwhMqttResult)
-				if err := c.Client.SubscribeMessage(*task.SubTopic, func(payload []byte) {
+				if err := c.Client.SubscribeMessage(*task.ResTopic, func(payload []byte) {
 					var fwhMqttMsg FwhMqttMsg
 					if err := json.Unmarshal(payload, &fwhMqttMsg); err != nil {
 						zap.L().Error("Result Unmarshal failed.(FwhMqttMsg)", zap.String("topic", task.Topic), zap.String("payload", string(payload)), zap.String("err", err.Error()))
@@ -170,8 +175,8 @@ func (c *FwhMqttClient) fwhMqttWorker() {
 						return
 					}
 
-					if task.SubCmdType != nil {
-						if fwhMqttMsg.CmdType == *task.SubCmdType {
+					if task.ResCmdType != nil {
+						if fwhMqttMsg.CmdType == *task.ResCmdType {
 							resultCh <- FwhMqttResult{
 								Err:  nil,
 								Data: &fwhMqttMsg,
@@ -246,7 +251,7 @@ func (c *FwhMqttClient) fwhMqttWorker() {
 	}
 }
 
-func (c *FwhMqttClient) sendMessage(ctx context.Context, topic string, msg FwhMqttMsg, subTopic *string, subCmdType *uint) (*FwhMqttMsg, error) {
+func (c *FwhMqttClient) sendMessage(ctx context.Context, topicName string, msg FwhMqttMsg, resTopicName *string, resCmdType *uint) (*FwhMqttMsg, error) {
 	if c.Client == nil {
 		return nil, fmt.Errorf("Fwh Mqtt Client Uninitialized.")
 	}
@@ -254,11 +259,18 @@ func (c *FwhMqttClient) sendMessage(ctx context.Context, topic string, msg FwhMq
 	ctxSend, cancel := context.WithTimeout(ctx, sendMessageTimeout)
 	defer cancel()
 
+	topic := topicPrefixS2C + topicName
+	var resTopic *string
+	if resTopicName != nil {
+		resTopicStr := topicPrefixC2S + *resTopicName
+		resTopic = &resTopicStr
+	}
+
 	taskMsg := MqttMsg{
 		Topic:      topic,
 		Msg:        msg,
-		SubTopic:   subTopic,
-		SubCmdType: subCmdType,
+		ResTopic:   resTopic,
+		ResCmdType: resCmdType,
 		ResultCh:   make(chan FwhMqttResult, 1),
 		Ctx:        ctxSend,
 	}
@@ -285,13 +297,13 @@ func (c *FwhMqttClient) sendMessage(ctx context.Context, topic string, msg FwhMq
 }
 
 // 仅发送不等待响应结果
-func (c *FwhMqttClient) sendMessageOnce(ctx context.Context, topic string, msg FwhMqttMsg) error {
-	_, err := c.sendMessage(ctx, topic, msg, nil, nil)
+func (c *FwhMqttClient) sendMessageOnce(ctx context.Context, topicName string, msg FwhMqttMsg) error {
+	_, err := c.sendMessage(ctx, topicName, msg, nil, nil)
 	return err
 }
 
 // 发送消息后,等待响应结果
-func (c *FwhMqttClient) sendMessageWait(ctx context.Context, topic string, msg FwhMqttMsg, subTopic string, subCmdType uint) (*FwhMqttMsg, error) {
-	res, err := c.sendMessage(ctx, topic, msg, &subTopic, &subCmdType)
+func (c *FwhMqttClient) sendMessageWait(ctx context.Context, topicName string, msg FwhMqttMsg, resTopicName string, resCmdType uint) (*FwhMqttMsg, error) {
+	res, err := c.sendMessage(ctx, topicName, msg, &resTopicName, &resCmdType)
 	return res, err
 }

+ 2 - 2
service/repositories/mqtt/fwh_mqtt/fwh_mqtt_debug.go

@@ -20,7 +20,7 @@ func EnableXlog(ctx context.Context, sn string) (string, error) {
 		return "", err
 	}
 
-	res, err := fwhMqttClient.sendMessageWait(ctx, "fwh/s2c/debug/"+sn, FwhMqttMsg{
+	res, err := fwhMqttClient.sendMessageWait(ctx, "debug/"+sn, FwhMqttMsg{
 		CmdType:   2000, // xlog请求
 		Type:      0,    // IBG
 		Snno:      123,
@@ -29,7 +29,7 @@ func EnableXlog(ctx context.Context, sn string) (string, error) {
 		DataArea:  dataArea,
 		Len:       len(dataStr),
 		Crc:       strings.ToUpper(fmt.Sprintf("%08X", crc32.ChecksumIEEE(dataStr))),
-	}, "fwh/c2s/debug/"+sn, 2001)
+	}, "debug/"+sn, 2001)
 	if err != nil {
 		return "", err
 	}

+ 118 - 19
service/repositories/mqtt/fwh_mqtt/fwh_mqtt_fault_record.go

@@ -5,38 +5,44 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"fwh-toolbox-server/dto"
 	"hash/crc32"
 	"strings"
 	"time"
 )
 
-type FaultRecordParaListItem struct {
+const (
+	FaultRecordReqCmdType = 377 // 请求协议类型
+	FaultRecordResCmdType = 378 // 应答协议类型
+)
+
+type ParamSettingParaListItem struct {
 	Id     int    `json:"id"`
 	SetVal string `json:"setVal"`
 }
 
-type FaultRecordInfoItem struct {
-	Sn       string                    `json:"sn"` // aPower SN
-	ParaList []FaultRecordParaListItem `json:"paraList"`
+type ParamSettingInfoItem struct {
+	Sn       string                     `json:"sn"` // aGate/aPower SN
+	ParaList []ParamSettingParaListItem `json:"paraList"`
 }
 
-type FaultRecordListItem struct {
-	Type string                `json:"type"`
-	Info []FaultRecordInfoItem `json:"info"`
+type ParamSettingListItem struct {
+	Type string                 `json:"type"` // 'pe' or 'aGate' or 'mppt'
+	Info []ParamSettingInfoItem `json:"info"`
 }
 
-type FaultRecordData struct {
-	List []FaultRecordListItem `json:"list"`
+type ParamSettingData struct {
+	List []ParamSettingListItem `json:"list"`
 }
 
 type FaultRecordDataArea struct {
-	Cmd  int             `json:"cmd"`
-	Data FaultRecordData `json:"data"`
+	Cmd  int         `json:"cmd"` // 0:实时数据获取;1:参数设置;2:文件获取
+	Data interface{} `json:"data"`
 }
 
-type FaultRecordChannelConfig struct {
-	Sn       string                    `json:"sn"` // aPower SN
-	ParaList []FaultRecordParaListItem `json:"paraList"`
+type FaultRecordResponse struct {
+	Cmd    int `json:"cmd"`    // 0:实时数据获取;1:参数设置;2:文件获取
+	Result int `json:"result"` // 0:成功;-1:失败
 }
 
 func ChargeChannel(ctx context.Context, sn string) error {
@@ -52,19 +58,19 @@ func ChargeChannel(ctx context.Context, sn string) error {
 							"paraList": []interface{}{
 								map[string]interface{}{
 									"id":     6,
-									"setVal": "010300014252",
+									"setVal": "010100015984",
 								},
 								map[string]interface{}{
 									"id":     6,
-									"setVal": "020100014C14",
+									"setVal": "020100015981",
 								},
 								map[string]interface{}{
 									"id":     6,
-									"setVal": "030100013102",
+									"setVal": "030100015983",
 								},
 								map[string]interface{}{
 									"id":     6,
-									"setVal": "040100013103",
+									"setVal": "04030003FD34",
 								},
 							},
 						},
@@ -79,7 +85,7 @@ func ChargeChannel(ctx context.Context, sn string) error {
 		return err
 	}
 
-	err = fwhMqttClient.sendMessageOnce(ctx, "fwh/s2c/mng/"+sn, FwhMqttMsg{
+	err = fwhMqttClient.sendMessageOnce(ctx, "mng/"+sn, FwhMqttMsg{
 		CmdType:   377, // 故障录波请求
 		Type:      0,   // IBG
 		Snno:      123,
@@ -95,3 +101,96 @@ func ChargeChannel(ctx context.Context, sn string) error {
 
 	return nil
 }
+
+func FaultRecordParamSetting(ctx context.Context, data dto.FaultRecordParamInfo) error {
+	siteSn := data.Sn
+
+	peParaList := []ParamSettingInfoItem{}
+	agateParaList := []ParamSettingInfoItem{}
+
+	for _, value := range data.List {
+		var paraSettingList []ParamSettingParaListItem
+		for _, v := range value.ParaList {
+			paraSettingList = append(paraSettingList, ParamSettingParaListItem{
+				Id:     v.Id,
+				SetVal: v.Val,
+			})
+		}
+
+		switch value.Type {
+		case "pe":
+			peParaList = append(peParaList, ParamSettingInfoItem{
+				Sn:       value.Sn,
+				ParaList: paraSettingList,
+			})
+		case "agate":
+			agateParaList = append(agateParaList, ParamSettingInfoItem{
+				Sn:       value.Sn,
+				ParaList: paraSettingList,
+			})
+		}
+	}
+
+	paraData := ParamSettingData{
+		List: []ParamSettingListItem{},
+	}
+
+	if len(agateParaList) > 0 {
+		paraData.List = append(paraData.List, ParamSettingListItem{
+			Type: "aGate",
+			Info: agateParaList,
+		})
+	}
+
+	if len(peParaList) > 0 {
+		paraData.List = append(paraData.List, ParamSettingListItem{
+			Type: "pe",
+			Info: peParaList,
+		})
+	}
+
+	dataArea := FaultRecordDataArea{
+		Cmd:  1, // 参数设置
+		Data: paraData,
+	}
+
+	dataStr, err := json.Marshal(dataArea)
+	if err != nil {
+		return err
+	}
+
+	fmt.Println(string(dataStr))
+
+	msg, err := fwhMqttClient.sendMessageWait(ctx, "mng/"+siteSn, FwhMqttMsg{
+		CmdType:   FaultRecordReqCmdType, // 故障录波请求
+		Type:      0,                     // IBG
+		Snno:      123,
+		TimeStamp: time.Now().Unix(),
+		EquipNo:   siteSn,
+		DataArea:  dataArea,
+		Len:       len(dataStr),
+		Crc:       strings.ToUpper(fmt.Sprintf("%08X", crc32.ChecksumIEEE(dataStr))),
+	}, "mng/"+siteSn, FaultRecordResCmdType)
+	if err != nil {
+		return err
+	}
+
+	if msg == nil {
+		return fmt.Errorf("FaultRecordParamSetting response data is empty.")
+	}
+
+	res, ok := msg.DataArea.(FaultRecordResponse)
+	if !ok {
+		dataAreaStr, err := json.Marshal(msg.DataArea)
+		if err != nil {
+			return err
+		}
+		return fmt.Errorf("FaultRecordParamSetting response data type error.(%s)", dataAreaStr)
+	}
+
+	if res.Result != 0 {
+		return fmt.Errorf("FaultRecord param setting failed.")
+	}
+
+	return nil
+}

+ 17 - 0
service/repositories/repositories.go

@@ -1,6 +1,7 @@
 package repositories
 
 import (
+	"fwh-toolbox-server/repositories/api/acceleronix"
 	"fwh-toolbox-server/repositories/api/fwh_pro"
 	"fwh-toolbox-server/repositories/api/smartHub"
 	"fwh-toolbox-server/repositories/events"
@@ -66,6 +67,14 @@ var QueryTouTemplateDetailInfo = fwh_pro.QueryTouTemplateDetailInfo
 
 // SmartHub
 var QuerySmartHubBatchSimCardInfo = smartHub.QueryBatchSimCardInfo // 批量查询SIM卡信息
+var QuerySmartHubSimCardInfo = smartHub.QuerySimCardInfo           // 获取SIM卡详细信息
+
+// Acceleronix
+var QueryAcceleronixSimCardList = acceleronix.QuerySimCardList                   // 查询SIM卡列表
+var QueryAcceleronixSimCardInfo = acceleronix.QuerySimCardInfo                   // 查询SIM卡详情
+var QueryAcceleronixBatchSimCardDataInfo = acceleronix.QueryBatchSimCardDataInfo // 批量获取SIM卡数据用量信息
+var QueryAcceleronixDataPoolList = acceleronix.QueryDataPoolList                 // 查询流量池列表
+var QueryAcceleronixDataPoolSimInfo = acceleronix.QueryDataPoolSimInfo           // 查询流量池SIM详情
 
 /*************** EVENTS ***************/
 // Xlog
@@ -155,6 +164,13 @@ var GetSiteInfoUpdateNewRecord = mongo.GetSiteInfoUpdateNewRecord
 var GetSiteInfoManageList = mongo.GetSiteInfoManageList
 var InsertSiteInfoData = mongo.InsertSiteInfoData
 
+// SimCardManage
+var GetSimCardBatchInfo = mongo.GetSimCardBatchInfo
+var GetSimCardInfo = mongo.GetSimCardInfo
+var GetSimCardMonthlyData = mongo.GetSimCardMonthlyData
+var UpdateSimCardInfo = mongo.UpdateSimCardInfo
+var UpdateSimCardMonthlyData = mongo.UpdateSimCardMonthlyData
+
 /*************** PGSQL ***************/
 
 // Alarm Snapshot
@@ -205,3 +221,4 @@ var FwhMqttEnableXlog = fwh_mqtt.EnableXlog
 
 // FwhMqtt - Fault Record
 var FwhMqttFaultRecordChargeChannel = fwh_mqtt.ChargeChannel
+var FwhMqttFaultRecordParamSetting = fwh_mqtt.FaultRecordParamSetting

+ 20 - 0
service/routes/despatchConfigRoutes.go

@@ -0,0 +1,20 @@
+/*
+ * Despatch Config API List
+ * /despatchConfig/touTemplatePro/list - GET 查询模版列表
+ * /despatchConfig/touTemplatePro/info - GET 查询模版信息
+ */
+package routes
+
+import (
+	"fwh-toolbox-server/controllers"
+
+	"github.com/gofiber/fiber/v2"
+)
+
+func DespatchConfigSetup(api fiber.Router) {
+	// 查询模版列表
+	api.Get("/despatchConfig/touTemplatePro/list", controllers.QueryTouTemplateProList)
+
+	// 查询模版信息
+	api.Get("/despatchConfig/touTemplatePro/info", controllers.QueryTouTemplateProDetailInfo)
+}

+ 16 - 0
service/routes/faultRecordRoutes.go

@@ -0,0 +1,16 @@
+/*
+ * Fault Record API List
+ * /faultRecord/paramSetting - POST 设置故障录波参数
+ */
+package routes
+
+import (
+	"fwh-toolbox-server/controllers"
+
+	"github.com/gofiber/fiber/v2"
+)
+
+func FaultRecordSetup(api fiber.Router) {
+	// 设置故障录波参数
+	api.Post("/faultRecord/paramSetting", controllers.SetFaultRecordParam)
+}

+ 2 - 0
service/routes/routes.go

@@ -30,6 +30,8 @@ func Setup(app *fiber.App) {
 	AutoUpdateConfigSetup(api)
 	XlogSetup(api)
 	SiteInfoManageSetup(api)
+	DespatchConfigSetup(api)
+	FaultRecordSetup(api)
 
 	app.Get("/swagger/*", swagger.HandlerDefault)
 

+ 9 - 0
service/scheduler/scheduler.go

@@ -11,6 +11,15 @@ import (
 func Init() *cron.Cron {
 	c := cron.New()
 
+	// UTC时间 0点 定时任务
+	c.AddFunc("CRON_TZ=UTC 0 0 * * *", func() {
+		log.Println("run at UTC midnight:", time.Now().UTC())
+		log.Println("run at Local midnight:", time.Now())
+
+		// SIM卡每日信息更新任务
+		
+	})
+
 	// c.AddFunc("@every 1h", func() {
 	// 	log.Println("每小时任务执行")
 	// 	// do job

+ 16 - 0
service/services/despatchConfigServices.go

@@ -0,0 +1,16 @@
+package services
+
+import (
+	"fwh-toolbox-server/dto"
+	"fwh-toolbox-server/repositories"
+)
+
+// 查询模版列表
+func GetTouTemplateProList(pageNum uint, pageSize uint, tempType *int, status *int, search string) (*dto.ResPage[interface{}], error) {
+	return repositories.QueryTouTemplateInfoList(search, pageSize, pageNum, tempType, status)
+}
+
+// 查询模版信息
+func GetTouTemplateProDetailInfo(id uint) (*interface{}, error) {
+	return repositories.QueryTouTemplateDetailInfo(id)
+}

+ 12 - 0
service/services/faultRecordServices.go

@@ -0,0 +1,12 @@
+package services
+
+import (
+	"context"
+	"fwh-toolbox-server/dto"
+	"fwh-toolbox-server/repositories"
+)
+
+// 设置故障录波参数
+func SetFaultRecordParam(ctx context.Context, para dto.FaultRecordParamInfo) error {
+	return repositories.FwhMqttFaultRecordParamSetting(ctx, para)
+}

+ 2 - 0
service/utils/httpClient/httpClient.go

@@ -146,6 +146,7 @@ func (c *HttpClient) HttpGet(url string, params *url.Values, opt *HttpClientOpti
 		return &Response{
 			Status:        resp.Status,
 			StatusCode:    resp.StatusCode,
+			Header:        resp.Header,
 			Body:          respBody,
 			ContentLength: resp.ContentLength,
 		}, nil
@@ -260,6 +261,7 @@ func (c *HttpClient) HttpPost(url string, body any, opt *HttpClientOptions) (*Re
 		return &Response{
 			Status:        resp.Status,
 			StatusCode:    resp.StatusCode,
+			Header:        resp.Header,
 			Body:          respBody,
 			ContentLength: resp.ContentLength,
 		}, nil

+ 66 - 0
src/router/record.ts

@@ -428,6 +428,72 @@ export const routeRecordRaw: MergeMenuMixedOptions[] = [
       },
     ],
   },
+  {
+    path: 'despatch-config',
+    key: 'despatch-config',
+    name: 'despatchConfig',
+    icon: 'i-ph:virus-duotone',
+    label: '调度配置',
+    children: [
+      {
+        path: 'tou-template-pro',
+        key: 'tou-template-pro',
+        name: 'touTemplatePro',
+        icon: 'i-ph:tidal-logo-duotone',
+        label: '调度模版(生产)',
+        component: 'despatch-config/touTemplatePro',
+        meta: {
+          showTab: true,
+          enableMultiTab: true,
+        },
+      },
+      {
+        path: 'tou-template-test',
+        key: 'tou-template-test',
+        name: 'touTemplateTest',
+        icon: 'i-ph:tidal-logo-duotone',
+        label: '调度模版(测试)',
+        component: 'despatch-config/touTemplateTest',
+        meta: {
+          showTab: true,
+          enableMultiTab: true,
+        },
+      },
+    ],
+  },
+  {
+    path: 'fault-record',
+    key: 'fault-record',
+    name: 'faultRecord',
+    icon: 'i-ph:castle-turret-duotone',
+    label: '故障录波配置',
+    children: [
+      {
+        path: 'fault-record-read',
+        key: 'fault-record-read',
+        name: 'faultRecordRead',
+        icon: 'i-mdi:account-eye-outline',
+        label: '读取配置',
+        component: 'fault-record/read/index',
+        meta: {
+          showTab: true,
+          enableMultiTab: true,
+        },
+      },
+      {
+        path: 'fault-record-write',
+        key: 'fault-record-write',
+        name: 'faultRecordWrite',
+        icon: 'i-mdi:account-edit-outline',
+        label: '写入配置',
+        component: 'fault-record/write/index',
+        meta: {
+          showTab: true,
+          enableMultiTab: true,
+        },
+      },
+    ],
+  },
   {
     path: 'about',
     key: 'about',

+ 39 - 0
src/service/despatchConfig/index.ts

@@ -0,0 +1,39 @@
+// API List
+// /despatchConfig/touTemplatePro/list - GET 查询模版列表
+// /despatchConfig/touTemplatePro/info - GET 查询模版信息
+
+import http from '@/service/serverReq'
+import type { TemplateListFilter, TemplateItemInfoPage, TemplateDetailInfo } from './interface'
+
+const API_List = {
+  touTemplateProList: '/despatchConfig/touTemplatePro/list',
+  touTemplateProInfo: '/despatchConfig/touTemplatePro/info',
+}
+
+// GET 查询模版列表
+export const getTouTemplateProList = async (
+  pageSize: number,
+  pageNum: number,
+  filter?: TemplateListFilter,
+): Promise<TemplateItemInfoPage> => {
+  const res = await http.get<TemplateItemInfoPage>(API_List.touTemplateProList, {
+    params: {
+      pageSize,
+      pageNum,
+      tempType: filter ? (filter.tempType ?? '') : '',
+      status: filter ? (filter.status ?? '') : '',
+      search: filter ? (filter.search ?? '') : '',
+    },
+  })
+  return res
+}
+
+// GET 查询模版信息
+export const getTouTemplateProDetailInfo = async (id: number): Promise<TemplateDetailInfo> => {
+  const res = await http.get<TemplateDetailInfo>(API_List.touTemplateProInfo, {
+    params: {
+      id,
+    },
+  })
+  return res
+}

+ 119 - 0
src/service/despatchConfig/interface.ts

@@ -0,0 +1,119 @@
+import type { ResPage } from '../interface'
+
+export interface TemplateListFilter {
+  tempType?: number
+  status?: number
+  search: string
+}
+
+export interface TemplateItemInfo {
+  id: number
+  electricCompany: string
+  eleCompanyFullName: string
+  derSchdule?: string
+  gridType: string // 电网套餐类型
+  status: number // 发表状态 0-未发布 2-已发布
+  sourceType: number // 模板类型 0-系统模板 1-API模板 2-用户推荐
+  countryId: number
+  countryName: string
+  provinceName: string
+  provinceId: number
+  updateTime: string // format: 'YYYY-MM-DD HH:mm:SS'
+  electricityType: number
+}
+
+export interface TemplateItemInfoPage extends ResPage<TemplateItemInfo[]> {}
+
+export interface TemplateDetailInfo {
+  template: {
+    id: number
+    gatewayId?: string
+    templateId: number
+    templateFromType: 0
+    electricCompany: string
+    eleCompanyFullName: string
+    eletricCompanyId: number
+    sdcpCompanyFlag?: number
+    derSchdule?: string
+    gridType: string
+    name: string
+    electricityType: number
+    status: number
+    sourceType: number
+    countryId: number
+    countryZh: string
+    countryEn: string
+    provinceEn: string
+    provinceId: number
+    provinceZh: string
+    env?: string
+    mookRunCount: number
+    priority?: number
+    solarChargeMin?: number
+    workMode: number
+    updateTime: string // format: 'YYYY-MM-DD HH:mm:SS'
+    displayFlag: number
+    templateType?: number
+    ptoDate?: string
+    nbtType?: number
+    classification?: number
+    remark?: string
+    tariffName?: string
+    templateName?: string
+  }
+  strategyList: {
+    id: number
+    seasonName: string // 策略名(季节名)
+    month: string // 生效月份 exp.'1,2,3,4,5,6,7,8,9,10,11,12'
+    templateId: number
+    // fromType?: null
+    dayName: string
+    dayType: number // 日期类型:1、工作日;2、周末;3、整周(每天)
+    ccociateSn: string
+    ccociateType: number
+    eleticRatePeak?: number
+    eleticRateShoulder?: number
+    eleticRateValley?: number
+    eleticRateSharp?: number
+    eleticRateSuperOffPeak?: number
+    eleticRateGridFee?: number
+    eleticSellPeak?: number
+    eleticSellShoulder?: number
+    eleticSellValley?: number
+    eleticSellSharp?: number
+    eleticSellSuperOffPeak?: number
+    detailVoList: {
+      id: number
+      name: string // 子策略名
+      strategyId: number
+      startHourTime: string // exp.'00:00'
+      endHourTime: string // exp.'24:00'
+      waveType: number // 峰平谷 0:谷;1:平;2:峰;3:尖峰;4:低谷
+      briefDescribe?: string // 描述
+      solarPriority: string // exp.'1,2,0' 光伏电能优先级 [0]:市电, [1]:负载, [2]:FHP;优先级为0/1/2/3,可相同,若相同则表明需要按比例或者功率分配,0为不涉及,数字越大优先级越高
+      gridDischargeMax?: number // 电池最大允许馈网功率 可选,默认不能馈网;0:不可馈网,-1:不限制
+      gridChargeMax?: number // 电网允许最大充电功率 可选,默认不允许;0:不允许,-1:不限制
+      chargeMax?: number // 电池最大允许充电功率 可选,按单台功率计算,-1:无限制,默认-1
+      chargePower?: number // 电池充电功率 可选,按单台功率计算,默认未设置-1
+      gridFeedMax?: number // 电网允许最大馈网功率 0:不允许,-1:不限制,-2:未设置,可选,默认-2(aPbox功率算在这里吗?不算)
+      dischargePower?: number // 电池放电功率 可选,按单台功率计算,默认未设置-1
+      dischargeMax?: number // 电池最大允许放电功率 可选,电池需要放电时可设置,按单台功率计算,-1:无限制,默认-1
+      solarCutoff?: number // 主动断开光伏继电器 可选,0:不控制(默认);1:断开;2:闭合
+      gridMax?: number // 电网允许最大取电功率 0:不允许,-1:不限制,可选,默认不限制
+      loadPriority: string // exp.'1,2,0' 负载用电优先级 可选,[0]:市电, [1]:光伏, [2]:FHP;优先级为0/1/2/3,可相同,若相同则表明需要按比例或者功率分配,0为不涉及,数字越大优先级越高
+      maxChargeSoc?: number // 预留SOC,供停电后使用,可选,默认100%
+      minDischargeSoc?: number // 放电截止SOC 预留SOC,供停电后使用,可选,默认20%
+      heatEnable?: number // 充电加热使能 0:未设置(默认);1:不使能;2:使能
+      powerOffApower: string // 主动关闭APower 可选,0:不控制(默认);1:断开;2:闭合
+      offGrid?: number // 主动断开市电继电器 可选,0:不控制(默认);1:断开;2:闭合
+      gcaoMax?: number // 低于预留SOC电网充电使能 可选;0:不允许,-1:不限制;默认未设置(-3);如果使能之后没有设置值,则为2.5kw*N(用-2标识)
+      rampTime?: number // 爬坡时间 0~300,默认值0,BB套餐默认300
+      useModeFlag: number
+      dispatchId: number
+      dispatch?: string
+      buyRate?: number
+      sellRate?: number
+    }[]
+  }[]
+  // bbDefaultVo?: null
+}

+ 15 - 0
src/service/faultRecord/index.ts

@@ -0,0 +1,15 @@
+// API List
+// /faultRecord/paramSetting - POST 设置故障录波参数
+
+import http from '@/service/serverReq'
+import type { FaultRecordParamInfo } from './interface'
+
+const API_List = {
+  faultRecordParamSetting: '/faultRecord/paramSetting',
+}
+
+// POST 设置故障录波参数
+export const setFaultRecordParam = async (config: FaultRecordParamInfo) => {
+  const url = API_List.faultRecordParamSetting
+  const res = await http.post<FaultRecordParamInfo>(url, config)
+}

+ 11 - 0
src/service/faultRecord/interface.ts

@@ -0,0 +1,11 @@
+export interface FaultRecordParamInfo {
+  sn: string // aGate SN
+  list: {
+    type: 'pe' | 'agate'
+    sn: string // aGate SN(DSP) / aPower SN
+    paraList: {
+      id: number
+      val: string
+    }[]
+  }[]
+}

+ 17 - 0
src/stores/user.ts

@@ -27,6 +27,7 @@ interface UserPermissions {
 interface UserPermissionsItems {
   xlog?: boolean // xlog操作权限
   autoUpdate?: boolean // 自动升级配置页面操作权限
+  faultRecord?: boolean // 故障录波操作权限
 }
 
 const DEFAULT_USER: User = {
@@ -58,6 +59,12 @@ const autoUpdateMenuNames: string[] = [
   'autoUpdateConfigFile', // 配置文件管理
 ] as const
 
+const faultRecordMenuNames: string[] = [
+  'faultRecord', // 故障录波主菜单
+  'faultRecordRead', // 故障录波参数读界面
+  'faultRecordWrite', // 故障录波参数写界面
+] as const
+
 const parsePermissions = (payload: JwtPayload): User => {
   return {
     avatar: '',
@@ -74,6 +81,9 @@ const parsePermissions = (payload: JwtPayload): User => {
           case 'autoUpdate':
             acc.autoUpdate = true
             break
+          case 'faultRecord':
+            acc.faultRecord = true
+            break
         }
         return acc
       }, {}),
@@ -161,6 +171,13 @@ export const useUserStore = defineStore('userStore', () => {
                     return false
                   }
                 }
+
+                // faultRecord界面控制
+                if (permissionsItems.faultRecord !== true) {
+                  if (faultRecordMenuNames.includes(route.name)) {
+                    return false
+                  }
+                }
               }
               return true
             }),

+ 668 - 0
src/views/despatch-config/component/templateTable.vue

@@ -0,0 +1,668 @@
+<template>
+  <NList>
+    <template #header>
+      <div class="flex gap-2">
+        <NButton disabled :focusable="false" type="success" @click="downloadXlsx">
+          <template #icon><span class="i-ph:download" /></template>
+          下载xlsx
+        </NButton>
+        <NButton :focusable="false" type="success" @click="downloadJson">
+          <template #icon><span class="i-ph:download" /></template>
+          下载json
+        </NButton>
+      </div>
+    </template>
+    <NListItem
+      v-for="(strategy, name) in transformStrategyMap(props.data.strategyList)"
+      :key="name"
+    >
+      <NThing :title="name">
+        <NDataTable
+          :columns="createColumns()"
+          :data="createData(strategy.dayStrategy)"
+          :bordered="false"
+          :single-line="false"
+        />
+      </NThing>
+    </NListItem>
+  </NList>
+</template>
+
+<script setup lang="ts">
+import { NList, NListItem, NDataTable, NThing, NTag, NButton, NTooltip } from 'naive-ui'
+import type { DataTableColumns } from 'naive-ui'
+import type { TemplateDetailInfo } from '@/service/despatchConfig/interface'
+import { saveAs } from 'file-saver'
+
+const props = defineProps<{
+  data: TemplateDetailInfo
+}>()
+
+type PeakType = 'super-off-peak' | 'off-peak' | 'mid-peak' | 'on-peak' | 'super-on-peak' | 'unknown'
+
+type DespatchType =
+  | 'A - Excess solar export to grid(只放不充)'
+  | 'B - aPower standby(待机)'
+  | 'C - Excess solar charge aPower(只充不放)'
+  | 'D - Self-consumption(自发自用)'
+  | 'E - aPower charge from solar(优先充电)'
+  | 'F - aPower supply home(优先供载)'
+  | 'G - aPower charge from grid(电网备电)'
+  | 'H - aPower export to the grid(满额馈⽹)'
+  | 'unknown'
+
+const DespatchTypeMap: Record<string, DespatchType> = {
+  A: 'A - Excess solar export to grid(只放不充)',
+  B: 'B - aPower standby(待机)',
+  C: 'C - Excess solar charge aPower(只充不放)',
+  D: 'D - Self-consumption(自发自用)',
+  E: 'E - aPower charge from solar(优先充电)',
+  F: 'F - aPower supply home(优先供载)',
+  G: 'G - aPower charge from grid(电网备电)',
+  H: 'H - aPower export to the grid(满额馈⽹)',
+}
+
+type TemplateStrategyList = TemplateDetailInfo['strategyList']
+type TemplateSubStrategy = TemplateStrategyList[number]['detailVoList'][number]
+
+interface StrategyMap {
+  [name: string]: StrategyData
+}
+
+interface StrategyData {
+  month: string // 生效月份
+  dayStrategy:
+    | {
+        type: 'everyday'
+        everyday: SubStrategyData[]
+      }
+    | {
+        type: 'workdays&holidays'
+        workdays: SubStrategyData[]
+        holidays: SubStrategyData[]
+      }
+}
+
+type DayStrategyType = 'everyday' | 'workdays&holidays'
+type DayStrategy = StrategyData['dayStrategy']
+
+interface SubStrategyData {
+  dayType: string
+  timeRange: string
+  peakType: PeakType
+  despatch: string[]
+  remark: string
+  data: TemplateSubStrategy
+  dayTypeRowSpan?: number // dayType的列合并数
+}
+
+const makeSubStrategyData = (data: TemplateSubStrategy, type: DayStrategyType): SubStrategyData => {
+  return {
+    dayType: type,
+    timeRange: `${data.startHourTime} - ${data.endHourTime}`,
+    peakType: ((): PeakType => {
+      switch (data.waveType) {
+        case 0:
+          return 'off-peak'
+        case 1:
+          return 'mid-peak'
+        case 2:
+          return 'on-peak'
+        case 3:
+          return 'super-on-peak'
+        case 4:
+          return 'super-off-peak'
+        default:
+          return 'unknown'
+      }
+    })(),
+    despatch: ((): string[] => {
+      if (data.solarPriority === '1,2,0' && data.loadPriority === '1,3,2') {
+        // A
+        return [DespatchTypeMap.A]
+      } else if (data.solarPriority === '1,2,0' && data.loadPriority === '1,2,0') {
+        // B
+        return [DespatchTypeMap.B]
+      } else if (data.solarPriority === '1,3,2' && data.loadPriority === '1,2,0') {
+        // C
+        return [DespatchTypeMap.C]
+      } else if (data.solarPriority === '1,3,2' && data.loadPriority === '1,3,2') {
+        // D
+        return [DespatchTypeMap.D]
+      } else if (data.solarPriority === '1,2,3' && data.loadPriority === '1,2,0') {
+        // E/G
+        return [DespatchTypeMap.E, DespatchTypeMap.G]
+      } else if (data.solarPriority === '1,2,0' && data.loadPriority === '1,2,3') {
+        // F/H
+        return [DespatchTypeMap.F, DespatchTypeMap.H]
+      } else if (data.dischargePower != null) {
+        return [`aPower discharge ${data.dischargePower}kw`]
+      } else if (data.chargePower != null) {
+        return [`aPower charge ${data.chargePower}kw`]
+      } else {
+        return ['unknown']
+      }
+    })(),
+    remark: '',
+    data,
+  }
+}
+
+const transformStrategyMap = (dataList: TemplateStrategyList): StrategyMap => {
+  const strategyMap: StrategyMap = {}
+  dataList.forEach((d) => {
+    if (strategyMap[d.seasonName] === undefined) {
+      strategyMap[d.seasonName] = {
+        month: d.month,
+        dayStrategy: (() => {
+          if (d.dayType === 1) {
+            // 工作日
+            return {
+              type: 'workdays&holidays',
+              workdays: d.detailVoList.map((item) =>
+                makeSubStrategyData(item, 'workdays&holidays'),
+              ),
+              holidays: [],
+            }
+          } else if (d.dayType === 2) {
+            // 节假日
+            return {
+              type: 'workdays&holidays',
+              workdays: [],
+              holidays: d.detailVoList.map((item) =>
+                makeSubStrategyData(item, 'workdays&holidays'),
+              ),
+            }
+          } else if (d.dayType === 3) {
+            // 每天
+            return {
+              type: 'everyday',
+              everyday: d.detailVoList.map((item) => makeSubStrategyData(item, 'everyday')),
+            }
+          } else {
+            throw new Error(`未知dayType值(${d.dayType})`)
+          }
+        })(),
+      }
+    } else {
+      if (strategyMap[d.seasonName].month !== d.month) {
+        throw new Error(
+          `${d.seasonName}的month配置错误(${strategyMap[d.seasonName].month} != ${d.month})`,
+        )
+      }
+
+      const dayStrategy = strategyMap[d.seasonName].dayStrategy
+      if (d.dayType === 1) {
+        if (dayStrategy.type === 'workdays&holidays') {
+          dayStrategy.workdays = d.detailVoList.map((item) =>
+            makeSubStrategyData(item, 'workdays&holidays'),
+          )
+        } else {
+          throw new Error(
+            `${d.seasonName}的dayStrategy类型不匹配(dayType: ${d.dayType}, dayStrategyType: ${strategyMap[d.seasonName].dayStrategy.type})`,
+          )
+        }
+      } else if (d.dayType === 2) {
+        if (dayStrategy.type === 'workdays&holidays') {
+          dayStrategy.holidays = d.detailVoList.map((item) =>
+            makeSubStrategyData(item, 'workdays&holidays'),
+          )
+        } else {
+          throw new Error(
+            `${d.seasonName}的dayStrategy类型不匹配(dayType: ${d.dayType}, dayStrategyType: ${strategyMap[d.seasonName].dayStrategy.type})`,
+          )
+        }
+      } else if (d.dayType === 3) {
+        // 不应该出现两个everyday配置
+        throw new Error(`${d.seasonName}的dayStrategy-everyday类型数据冲突`)
+      } else {
+        throw new Error(`未知dayType值(${d.dayType})`)
+      }
+      strategyMap[d.seasonName].dayStrategy = dayStrategy
+    }
+  })
+  return strategyMap
+}
+
+const createColumns = (): DataTableColumns<SubStrategyData> => {
+  return [
+    {
+      title: 'DayType',
+      key: 'dayType',
+      width: 100,
+      fixed: 'left',
+      rowSpan: (rowData, rowIndex) => rowData.dayTypeRowSpan ?? 1,
+    },
+    {
+      title: 'TimeRang',
+      key: 'timeRange',
+      width: 120,
+    },
+    {
+      title: 'PeakType',
+      key: 'peakType',
+      width: 150,
+    },
+    {
+      title: 'DespatchType',
+      key: 'despatch',
+      width: 320,
+      render: (rowData, rowIndex) => {
+        return h(
+          'div',
+          {
+            class: 'flex flex-col gap-2 items-start',
+          },
+          rowData.despatch.map((despatchTag) => {
+            return h(
+              NTag,
+              {
+                type: 'info',
+                bordered: false,
+              },
+              {
+                default: () => despatchTag,
+              },
+            )
+          }),
+        )
+      },
+    },
+    {
+      title: 'Remark',
+      key: 'remark',
+      // width: 250,
+      ellipsis: {
+        tooltip: true,
+      },
+    },
+    {
+      title: 'Data',
+      key: 'data',
+      fixed: 'right',
+      width: 60,
+      render(rowData, rowIndex) {
+        return h(
+          NTooltip,
+          { placement: 'left' },
+          {
+            trigger: () =>
+              h(
+                NButton,
+                {
+                  quaternary: true,
+                  circle: true,
+                  size: 'small',
+                },
+                {
+                  icon: () =>
+                    h('span', {
+                      class: 'i-ph:info-duotone',
+                    }),
+                },
+              ),
+
+            default: () =>
+              h(
+                'pre',
+                {
+                  class: 'text-xs whitespace-pre-wrap',
+                  style: {
+                    maxWidth: '400px',
+                  },
+                },
+                JSON.stringify(rowData.data, null, 2),
+              ),
+          },
+        )
+      },
+    },
+  ]
+}
+
+const createData = (data: DayStrategy): SubStrategyData[] => {
+  if (data.type === 'everyday') {
+    if (data.everyday.length > 0) {
+      data.everyday[0].dayTypeRowSpan = data.everyday.length
+    }
+    return data.everyday
+  } else if (data.type === 'workdays&holidays') {
+    if (data.workdays.length > 0) {
+      data.workdays[0].dayTypeRowSpan = data.workdays.length
+    }
+    if (data.holidays.length > 0) {
+      data.holidays[0].dayTypeRowSpan = data.holidays.length
+    }
+    return [...data.workdays, ...data.holidays]
+  } else {
+    throw new Error(`未知DayStrategy(${data})`)
+  }
+}
+
+const downloadXlsx = () => {}
+
+const downloadJson = () => {
+  const obj = createTemplateJson(props.data)
+  const jsonStr = JSON.stringify(
+    obj,
+    (_, value) => {
+      if (value === undefined || value === null) {
+        return undefined
+      }
+
+      return value
+    },
+    2,
+  )
+
+  // 创建一个 Blob 对象,指定 MIME 类型为 'application/json'
+  const blob = new Blob([jsonStr], {
+    type: 'application/json',
+  })
+
+  let fileName = obj.base.utilityAbbreviation
+  if (obj.base.derSchedule !== '') {
+    fileName = `${fileName}_${obj.base.derSchedule}`
+  }
+  if (obj.base.scheduleName !== '') {
+    fileName = `${fileName}_${obj.base.scheduleName}`
+  }
+
+  // 获取当前年份、月份、日期、小时和分钟
+  const now = new Date()
+  const year = now.getFullYear()
+  const month = String(now.getMonth() + 1).padStart(2, '0') // 月份从0开始,需要加1并确保两位数
+  const day = String(now.getDate()).padStart(2, '0') // 确保日期为两位数
+  const hours = String(now.getHours()).padStart(2, '0') // 确保小时为两位数
+  const minutes = String(now.getMinutes()).padStart(2, '0') // 确保分钟为两位数
+  // 日期时间后缀
+  const timeSuffix = `${year}${month}${day}_${hours}${minutes}`
+
+  fileName = `${fileName}_${timeSuffix}.json`
+
+  saveAs(blob, fileName)
+}
+
+interface ScheduleConfig {
+  base: ScheduleBaseConfig
+  table: ScheduleTableConfig
+}
+
+interface ScheduleBaseConfig {
+  templateId?: number
+  country: number
+  area: number
+  derSchedule: string
+  utilityCompany: string
+  utilityAbbreviation: string
+  tariffSchedule: string
+  tariffType: number
+  scheduleName: string
+}
+
+interface ScheduleTableConfig {
+  tables: ScheduleTable[]
+}
+
+interface ScheduleTable {
+  key: number
+  subScheduleName: string
+  checkedMonth: string[]
+  dayType: string
+  timePeriodTable: TimePeriodData[] // 用于日期类型:每天、工作日
+  timePeriodTableHolidays: TimePeriodData[] // 用于日期类型:周末、节假日
+  tariffTable: TariffTable
+  tariffTableHolidays: TariffTable
+}
+
+interface TimePeriodData {
+  id: number
+  timeRange: {
+    startTime: string
+    endTime: string
+  }
+  rateType: number
+  rateName?: string
+  schedulingPolicy: PolicyName
+  customSchedule?: CustomSchedule
+  dischargeMinSOC?: number
+}
+
+interface CustomSchedule {
+  solarPriority?: string // 光伏优先级(数字大的优先级高,0为不执行) [0]:市电 [1]:负载 [2]:电池
+  loadPriority?: string // 负载优先级(数字大的优先级高,0为不执行) [0]:市电 [1]:光伏 [2]:电池
+
+  chargePower?: number // 单台电池充电功率,单位kw,精度0.1kw
+  dischargePower?: number // 单台电池放电功率,单位kw,精度0.1kw
+  chargeMax?: number // 单台电池最大充电功率,单位kw,精度0.1kw
+  dischargeMax?: number // 单台电池最大放电功率,单位kw,精度0.1kw
+
+  gridDischargeMax?: number //
+  gridChargeMax?: number //
+
+  gridFeedMax?: number //
+
+  solarCutoff?: number //
+  gridMax?: number //
+
+  maxChargeSoc?: number //
+  minDischargeSoc?: number //
+  heatEnable?: number //
+  powerOffApower?: string //
+  offGrid?: number // 主动离网,0:不控制(默认),1:断开,2:闭合
+  gcaoMax?: number //(预留SOC补电状态)电网充电使能,0:不允许,1:允许电网充电
+  rampTime?: number // 爬坡时间,0~300s
+}
+
+export interface TariffTable {
+  rateOffPeak?: number
+  rateMidPeak?: number
+  rateOnPeak?: number
+  rateSuperOnPeak?: number
+  rateSuperOffPeak?: number
+  sellOffPeak?: number
+  sellMidPeak?: number
+  sellOnPeak?: number
+  sellSuperOnPeak?: number
+  sellSuperOffPeak?: number
+}
+
+type MonthType =
+  | 'Jan'
+  | 'Feb'
+  | 'Mar'
+  | 'Apr'
+  | 'May'
+  | 'Jun'
+  | 'Jul'
+  | 'Aug'
+  | 'Sep'
+  | 'Oct'
+  | 'Nov'
+  | 'Dec'
+
+// 必须同配置编辑器的schedulingPolicyConfig定义相同
+type PolicyName =
+  | 'Self-Consumption' // 自发自用 = D
+  | 'PV surplus charging' // 光伏剩余充电 = C
+  | 'PV priority charging' // 光伏优先充电 = E
+  | 'Battery priority load' // 电池优先供载 = F
+  | 'Battery not dispatched' // 电池不参与调度 = B
+  | 'Battery forced discharge' // 电池强制放电
+  | 'Backup Only' // 仅备电
+
+const createTemplateJson = (info: TemplateDetailInfo): ScheduleConfig => {
+  return {
+    base: {
+      templateId: info.template.id,
+      country: info.template.countryId,
+      area: info.template.provinceId,
+      derSchedule: info.template.derSchdule ?? '',
+      utilityCompany: info.template.eleCompanyFullName,
+      utilityAbbreviation: info.template.electricCompany,
+      tariffSchedule: info.template.gridType,
+      tariffType: info.template.electricityType,
+      scheduleName: info.template.name,
+    },
+    table: {
+      tables: ((): ScheduleTable[] => {
+        const strategyMap = new Map<string, ScheduleTable>()
+
+        info.strategyList.forEach((strategy) => {
+          const key = `${strategy.seasonName} - ${strategy.month}`
+          const old = strategyMap.get(key)
+
+          const timePeriodList: TimePeriodData[] = strategy.detailVoList.map<TimePeriodData>(
+            (detail) => {
+              return {
+                id: Date.now(),
+                timeRange: {
+                  startTime: detail.startHourTime,
+                  endTime: detail.endHourTime,
+                },
+                rateType: detail.waveType,
+                rateName: detail.name,
+                schedulingPolicy: ((): PolicyName => {
+                  if (detail.solarPriority === '1,2,0' && detail.loadPriority === '1,3,2') {
+                    // A -
+                    throw new Error(`调度A(${DespatchTypeMap.A})未实现`)
+                  } else if (detail.solarPriority === '1,2,0' && detail.loadPriority === '1,2,0') {
+                    // B - 电池不参与调度
+                    return 'Battery not dispatched'
+                  } else if (detail.solarPriority === '1,3,2' && detail.loadPriority === '1,2,0') {
+                    // C - 光伏剩余充电
+                    return 'PV surplus charging'
+                  } else if (detail.solarPriority === '1,3,2' && detail.loadPriority === '1,3,2') {
+                    // D - 自发自用
+                    return 'Self-Consumption'
+                  } else if (detail.solarPriority === '1,2,3' && detail.loadPriority === '1,2,0') {
+                    // E/G - 光伏优先充电
+                    return 'PV priority charging'
+                  } else if (detail.solarPriority === '1,2,0' && detail.loadPriority === '1,2,3') {
+                    // F/H - 电池优先供载
+                    return 'Battery priority load'
+                  } else if (detail.dischargePower != null) {
+                    // 电池强制放电
+                    return 'Battery forced discharge'
+                  } else if (detail.chargePower != null) {
+                    // 仅备电
+                    return 'Backup Only'
+                  } else {
+                    throw new Error(`未知的调度逻辑\n${JSON.stringify(detail, null, 2)}`)
+                  }
+                })(),
+                // customSchedule?: CustomSchedule, // 自定义功能暂不完善,未实现
+                dischargeMinSOC: detail.minDischargeSoc ?? undefined,
+              }
+            },
+          )
+
+          const tariffTable: TariffTable = {
+            rateOffPeak: strategy.eleticRateValley ?? undefined, // 谷期电价
+            rateMidPeak: strategy.eleticRateShoulder ?? undefined, // 平期电价
+            rateOnPeak: strategy.eleticRatePeak ?? undefined, // 峰期电价
+            rateSuperOnPeak: strategy.eleticRateSharp ?? undefined, // 尖峰期电价
+            rateSuperOffPeak: strategy.eleticRateSuperOffPeak ?? undefined, // 超低谷期电价
+            sellOffPeak: strategy.eleticSellValley ?? undefined, // 谷期馈网电价
+            sellMidPeak: strategy.eleticSellShoulder ?? undefined, // 平期馈网电价
+            sellOnPeak: strategy.eleticSellPeak ?? undefined, // 峰期馈网电价
+            sellSuperOnPeak: strategy.eleticSellSharp ?? undefined, // 尖峰期馈网电价
+            sellSuperOffPeak: strategy.eleticSellSuperOffPeak ?? undefined, // 超低谷期馈网电价
+          }
+
+          if (old === undefined) {
+            strategyMap.set(key, {
+              key: Date.now(),
+              subScheduleName: strategy.seasonName,
+              checkedMonth: strategy.month.split(',').map<MonthType>((m) => {
+                switch (m.trim()) {
+                  case '1':
+                    return 'Jan'
+                  case '2':
+                    return 'Feb'
+                  case '3':
+                    return 'Mar'
+                  case '4':
+                    return 'Apr'
+                  case '5':
+                    return 'May'
+                  case '6':
+                    return 'Jun'
+                  case '7':
+                    return 'Jul'
+                  case '8':
+                    return 'Aug'
+                  case '9':
+                    return 'Sep'
+                  case '10':
+                    return 'Oct'
+                  case '11':
+                    return 'Nov'
+                  case '12':
+                    return 'Dec'
+                  default:
+                    throw new Error(`错误的month(${strategy.month})`)
+                }
+              }),
+              dayType: (() => {
+                switch (strategy.dayType) {
+                  case 1:
+                    return 'WeekdaysAndWeekends' // Weekdays
+                  case 2:
+                    return 'WeekdaysAndWeekends' // Holidays
+                  case 3:
+                    return 'Everyday'
+                  default:
+                    throw new Error(`错误的dayType(${strategy.dayType})`)
+                }
+              })(),
+              timePeriodTable: ((): TimePeriodData[] => {
+                if (strategy.dayType === 1 || strategy.dayType === 3) {
+                  return timePeriodList
+                }
+                return []
+              })(), // 用于日期类型:每天、工作日
+              timePeriodTableHolidays: ((): TimePeriodData[] => {
+                if (strategy.dayType === 2) {
+                  return timePeriodList
+                }
+                return []
+              })(), // 用于日期类型:周末、节假日
+              tariffTable: ((): TariffTable => {
+                if (strategy.dayType === 1 || strategy.dayType === 3) {
+                  return tariffTable
+                }
+                return {}
+              })(), // 用于日期类型:每天、工作日
+              tariffTableHolidays: ((): TariffTable => {
+                if (strategy.dayType === 2) {
+                  return tariffTable
+                }
+                return {}
+              })(), // 用于日期类型:周末、节假日
+            })
+          } else {
+            if (strategy.dayType === 1) {
+              // 工作日
+              strategyMap.set(key, {
+                ...old,
+                timePeriodTable: timePeriodList,
+                tariffTable,
+              })
+            } else if (strategy.dayType === 2) {
+              // 节假日
+              strategyMap.set(key, {
+                ...old,
+                timePeriodTableHolidays: timePeriodList,
+                tariffTableHolidays: tariffTable,
+              })
+            }
+          }
+        })
+
+        return [...strategyMap.values()]
+      })(),
+    },
+  }
+}
+</script>

+ 417 - 0
src/views/despatch-config/touTemplatePro.vue

@@ -0,0 +1,417 @@
+<template>
+  <div class="main-wrap p-4 flex flex-col gap-y-4">
+    <!-- <NCard
+      ref="mainCardRef"
+      class="h-full"
+      content-class="h-full flex flex-col"
+      :style="cardHeight"
+    > -->
+    <NCard ref="mainCardRef" class="h-full" content-class="h-full flex flex-col">
+      <div class="mb-2 flex gap-x-4">
+        <NForm ref="formRef" :model="form" :show-label="false" inline label-placement="left">
+          <NFormItem label="模版类型" path="tempType">
+            <NSelect
+              v-model:value="form.tempType"
+              :options="tempTypeOptions"
+              style="width: 120px"
+              clearable
+              placeholder="模版类型"
+            />
+          </NFormItem>
+          <NFormItem label="发布状态" path="status">
+            <NSelect
+              v-model:value="form.status"
+              :options="statusOptions"
+              style="width: 120px"
+              clearable
+              placeholder="发布状态"
+            />
+          </NFormItem>
+          <NFormItem label="搜索" path="search">
+            <NInput v-model:value="form.search" style="width: 250px" clearable placeholder="搜索" />
+          </NFormItem>
+        </NForm>
+        <div class="flex gap-2">
+          <NButton
+            type="info"
+            :loading="isRequestLoading"
+            :disabled="isRequestLoading"
+            @click="handleQueryClick"
+          >
+            <template #icon>
+              <span class="i-ph:magnifying-glass" />
+            </template>
+            查询
+          </NButton>
+          <NButton type="warning" @click="resetForm">
+            <template #icon>
+              <span class="i-ph:arrow-clockwise" />
+            </template>
+            重置
+          </NButton>
+        </div>
+      </div>
+      <div>
+        <NDataTable
+          ref="dataTableRef"
+          v-model:checked-row-keys="checkedRowKeys"
+          :remote="true"
+          :min-height="166.6"
+          :max-height="maxHeight"
+          size="small"
+          :columns="tableColumns"
+          :data="tableData"
+          :row-key="tableRowKey"
+          :loading="isRequestLoading"
+          striped
+          :single-line="false"
+          :row-props="rowProps"
+          :scroll-x="dataTableMaxWidth"
+        />
+        <div class="mt-3 flex items-end justify-between">
+          <div class="flex gap-x-3 items-center">
+            <span>已选择&nbsp;{{ checkedRowKeys.length }}&nbsp; 条</span>
+          </div>
+          <NPagination v-bind="tablePagination" :prefix="paginationPrefix" />
+        </div>
+      </div>
+    </NCard>
+    <NDropdown
+      placement="bottom-start"
+      trigger="manual"
+      v-bind="dropdownOptions"
+      :show="showDropdown"
+    />
+  </div>
+</template>
+
+<script setup lang="tsx">
+import {
+  useModal,
+  NCard,
+  NForm,
+  NFormItem,
+  NInput,
+  NSelect,
+  NButton,
+  NDataTable,
+  NNumberAnimation,
+  NDropdown,
+} from 'naive-ui'
+import type { DataTableColumns, PaginationProps, DropdownProps, ModalOptions } from 'naive-ui'
+import { getTouTemplateProList, getTouTemplateProDetailInfo } from '@/service/despatchConfig/index'
+import type {
+  TemplateListFilter,
+  TemplateItemInfo,
+  TemplateDetailInfo,
+} from '@/service/despatchConfig/interface'
+// import { useFullCard } from '@/composable/comp/useFullCard'
+import { useDataTable } from '@/composable/comp/useDataTable'
+import { useResettableReactive } from '@/composable/useResettable'
+import TemplateTable from './component/templateTable.vue'
+
+const model = useModal()
+
+const mainCardRef = useTemplateRef<InstanceType<typeof NCard>>('mainCardRef')
+const formRef = useTemplateRef<InstanceType<typeof NForm>>('formRef')
+const dataTableRef = useTemplateRef<InstanceType<typeof NDataTable>>('dataTableRef')
+
+// const { height } = useFullCard(mainCardRef)
+const { maxHeight } = useDataTable(dataTableRef)
+
+const isRequestLoading = ref(false)
+const showDropdown = ref(false)
+const checkedRowKeys = ref<Array<number | string>>([])
+const contextmenuID = ref<number | undefined>(undefined)
+
+interface TemplateFilterForm extends Partial<TemplateListFilter> {}
+
+const [form, , resetForm] = useResettableReactive<TemplateFilterForm>({
+  // tempType: 0,
+  // status: 0,
+  search: '',
+})
+
+const tempTypeOptions = [
+  { label: '系统模版', value: 0 },
+  { label: 'API模版', value: 1 },
+  { label: '用户推荐', value: 2 },
+]
+
+const statusOptions = [
+  { label: '未发布', value: 0 },
+  { label: '已发布', value: 2 },
+]
+
+// const cardHeight = computed(() => {
+//   return { height: `${height.value === undefined || height.value === 0 ? 480 : height.value}px` }
+// })
+
+// 表格右键菜单
+function rowProps(row: TemplateItemInfo) {
+  return {
+    onContextmenu: (e: MouseEvent) => {
+      e.preventDefault()
+      showDropdown.value = false
+      nextTick().then(() => {
+        contextmenuID.value = row.id
+        showDropdown.value = true
+        dropdownOptions.x = e.clientX
+        dropdownOptions.y = e.clientY
+      })
+    },
+  }
+}
+
+const dataTableMaxWidth = ref<number | undefined>(undefined)
+const tableData = ref<TemplateItemInfo[]>([])
+const tableColumns = createColumns()
+
+const tablePagination = reactive<PaginationProps>({
+  page: 1,
+  pageSize: 10,
+  showSizePicker: true,
+  pageSizes: [10, 20, 50, 100],
+  itemCount: 0,
+  showQuickJumper: true,
+  showQuickJumpDropdown: true,
+  onUpdatePage: (page: number) => {
+    tablePagination.page = page
+    handleListQuery()
+  },
+  onUpdatePageSize: (pageSize: number) => {
+    tablePagination.pageSize = pageSize
+    tablePagination.page = 1
+    handleListQuery()
+  },
+})
+
+const prevUserListTotal = ref(0)
+
+const paginationPrefix: PaginationProps['prefix'] = (info) => {
+  const { itemCount } = info
+  return (
+    itemCount && (
+      <div>
+        <span>总&nbsp; </span>
+        <NNumberAnimation
+          from={prevUserListTotal.value}
+          to={itemCount}
+          onFinish={() => {
+            prevUserListTotal.value = itemCount
+          }}
+        />
+        <span>&nbsp;条 </span>
+      </div>
+    )
+  )
+}
+
+const dropdownOptions = reactive<DropdownProps>({
+  x: 0,
+  y: 0,
+  options: [
+    {
+      label: () => <span class="text-green-500"> 查看 </span>,
+      key: 'view',
+    },
+  ],
+  onClickoutside: () => {
+    showDropdown.value = false
+  },
+  onSelect: (key: string) => {
+    showDropdown.value = false
+    if (contextmenuID.value === undefined) return
+    if (key === 'view') {
+      itemView(contextmenuID.value)
+    }
+  },
+})
+
+const tableRowKey = (rowData: TemplateItemInfo) => {
+  return rowData.id
+}
+
+const sourceTypeMap: Record<number, string> = {
+  0: '系统模板',
+  1: 'API模板',
+  2: '用户推荐',
+}
+
+const statusMap: Record<number, string> = {
+  0: '未发布',
+  2: '已发布',
+}
+
+function createColumns(): DataTableColumns<TemplateItemInfo> {
+  const columns: DataTableColumns<TemplateItemInfo> = [
+    {
+      title: 'ID',
+      key: 'id',
+      width: 60,
+      fixed: 'left',
+    },
+    {
+      title: '类型',
+      key: 'sourceType',
+      width: 80,
+      render(row) {
+        return sourceTypeMap[row.sourceType] ?? `未知(${row.sourceType})`
+      },
+    },
+    {
+      title: '国家',
+      key: 'countryName',
+      width: 80,
+    },
+    {
+      title: '地区',
+      key: 'provinceName',
+      width: 100,
+      ellipsis: {
+        tooltip: true,
+      },
+    },
+    {
+      title: 'DER计划',
+      key: 'derSchdule',
+      width: 100,
+      ellipsis: {
+        tooltip: true,
+      },
+    },
+    {
+      title: '电力公司',
+      key: 'electricCompany',
+      width: 100,
+      ellipsis: {
+        tooltip: true,
+      },
+    },
+    {
+      title: '资费套餐',
+      key: 'gridType',
+      width: 100,
+      ellipsis: {
+        tooltip: true,
+      },
+    },
+    {
+      title: '发布状态',
+      key: 'status',
+      width: 80,
+      render(row) {
+        return statusMap[row.status] ?? `未知(${row.status})`
+      },
+    },
+    {
+      title: '更新时间',
+      key: 'updateTime',
+      width: 200,
+    },
+    {
+      title: '操作',
+      key: 'operate',
+      width: 80,
+      fixed: 'right',
+      render(row) {
+        return h('div', { class: 'flex justify-center gap-4 p-x-4' }, [
+          h(
+            NButton,
+            {
+              type: 'success',
+              strong: true,
+              size: 'small',
+              onClick: () => itemView(row.id),
+            },
+            {
+              default: () => '查看',
+            },
+          ),
+        ])
+      },
+    },
+  ]
+
+  dataTableMaxWidth.value = columns
+    .map((item) => {
+      if (item.minWidth === undefined) {
+        if (item.width === undefined) {
+          return 0
+        } else if (typeof item.width === 'string') {
+          return Number.parseFloat(item.width)
+        }
+        return item.width
+      } else if (typeof item.minWidth === 'string') {
+        return Number.parseFloat(item.minWidth)
+      } else {
+        return item.minWidth
+      }
+    })
+    .reduce((acc: number, cur: number) => acc + cur, 0)
+
+  return columns
+}
+
+const itemView = async (id: number) => {
+  const data = await getTouTemplateProDetailInfo(id)
+  const opt: ModalOptions = {}
+  opt.preset = 'card'
+  opt.style = {
+    maxWidth: '1200px',
+  }
+
+  // 标题
+  if (data.template.derSchdule != null && data.template.derSchdule !== '') {
+    opt.title = `${data.template.derSchdule} - ${data.template.gridType}`
+  } else {
+    opt.title = data.template.gridType
+  }
+
+  // 内容
+  opt.content = () =>
+    h(TemplateTable, {
+      data,
+    })
+
+  model.create(opt)
+}
+
+const queryTouTemplateProList = async (
+  pageSize: number,
+  pageNum: number,
+  filter?: TemplateListFilter,
+) => {
+  const res = await getTouTemplateProList(pageSize, pageNum, filter)
+  if (res.data === null) {
+    tableData.value = []
+  } else {
+    tableData.value = res.data
+  }
+  tablePagination.itemCount = res.total
+}
+
+const handleQueryClick = () => {
+  // 请求查询数据
+  handleListQuery()
+}
+
+const handleListQuery = async () => {
+  const pageSize = tablePagination.pageSize ?? 10
+  const page = tablePagination.page ?? 1
+
+  // 处理过滤条件
+  const filter: TemplateListFilter = {
+    tempType: form.tempType,
+    status: form.status,
+    search: form.search ?? '',
+  }
+
+  queryTouTemplateProList(pageSize, page, filter)
+}
+
+onMounted(async () => {
+  // 请求查询数据
+  await handleListQuery()
+})
+</script>

+ 24 - 0
src/views/despatch-config/touTemplateTest.vue

@@ -0,0 +1,24 @@
+<template>
+  <div class="main-wrap p-4 flex flex-col gap-y-4">
+    <NCard
+      ref="mainCardRef"
+      class="h-full"
+      content-class="h-full flex flex-col"
+      :style="cardHeight"
+    >
+    </NCard>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { NCard } from 'naive-ui'
+import { useFullCard } from '@/composable/comp/useFullCard'
+
+const mainCardRef = useTemplateRef<InstanceType<typeof NCard>>('mainCardRef')
+
+const { height } = useFullCard(mainCardRef)
+
+const cardHeight = computed(() => {
+  return { height: `${height.value === undefined || height.value === 0 ? 480 : height.value}px` }
+})
+</script>

+ 3 - 0
src/views/fault-record/read/index.vue

@@ -0,0 +1,3 @@
+<template></template>
+
+<script setup lang="ts"></script>

+ 265 - 0
src/views/fault-record/write/index.vue

@@ -0,0 +1,265 @@
+<template>
+  <NCard>
+    <NForm ref="formRef" :model="formValue" :rules="formRules">
+      <NFormItem label="aGate SN" path="sn">
+        <NInput v-model:value="formValue.sn" type="text" placeholder="aGate SN" />
+      </NFormItem>
+      <NButton type="info" @click="clickSaveConfig">
+        <template #icon>
+          <span class="i-material-symbols:save-outline" />
+        </template>
+        写入配置
+      </NButton>
+      <NDivider />
+      <NFormItem content-class="flex flex-col gap-2" path="list" :show-label="false">
+        <div
+          v-for="(list, index) in formValue.list"
+          :key="list.key"
+          class="flex gap-2 w-full items-center"
+        >
+          <NButton
+            circle
+            @click="
+              () => {
+                clickDelSubModel(index)
+              }
+            "
+          >
+            <template #icon>
+              <span class="i-material-symbols:delete-outline" />
+            </template>
+          </NButton>
+          <NCard class="flex-1">
+            <NFormItem label="sub SN" :path="`list.${index}.sn`" :rule="createSubSnRule(index)">
+              <div class="flex gap-2 w-full">
+                <NSelect
+                  v-model:value="formValue.list[index].type"
+                  class="w-[120px]"
+                  :options="optionsSubSnType"
+                />
+                <NInput
+                  class="flex-1"
+                  :value="computedSubSn(index).value"
+                  type="text"
+                  placeholder="aGate SN(DSP) or aPower SN"
+                  :disabled="computedSubSnDisabled(index).value"
+                  @update:value="
+                    (value) => {
+                      updateSubSn(index, value)
+                    }
+                  "
+                />
+              </div>
+            </NFormItem>
+            <NFormItem
+              label="Param List"
+              :path="`list.${index}.paraList`"
+              :rule="createParamListRule()"
+            >
+              <NDynamicInput
+                v-model:value="formValue.list[index].paraList"
+                :on-create="onCreateParam"
+              >
+                <template #create-button-default> 新增参数 </template>
+                <template #default="{ value }">
+                  <div style="display: flex; align-items: center; width: 100%">
+                    <NInputNumber
+                      v-model:value="value.id"
+                      style="margin-right: 12px; width: 160px"
+                      :min="0"
+                      :show-button="false"
+                    />
+                    <NInput v-model:value="value.val" type="text" placeholder="Value" />
+                  </div>
+                </template>
+              </NDynamicInput>
+            </NFormItem>
+          </NCard>
+        </div>
+      </NFormItem>
+      <NButton @click="clickAddSubModel">
+        <template #icon>
+          <span class="i-material-symbols:add-rounded" />
+        </template>
+        新增子模块
+      </NButton>
+    </NForm>
+  </NCard>
+</template>
+
+<script setup lang="ts">
+import {
+  useMessage,
+  NCard,
+  NForm,
+  NFormItem,
+  NInput,
+  NInputNumber,
+  NDivider,
+  NButton,
+  NDynamicInput,
+  NSelect,
+} from 'naive-ui'
+import type { FormRules, FormItemRule } from 'naive-ui'
+import { setFaultRecordParam } from '@/service/faultRecord'
+import type { FaultRecordParamInfo } from '@/service/faultRecord/interface'
+
+const message = useMessage()
+
+const formRef = useTemplateRef<InstanceType<typeof NForm>>('formRef')
+
+interface FaultRecordParamItem {
+  key: string
+  id: number
+  val: string
+}
+
+interface FaultRecordParam {
+  sn: string // aGate SN
+  list: {
+    key: string
+    type: 'pe' | 'agate'
+    sn: string // aGate SN(DSP) / aPower SN
+    paraList: FaultRecordParamItem[]
+  }[]
+}
+
+const optionsSubSnType = [
+  { label: 'PE', value: 'pe' },
+  { label: 'aGate', value: 'agate' },
+]
+
+const formValue = ref<FaultRecordParam>({
+  sn: '',
+  list: [],
+})
+
+const formRules: FormRules = {
+  sn: {
+    required: true,
+    message: '请输入aGate SN',
+    trigger: ['blur', 'input'],
+  },
+  list: {
+    type: 'array',
+    required: true,
+    message: '至少添加一个模块',
+  },
+}
+
+const createSubSnRule = (index: number): FormItemRule => {
+  return {
+    required: true,
+    trigger: ['blur', 'input'],
+    validator: (rule: FormItemRule, value: string) => {
+      if (formValue.value.list[index].type === 'agate') return true
+      if (value.length > 0) return true
+      return new Error('请输入子设备SN')
+    },
+  }
+}
+
+const HEX_REGEX = /^[0-9a-f]+$/i
+const createParamListRule = (): FormItemRule | FormItemRule[] => {
+  return [
+    {
+      type: 'array',
+      required: true,
+      message: '至少添加一个参数',
+    },
+    {
+      required: true,
+      validator: (rule: FormItemRule, value: FaultRecordParamItem[]) => {
+        for (const item of value) {
+          if (item.val.length === 0) {
+            return new Error('请填写参数值')
+          }
+
+          const isValidHex = HEX_REGEX.test(item.val)
+          if (!isValidHex) {
+            return new Error('请填写正确参数值 (0-9,a-f,A-F)')
+          }
+        }
+        return true
+      },
+    },
+  ]
+}
+
+const clickAddSubModel = () => {
+  formValue.value.list.push({
+    key: crypto.randomUUID(),
+    type: 'pe',
+    sn: '',
+    paraList: [],
+  })
+}
+
+const clickDelSubModel = (index: number) => {
+  formValue.value.list.splice(index, 1)
+}
+
+const onCreateParam = () => {
+  return {
+    key: crypto.randomUUID(),
+    id: 0,
+    val: '',
+  }
+}
+
+const computedSubSn = (index: number) => {
+  return computed(() => {
+    if (formValue.value.list[index].type === 'agate') {
+      return formValue.value.sn
+    } else {
+      return formValue.value.list[index].sn
+    }
+  })
+}
+
+const computedSubSnDisabled = (index: number) => {
+  return computed(() => {
+    if (formValue.value.list[index].type === 'agate') {
+      return true
+    } else {
+      return false
+    }
+  })
+}
+
+const updateSubSn = (index: number, value: string) => {
+  formValue.value.list[index].sn = value
+}
+
+const transformFaultRecordParam = (para: FaultRecordParam): FaultRecordParamInfo => {
+  return {
+    sn: para.sn,
+    list: para.list.map((s) => {
+      return {
+        type: s.type,
+        sn: s.type === 'agate' ? para.sn : s.sn,
+        paraList: s.paraList.map((p) => {
+          return {
+            id: p.id,
+            val: p.val,
+          }
+        }),
+      }
+    }),
+  }
+}
+
+const clickSaveConfig = () => {
+  formRef.value?.validate((errors) => {
+    if (errors) {
+      console.log(errors)
+      return
+    }
+
+    const res = setFaultRecordParam(transformFaultRecordParam(formValue.value))
+    res.then(() => {
+      message.success('写入成功')
+    })
+  })
+}
+</script>