소스 검색

feat:新增SIM卡管理界面

Jerry Liang 3 달 전
부모
커밋
272d305422
61개의 변경된 파일과 3661개의 추가작업 그리고 487개의 파일을 삭제
  1. 8 1
      service/.vscode/settings.json
  2. 1 0
      service/cmd/showTodayErrorLog.sh
  3. 13 3
      service/config/config.go
  4. 69 0
      service/controllers/simCardManageControllers.go
  5. 38 26
      service/domain/acceleronixDomain.go
  6. 53 0
      service/domain/simCardDomain.go
  7. 29 0
      service/dto/simCardDTO.go
  8. 10 6
      service/go.mod
  9. 248 0
      service/go.sum
  10. 50 4
      service/main.go
  11. 1 0
      service/repositories/api/acceleronix/acceleronixHttpClient.go
  12. 36 5
      service/repositories/api/acceleronix/simCardManage.go
  13. 2 4
      service/repositories/api/smartHub/simCardManage.go
  14. 2 2
      service/repositories/api/smartHub/test/main.go
  15. 181 51
      service/repositories/mongo/simCardManageRepo.go
  16. 37 0
      service/repositories/mongo/siteInfoManageRepo.go
  17. 6 8
      service/repositories/mqtt/fwh_mqtt/fwh_mqtt_client.go
  18. 11 7
      service/repositories/mqtt/fwh_mqtt/fwh_mqtt_fault_record.go
  19. 307 0
      service/repositories/mqtt/fwh_mqtt_test/fwh_mqtt_client.go
  20. 43 0
      service/repositories/mqtt/fwh_mqtt_test/fwh_mqtt_debug.go
  21. 202 0
      service/repositories/mqtt/fwh_mqtt_test/fwh_mqtt_fault_record.go
  22. 71 0
      service/repositories/mqtt/fwh_mqtt_test/test/main.go
  23. 4 1
      service/repositories/repositories.go
  24. 1 0
      service/routes/routes.go
  25. 20 0
      service/routes/simCardManageRoutes.go
  26. 6 3
      service/scheduler/scheduler.go
  27. 2 2
      service/services/backgroundTaskServices.go
  28. 2 2
      service/services/batchDownloadServices.go
  29. 62 0
      service/services/simCardManageServices.go
  30. 2 2
      service/services/siteInfoManageServices.go
  31. 3 3
      service/services/sitesAnalysisServices.go
  32. 58 0
      service/task/core/taskCore.go
  33. 63 0
      service/task/plugin/aggregator.go
  34. 138 0
      service/task/plugin/scheduler.go
  35. 1 1
      service/task/queue/fwhQueue/fwhTaskAlarmStatistics.go
  36. 1 1
      service/task/queue/fwhQueue/fwhTaskBatchDownload.go
  37. 2 2
      service/task/queue/fwhQueue/fwhTaskQueue.go
  38. 1 1
      service/task/queue/fwhQueue/fwhTaskShutdownStatistics.go
  39. 1 1
      service/task/queue/fwhQueue/fwhTaskSiteAnalysis.go
  40. 1 1
      service/task/queue/fwhQueue/fwhTaskSiteInfoUpdate.go
  41. 178 0
      service/task/queue/simCardQueue/simCardTaskAcceleronix.go
  42. 293 0
      service/task/queue/simCardQueue/simCardTaskQueue.go
  43. 136 0
      service/task/queue/simCardQueue/simCardTaskSmartHub.go
  44. 16 0
      service/task/queue/taskQueue.go
  45. 28 0
      service/task/task.go
  46. 10 0
      service/task/taskList.go
  47. 8 6
      service/utils/httpClient/httpClient.go
  48. 18 1
      service/utils/mqttClient/mqttClient.go
  49. 36 4
      src/composable/useDiscreteApi.ts
  50. 23 12
      src/router/record.ts
  51. 9 2
      src/service/fetchClient.ts
  52. 33 0
      src/service/simCardManage/index.ts
  53. 32 0
      src/service/simCardManage/interface.ts
  54. 2 0
      src/stores/initStores.ts
  55. 59 0
      src/stores/modules/views/faultRecordState.ts
  56. 57 30
      src/views/fault-record/write/index.vue
  57. 232 0
      src/views/sim-card-manage/component/simCardDataView.vue
  58. 372 0
      src/views/sim-card-manage/index.vue
  59. 5 216
      src/views/test/index.vue
  60. 218 0
      src/views/test/index.vue_bak
  61. 110 79
      src/views/xlog/component/xlogCommand.vue

+ 8 - 1
service/.vscode/settings.json

@@ -28,7 +28,14 @@
     "imsi",
     "msisdn",
     "acceleronix",
-    "xlogaddr"
+    "xlogaddr",
+    "casdoor",
+    "evio",
+    "asynq",
+    "asynqmon",
+    "gofiber",
+    "hibiken",
+    "cronspec"
   ],
   "editor.tabSize": 2
 }

+ 1 - 0
service/cmd/showTodayErrorLog.sh

@@ -0,0 +1 @@
+journalctl -u fwh-toolbox-server --since today | grep "ERROR"

+ 13 - 3
service/config/config.go

@@ -19,6 +19,7 @@ type AppConfig struct {
 	MinioAccessKey  string
 	MinioSecretKey  string
 	MinioBucketName string
+	AsynqmonPort    string
 }
 
 type FwhApiConfig struct {
@@ -36,9 +37,10 @@ type FwhMqttConfig struct {
 }
 
 type Config struct {
-	App     AppConfig
-	FwhApi  FwhApiConfig
-	FwhMqtt FwhMqttConfig
+	App         AppConfig
+	FwhApi      FwhApiConfig
+	FwhMqtt     FwhMqttConfig
+	FwhMqttTest FwhMqttConfig
 }
 
 // 需先执行LoadConfig后才能读取ENV
@@ -64,6 +66,7 @@ func LoadConfig() (*Config, error) {
 	v.SetDefault("MINIO_ACCESS_KEY", "fwh-toolbox")
 	v.SetDefault("MINIO_SECRET_KEY", "fwh-toolbox")
 	v.SetDefault("MINIO_BUCKET_NAME", "fwh-toolbox")
+	v.SetDefault("ASYNQMON_PORT", "8081")
 	// FWH API
 	v.SetDefault("FWH_ENERGY", "energy.fwh-toolbox.top")
 	v.SetDefault("FWH_OSS", "energy-oss.fwh-toolbox.top")
@@ -113,6 +116,7 @@ func LoadConfig() (*Config, error) {
 			MinioAccessKey:  v.GetString("MINIO_ACCESS_KEY"),
 			MinioSecretKey:  v.GetString("MINIO_SECRET_KEY"),
 			MinioBucketName: v.GetString("MINIO_BUCKET_NAME"),
+			AsynqmonPort:    v.GetString("ASYNQMON_PORT"),
 		},
 		FwhApi: FwhApiConfig{
 			FwhEnergy:   v.GetString("FWH_ENERGY"),
@@ -126,5 +130,11 @@ func LoadConfig() (*Config, error) {
 			FwhMqttCertUrl: v.GetString("FWH_MQTT_CERT"),
 			FwhMqttKeyUrl:  v.GetString("FWH_MQTT_KEY"),
 		},
+		FwhMqttTest: FwhMqttConfig{
+			FwhMqttBroker:  v.GetString("FWH_MQTT_BROKER"),
+			FwhMqttCaUrl:   v.GetString("FWH_MQTT_CA"),
+			FwhMqttCertUrl: v.GetString("FWH_MQTT_CERT"),
+			FwhMqttKeyUrl:  v.GetString("FWH_MQTT_KEY"),
+		},
 	}, nil
 }

+ 69 - 0
service/controllers/simCardManageControllers.go

@@ -0,0 +1,69 @@
+/*
+ * SIM Card Manage API List
+ * /simCardManage/info - GET 查询SIM卡信息
+ * /simCardManage/monthly - GET 查询SIM卡月数据
+ */
+package controllers
+
+import (
+	"fwh-toolbox-server/services"
+	"regexp"
+	"strconv"
+
+	"github.com/gofiber/fiber/v2"
+	"go.uber.org/zap"
+)
+
+var yearMonthRegexp = regexp.MustCompile(`^\d{4}-(0[1-9]|1[0-2])$`)
+
+// 查询SIM卡信息
+// /simCardManage/info
+func GetSimCardBatchInfo(c *fiber.Ctx) error {
+	search := c.Query("search", "")
+	pageStr := c.Query("page", "1")
+	page, err := strconv.ParseUint(pageStr, 10, 64)
+	if err != nil {
+		zap.L().Warn("Invalid request parameter", zap.String("err", err.Error()), zap.String("page", pageStr))
+		return Fail(c, "Invalid request parameter (page)")
+	}
+	sizeStr := c.Query("size", "20")
+	size, err := strconv.ParseUint(sizeStr, 10, 64)
+	if err != nil {
+		zap.L().Warn("Invalid request parameter", zap.String("err", err.Error()), zap.String("size", sizeStr))
+		return Fail(c, "Invalid request parameter (size)")
+	}
+	status := c.Query("status", "")
+	var statusPtr *string
+	if status != "" {
+		statusPtr = &status
+	}
+
+	date, err := services.GetSimCardBatchInfo(uint(page), uint(size), search, statusPtr)
+	if err != nil {
+		return Error(c, "Get SIM card info failed.")
+	}
+	return Success(c, date)
+}
+
+// 查询SIM卡月数据
+// /simCardManage/monthly
+func GetSimCardMonthlyData(c *fiber.Ctx) error {
+	id := c.Query("id", "")
+	month := c.Query("month", "")
+
+	if id == "" {
+		zap.L().Warn("id parameter is empty")
+		return Fail(c, "id parameter is empty")
+	}
+
+	if !yearMonthRegexp.MatchString(month) {
+		zap.L().Warn("month parameter is in the wrong format", zap.String("month", month))
+		return Fail(c, "month parameter is in the wrong format")
+	}
+
+	date, err := services.GetSimCardMonthlyData(month, id)
+	if err != nil {
+		return Error(c, "Get SIM card monthly data failed.")
+	}
+	return Success(c, date)
+}

+ 38 - 26
service/domain/acceleronixDomain.go

@@ -15,6 +15,13 @@ const (
 	SCS_Cancellation                                       // 10 注销
 )
 
+// 查询错误 - 响应
+type AcceleronixResponseError struct {
+	Code  int    `json:"code"` // 响应码
+	Msg   string `json:"msg"`  // 响应描述
+	ReqId string `json:"reqId"`
+}
+
 // 查询SIM列表 - 请求结构体
 type AcceleronixSimCardListRequestBody struct {
 	EnterpriseCode      *string                  `json:"enterpriseCode,omitempty"`      // 企业编码
@@ -30,34 +37,39 @@ type AcceleronixSimCardListRequestBody struct {
 	SimType             *string                  `json:"simType,omitempty"`             // 卡类型
 }
 
+// 查询SIM卡列表项详情
+type AcceleronixSimCardInfoItem 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"`              // 卡类型
+}
+
+type AcceleronixSimCardListData struct {
+	Current int64                        `json:"current"` // 当前页码
+	List    []AcceleronixSimCardInfoItem `json:"list"`    // 数据集合
+	Pages   int64                        `json:"pages"`   // 总页码
+	Size    int64                        `json:"size"`    // 每页数量
+	Total   int64                        `json:"total"`   // 总条数
+}
+
 // 查询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"`
+	Code  int                        `json:"code"` // 响应码
+	Data  AcceleronixSimCardListData `json:"data"` // 响应体
+	Msg   string                     `json:"msg"`  // 响应描述
+	ReqId string                     `json:"reqId"`
 }
 
 // 查询SIM卡详情 - 响应

+ 53 - 0
service/domain/simCardDomain.go

@@ -0,0 +1,53 @@
+package domain
+
+import (
+	"time"
+
+	"go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type SimCardStatus string
+
+const (
+	SimStatusUnknown      SimCardStatus = "UNKNOWN"       // 未知
+	SimStatusNotActivated SimCardStatus = "NOT_ACTIVATED" // 未激活
+	SimStatusActivated    SimCardStatus = "ACTIVATED"     // 已激活
+	SimStatusDeactivated  SimCardStatus = "DEACTIVATED"   // 已停用
+	SimStatusRetired      SimCardStatus = "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         SimCardStatus `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 {
+	ID           primitive.ObjectID          `bson:"_id,omitempty" json:"id"`
+	SimId        string                      `bson:"simId" 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[uint8]SimCardDailyUsage `bson:"dailyUsage" json:"dailyUsage"`     // 日用量数据信息 (index=当月日数1-31)
+}
+
+type SimCardDayData struct {
+	SimId        string            `bson:"simId" 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)
+	Day          uint8             `bson:"day" json:"day"`                   // 当天日期值 1-31
+	DailyUsage   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)
+}

+ 29 - 0
service/dto/simCardDTO.go

@@ -0,0 +1,29 @@
+package dto
+
+import (
+	"fwh-toolbox-server/domain"
+	"time"
+)
+
+type SimCardStatus = domain.SimCardStatus
+
+// const (
+// 	SimStatusUnknown      SimCardStatus = "UNKNOWN"       // 未知
+// 	SimStatusNotActivated SimCardStatus = "NOT_ACTIVATED" // 未激活
+// 	SimStatusActivated    SimCardStatus = "ACTIVATED"     // 已激活
+// 	SimStatusDeactivated  SimCardStatus = "DEACTIVATED"   // 已停用
+// 	SimStatusRetired      SimCardStatus = "RETIRED"       // 失效
+// )
+
+type SimCardInfo struct {
+	GICCID         string        `json:"giccid"`         // 只取前19位不取检验码
+	SN             []string      `json:"sn"`             // 相关的SN列表
+	IMEI           string        `json:"imei"`           // 国际移动用户识别码
+	IMSI           string        `json:"imsi"`           // 国际移动用户识别码
+	MSISDN         string        `json:"msisdn"`         // 移动用户号码
+	Status         SimCardStatus `json:"status"`         // 卡状态
+	ExpirationTime string        `json:"expirationTime"` // 到期时间(后付费卡没有到期时间) 格式:yyyy-MM-dd
+	Month          string        `json:"month"`          // 统计月份 格式:yyyy-MM
+	AllDataUsage   float64       `json:"allDataUsage"`   // 月数据用量(KB)
+	UpdateTime     time.Time     `json:"updateTime"`     // UTC时间
+}

+ 10 - 6
service/go.mod

@@ -1,8 +1,6 @@
 module fwh-toolbox-server
 
-go 1.24.0
-
-toolchain go1.24.5
+go 1.25.0
 
 require (
 	github.com/bwmarrin/snowflake v0.3.0
@@ -11,7 +9,7 @@ require (
 	github.com/gofiber/fiber/v2 v2.52.9
 	github.com/gofiber/swagger v1.1.1
 	github.com/google/uuid v1.6.0
-	github.com/redis/go-redis/v9 v9.14.0
+	github.com/redis/go-redis/v9 v9.21.0
 	github.com/robfig/cron/v3 v3.0.1
 	github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
 	github.com/spf13/viper v1.20.1
@@ -31,13 +29,19 @@ require (
 
 require (
 	github.com/eclipse/paho.mqtt.golang v1.5.1 // indirect
+	github.com/gorilla/mux v1.8.1 // indirect
 	github.com/gorilla/websocket v1.5.3 // indirect
+	github.com/hibiken/asynq v0.26.0 // indirect
+	github.com/hibiken/asynqmon v0.7.2 // indirect
 	github.com/mattn/go-sqlite3 v1.14.16 // indirect
 	github.com/richardlehane/mscfb v1.0.4 // indirect
 	github.com/richardlehane/msoleps v1.0.4 // indirect
 	github.com/tiendc/go-deepcopy v1.7.1 // indirect
 	github.com/xuri/efp v0.0.1 // indirect
 	github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
+	go.uber.org/atomic v1.11.0 // indirect
+	golang.org/x/time v0.15.0 // indirect
+	google.golang.org/protobuf v1.36.11 // indirect
 )
 
 require (
@@ -88,7 +92,7 @@ require (
 	github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
 	github.com/sourcegraph/conc v0.3.0 // indirect
 	github.com/spf13/afero v1.12.0 // indirect
-	github.com/spf13/cast v1.7.1 // indirect
+	github.com/spf13/cast v1.10.0 // indirect
 	github.com/spf13/pflag v1.0.7 // indirect
 	github.com/subosito/gotenv v1.6.0 // indirect
 	github.com/swaggo/files/v2 v2.0.2 // indirect
@@ -106,7 +110,7 @@ require (
 	golang.org/x/mod v0.28.0 // indirect
 	golang.org/x/net v0.46.0 // indirect
 	golang.org/x/sync v0.17.0 // indirect
-	golang.org/x/sys v0.37.0 // indirect
+	golang.org/x/sys v0.46.0 // indirect
 	golang.org/x/text v0.30.0 // indirect
 	golang.org/x/tools v0.37.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect

+ 248 - 0
service/go.sum

@@ -1,23 +1,41 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
 github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
 github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
 github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
 github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
 github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
+github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
 github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
 github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
 github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
 github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
 github.com/casdoor/casdoor-go-sdk v1.21.0 h1:EgDhM1WGmYEqG0Pa9PPMkyAISQkrSR6+mrTsL+QZ2Bg=
 github.com/casdoor/casdoor-go-sdk v1.21.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -28,16 +46,27 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
 github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek=
 github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
+github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
 github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/go-echarts/go-echarts/v2 v2.6.6 h1:3edScZCB36YmWeSu4JiT8Jnh/gTrpeV1dWmEoQpc46I=
 github.com/go-echarts/go-echarts/v2 v2.6.6/go.mod h1:Z+spPygZRIEyqod69r0WMnkN5RV3MwhYDtw601w3G8w=
 github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
 github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
 github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
 github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
 github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@@ -48,8 +77,12 @@ github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7
 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
 github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
 github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
+github.com/go-redis/redis/v8 v8.11.2/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M=
+github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
 github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
 github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
 github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
@@ -60,20 +93,61 @@ github.com/gofiber/swagger v1.1.1 h1:FZVhVQQ9s1ZKLHL/O0loLh49bYB5l1HEAgxDlcTtkRA
 github.com/gofiber/swagger v1.1.1/go.mod h1:vtvY/sQAMc/lGTUCg0lqmBL7Ht9O7uzChpbvJeJQINw=
 github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
 github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
 github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/hibiken/asynq v0.19.0/go.mod h1:tyc63ojaW8SJ5SBm8mvI4DDONsguP5HE85EEl4Qr5Ig=
+github.com/hibiken/asynq v0.24.1/go.mod h1:u5qVeSbrnfT+vtG5Mq8ZPzQu/BmCKMHvTGb91uy9Tts=
+github.com/hibiken/asynq v0.26.0 h1:1Zxr92MlDnb1Zt/QR5g2vSCqUS03i95lUfqx5X7/wrw=
+github.com/hibiken/asynq v0.26.0/go.mod h1:Qk4e57bTnWDoyJ67VkchuV6VzSM9IQW2nPvAGuDyw58=
+github.com/hibiken/asynq/x v0.0.0-20211219150637-8dfabfccb3be/go.mod h1:VmxwMfMKyb6gyv8xG0oOBMXIhquWKPx+zPtbVBd2Q1s=
+github.com/hibiken/asynqmon v0.7.2 h1:YohWgTIPwtMyZ6khBDcVUz9BdSdQW2Dxn8SoxtbmjSg=
+github.com/hibiken/asynqmon v0.7.2/go.mod h1:jUbrpFNDwoJ6avGNjHIazFuCmQj78C3dbJowV0x9x8E=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
 github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -88,6 +162,12 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
 github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/kavu/go_reuseport v1.5.0 h1:UNuiY2OblcqAtVDE8Gsg1kZz8zbBWg907sP1ceBV+bk=
 github.com/kavu/go_reuseport v1.5.0/go.mod h1:CG8Ee7ceMFSMnx/xr25Vm0qXaj2Z4i5PWoUx+JZ5/CU=
 github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -95,7 +175,11 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW
 github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
 github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -116,6 +200,7 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
 github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
 github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
 github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
 github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg=
@@ -124,17 +209,56 @@ github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
 github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
 github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
 github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
 github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
+github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
+github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
 github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
 github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
 github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
 github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
+github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
+github.com/redis/go-redis/v9 v9.0.4/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
 github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
 github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
+github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
+github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
 github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
 github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
 github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
@@ -144,8 +268,10 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
 github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
 github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
 github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
@@ -154,18 +280,29 @@ github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1Avp
 github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
 github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
 github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
 github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
 github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
 github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
 github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
 github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
 github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
+github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
 github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
 github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
 github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
@@ -202,67 +339,176 @@ github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBL
 github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
 go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
+go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
 golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
 golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
 golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
 golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
 golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
 golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
 golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
+golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
 golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
+golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
 golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -289,3 +535,5 @@ gorm.io/hints v1.1.2 h1:b5j0kwk5p4+3BtDtYqqfY+ATSxjj+6ptPgVveuynn9o=
 gorm.io/hints v1.1.2/go.mod h1:/ARdpUHAtyEMCh5NNi3tI7FsGh+Cj/MIUlvNxCNCFWg=
 gorm.io/plugin/dbresolver v1.6.2 h1:F4b85TenghUeITqe3+epPSUtHH7RIk3fXr5l83DF8Pc=
 gorm.io/plugin/dbresolver v1.6.2/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

+ 50 - 4
service/main.go

@@ -6,9 +6,13 @@ import (
 	"fwh-toolbox-server/database"
 	"fwh-toolbox-server/repositories"
 	"fwh-toolbox-server/repositories/api/fwh_pro"
-	"fwh-toolbox-server/taskQueue"
+	"fwh-toolbox-server/task"
 	"fwh-toolbox-server/utils/casdoor"
 	"fwh-toolbox-server/utils/minio"
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
 
 	// "fwh-toolbox-server/database"
 	_ "fwh-toolbox-server/docs" // swag 自动生成文档
@@ -20,7 +24,10 @@ import (
 
 	"github.com/gofiber/fiber/v2"
 	"github.com/gofiber/swagger"
+	"github.com/hibiken/asynq"
+	"github.com/hibiken/asynqmon"
 	"go.uber.org/zap"
+	"golang.org/x/sys/unix"
 )
 
 func main() {
@@ -55,8 +62,11 @@ func main() {
 	// 初始化计划任务
 	scheduler.Init()
 
-	// 初始化任务队列
-	taskQueue.InitTaskQueue()
+	// 初始化任务
+	task.InitTask(task.TaskRedisOpt{
+		Addr: cfg.App.RedisAddr,
+		DB:   12,
+	})
 
 	// 初始化Casdoor
 	casdoor.InitAuthConfig()
@@ -77,7 +87,6 @@ func main() {
 	routes.Setup(app)
 
 	// 启动第一个监听
-	// go func() {
 	go func() {
 		if err := networkEvents.InitEvio(); err != nil {
 			zap.L().Error("networkEvents init failed.", zap.String("err", err.Error()))
@@ -85,6 +94,29 @@ func main() {
 		}
 	}()
 
+	// 启动Asynqmon界面
+	go func() {
+		h := asynqmon.New(asynqmon.Options{
+			RootPath:     "/asynqmon", // RootPath specifies the root for asynqmon app
+			RedisConnOpt: asynq.RedisClientOpt{Addr: cfg.App.RedisAddr, DB: 12},
+		})
+
+		// Note: We need the tailing slash when using net/http.ServeMux.
+		http.Handle("/", h)
+
+		listenAddr := "0.0.0.0:" + cfg.App.AsynqmonPort
+
+		// Go to http://localhost:8080/asynqmon to see asynqmon homepage.
+		err = http.ListenAndServe(listenAddr, nil)
+		if err != nil {
+			zap.L().Error("Asynqmon port listening failed.", zap.String("err", err.Error()))
+			panic(err)
+		}
+
+		log.Println("asynqmon shutdown finished")
+		zap.L().Info("Asynqmon started successfully.", zap.String("addr", listenAddr))
+	}()
+
 	// 提前登录FWH
 	err = fwh_pro.Login()
 	if err != nil {
@@ -92,6 +124,20 @@ func main() {
 		// panic(err)
 	}
 
+	// TEST
+	// err = task.CreateTaskSimCardCollect()
+	// if err != nil {
+	// 	zap.L().Error("CreateTaskSimCardCollect failed.", zap.String("err", err.Error()), zap.String("time(UTC)", time.Now().UTC().Format(time.DateTime)))
+	// }
+
+	go func() {
+		sigs := make(chan os.Signal, 1)
+		signal.Notify(sigs, unix.SIGTERM, unix.SIGINT)
+		<-sigs
+		log.Println("fiber shutdown")
+		app.Shutdown()
+	}()
+
 	err = app.Listen("localhost:" + cfg.App.AppPort)
 	if err != nil {
 		zap.L().Error("Port listening failed.", zap.String("err", err.Error()))

+ 1 - 0
service/repositories/api/acceleronix/acceleronixHttpClient.go

@@ -80,5 +80,6 @@ func acceleronixHttpPost(url string, body any) (*httpClient.Response, error) {
 			"Accept":    {"application/json", "text/plain", "*/*"},
 		},
 	}
+
 	return acceleronixHttpClient.HttpPost(mainUrl+url, body, &opt)
 }

+ 36 - 5
service/repositories/api/acceleronix/simCardManage.go

@@ -1,7 +1,9 @@
 package acceleronix
 
 import (
+	"bytes"
 	"encoding/json"
+	"errors"
 	"fwh-toolbox-server/domain"
 
 	"go.uber.org/zap"
@@ -24,6 +26,7 @@ func QuerySimCardList(page uint, size uint) (*domain.AcceleronixSimCardListRespo
 		return nil, err
 	}
 
+	zap.L().Debug("QuerySimCardList", zap.Int("DataNum", len(resList.Data.List)), zap.Int64("Page", resList.Data.Current), zap.Int64("Size", resList.Data.Size), zap.Int64("Total", resList.Data.Total))
 	return &resList, nil
 }
 
@@ -43,6 +46,7 @@ func QuerySimCardInfo(iccid string) (*domain.AcceleronixSimCardInfoResponse, err
 		return nil, err
 	}
 
+	zap.L().Debug("QuerySimCardInfo", zap.String("GICCID", iccid))
 	return &resInfo, nil
 }
 
@@ -58,13 +62,38 @@ func QueryBatchSimCardDataInfo(iccidList []string, month string) ([]domain.Accel
 		return nil, err
 	}
 
-	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
+	// 处理多种格式Body(成功:数组,失败:Struct)
+	bodyBytes := bytes.TrimSpace(res.Body)
+
+	if len(bodyBytes) == 0 {
+		zap.L().Error("QueryBatchSimCardDataInfo Body is empty.", zap.String("body", string(res.Body)))
+		return nil, errors.New("QueryBatchSimCardDataInfo empty response body")
 	}
 
-	return resList, nil
+	switch bodyBytes[0] {
+	case '[':
+		var result []domain.AcceleronixSimCardDataUsageInfoResponse
+		if err := json.Unmarshal(bodyBytes, &result); err != nil {
+			zap.L().Error("QueryBatchSimCardDataInfo Unmarshal failed.", zap.String("err", err.Error()), zap.String("body", string(res.Body)))
+			return nil, err
+		}
+
+		zap.L().Debug("QueryBatchSimCardDataInfo", zap.Strings("GICCIDs", iccidList), zap.String("month", month))
+		return result, nil
+
+	case '{':
+		var apiErr domain.AcceleronixResponseError
+		if err := json.Unmarshal(bodyBytes, &apiErr); err == nil {
+			zap.L().Error("QueryBatchSimCardDataInfo Error struct unmarshal failed.", zap.String("body", string(res.Body)))
+			return nil, err
+		}
+
+		zap.L().Error("QueryBatchSimCardDataInfo AcceleronixResponseError", zap.Int("Code", apiErr.Code), zap.String("Msg", apiErr.Msg))
+		return nil, errors.New("QueryBatchSimCardDataInfo AcceleronixResponseError")
+
+	default:
+		return nil, errors.New("QueryBatchSimCardDataInfo invalid body")
+	}
 }
 
 // 查询流量池列表
@@ -84,6 +113,7 @@ func QueryDataPoolList(page uint, size uint) (*domain.AcceleronixDataPoolListRes
 		return nil, err
 	}
 
+	zap.L().Debug("QueryDataPoolList", zap.Int("DataNum", len(resList.Data.List)), zap.Int64("Page", resList.Data.Current), zap.Int64("Size", resList.Data.Size), zap.Int64("Total", resList.Data.Total))
 	return &resList, nil
 }
 
@@ -105,5 +135,6 @@ func QueryDataPoolSimInfo(dataPoolId string, page uint, size uint) (*domain.Acce
 		return nil, err
 	}
 
+	zap.L().Debug("QueryDataPoolSimInfo", zap.String("PoolId", dataPoolId), zap.Int("DataNum", len(resInfo.Data.List)), zap.Int64("Page", resInfo.Data.Current), zap.Int64("Size", resInfo.Data.Size), zap.Int64("Total", resInfo.Data.Total))
 	return &resInfo, nil
 }

+ 2 - 4
service/repositories/api/smartHub/simCardManage.go

@@ -2,7 +2,6 @@ package smartHub
 
 import (
 	"encoding/json"
-	"fmt"
 	"fwh-toolbox-server/domain"
 	"net/url"
 	"strconv"
@@ -23,13 +22,12 @@ 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
 	}
 
+	zap.L().Debug("QueryBatchSimCardInfo", zap.Int("DataNum", len(infoList.Data)), zap.Uint("Page", infoList.Page), zap.Uint("Size", infoList.Size), zap.Uint64("Total", infoList.Total))
 	return &infoList, nil
 }
 
@@ -41,11 +39,11 @@ func QuerySimCardInfo(giccid string) (*domain.SmartHubSimCardInfo, error) {
 	}
 
 	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
 	}
 
+	zap.L().Debug("QuerySimCardInfo", zap.String("GICCID", giccid))
 	return &info, nil
 }

+ 2 - 2
service/repositories/api/smartHub/test/main.go

@@ -6,8 +6,8 @@ import (
 )
 
 func main() {
-	// 批量获取SIM卡详细信息
-	infoList, err := smartHub.QueryBatchSimCardInfo(1, 50)
+	// 批量获取SIM卡详细信息 最多每页50项
+	infoList, err := smartHub.QueryBatchSimCardInfo(1, 100)
 	if err != nil {
 		panic(err)
 	}

+ 181 - 51
service/repositories/mongo/simCardManageRepo.go

@@ -2,12 +2,16 @@ package mongo
 
 import (
 	"context"
+	"fmt"
 	"fwh-toolbox-server/database"
+	"fwh-toolbox-server/domain"
 	"fwh-toolbox-server/dto"
 	"time"
 
 	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo"
 	"go.mongodb.org/mongo-driver/mongo/options"
+	"go.uber.org/zap"
 )
 
 const (
@@ -15,61 +19,47 @@ const (
 	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) {
+func GetSimCardBatchInfo(page, size uint, search string, status *string) (*dto.ResPage[domain.SimCardInfo], error) {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
 
 	collection := database.GetCollection(CollectionName_SimCardManage)
 
+	// 查询条件
+	filter := bson.M{}
+
+	// 搜索 _id (GICCID)
+	if search != "" {
+		filter["_id"] = bson.M{
+			"$regex":   search,
+			"$options": "i", // 不区分大小写,可去掉
+		}
+	}
+
+	// 过滤状态
+	if status != nil {
+		filter["status"] = bson.M{
+			"$eq": *status,
+		}
+	}
+
 	findOptions := options.Find().
 		SetSkip(int64((page - 1) * size)).
-		SetLimit(int64(size))
+		SetLimit(int64(size)).
+		SetSort(bson.D{
+			{Key: "allDataUsage", Value: -1}, // 降序
+		})
 
-	cursor, err := collection.Find(ctx, bson.M{}, findOptions)
+	cursor, err := collection.Find(ctx, filter, findOptions)
 	if err != nil {
 		return nil, err
 	}
 	defer cursor.Close(ctx)
 
-	var infoList []SimCardInfo
+	var infoList []domain.SimCardInfo
 	for cursor.Next(ctx) {
-		var info SimCardInfo
+		var info domain.SimCardInfo
 		if err := cursor.Decode(&info); err != nil {
 			return nil, err
 		}
@@ -82,7 +72,7 @@ func GetSimCardBatchInfo(page, size uint) (*dto.ResPage[SimCardInfo], error) {
 		return nil, err
 	}
 
-	res := dto.ResPage[SimCardInfo]{
+	res := dto.ResPage[domain.SimCardInfo]{
 		Data:     infoList,
 		PageSize: size,
 		PageNum:  page,
@@ -93,13 +83,13 @@ func GetSimCardBatchInfo(page, size uint) (*dto.ResPage[SimCardInfo], error) {
 }
 
 // 查询SIM卡信息(单卡)
-func GetSimCardInfo(id string) (*SimCardInfo, error) {
+func GetSimCardInfo(id string) (*domain.SimCardInfo, error) {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
 
 	collection := database.GetCollection(CollectionName_SimCardManage)
 
-	var info SimCardInfo
+	var info domain.SimCardInfo
 	err := collection.FindOne(ctx, bson.M{"_id": id}).Decode(&info)
 	if err != nil {
 		return nil, err
@@ -109,14 +99,14 @@ func GetSimCardInfo(id string) (*SimCardInfo, error) {
 }
 
 // 查询SIM卡月数据(单卡)
-func GetSimCardMonthlyData(month string, id string) (*SimCardMonthlyData, error) {
+func GetSimCardMonthlyData(month string, id string) (*domain.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)
+	var info domain.SimCardMonthlyData
+	err := collection.FindOne(ctx, bson.M{"simId": id, "month": month}).Decode(&info)
 	if err != nil {
 		return nil, err
 	}
@@ -125,7 +115,7 @@ func GetSimCardMonthlyData(month string, id string) (*SimCardMonthlyData, error)
 }
 
 // 插入SIM卡基本信息数据
-func UpdateSimCardInfo(info SimCardInfo) error {
+func UpdateSimCardInfo(info domain.SimCardInfo) error {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
 
@@ -141,19 +131,159 @@ func UpdateSimCardInfo(info SimCardInfo) error {
 	return nil
 }
 
-// 插入SIM卡月流量数据
-func UpdateSimCardMonthlyData(data SimCardMonthlyData) error {
+// 批量插入SIM卡基本信息数据
+func UpdateSimCardBatchInfo(infoList []domain.SimCardInfo) (int64, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_SimCardManage)
+
+	batchSize := 1000
+	var totalInserted int64
+
+	for start := 0; start < len(infoList); start += batchSize {
+
+		end := start + batchSize
+		if end > len(infoList) {
+			end = len(infoList)
+		}
+
+		models := make([]mongo.WriteModel, 0, end-start)
+
+		for _, doc := range infoList[start:end] {
+
+			model := mongo.NewReplaceOneModel().
+				SetFilter(bson.M{
+					"_id": doc.GICCID,
+				}).
+				SetReplacement(doc).
+				SetUpsert(true)
+
+			models = append(models, model)
+		}
+
+		result, err := collection.BulkWrite(
+			ctx,
+			models,
+			options.BulkWrite().
+				SetOrdered(false),
+		)
+
+		if err != nil {
+			zap.L().Error("UpdateSimCardBatchInfo bulk insert failed.", zap.String("err", err.Error()), zap.Int("start", start), zap.Int("end", end), zap.Int64("totalInserted", totalInserted))
+			return totalInserted, fmt.Errorf(
+				"bulk insert failed (%d~%d): %w",
+				start,
+				end,
+				err,
+			)
+		}
+
+		totalInserted += result.InsertedCount
+	}
+
+	return totalInserted, nil
+}
+
+// 插入SIM卡日流量数据
+func UpdateSimCardDayData(data domain.SimCardDayData) error {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
 
 	collection := database.GetCollection(CollectionName_SimCardMonthlyData)
 
-	filter := bson.M{"_id": data.SimId} // 按ID匹配
+	dailyUsageField := fmt.Sprintf(
+		"dailyUsage.%d",
+		data.Day,
+	)
+
+	update := bson.M{
+		"$set": bson.M{
+			"allDataUsage":  data.AllDataUsage,
+			"updateTime":    data.UpdateTime,
+			dailyUsageField: data.DailyUsage,
+		},
+		"$setOnInsert": bson.M{
+			"simId": data.SimId,
+			"month": data.Month,
+		},
+	}
+
+	filter := bson.M{"simId": data.SimId, "month": data.Month} // 按ID和month匹配
 
-	_, err := collection.ReplaceOne(ctx, filter, data, options.Replace().SetUpsert(true))
+	_, err := collection.UpdateOne(ctx, filter, update, options.Update().SetUpsert(true))
 	if err != nil {
 		return err
 	}
 
 	return nil
 }
+
+// 批量插入SIM卡日流量数据
+func UpdateSimCardBatchDayData(dataList []domain.SimCardDayData) (int64, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_SimCardMonthlyData)
+
+	batchSize := 1000
+	var totalInserted int64
+
+	for start := 0; start < len(dataList); start += batchSize {
+
+		end := start + batchSize
+		if end > len(dataList) {
+			end = len(dataList)
+		}
+
+		models := make([]mongo.WriteModel, 0, end-start)
+
+		for _, doc := range dataList[start:end] {
+
+			dailyUsageField := fmt.Sprintf("dailyUsage.%d", doc.Day)
+
+			update := bson.M{
+				"$set": bson.M{
+					"allDataUsage":  doc.AllDataUsage,
+					"updateTime":    doc.UpdateTime,
+					dailyUsageField: doc.DailyUsage,
+				},
+				"$setOnInsert": bson.M{
+					"simId": doc.SimId,
+					"month": doc.Month,
+				},
+			}
+
+			model := mongo.NewUpdateOneModel().
+				SetFilter(bson.M{
+					"simId": doc.SimId,
+					"month": doc.Month,
+				}).
+				SetUpdate(update).
+				SetUpsert(true)
+
+			models = append(models, model)
+		}
+
+		result, err := collection.BulkWrite(
+			ctx,
+			models,
+			options.BulkWrite().
+				SetOrdered(false),
+		)
+
+		if err != nil {
+			zap.L().Error("UpdateSimCardBatchMonthlyData bulk insert failed.", zap.String("err", err.Error()), zap.Int("start", start), zap.Int("end", end), zap.Int64("totalInserted", totalInserted))
+			return totalInserted, fmt.Errorf(
+				"bulk insert failed (%d~%d): %w",
+				start,
+				end,
+				err,
+			)
+		}
+
+		totalInserted += result.InsertedCount
+	}
+
+	return totalInserted, nil
+}

+ 37 - 0
service/repositories/mongo/siteInfoManageRepo.go

@@ -3,6 +3,7 @@ package mongo
 import (
 	"context"
 	"errors"
+	"fmt"
 	"fwh-toolbox-server/database"
 	"fwh-toolbox-server/domain"
 	"fwh-toolbox-server/dto"
@@ -263,6 +264,42 @@ func GetSiteInfoManageList(page, size uint, filter *domain.SiteInfoFilter, searc
 	return &res, nil
 }
 
+// 从数据库获取站点信息列表(基于GICCID)
+func GetSiteInfoManageListFromGiccid(giccid string) ([]domain.SiteInfoDomain, error) {
+	// 判断GICCID的有效性
+	if len(giccid) != 19 {
+		return nil, fmt.Errorf("giccid len is %s, not 19", giccid)
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_SiteInfoManageData)
+
+	// 条件
+	filter := bson.M{
+		"iccId": bson.M{
+			"$regex": giccid, "$options": "i"},
+	}
+
+	cursor, err := collection.Find(ctx, filter, options.Find())
+	if err != nil {
+		return nil, err
+	}
+	defer cursor.Close(ctx)
+
+	var infoList []domain.SiteInfoDomain
+	for cursor.Next(ctx) {
+		var info domain.SiteInfoDomain
+		if err := cursor.Decode(&info); err != nil {
+			return nil, err
+		}
+		infoList = append(infoList, info)
+	}
+
+	return infoList, nil
+}
+
 // 插入站点信息数据
 func InsertSiteInfoData(data domain.SiteInfoDomain) error {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

+ 6 - 8
service/repositories/mqtt/fwh_mqtt/fwh_mqtt_client.go

@@ -110,15 +110,13 @@ func (c *FwhMqttClient) fwhMqttWorker() {
 				break
 			}
 
-			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
+			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
 			}
 
 			// 订阅响应消息 - 请求响应无法一一对应,不支持并发请求,改成简单的队列请求

+ 11 - 7
service/repositories/mqtt/fwh_mqtt/fwh_mqtt_fault_record.go

@@ -9,6 +9,8 @@ import (
 	"hash/crc32"
 	"strings"
 	"time"
+
+	"go.uber.org/zap"
 )
 
 const (
@@ -179,13 +181,15 @@ func FaultRecordParamSetting(ctx context.Context, data dto.FaultRecordParamInfo)
 		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)
+	var res FaultRecordResponse
+	b, err := json.Marshal(msg.DataArea)
+	if err != nil {
+		return err
+	}
+
+	if err := json.Unmarshal(b, &res); err != nil {
+		zap.L().Error("FaultRecordParamSetting response data type unmarshal error.", zap.String("DataArea", string(b)), zap.String("err", err.Error()))
+		return err
 	}
 
 	if res.Result != 0 {

+ 307 - 0
service/repositories/mqtt/fwh_mqtt_test/fwh_mqtt_client.go

@@ -0,0 +1,307 @@
+package fwh_mqtt_test
+
+import (
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/json"
+	"fmt"
+	"fwh-toolbox-server/config"
+	"fwh-toolbox-server/utils/mqttClient"
+	"os"
+	"strconv"
+	"time"
+
+	"go.uber.org/zap"
+)
+
+const (
+	fwhClientIdPrefix  = "fwh_xlog_mqtt_client_"
+	sleepTimeout       = 1 * time.Minute
+	sendMessageTimeout = 1 * time.Minute
+
+	topicPrefixS2C = "fwh/s2c/"
+	topicPrefixC2S = "fwh/c2s/"
+)
+
+type FwhMqttResult struct {
+	Err  error
+	Data *FwhMqttMsg
+}
+
+type FwhMqttMsg struct {
+	CmdType   uint        `json:"cmdType"`   // 业务类型标识
+	Crc       string      `json:"crc"`       // dataArea 数据字符串的CRC
+	DataArea  interface{} `json:"dataArea"`  // 数据域
+	EquipNo   string      `json:"equipNo"`   // 设备编号 SN
+	Len       int         `json:"len"`       // dataArea 数据长度
+	Snno      uint        `json:"snno"`      // 序号
+	TimeStamp int64       `json:"timeStamp"` // 时间戳
+	Type      uint        `json:"type"`      // 设备类型 0:设备网关IBG
+}
+
+type MqttMsg struct {
+	Topic      string // 发送 Topic
+	Msg        FwhMqttMsg
+	ResTopic   *string // 响应 Topic
+	ResCmdType *uint
+	ResultCh   chan FwhMqttResult
+	Ctx        context.Context
+}
+
+type ResultChan struct {
+	ch  chan FwhMqttMsg
+	ctx context.Context
+}
+
+type FwhMqttClient struct {
+	Client *mqttClient.MqttClient
+	TaskCh chan MqttMsg
+}
+
+var fwhMqttClient = FwhMqttClient{
+	Client: nil,
+	TaskCh: make(chan MqttMsg, 1000),
+}
+
+func InitFwhMqttClient(cfg *config.FwhMqttConfig) error {
+	// 证书配置
+	caCert, err := os.ReadFile(cfg.FwhMqttCaUrl)
+	if err != nil {
+		panic(err)
+	}
+
+	caCertPool := x509.NewCertPool()
+	caCertPool.AppendCertsFromPEM(caCert)
+
+	cert, err := tls.LoadX509KeyPair(cfg.FwhMqttCertUrl, cfg.FwhMqttKeyUrl)
+	if err != nil {
+		panic(err)
+	}
+
+	client, err := mqttClient.CreateMqttClient(mqttClient.MqttClientOpt{
+		Broker:   cfg.FwhMqttBroker,
+		ClientID: fwhClientIdPrefix + strconv.FormatInt(time.Now().Unix(), 10),
+		TLS: &tls.Config{
+			Certificates: []tls.Certificate{cert},
+			RootCAs:      caCertPool,
+		},
+	})
+	if err != nil {
+		zap.L().Error("Init FWH MQTT client failed.")
+		return err
+	}
+	fwhMqttClient.Client = client
+
+	zap.L().Info("Init FWH MQTT client success.")
+
+	go fwhMqttClient.fwhMqttWorker()
+
+	return nil
+}
+
+func (c *FwhMqttClient) fwhMqttWorker() {
+	for {
+		select {
+		case task, ok := <-c.TaskCh:
+			if !ok {
+				zap.L().Error("Channel closed, worker exiting, recreate.")
+				c.TaskCh = make(chan MqttMsg, 1000)
+				break
+			}
+
+			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
+			}
+
+			// 订阅响应消息 - 请求响应无法一一对应,不支持并发请求,改成简单的队列请求
+			// if _, ok := resultChListMap[task.subTopic]; !ok {
+			// 	resultChListMap[task.subTopic] = []*ResultChan{}
+			// 	resultChListMap[task.subTopic] = append(resultChListMap[task.subTopic], &ResultChan{ch: task.resultCh, ctx: task.ctx})
+			// 	if err := client.SubscribeMessage(task.subTopic, func(payload []byte) {
+			// 		var newList []*ResultChan
+			// 		for _, ch := range resultChListMap[task.subTopic] {
+			// 			if err := ch.ctx.Err(); err != nil {
+			// 				switch err {
+			// 				case context.DeadlineExceeded:
+			// 					zap.L().Warn("Waiting for FWH MQTT message timeout.", zap.String("topic", task.topic))
+			// 				case context.Canceled:
+			// 					zap.L().Warn("Waiting for FWH MQTT message cancel.", zap.String("topic", task.topic))
+			// 				}
+			// 			} else {
+			// 				var fwhMqttMsg FwhMqttMsg
+			// 				err := json.Unmarshal(payload, &fwhMqttMsg)
+			// 				if err != nil {
+			// 					zap.L().Error("Result Unmarshal failed.(FwhMqttMsg)", zap.String("topic", task.topic), zap.String("payload", string(payload)), zap.String("err", err.Error()))
+			// 					newList = append(newList, ch)
+			// 					continue
+			// 				}
+
+			// 				if task.msg.Snno != fwhMqttMsg.Snno {
+			// 					newList = append(newList, ch)
+			// 					continue
+			// 				}
+
+			// 				ch.ch <- fwhMqttMsg
+			// 			}
+			// 		}
+			// 		resultChListMap[task.subTopic] = newList
+			// 	}); err != nil {
+			// 		zap.L().Error("FWH MQTT subscribe message failed.", zap.String("msg", fmt.Sprintf("%+v", task)))
+			// 		break
+			// 	}
+			// } else {
+			// 	resultChListMap[task.subTopic] = append(resultChListMap[task.subTopic], &ResultChan{ch: task.resultCh, ctx: task.ctx})
+			// }
+
+			var resultCh chan FwhMqttResult
+			if task.ResTopic != nil {
+				resultCh = make(chan FwhMqttResult)
+				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()))
+						// resultCh <- FwhMqttResult{
+						// 	Err:  err,
+						// 	Data: nil,
+						// }
+						return
+					}
+
+					if task.ResCmdType != nil {
+						if fwhMqttMsg.CmdType == *task.ResCmdType {
+							resultCh <- FwhMqttResult{
+								Err:  nil,
+								Data: &fwhMqttMsg,
+							}
+						}
+					} else {
+						resultCh <- FwhMqttResult{
+							Err:  nil,
+							Data: &fwhMqttMsg,
+						}
+					}
+				}); err != nil {
+					zap.L().Error("FWH MQTT subscribe message failed.", zap.String("msg", fmt.Sprintf("%+v", task)), zap.String("err", err.Error()))
+					task.ResultCh <- FwhMqttResult{
+						Err:  err,
+						Data: nil,
+					}
+					break
+				}
+			}
+
+			msgByte, err := json.Marshal(task.Msg)
+			if err != nil {
+				zap.L().Error("FWH MQTT message marshal failed.", zap.String("msg", fmt.Sprintf("%+v", task)), zap.String("err", err.Error()))
+				task.ResultCh <- FwhMqttResult{
+					Err:  err,
+					Data: nil,
+				}
+				break
+			}
+
+			if err := c.Client.SendMessageOnce(task.Topic, msgByte); err != nil {
+				zap.L().Error("FWH MQTT send message failed.", zap.String("msg", fmt.Sprintf("%+v", task)), zap.String("err", err.Error()))
+				task.ResultCh <- FwhMqttResult{
+					Err:  err,
+					Data: nil,
+				}
+				break
+			}
+
+			if resultCh != nil {
+				select {
+				case res, ok := <-resultCh:
+					if !ok {
+						task.ResultCh <- FwhMqttResult{
+							Err:  fmt.Errorf("FWH MQTT received message channel closed."),
+							Data: nil,
+						}
+						break
+					}
+
+					task.ResultCh <- res
+				case <-task.Ctx.Done():
+					task.ResultCh <- FwhMqttResult{
+						Err:  fmt.Errorf("FWH MQTT received message timeout."),
+						Data: nil,
+					}
+				}
+			} else {
+				task.ResultCh <- FwhMqttResult{
+					Err:  nil,
+					Data: nil,
+				}
+			}
+
+		case <-time.After(sleepTimeout):
+			if c.Client.IsConnected() {
+				zap.L().Info("FWH MQTT no task received within timeout, disconnect.")
+				c.Client.Disconnect()
+			}
+		}
+	}
+}
+
+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.")
+	}
+
+	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,
+		ResTopic:   resTopic,
+		ResCmdType: resCmdType,
+		ResultCh:   make(chan FwhMqttResult, 1),
+		Ctx:        ctxSend,
+	}
+	c.TaskCh <- taskMsg
+
+	for {
+		select {
+		case result, ok := <-taskMsg.ResultCh:
+			if !ok {
+				zap.L().Error("Result channel closed, worker exiting, recreate.", zap.String("topic", topic), zap.String("msg", fmt.Sprintf("%+v", msg)))
+				return nil, fmt.Errorf("Result channel closed, worker exiting, recreate.")
+			}
+
+			if result.Err != nil {
+				return nil, result.Err
+			}
+
+			return result.Data, nil
+		case <-ctxSend.Done():
+			zap.L().Info("Result received timeout.", zap.String("topic", topic), zap.String("msg", fmt.Sprintf("%+v", msg)))
+			return nil, fmt.Errorf("Result received timeout.")
+		}
+	}
+}
+
+// 仅发送不等待响应结果
+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, topicName string, msg FwhMqttMsg, resTopicName string, resCmdType uint) (*FwhMqttMsg, error) {
+	res, err := c.sendMessage(ctx, topicName, msg, &resTopicName, &resCmdType)
+	return res, err
+}

+ 43 - 0
service/repositories/mqtt/fwh_mqtt_test/fwh_mqtt_debug.go

@@ -0,0 +1,43 @@
+// debug 设备调试数据
+package fwh_mqtt_test
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"hash/crc32"
+	"strings"
+	"time"
+)
+
+func EnableXlog(ctx context.Context, sn string) (string, error) {
+	dataArea := map[string]string{
+		"xlogaddr": "18.188.130.67:9081",
+	}
+
+	dataStr, err := json.Marshal(dataArea)
+	if err != nil {
+		return "", err
+	}
+
+	res, err := fwhMqttClient.sendMessageWait(ctx, "debug/"+sn, FwhMqttMsg{
+		CmdType:   2000, // xlog请求
+		Type:      0,    // IBG
+		Snno:      123,
+		TimeStamp: time.Now().Unix(),
+		EquipNo:   sn,
+		DataArea:  dataArea,
+		Len:       len(dataStr),
+		Crc:       strings.ToUpper(fmt.Sprintf("%08X", crc32.ChecksumIEEE(dataStr))),
+	}, "debug/"+sn, 2001)
+	if err != nil {
+		return "", err
+	}
+
+	b, err := json.Marshal(res.DataArea)
+	if err != nil {
+		return "", err
+	}
+
+	return string(b), nil
+}

+ 202 - 0
service/repositories/mqtt/fwh_mqtt_test/fwh_mqtt_fault_record.go

@@ -0,0 +1,202 @@
+// fault record 故障录波请求
+package fwh_mqtt_test
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"fwh-toolbox-server/dto"
+	"hash/crc32"
+	"strings"
+	"time"
+
+	"go.uber.org/zap"
+)
+
+const (
+	FaultRecordReqCmdType = 377 // 请求协议类型
+	FaultRecordResCmdType = 378 // 应答协议类型
+)
+
+type ParamSettingParaListItem struct {
+	Id     int    `json:"id"`
+	SetVal string `json:"setVal"`
+}
+
+type ParamSettingInfoItem struct {
+	Sn       string                     `json:"sn"` // aGate/aPower SN
+	ParaList []ParamSettingParaListItem `json:"paraList"`
+}
+
+type ParamSettingListItem struct {
+	Type string                 `json:"type"` // 'pe' or 'aGate' or 'mppt'
+	Info []ParamSettingInfoItem `json:"info"`
+}
+
+type ParamSettingData struct {
+	List []ParamSettingListItem `json:"list"`
+}
+
+type FaultRecordDataArea struct {
+	Cmd  int         `json:"cmd"` // 0:实时数据获取;1:参数设置;2:文件获取
+	Data interface{} `json:"data"`
+}
+
+type FaultRecordResponse struct {
+	Cmd    int `json:"cmd"`    // 0:实时数据获取;1:参数设置;2:文件获取
+	Result int `json:"result"` // 0:成功;-1:失败
+}
+
+func ChargeChannel(ctx context.Context, sn string) error {
+	dataArea := map[string]interface{}{
+		"cmd": 1,
+		"data": map[string]interface{}{
+			"list": []interface{}{
+				map[string]interface{}{
+					"type": "pe",
+					"info": []interface{}{
+						map[string]interface{}{
+							"sn": "10070025A05X25310280",
+							"paraList": []interface{}{
+								map[string]interface{}{
+									"id":     6,
+									"setVal": "010100015984",
+								},
+								map[string]interface{}{
+									"id":     6,
+									"setVal": "020100015981",
+								},
+								map[string]interface{}{
+									"id":     6,
+									"setVal": "030100015983",
+								},
+								map[string]interface{}{
+									"id":     6,
+									"setVal": "04030003FD34",
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	dataStr, err := json.Marshal(dataArea)
+	if err != nil {
+		return err
+	}
+
+	fmt.Println(string(dataStr))
+
+	err = fwhMqttClient.sendMessageOnce(ctx, "mng/"+sn, FwhMqttMsg{
+		CmdType:   377, // 故障录波请求
+		Type:      0,   // IBG
+		Snno:      123,
+		TimeStamp: time.Now().Unix(),
+		EquipNo:   sn,
+		DataArea:  dataArea,
+		Len:       len(dataStr),
+		Crc:       strings.ToUpper(fmt.Sprintf("%08X", crc32.ChecksumIEEE(dataStr))),
+	})
+	if err != nil {
+		return err
+	}
+
+	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.")
+	}
+
+	var res FaultRecordResponse
+	b, err := json.Marshal(msg.DataArea)
+	if err != nil {
+		return err
+	}
+
+	if err := json.Unmarshal(b, &res); err != nil {
+		zap.L().Error("FaultRecordParamSetting response data type unmarshal error.", zap.String("DataArea", string(b)), zap.String("err", err.Error()))
+		return err
+	}
+
+	if res.Result != 0 {
+		return fmt.Errorf("FaultRecord param setting failed.")
+	}
+
+	return nil
+}

+ 71 - 0
service/repositories/mqtt/fwh_mqtt_test/test/main.go

@@ -0,0 +1,71 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"fwh-toolbox-server/config"
+	"fwh-toolbox-server/dto"
+	"fwh-toolbox-server/logger"
+	"fwh-toolbox-server/repositories/mqtt/fwh_mqtt_test"
+	"time"
+)
+
+func main() {
+	err := logger.Init("dev")
+	if err != nil {
+		panic(err)
+	}
+	defer logger.Log.Sync() // 退出前同步zap缓冲区
+
+	mqttConfig := config.FwhMqttConfig{
+		FwhMqttBroker:  "tls://a35sger20yyec4-ats.iot.us-west-2.amazonaws.com:8883",
+		FwhMqttCaUrl:   "/var/www/fwh-toolbox/server/cert/test/AmazonRootCA1.pem",
+		FwhMqttCertUrl: "/var/www/fwh-toolbox/server/cert/test/01.certificate.pem.crt",
+		FwhMqttKeyUrl:  "/var/www/fwh-toolbox/server/cert/test/01.private.pem.key",
+	}
+	fwh_mqtt_test.InitFwhMqttClient(&mqttConfig)
+
+	// fwh_mqtt_test.ChargeChannel(context.Background(), "EamonAux")
+	// time.Sleep(2 * time.Minute)
+	// fwh_mqtt_test.ChargeChannel(context.Background(), "EamonAux")
+	// time.Sleep(2 * time.Minute)
+
+	data := dto.FaultRecordParamInfo{
+		Sn: "EamonAux",
+		List: []dto.FaultRecordSubParam{
+			{
+				Type: "pe",
+				Sn:   "10070025A05X25310280",
+				ParaList: []dto.FaultRecordSubParamItem{
+					{
+						Id:  6,
+						Val: "010100015984",
+					},
+					{
+						Id:  6,
+						Val: "020100015981",
+					},
+					{
+						Id:  6,
+						Val: "030100015983",
+					},
+					{
+						Id:  6,
+						Val: "04030003FD34",
+					},
+				},
+			},
+		},
+	}
+
+	err = fwh_mqtt_test.FaultRecordParamSetting(context.Background(), data)
+	if err != nil {
+		fmt.Println(err.Error())
+	}
+	time.Sleep(2 * time.Minute)
+	err = fwh_mqtt_test.FaultRecordParamSetting(context.Background(), data)
+	if err != nil {
+		fmt.Println(err.Error())
+	}
+	time.Sleep(2 * time.Minute)
+}

+ 4 - 1
service/repositories/repositories.go

@@ -163,13 +163,16 @@ var UpdateSiteInfoUpdateResult = mongo.UpdateSiteInfoUpdateResult
 var GetSiteInfoUpdateNewRecord = mongo.GetSiteInfoUpdateNewRecord
 var GetSiteInfoManageList = mongo.GetSiteInfoManageList
 var InsertSiteInfoData = mongo.InsertSiteInfoData
+var GetSiteInfoManageListFromGiccid = mongo.GetSiteInfoManageListFromGiccid
 
 // SimCardManage
 var GetSimCardBatchInfo = mongo.GetSimCardBatchInfo
 var GetSimCardInfo = mongo.GetSimCardInfo
 var GetSimCardMonthlyData = mongo.GetSimCardMonthlyData
 var UpdateSimCardInfo = mongo.UpdateSimCardInfo
-var UpdateSimCardMonthlyData = mongo.UpdateSimCardMonthlyData
+var UpdateSimCardBatchInfo = mongo.UpdateSimCardBatchInfo
+var UpdateSimCardDayData = mongo.UpdateSimCardDayData
+var UpdateSimCardBatchDayData = mongo.UpdateSimCardBatchDayData
 
 /*************** PGSQL ***************/
 

+ 1 - 0
service/routes/routes.go

@@ -32,6 +32,7 @@ func Setup(app *fiber.App) {
 	SiteInfoManageSetup(api)
 	DespatchConfigSetup(api)
 	FaultRecordSetup(api)
+	SimCardManageSetup(api)
 
 	app.Get("/swagger/*", swagger.HandlerDefault)
 

+ 20 - 0
service/routes/simCardManageRoutes.go

@@ -0,0 +1,20 @@
+/*
+ * SIM Card Manage API List
+ * /simCardManage/info - GET 查询SIM卡信息
+ * /simCardManage/monthly - GET 查询SIM卡月数据
+ */
+package routes
+
+import (
+	"fwh-toolbox-server/controllers"
+
+	"github.com/gofiber/fiber/v2"
+)
+
+func SimCardManageSetup(api fiber.Router) {
+	// 查询SIM卡信息
+	api.Get("/simCardManage/info", controllers.GetSimCardBatchInfo)
+
+	// 查询SIM卡月数据
+	api.Get("/simCardManage/monthly", controllers.GetSimCardMonthlyData)
+}

+ 6 - 3
service/scheduler/scheduler.go

@@ -11,13 +11,16 @@ import (
 func Init() *cron.Cron {
 	c := cron.New()
 
-	// UTC时间 0点 定时任务
-	c.AddFunc("CRON_TZ=UTC 0 0 * * *", func() {
+	// UTC时间 23点55分 定时任务
+	c.AddFunc("CRON_TZ=UTC 0 23 * * *", func() {
 		log.Println("run at UTC midnight:", time.Now().UTC())
 		log.Println("run at Local midnight:", time.Now())
 
 		// SIM卡每日信息更新任务
-		
+		// err := task.CreateTaskSimCardCollect()
+		// if err != nil {
+		// 	zap.L().Error("CreateTaskSimCardCollect failed.", zap.String("err", err.Error()), zap.String("time(UTC)", time.Now().UTC().Format(time.DateTime)))
+		// }
 	})
 
 	// c.AddFunc("@every 1h", func() {

+ 2 - 2
service/services/backgroundTaskServices.go

@@ -2,10 +2,10 @@ package services
 
 import (
 	"fwh-toolbox-server/dto"
-	"fwh-toolbox-server/taskQueue"
+	"fwh-toolbox-server/task/queue/fwhQueue"
 )
 
 // 从任务流获取后台任务信息
 func GetBackgroundTaskInfoList() ([]dto.BackgroundTaskItem, error) {
-	return taskQueue.GetTaskInfoList()
+	return fwhQueue.GetTaskInfoList()
 }

+ 2 - 2
service/services/batchDownloadServices.go

@@ -9,7 +9,7 @@ import (
 	"fwh-toolbox-server/domain"
 	"fwh-toolbox-server/dto"
 	"fwh-toolbox-server/repositories"
-	"fwh-toolbox-server/taskQueue"
+	"fwh-toolbox-server/task/queue/fwhQueue"
 	"fwh-toolbox-server/utils/minio"
 	"io"
 	"reflect"
@@ -31,7 +31,7 @@ func InsertBatchDownloadConfig(writeConfig domain.BatchDownloadConfigWrite) erro
 		return err
 	}
 
-	taskQueue.EnqueueBatchDownloadTask(domain.TaskBatchDownload{
+	fwhQueue.EnqueueBatchDownloadTask(domain.TaskBatchDownload{
 		Id:         id,
 		Sn:         writeConfig.Sn,
 		TimePeriod: writeConfig.TimePeriod,

+ 62 - 0
service/services/simCardManageServices.go

@@ -0,0 +1,62 @@
+package services
+
+import (
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/dto"
+	"fwh-toolbox-server/repositories"
+
+	"go.uber.org/zap"
+)
+
+// 查询SIM卡信息(批量)
+func GetSimCardBatchInfo(page uint, size uint, search string, status *string) (*dto.ResPage[dto.SimCardInfo], error) {
+	infoList, err := repositories.GetSimCardBatchInfo(page, size, search, status)
+	if err != nil {
+		zap.L().Error("repositories.GetSimCardBatchInfo failed.", zap.String("err", err.Error()), zap.Uint("page", page), zap.Uint("size", size), zap.String("search", search), zap.Any("status", status))
+		return nil, err
+	}
+
+	result := dto.ResPage[dto.SimCardInfo]{
+		Data:     []dto.SimCardInfo{},
+		PageSize: infoList.PageSize,
+		PageNum:  infoList.PageNum,
+		Total:    infoList.Total,
+	}
+
+	for _, info := range infoList.Data {
+		snList := []string{}
+		siteList, err := repositories.GetSiteInfoManageListFromGiccid(info.GICCID)
+		if err != nil {
+			zap.L().Error("repositories.GetSiteInfoManageListFromGiccid failed.", zap.String("err", err.Error()), zap.String("GICCID", info.GICCID))
+		} else {
+			for _, site := range siteList {
+				snList = append(snList, site.Sn)
+			}
+		}
+
+		result.Data = append(result.Data, dto.SimCardInfo{
+			GICCID:         info.GICCID,
+			SN:             snList,
+			IMEI:           info.IMEI,
+			IMSI:           info.IMSI,
+			MSISDN:         info.MSISDN,
+			Status:         info.Status,
+			ExpirationTime: info.ExpirationTime,
+			Month:          info.Month,
+			AllDataUsage:   info.AllDataUsage,
+			UpdateTime:     info.UpdateTime,
+		})
+	}
+
+	return &result, nil
+}
+
+// 查询SIM卡信息(单卡)
+func GetSimCardInfo(id string) (*domain.SimCardInfo, error) {
+	return repositories.GetSimCardInfo(id)
+}
+
+// 查询SIM卡月数据(单卡)
+func GetSimCardMonthlyData(month string, id string) (*domain.SimCardMonthlyData, error) {
+	return repositories.GetSimCardMonthlyData(month, id)
+}

+ 2 - 2
service/services/siteInfoManageServices.go

@@ -6,7 +6,7 @@ import (
 	"fwh-toolbox-server/domain"
 	"fwh-toolbox-server/dto"
 	"fwh-toolbox-server/repositories"
-	"fwh-toolbox-server/taskQueue"
+	"fwh-toolbox-server/task/queue/fwhQueue"
 	"fwh-toolbox-server/utils/global"
 	"math"
 	"regexp"
@@ -31,7 +31,7 @@ func UpdateSiteInfoDate() error {
 		return err
 	}
 
-	if err := taskQueue.EnqueueSiteInfoUpdateTask(domain.TaskSiteInfoUpdate{
+	if err := fwhQueue.EnqueueSiteInfoUpdateTask(domain.TaskSiteInfoUpdate{
 		Id: *id,
 	}); err != nil {
 		return err

+ 3 - 3
service/services/sitesAnalysisServices.go

@@ -4,7 +4,7 @@ import (
 	"fwh-toolbox-server/domain"
 	"fwh-toolbox-server/dto"
 	"fwh-toolbox-server/repositories"
-	"fwh-toolbox-server/taskQueue"
+	"fwh-toolbox-server/task/queue/fwhQueue"
 
 	"go.mongodb.org/mongo-driver/bson/primitive"
 )
@@ -26,7 +26,7 @@ func InsertSiteAnalysisConfig(writeConfig domain.SiteAnalysisConfigWrite) error
 		return err
 	}
 
-	taskQueue.EnqueueSiteAnalysisTask(domain.TaskSiteAnalysis{
+	fwhQueue.EnqueueSiteAnalysisTask(domain.TaskSiteAnalysis{
 		Id:         id,
 		Sn:         writeConfig.Sn,
 		TimePeriod: writeConfig.TimePeriod,
@@ -46,7 +46,7 @@ func UpdateSiteAnalysisConfig(id primitive.ObjectID, writeConfig domain.SiteAnal
 		return err
 	}
 
-	taskQueue.EnqueueSiteAnalysisTask(domain.TaskSiteAnalysis{
+	fwhQueue.EnqueueSiteAnalysisTask(domain.TaskSiteAnalysis{
 		Id:         writeConfig.Id,
 		Sn:         writeConfig.Sn,
 		TimePeriod: writeConfig.TimePeriod,

+ 58 - 0
service/task/core/taskCore.go

@@ -0,0 +1,58 @@
+package task_core
+
+import (
+	task_pg "fwh-toolbox-server/task/plugin"
+
+	"github.com/hibiken/asynq"
+	"go.uber.org/zap"
+)
+
+// type TaskCore interface {
+// 	CreateTask() *asynq.Client
+// }
+
+type TaskCore struct {
+	redisOpt asynq.RedisClientOpt
+	client   *asynq.Client
+
+	Scheduler *task_pg.SchedulerManager
+}
+
+type TaskConfig struct {
+	Name   string // 任务名
+	SvrCfg asynq.Config
+	Mux    *asynq.ServeMux
+	Agg    *task_pg.AggregatorManager // 可选
+}
+
+// 创建TaskCore
+func NewTaskCore(redisOpt asynq.RedisClientOpt) *TaskCore {
+	return &TaskCore{
+		redisOpt:  redisOpt,
+		client:    asynq.NewClient(redisOpt),
+		Scheduler: task_pg.NewSchedulerManager(redisOpt),
+	}
+}
+
+// 创建任务
+func (tc *TaskCore) CreateTask(config TaskConfig) *asynq.Client {
+	if config.Mux != nil {
+		svr := asynq.NewServer(tc.redisOpt, config.SvrCfg)
+
+		go func() {
+			err := svr.Run(config.Mux)
+			if err != nil {
+				zap.L().Error("could not run server", zap.String("err", err.Error()), zap.String("taskName", config.Name))
+				return
+			}
+			zap.L().Warn("asynq server shutdown", zap.String("taskName", config.Name))
+		}()
+	} else {
+		zap.L().Warn("asynq not server", zap.String("taskName", config.Name))
+	}
+
+	if tc.client == nil {
+		tc.client = asynq.NewClient(tc.redisOpt)
+	}
+	return tc.client
+}

+ 63 - 0
service/task/plugin/aggregator.go

@@ -0,0 +1,63 @@
+package task_pg
+
+import (
+	"fmt"
+	"sync"
+
+	"github.com/hibiken/asynq"
+	"go.uber.org/zap"
+)
+
+type AggregatorFunc func(tasks []*asynq.Task) *asynq.Task
+
+type AggregatorManager struct {
+	mu          sync.RWMutex
+	aggregators map[string]AggregatorFunc // 聚合器映射表
+}
+
+func NewAggregatorManager() *AggregatorManager {
+	return &AggregatorManager{
+		aggregators: make(map[string]AggregatorFunc),
+	}
+}
+
+func (manager *AggregatorManager) Aggregate(group string, tasks []*asynq.Task) *asynq.Task {
+	manager.mu.RLock()
+	fn, ok := manager.aggregators[group]
+	manager.mu.RUnlock()
+
+	if !ok {
+		zap.L().Error("aggregator: group not found", zap.String("group", group))
+		return nil // 或记录错误
+	}
+
+	return fn(tasks)
+}
+
+func (manager *AggregatorManager) Register(group string, fn AggregatorFunc) error {
+	manager.mu.Lock()
+	defer manager.mu.Unlock()
+
+	if group == "" {
+		return fmt.Errorf("AggregatorManager: group is empty")
+	}
+
+	if fn == nil {
+		return fmt.Errorf("AggregatorManager: nil AggregatorFunc")
+	}
+
+	if _, ok := manager.aggregators[group]; ok {
+		panic(fmt.Sprintf("aggregator: group [%s] already registered", group))
+	}
+
+	manager.aggregators[group] = fn
+
+	return nil
+}
+
+func (manager *AggregatorManager) Unregister(group string) {
+	manager.mu.Lock()
+	defer manager.mu.Unlock()
+
+	delete(manager.aggregators, group)
+}

+ 138 - 0
service/task/plugin/scheduler.go

@@ -0,0 +1,138 @@
+package task_pg
+
+import (
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/hibiken/asynq"
+	"go.uber.org/zap"
+)
+
+type SchedulerManager struct {
+	utc *asynq.Scheduler // UTC时间
+	zh  *asynq.Scheduler // 中国时间(上海)
+	sg  *asynq.Scheduler // 新加坡时间
+	la  *asynq.Scheduler // 洛杉矶时间
+	ny  *asynq.Scheduler // 纽约时间
+}
+
+var once sync.Once
+var schedulerManager SchedulerManager
+
+func NewSchedulerManager(redisOpt asynq.RedisClientOpt) *SchedulerManager {
+	once.Do(func() {
+		zh_loc, err := time.LoadLocation("Asia/Shanghai")
+		if err != nil {
+			zap.L().Error("Load timezone Asia/Shanghai failed.", zap.String("err", err.Error()))
+		}
+		sg_loc, err := time.LoadLocation("Asia/Singapore")
+		if err != nil {
+			zap.L().Error("Load timezone Asia/Singapore failed.", zap.String("err", err.Error()))
+		}
+		la_loc, err := time.LoadLocation("America/Los_Angeles")
+		if err != nil {
+			zap.L().Error("Load timezone America/Los_Angeles failed.", zap.String("err", err.Error()))
+		}
+		ny_loc, err := time.LoadLocation("America/New_York")
+		if err != nil {
+			zap.L().Error("Load timezone America/New_York failed.", zap.String("err", err.Error()))
+		}
+
+		schedulerManager = SchedulerManager{
+			utc: asynq.NewScheduler(redisOpt, &asynq.SchedulerOpts{Location: time.UTC}),
+			zh:  asynq.NewScheduler(redisOpt, &asynq.SchedulerOpts{Location: zh_loc}),
+			sg:  asynq.NewScheduler(redisOpt, &asynq.SchedulerOpts{Location: sg_loc}),
+			la:  asynq.NewScheduler(redisOpt, &asynq.SchedulerOpts{Location: la_loc}),
+			ny:  asynq.NewScheduler(redisOpt, &asynq.SchedulerOpts{Location: ny_loc}),
+		}
+
+		schedulerManager.utc.Start()
+		schedulerManager.zh.Start()
+		schedulerManager.sg.Start()
+		schedulerManager.la.Start()
+		schedulerManager.ny.Start()
+	})
+
+	return &schedulerManager
+}
+
+// 注册UTC Scheduler
+func (sm *SchedulerManager) RegisterUtcScheduler(name string, cronspec string, task *asynq.Task, opts ...asynq.Option) error {
+	if sm.utc == nil {
+		return fmt.Errorf("RegisterUtcScheduler failed, SchedulerManager utc not initialized.(%s)", name)
+	}
+
+	entryID, err := sm.utc.Register(cronspec, task, opts...)
+	if err != nil {
+		zap.L().Error("UTC Scheduler register failed.", zap.String("err", err.Error()), zap.String("name", name))
+		return err
+	}
+
+	zap.L().Info("UTC Scheduler register successful.", zap.String("name", name), zap.String("entryID", entryID))
+	return nil
+}
+
+// 注册ZH(China) Scheduler
+func (sm *SchedulerManager) RegisterZhScheduler(name string, cronspec string, task *asynq.Task, opts ...asynq.Option) error {
+	if sm.zh == nil {
+		return fmt.Errorf("RegisterUtcScheduler failed, SchedulerManager zh not initialized.")
+	}
+
+	entryID, err := sm.zh.Register(cronspec, task, opts...)
+	if err != nil {
+		zap.L().Error("ZH Scheduler register failed.", zap.String("err", err.Error()), zap.String("name", name))
+		return err
+	}
+
+	zap.L().Info("ZH Scheduler register successful.", zap.String("name", name), zap.String("entryID", entryID))
+	return nil
+}
+
+// 注册SG(Singapore) Scheduler
+func (sm *SchedulerManager) RegisterSgScheduler(name string, cronspec string, task *asynq.Task, opts ...asynq.Option) error {
+	if sm.sg == nil {
+		return fmt.Errorf("RegisterUtcScheduler failed, SchedulerManager sg not initialized.")
+	}
+
+	entryID, err := sm.sg.Register(cronspec, task, opts...)
+	if err != nil {
+		zap.L().Error("SG Scheduler register failed.", zap.String("err", err.Error()), zap.String("name", name))
+		return err
+	}
+
+	zap.L().Info("SG Scheduler register successful.", zap.String("name", name), zap.String("entryID", entryID))
+	return nil
+}
+
+// 注册LA(US/Los Angeles) Scheduler
+func (sm *SchedulerManager) RegisterLaScheduler(name string, cronspec string, task *asynq.Task, opts ...asynq.Option) error {
+	if sm.la == nil {
+		return fmt.Errorf("RegisterUtcScheduler failed, SchedulerManager la not initialized.")
+	}
+
+	entryID, err := sm.la.Register(cronspec, task, opts...)
+	if err != nil {
+		zap.L().Error("LA Scheduler register failed.", zap.String("err", err.Error()), zap.String("name", name))
+		return err
+	}
+
+	zap.L().Info("LA Scheduler register successful.", zap.String("name", name), zap.String("entryID", entryID))
+	return nil
+}
+
+// 注册NY(US/New York) Scheduler
+func (sm *SchedulerManager) RegisterNyScheduler(name string, cronspec string, task *asynq.Task, opts ...asynq.Option) error {
+	if sm.ny == nil {
+		return fmt.Errorf("RegisterUtcScheduler failed, SchedulerManager ny not initialized.")
+	}
+
+	entryID, err := sm.ny.Register(cronspec, task, opts...)
+	if err != nil {
+		zap.L().Error("NY Scheduler register failed.", zap.String("err", err.Error()), zap.String("name", name))
+		return err
+	}
+
+	zap.L().Info("NY Scheduler register successful.", zap.String("name", name), zap.String("entryID", entryID))
+	return nil
+}

+ 1 - 1
service/taskQueue/taskAlarmStatistics.go → service/task/queue/fwhQueue/fwhTaskAlarmStatistics.go

@@ -1,4 +1,4 @@
-package taskQueue
+package fwhQueue
 
 import (
 	"context"

+ 1 - 1
service/taskQueue/taskBatchDownload.go → service/task/queue/fwhQueue/fwhTaskBatchDownload.go

@@ -1,4 +1,4 @@
-package taskQueue
+package fwhQueue
 
 import (
 	"context"

+ 2 - 2
service/taskQueue/taskQueue.go → service/task/queue/fwhQueue/fwhTaskQueue.go

@@ -1,4 +1,4 @@
-package taskQueue
+package fwhQueue
 
 import (
 	"context"
@@ -51,7 +51,7 @@ type TaskPayload[T any] struct {
 	Payload T `json:"payload"`
 }
 
-func InitTaskQueue() {
+func InitFwhTaskQueue() {
 	// 确保消费组存在(忽略已存在错误)
 	_ = cache.XGroupCreate(ctx, streamName, groupName, "$")
 

+ 1 - 1
service/taskQueue/taskShutdownStatistics.go → service/task/queue/fwhQueue/fwhTaskShutdownStatistics.go

@@ -1,4 +1,4 @@
-package taskQueue
+package fwhQueue
 
 import (
 	"context"

+ 1 - 1
service/taskQueue/taskSiteAnalysis.go → service/task/queue/fwhQueue/fwhTaskSiteAnalysis.go

@@ -1,4 +1,4 @@
-package taskQueue
+package fwhQueue
 
 import (
 	"context"

+ 1 - 1
service/taskQueue/taskSiteInfoUpdate.go → service/task/queue/fwhQueue/fwhTaskSiteInfoUpdate.go

@@ -1,4 +1,4 @@
-package taskQueue
+package fwhQueue
 
 import (
 	"context"

+ 178 - 0
service/task/queue/simCardQueue/simCardTaskAcceleronix.go

@@ -0,0 +1,178 @@
+package simCardQueue
+
+import (
+	"context"
+	"encoding/json"
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/repositories"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/hibiken/asynq"
+	"go.uber.org/zap"
+)
+
+const (
+	TaskTypeSimCardAcceleronixSyncOnePage = "SimCard:Acceleronix:SyncOnePage"
+
+	AcceleronixSimCardInfoPageSize = 50 // Acceleronix平台每次查询页数
+)
+
+// 数据类型转换 AcceleronixInfo -> SimCardInfo
+func (processor *SimCardProcessor) acceleronixInfoToSimCardInfo(infoList []domain.AcceleronixSimCardInfoItem, dataUsage map[string]*domain.AcceleronixSimCardDataUsageInfoResponse) []domain.SimCardInfo {
+	var simCardInfoList []domain.SimCardInfo
+
+	for _, info := range infoList {
+		var simCardInfo domain.SimCardInfo
+
+		simCardInfo.GICCID = info.Iccid[:19] // 只取前19位不取检验码
+		simCardInfo.IMEI = info.Imei
+		simCardInfo.IMSI = info.Imsi
+		simCardInfo.MSISDN = info.Msisdn
+
+		switch info.SimState {
+		case domain.SCS_PreActivation: // 2 预激活
+			simCardInfo.Status = domain.SimStatusNotActivated
+			break
+		case domain.SCS_Test: // 3 测试
+			simCardInfo.Status = domain.SimStatusNotActivated
+			break
+		case domain.SCS_Silence: // 4 沉默
+			simCardInfo.Status = domain.SimStatusNotActivated
+			break
+		case domain.SCS_Standby: // 5 待机
+			simCardInfo.Status = domain.SimStatusNotActivated
+			break
+		case domain.SCS_Activated: // 6 激活
+			simCardInfo.Status = domain.SimStatusActivated
+			break
+		case domain.SCS_Deactivated: // 7 停机
+			simCardInfo.Status = domain.SimStatusDeactivated
+			break
+		case domain.SCS_Pause: // 8 暂停
+			simCardInfo.Status = domain.SimStatusDeactivated
+			break
+		case domain.SCS_PreCancellation: // 9 预注销
+			simCardInfo.Status = domain.SimStatusDeactivated
+			break
+		case domain.SCS_Cancellation: // 10 注销
+			simCardInfo.Status = domain.SimStatusRetired
+			break
+		default:
+			simCardInfo.Status = domain.SimStatusUnknown
+			break
+		}
+
+		simCardInfo.ExpirationTime = info.ExpirationTime // 到期时间(后付费卡没有到期时间) 格式:yyyy-MM-dd
+
+		usageInfo := dataUsage[info.Iccid]
+		if usageInfo == nil {
+			zap.L().Warn("AcceleronixSimCardDataUsageInfo not exist.", zap.String("Iccid", info.Iccid), zap.Any("dataUsageMap", dataUsage))
+		} else {
+			simCardInfo.Month = usageInfo.Month // 统计月份 格式:yyyy-MM
+			totalDataUsage, err := strconv.ParseFloat(usageInfo.TotalDataUsage, 64)
+			if err != nil {
+				zap.L().Warn("TotalDataUsage ParseFloat failed.", zap.String("err", err.Error()), zap.String("TotalDataUsage", usageInfo.TotalDataUsage))
+				simCardInfo.AllDataUsage = 0
+			} else {
+				simCardInfo.AllDataUsage = totalDataUsage * 1024 // 月数据用量(KB)
+			}
+		}
+
+		simCardInfo.UpdateTime = time.Now().UTC() // UTC时间
+
+		simCardInfoList = append(simCardInfoList, simCardInfo)
+	}
+
+	return simCardInfoList
+}
+
+// 处理Acceleronix信息收集任务
+func (processor *SimCardProcessor) handleSimCardAcceleronixSyncOnePageTask(ctx context.Context, task *asynq.Task) error {
+	payloadStr := task.Payload()
+
+	var payload SimCardSyncOnePagePayload
+	err := json.Unmarshal(payloadStr, &payload)
+	if err != nil {
+		zap.L().Error("handleSimCardAcceleronixSyncOnePageTask payload error.", zap.String("err", err.Error()), zap.String("payload", string(payloadStr)))
+		return err
+	}
+
+	// 请求批量查询Acceleronix批量数据信息
+	// 查询Sim卡基本信息列表
+	respInfoList, err := repositories.QueryAcceleronixSimCardList(uint(payload.Page), uint(payload.Size))
+	if err != nil {
+		zap.L().Error("handleSimCardAcceleronixSyncOnePageTask QueryAcceleronixSimCardList failed.", zap.String("err", err.Error()), zap.Uint64("page", payload.Page), zap.Uint64("size", payload.Size))
+		return err
+	}
+
+	idList := []string{}
+	for _, info := range respInfoList.Data.List {
+		idList = append(idList, info.Iccid)
+	}
+
+	// 查询
+	mouth := strings.ReplaceAll(time.Now().UTC().Format(time.DateOnly)[:7], "-", "") // 统计月份 格式:yyyyMM
+	respUsageList, err := repositories.QueryAcceleronixBatchSimCardDataInfo(idList, mouth)
+	if err != nil {
+		zap.L().Error("handleSimCardAcceleronixSyncOnePageTask QueryAcceleronixBatchSimCardDataInfo failed.", zap.String("err", err.Error()), zap.Strings("idList", idList), zap.String("mouth", mouth))
+		return err
+	}
+
+	// 生成数据用量信息map
+	dataUsageMap := make(map[string]*domain.AcceleronixSimCardDataUsageInfoResponse)
+	for _, info := range respUsageList {
+		dataUsageMap[info.Iccid] = &info
+	}
+
+	// 转换数据结构
+	infoList := processor.acceleronixInfoToSimCardInfo(respInfoList.Data.List, dataUsageMap)
+	uuidStr := uuid.NewString()
+	processor.infoCacheMap[uuidStr] = infoList
+	zap.L().Debug("Acceleronix get infoList.", zap.Int("len", len(infoList)))
+
+	// 创建聚合组任务
+	aggTask := asynq.NewTask(
+		TaskTypeSimCardAggregation,
+		[]byte(uuidStr),
+		asynq.Queue("default"),
+		asynq.Unique(5*time.Minute),
+		asynq.Group(GroupNameSimCardAggregation),
+	)
+	_, err = TaskClient.Enqueue(aggTask)
+	if err != nil {
+		zap.L().Error("TaskTypeSimCardAggregation Enqueue failed.", zap.String("err", err.Error()), zap.Uint64("size", payload.Size), zap.Uint64("page", payload.Page))
+		return err
+	}
+
+	// 判断是否查完所有信息
+	if payload.Page*payload.Size > uint64(respInfoList.Data.Total) {
+		return nil
+	}
+
+	// 查询太快,间隔1s
+	time.Sleep(time.Second)
+
+	// 创建下一个任务(查询下一页)
+	payload.Page++
+	taskPayloadStr, err := json.Marshal(payload)
+
+	nextTask := asynq.NewTask(
+		TaskTypeSimCardAcceleronixSyncOnePage,
+		taskPayloadStr,
+		asynq.MaxRetry(5),
+		asynq.Queue("default"),
+		asynq.Timeout(time.Minute),
+		asynq.Unique(5*time.Minute),
+		asynq.Retention(7*24*time.Hour), // 成功后保留7天
+	)
+	_, err = TaskClient.Enqueue(nextTask)
+	if err != nil {
+		zap.L().Error("TaskTypeSimCardSyncOnePage Enqueue failed.", zap.String("err", err.Error()), zap.Uint64("size", payload.Size), zap.Uint64("page", payload.Page))
+		return err
+	}
+
+	return nil
+}

+ 293 - 0
service/task/queue/simCardQueue/simCardTaskQueue.go

@@ -0,0 +1,293 @@
+package simCardQueue
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/repositories"
+	task_core "fwh-toolbox-server/task/core"
+	task_pg "fwh-toolbox-server/task/plugin"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/hibiken/asynq"
+	"go.uber.org/zap"
+)
+
+/***********************************************************************
+ * 为了控制流量采用单线程队列查询
+ * SyncOnePage Task * 1								查询单页数据,并创建数据聚合任务
+ * 			|
+ *			▼
+ * SyncOnePage Task * 2								查询单页数据,并创建数据聚合任务
+ * 			...
+ *			▼
+ * SyncOnePage Task * N								查询单页数据,并创建数据聚合任务
+ * 			|
+ *			▼
+ * Aggregation (Group Aggregator)			聚合数据并创建,批量数据处理任务
+ * 			|
+ *			▼
+ * Bulk Task													将聚合的数据批量写入数据库
+ ***********************************************************************/
+
+const (
+	TaskTypeSimCardAggregation  = "SimCard:Aggregation"
+	GroupNameSimCardAggregation = "SimCard:GroupAggregation"
+	TaskTypeSimCardBulk         = "SimCard:Bulk"
+)
+
+type TaskCore interface {
+	CreateTask() *asynq.Client
+}
+
+type SimCardSyncOnePagePayload struct {
+	Size uint64 `json:"size"`
+	Page uint64 `json:"page"`
+}
+
+type SimCardProcessor struct {
+	// ... fields for struct
+	infoCacheMap map[string][]domain.SimCardInfo
+}
+
+var TaskClient *asynq.Client
+
+func InitSimCardTaskQueue(tc *task_core.TaskCore) {
+	processor := &SimCardProcessor{
+		infoCacheMap: make(map[string][]domain.SimCardInfo),
+	}
+
+	// 注册SmartHub收集任务处理函数
+	mux := asynq.NewServeMux()
+	mux.HandleFunc(TaskTypeSimCardSmartHubSyncOnePage, processor.handleSimCardSmartHubSyncOnePageTask)
+
+	// 注册Acceleronix收集任务处理函数
+	mux.HandleFunc(TaskTypeSimCardAcceleronixSyncOnePage, processor.handleSimCardAcceleronixSyncOnePageTask)
+
+	// 注册SimCard批处理任务
+	mux.HandleFunc(TaskTypeSimCardBulk, processor.handleSimCardBulkTask)
+
+	// 注册SimCard组聚合器
+	aggManager := task_pg.NewAggregatorManager()
+	err := aggManager.Register(GroupNameSimCardAggregation, func(tasks []*asynq.Task) *asynq.Task {
+		var aggInfoList []domain.SimCardInfo
+		for _, t := range tasks {
+			payloadStr := t.Payload()
+
+			infoList := processor.infoCacheMap[string(payloadStr)]
+			defer delete(processor.infoCacheMap, string(payloadStr))
+
+			aggInfoList = append(aggInfoList, infoList...)
+		}
+
+		uuidStr := uuid.NewString()
+		processor.infoCacheMap[uuidStr] = aggInfoList
+		zap.L().Debug("SimCardBulk get aggInfoList.", zap.Int("len", len(aggInfoList)))
+
+		bulkTask := asynq.NewTask(
+			TaskTypeSimCardBulk,
+			[]byte(uuidStr),
+			asynq.MaxRetry(5),
+			asynq.Queue("default"),
+			asynq.Timeout(time.Minute),
+			asynq.Unique(5*time.Minute),
+			asynq.Retention(7*24*time.Hour), // 成功后保留7天
+		)
+
+		return bulkTask
+	})
+	if err != nil {
+		zap.L().Error("GroupNameSimCardAggregation register failed.", zap.String("err", err.Error()))
+	}
+
+	TaskClient = tc.CreateTask(task_core.TaskConfig{
+		Name: "SimCardCollectTask",
+		SvrCfg: asynq.Config{
+			// Specify how many concurrent workers to use
+			Concurrency: 1, // 串行任务,只配置1个worker
+			// Optionally specify multiple queues with different priority.
+			Queues: map[string]int{
+				"critical": 6,
+				"default":  3,
+				"low":      1,
+			},
+			// See the godoc for other configuration options
+			GroupAggregator:  asynq.GroupAggregatorFunc(aggManager.Aggregate),
+			GroupGracePeriod: 30 * time.Second, // 宽限期,30秒没人提交开始聚合
+			GroupMaxDelay:    5 * time.Minute,  // 最长等待时间,5分钟必须聚合
+			GroupMaxSize:     100,              // 最大数量,数据总量到达100必须聚合
+		},
+		Mux: mux,
+		Agg: aggManager,
+	})
+
+	err = CreateSchedulerSimCardCollect(tc)
+	if err != nil {
+		zap.L().Error("CreateSchedulerSimCardCollect failed.", zap.String("err", err.Error()))
+	}
+}
+
+func CreateSchedulerSimCardCollect(tc *task_core.TaskCore) error {
+	payload := SimCardSyncOnePagePayload{
+		Size: SmartHubSimCardInfoPageSize,
+		Page: 0, // SmartHub从第0页开始
+	}
+	payloadStr, err := json.Marshal(payload)
+	if err != nil {
+		zap.L().Error("SimCardSyncOnePagePayload Marshal failed.", zap.String("err", err.Error()))
+		return err
+	}
+
+	// 创建SmartHub收集任务
+	taskSmartHub := asynq.NewTask(
+		TaskTypeSimCardSmartHubSyncOnePage,
+		payloadStr,
+		asynq.MaxRetry(5),
+		asynq.Queue("default"),
+		asynq.Timeout(time.Minute),
+		asynq.Unique(5*time.Minute),
+		asynq.Retention(7*24*time.Hour), // 成功后保留7天
+	)
+	err = tc.Scheduler.RegisterUtcScheduler("SmartHubSyncOnePageScheduler", "0 23 * * *", taskSmartHub)
+	if err != nil {
+		zap.L().Error("TaskTypeSimCardSmartHubSyncOnePage RegisterUtcScheduler failed.", zap.String("err", err.Error()), zap.Uint64("size", payload.Size), zap.Uint64("page", payload.Page))
+		return err
+	}
+
+	payload.Page = 1 // Acceleronix从第1页开始
+	payloadStr, err = json.Marshal(payload)
+	if err != nil {
+		zap.L().Error("SimCardSyncOnePagePayload Marshal failed.", zap.String("err", err.Error()))
+		return err
+	}
+
+	// 创建Acceleronix收集任务
+	taskAcceleronix := asynq.NewTask(
+		TaskTypeSimCardAcceleronixSyncOnePage,
+		payloadStr,
+		asynq.MaxRetry(5),
+		asynq.Queue("default"),
+		asynq.Timeout(30*time.Minute),
+		asynq.Unique(5*time.Minute),
+		asynq.Retention(7*24*time.Hour), // 成功后保留7天
+	)
+	err = tc.Scheduler.RegisterUtcScheduler("AcceleronixSyncOnePageScheduler", "0 23 * * *", taskAcceleronix)
+	if err != nil {
+		zap.L().Error("TaskTypeSimCardAcceleronixSyncOnePage RegisterUtcScheduler failed.", zap.String("err", err.Error()), zap.Uint64("size", payload.Size), zap.Uint64("page", payload.Page))
+		return err
+	}
+
+	return nil
+}
+
+// 创建SimCard信息收集任务
+func CreateTaskSimCardCollect() error {
+	if TaskClient == nil {
+		return fmt.Errorf("*asynq.Client Uninitialized.")
+	}
+
+	payload := SimCardSyncOnePagePayload{
+		Size: SmartHubSimCardInfoPageSize,
+		Page: 0, // SmartHub从第0页开始
+	}
+	payloadStr, err := json.Marshal(payload)
+	if err != nil {
+		zap.L().Error("SimCardSyncOnePagePayload Marshal failed.", zap.String("err", err.Error()))
+		return err
+	}
+
+	// 创建SmartHub收集任务
+	taskSmartHub := asynq.NewTask(
+		TaskTypeSimCardSmartHubSyncOnePage,
+		payloadStr,
+		asynq.MaxRetry(5),
+		asynq.Queue("default"),
+		asynq.Timeout(time.Minute),
+		asynq.Unique(5*time.Minute),
+		asynq.Retention(7*24*time.Hour), // 成功后保留7天
+	)
+	_, err = TaskClient.Enqueue(taskSmartHub)
+	if err != nil {
+		zap.L().Error("TaskTypeSimCardSmartHubSyncOnePage Enqueue failed.", zap.String("err", err.Error()), zap.Uint64("size", payload.Size), zap.Uint64("page", payload.Page))
+		return err
+	}
+
+	payload.Page = 1 // Acceleronix从第1页开始
+	payloadStr, err = json.Marshal(payload)
+	if err != nil {
+		zap.L().Error("SimCardSyncOnePagePayload Marshal failed.", zap.String("err", err.Error()))
+		return err
+	}
+
+	// 创建Acceleronix收集任务
+	taskAcceleronix := asynq.NewTask(
+		TaskTypeSimCardAcceleronixSyncOnePage,
+		payloadStr,
+		asynq.MaxRetry(5),
+		asynq.Queue("default"),
+		asynq.Timeout(30*time.Minute),
+		asynq.Unique(5*time.Minute),
+		asynq.Retention(7*24*time.Hour), // 成功后保留7天
+	)
+	_, err = TaskClient.Enqueue(taskAcceleronix)
+	if err != nil {
+		zap.L().Error("TaskTypeSimCardAcceleronixSyncOnePage Enqueue failed.", zap.String("err", err.Error()), zap.Uint64("size", payload.Size), zap.Uint64("page", payload.Page))
+		return err
+	}
+
+	return nil
+}
+
+// 保存SimCard信息到数据库
+func (processor *SimCardProcessor) saveSimCardInfo(infoList []domain.SimCardInfo) error {
+	zap.L().Info("saveSimCardInfo", zap.Int("Num", len(infoList)))
+
+	// 保存基本信息
+	_, err := repositories.UpdateSimCardBatchInfo(infoList)
+	if err != nil {
+		zap.L().Error("UpdateSimCardInfo failed.", zap.String("err", err.Error()), zap.Int("infoNum", len(infoList)))
+		return err
+	}
+
+	// 提取日数据
+	dayDataList := []domain.SimCardDayData{}
+	for _, info := range infoList {
+		dayData := domain.SimCardDayData{
+			SimId:        info.GICCID,                   // GICCID
+			UpdateTime:   info.UpdateTime,               // UTC时间
+			Month:        info.Month,                    // 统计月份 格式:yyyy-MM
+			AllDataUsage: info.AllDataUsage,             // 月数据用量(KB)
+			Day:          uint8(time.Now().UTC().Day()), // 当天日期值 1-31
+			DailyUsage: domain.SimCardDailyUsage{
+				UpdateTime: info.UpdateTime,                        // UTC时间
+				Day:        time.Now().UTC().Format(time.DateOnly), // 统计日期 格式:yyyy-MM-DD
+				DataUsage:  info.AllDataUsage,                      // 数据用量(KB)
+			}, // 日用量数据信息
+		}
+		dayDataList = append(dayDataList, dayData)
+	}
+
+	// 保存月数据信息
+	_, err = repositories.UpdateSimCardBatchDayData(dayDataList)
+	if err != nil {
+		zap.L().Error("UpdateSimCardBatchMonthlyData failed.", zap.String("err", err.Error()), zap.Int("infoNum", len(infoList)))
+		return err
+	}
+
+	return nil
+}
+
+// 处理SimCard批处理任务
+func (processor *SimCardProcessor) handleSimCardBulkTask(ctx context.Context, task *asynq.Task) error {
+	payloadStr := task.Payload()
+	originalList := processor.infoCacheMap[string(payloadStr)]
+	defer delete(processor.infoCacheMap, string(payloadStr))
+
+	// infoList := make([]domain.SimCardInfo, len(originalList))
+	// copy(infoList, originalList) // 这里使用拷贝,防止引用被删除
+	infoList := originalList
+
+	return processor.saveSimCardInfo(infoList)
+}

+ 136 - 0
service/task/queue/simCardQueue/simCardTaskSmartHub.go

@@ -0,0 +1,136 @@
+package simCardQueue
+
+import (
+	"context"
+	"encoding/json"
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/repositories"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/hibiken/asynq"
+	"go.uber.org/zap"
+)
+
+const (
+	TaskTypeSimCardSmartHubSyncOnePage = "SimCard:SmartHub:SyncOnePage"
+
+	SmartHubSimCardInfoPageSize = 50 // SmartHub平台每次查询页数
+)
+
+// 数据类型转换 SmartHubInfo -> SimCardInfo
+func (processor *SimCardProcessor) smartHubInfoToSimCardInfo(infoList []domain.SmartHubSimCardInfoItem) []domain.SimCardInfo {
+	var simCardInfoList []domain.SimCardInfo
+	for _, info := range infoList {
+		var simCardInfo domain.SimCardInfo
+
+		simCardInfo.GICCID = info.GICCID[:19] // 只取前19位不取检验码
+
+		if len(info.DeviceInfo) == 0 {
+			zap.L().Error("SmartHub SimCardInfo has no DeviceInfo", zap.String("GICCID", info.GICCID))
+		} else {
+			deviceInfo := info.DeviceInfo[0]
+			simCardInfo.IMEI = deviceInfo.IMEI     // 国际移动用户识别码
+			simCardInfo.IMSI = deviceInfo.IMSI     // 国际移动用户识别码
+			simCardInfo.MSISDN = deviceInfo.MSISDN // 移动用户号码
+		}
+
+		switch info.Status {
+		case domain.SmartHubSimStatusTestReady: // 可测试
+			simCardInfo.Status = domain.SimStatusNotActivated
+			break
+		case domain.SmartHubSimStatusActivateReady: // 可激活
+			simCardInfo.Status = domain.SimStatusNotActivated
+			break
+		case domain.SmartHubSimStatusActivated: // 已激活
+			simCardInfo.Status = domain.SimStatusActivated
+			break
+		case domain.SmartHubSimStatusDeactivated: // 已停用
+			simCardInfo.Status = domain.SimStatusDeactivated
+			break
+		case domain.SmartHubSimStatusInventory: // 库存
+			simCardInfo.Status = domain.SimStatusNotActivated
+			break
+		case domain.SmartHubSimStatusRetired: // 失效
+			simCardInfo.Status = domain.SimStatusRetired
+			break
+		default:
+			simCardInfo.Status = domain.SimStatusUnknown
+			break
+		}
+
+		simCardInfo.ExpirationTime = ""                                // SmartHub没有有效期
+		simCardInfo.Month = time.Now().UTC().Format(time.DateOnly)[:7] // 统计月份 格式:yyyy-MM
+		simCardInfo.AllDataUsage = info.CurrentCycleUsages.DataUsage   // 月数据用量(KB)
+		simCardInfo.UpdateTime = time.Now().UTC()                      // UTC时间
+
+		simCardInfoList = append(simCardInfoList, simCardInfo)
+	}
+
+	return simCardInfoList
+}
+
+// 处理SimCard信息收集任务
+func (processor *SimCardProcessor) handleSimCardSmartHubSyncOnePageTask(ctx context.Context, task *asynq.Task) error {
+	payloadStr := task.Payload()
+
+	var payload SimCardSyncOnePagePayload
+	err := json.Unmarshal(payloadStr, &payload)
+	if err != nil {
+		zap.L().Error("handleSimCardSmartHubSyncOnePageTask payload error.", zap.String("err", err.Error()), zap.String("payload", string(payloadStr)))
+		return err
+	}
+
+	// 请求批量查询SmartHub批量数据信息
+	// 获取指定页信息
+	resp, err := repositories.QuerySmartHubBatchSimCardInfo(uint(payload.Page), uint(payload.Size))
+	if err != nil {
+		zap.L().Error("handleSimCardSmartHubSyncOnePageTask QuerySmartHubBatchSimCardInfo failed.", zap.String("err", err.Error()), zap.Uint64("page", payload.Page), zap.Uint64("size", payload.Size))
+		return err
+	}
+
+	// 转换数据结构
+	infoList := processor.smartHubInfoToSimCardInfo(resp.Data)
+	uuidStr := uuid.NewString()
+	processor.infoCacheMap[uuidStr] = infoList
+
+	// 创建聚合组任务
+	aggTask := asynq.NewTask(
+		TaskTypeSimCardAggregation,
+		[]byte(uuidStr),
+		asynq.Queue("default"),
+		asynq.Unique(5*time.Minute),
+		asynq.Group(GroupNameSimCardAggregation),
+	)
+	_, err = TaskClient.Enqueue(aggTask)
+	if err != nil {
+		zap.L().Error("TaskTypeSimCardAggregation Enqueue failed.", zap.String("err", err.Error()), zap.Uint64("size", payload.Size), zap.Uint64("page", payload.Page))
+		return err
+	}
+
+	// 判断是否查完所有信息
+	if payload.Page*payload.Size > resp.Total {
+		return nil
+	}
+
+	// 创建下一个任务(查询下一页)
+	payload.Page++
+	taskPayloadStr, err := json.Marshal(payload)
+
+	nextTask := asynq.NewTask(
+		TaskTypeSimCardSmartHubSyncOnePage,
+		taskPayloadStr,
+		asynq.MaxRetry(5),
+		asynq.Queue("default"),
+		asynq.Timeout(time.Minute),
+		asynq.Unique(5*time.Minute),
+		asynq.Retention(7*24*time.Hour), // 成功后保留7天
+	)
+	_, err = TaskClient.Enqueue(nextTask)
+	if err != nil {
+		zap.L().Error("TaskTypeSimCardSyncOnePage Enqueue failed.", zap.String("err", err.Error()), zap.Uint64("size", payload.Size), zap.Uint64("page", payload.Page))
+		return err
+	}
+
+	return nil
+}

+ 16 - 0
service/task/queue/taskQueue.go

@@ -0,0 +1,16 @@
+package taskQueue
+
+import (
+	task_core "fwh-toolbox-server/task/core"
+	"fwh-toolbox-server/task/queue/fwhQueue"
+	"fwh-toolbox-server/task/queue/simCardQueue"
+)
+
+func InitTaskQueue(tc *task_core.TaskCore) {
+	if tc == nil {
+		panic("InitTaskQueue failed, TaskCore is nil.")
+	}
+
+	fwhQueue.InitFwhTaskQueue()
+	simCardQueue.InitSimCardTaskQueue(tc)
+}

+ 28 - 0
service/task/task.go

@@ -0,0 +1,28 @@
+package task
+
+import (
+	task_core "fwh-toolbox-server/task/core"
+	taskQueue "fwh-toolbox-server/task/queue"
+
+	"github.com/hibiken/asynq"
+)
+
+type TaskRedisOpt struct {
+	Addr     string
+	DB       int
+	Username string
+	Password string
+}
+
+func InitTask(redisOpt TaskRedisOpt) {
+	redisClientOpt := asynq.RedisClientOpt{
+		Addr:     redisOpt.Addr,
+		DB:       redisOpt.DB,
+		Username: redisOpt.Username,
+		Password: redisOpt.Password,
+	}
+
+	tc := task_core.NewTaskCore(redisClientOpt)
+
+	taskQueue.InitTaskQueue(tc)
+}

+ 10 - 0
service/task/taskList.go

@@ -0,0 +1,10 @@
+package task
+
+import "fwh-toolbox-server/task/queue/simCardQueue"
+
+// 对外任务表
+
+/*************** FWH ***************/
+
+/*************** SimCard ***************/
+var CreateTaskSimCardCollect = simCardQueue.CreateTaskSimCardCollect

+ 8 - 6
service/utils/httpClient/httpClient.go

@@ -70,12 +70,14 @@ type Response struct {
 
 func (c *HttpClient) responseInterceptor(response *Response, retry func(*string) (*Response, error)) *Response {
 	// 判断是否回复code 401
-	var data ResponseData
-	if err := json.Unmarshal(response.Body, &data); err != nil {
-		zap.L().Warn("ResponseData 解析失败", zap.String("Status", response.Status), zap.String("body", string(response.Body)), zap.String("err", err.Error()))
-		return response
-	}
-	if data.Code == 401 {
+	// var data ResponseData
+	// if err := json.Unmarshal(response.Body, &data); err != nil {
+	// 	zap.L().Warn("ResponseData 解析失败", zap.String("Status", response.Status), zap.String("body", string(response.Body)), zap.String("err", err.Error()))
+	// 	return response
+	// }
+	// if data.Code == 401 {
+	// TODO 调整401判断逻辑,待观察是否有问题
+	if response.StatusCode == 401 {
 		if c.reLogin == nil {
 			return response
 		}

+ 18 - 1
service/utils/mqttClient/mqttClient.go

@@ -3,6 +3,7 @@ package mqttClient
 import (
 	"crypto/tls"
 	"fmt"
+	"net/url"
 	"time"
 
 	mqtt "github.com/eclipse/paho.mqtt.golang"
@@ -57,7 +58,23 @@ func CreateMqttClient(opt MqttClientOpt) (*MqttClient, error) {
 	// 连接丢失回调
 	opts.OnConnectionLost = func(c mqtt.Client, err error) {
 		fmt.Printf("%s Connection lost: %s\n", clientID, err.Error())
-		zap.L().Info("Connection lost", zap.String("clientID", clientID), zap.String("err", err.Error()))
+		zap.L().Info("MQTT Connection lost", zap.String("clientID", clientID), zap.String("err", err.Error()))
+	}
+
+	// 重连回调
+	opts.OnReconnecting = func(c mqtt.Client, opts *mqtt.ClientOptions) {
+		zap.L().Info("MQTT Reconnecting", zap.String("clientID", clientID))
+	}
+
+	// 尝试连接回调
+	opts.OnConnectAttempt = func(broker *url.URL, tlsCfg *tls.Config) *tls.Config {
+		zap.L().Info("MQTT Attempt Connect", zap.String("clientID", clientID), zap.String("Broker", broker.String()))
+		return tlsCfg
+	}
+
+	// 连接事件回调
+	opts.OnConnectionNotification = func(c mqtt.Client, cn mqtt.ConnectionNotification) {
+		zap.L().Info("MQTT Connecting Notification", zap.String("clientID", clientID), zap.Int64("Notification", int64(cn.Type())))
 	}
 
 	// 创建客户端

+ 36 - 4
src/composable/useDiscreteApi.ts

@@ -14,10 +14,42 @@ export function getConfigProviderProps() {
     theme: theme.value,
     themeOverrides: themeOverrides.value,
     icons: {
-      info: () => h('span', { class: 'i-ph:info size-full' }),
-      success: () => h('span', { class: 'i-ph:seal-check size-full' }),
-      warning: () => h('span', { class: 'i-ph:warning size-full' }),
-      error: () => h('span', { class: 'i-ph:x-square size-full' }),
+      info: () =>
+        h('span', {
+          class: 'i-ph:info',
+          style: {
+            display: 'inline-block',
+            width: '18px',
+            height: '18px',
+          },
+        }),
+      success: () =>
+        h('span', {
+          class: 'i-ph:seal-check',
+          style: {
+            display: 'inline-block',
+            width: '18px',
+            height: '18px',
+          },
+        }),
+      warning: () =>
+        h('span', {
+          class: 'i-ph:warning',
+          style: {
+            display: 'inline-block',
+            width: '18px',
+            height: '18px',
+          },
+        }),
+      error: () =>
+        h('span', {
+          class: 'i-ph:x-circle',
+          style: {
+            display: 'inline-block',
+            width: '18px',
+            height: '18px',
+          },
+        }),
     },
   }))
 

+ 23 - 12
src/router/record.ts

@@ -24,6 +24,17 @@ export const routeRecordRaw: MergeMenuMixedOptions[] = [
     },
     component: 'site-info-manage/index',
   },
+  {
+    path: 'sim-card-manage',
+    name: 'SimCardManage',
+    icon: 'i-ph:sim-card',
+    label: 'SIM卡信息管理',
+    meta: {
+      componentName: 'SimCardManage',
+      showTab: true,
+    },
+    component: 'sim-card-manage/index',
+  },
   {
     path: 'shutdown-manage',
     name: 'shutdownManage',
@@ -506,16 +517,16 @@ export const routeRecordRaw: MergeMenuMixedOptions[] = [
       enableMultiTab: true,
     },
   },
-  // {
-  //   path: 'test',
-  //   key: 'test',
-  //   name: 'test',
-  //   icon: 'i-ph:test-tube-duotone',
-  //   label: '测试页',
-  //   component: 'test/index',
-  //   meta: {
-  //     showTab: true,
-  //     enableMultiTab: true,
-  //   },
-  // },
+  {
+    path: 'test',
+    key: 'test',
+    name: 'test',
+    icon: 'i-ph:test-tube-duotone',
+    label: '测试页',
+    component: 'test/index',
+    meta: {
+      showTab: true,
+      enableMultiTab: true,
+    },
+  },
 ]

+ 9 - 2
src/service/fetchClient.ts

@@ -98,8 +98,15 @@ export class FetchClient {
     // 构造完整 URL + 查询参数
     let fullURL = baseURL + url
     if (params) {
-      const query = new URLSearchParams(params).toString()
-      fullURL += fullURL.includes('?') ? '&' + query : '?' + query
+      const query = new URLSearchParams(
+        Object.entries(params).filter(([_, value]) => value !== undefined && value !== null) as [
+          string,
+          string,
+        ][],
+      ).toString()
+      if (query) {
+        fullURL += fullURL.includes('?') ? '&' + query : '?' + query
+      }
     }
 
     // Abort 控制

+ 33 - 0
src/service/simCardManage/index.ts

@@ -0,0 +1,33 @@
+// SIM Card Manage API List
+// /simCardManage/info - GET 查询SIM卡信息
+// /simCardManage/monthly - GET 查询SIM卡月数据
+
+import http from '@/service/serverReq'
+import type { SimCardInfoPage, SimCardMonthlyData } from './interface.ts'
+
+const API_List = {
+  simCardInfoList: '/simCardManage/info',
+  simCardMonthlyData: '/simCardManage/monthly',
+}
+
+// GET 查询SIM卡信息
+export const getSimCardInfo = async (
+  page: number,
+  size: number,
+  search: string,
+  status: string | undefined,
+): Promise<SimCardInfoPage> => {
+  const url = API_List.simCardInfoList
+  const res = await http.get<SimCardInfoPage>(url, { params: { page, size, search, status } })
+  return res
+}
+
+// GET 查询SIM卡月数据
+export const getSimCardMonthlyData = async (
+  id: string,
+  month: string,
+): Promise<SimCardMonthlyData> => {
+  const url = API_List.simCardMonthlyData
+  const res = await http.get<SimCardMonthlyData>(url, { params: { id, month } })
+  return res
+}

+ 32 - 0
src/service/simCardManage/interface.ts

@@ -0,0 +1,32 @@
+import type { ResPage } from '../interface'
+
+export type SimCardStatus = 'UNKNOWN' | 'NOT_ACTIVATED' | 'ACTIVATED' | 'DEACTIVATED' | 'RETIRED'
+
+export interface SimCardInfo {
+  giccid: string // 只取前19位不取检验码
+  sn: string[] // 关联的SN列表
+  imei: string // 国际移动用户识别码
+  imsi: string // 国际移动用户识别码
+  msisdn: string // 移动用户号码
+  status: string // 卡状态
+  expirationTime: string // 到期时间(后付费卡没有到期时间) 格式:yyyy-MM-dd
+  month: string // 统计月份 格式:yyyy-MM
+  allDataUsage: number // 月数据用量(KB)
+  updateTime: string // UTC时间
+}
+
+export type SimCardInfoPage = ResPage<SimCardInfo[]>
+
+export interface SimCardDailyUsage {
+  updateTime: string // UTC时间
+  day: string // 统计日期 格式:yyyy-MM-DD
+  dataUsage: number // 数据用量(KB)
+}
+
+export interface SimCardMonthlyData {
+  simId: string // GICCID
+  updateTime: string // UTC时间
+  month: string // 统计月份 格式:yyyy-MM
+  allDataUsage: number // 月数据用量(KB)
+  dailyUsage: Record<number, SimCardDailyUsage> // 日用量数据信息 (index=当月日数1-31)
+}

+ 2 - 0
src/stores/initStores.ts

@@ -24,6 +24,7 @@ import { useSitesMonitorConfigStore } from './modules/views/sitesMonitorConfig'
 import { useSiteRuntimeChartConfigStore } from './modules/views/siteRuntimeChartConfig'
 import { useXlogViewsStateStore } from './modules/views/xlogViewsState'
 import { useSiteInfoManageStateStore } from './modules/views/siteInfoManageState'
+import { useFaultRecordStateStore } from './modules/views/faultRecordState'
 
 // 全局注册表
 // type StoreDefinitionFn =
@@ -45,6 +46,7 @@ export const InitStores = () => {
   InitOneStore(useSiteRuntimeChartConfigStore())
   InitOneStore(useXlogViewsStateStore())
   InitOneStore(useSiteInfoManageStateStore())
+  InitOneStore(useFaultRecordStateStore())
 
   // 监听登录事件,重置Store
   globalThis.AppEvents.on('user:login', () => {

+ 59 - 0
src/stores/modules/views/faultRecordState.ts

@@ -0,0 +1,59 @@
+import { defineSetupStore } from '@/stores/public/template'
+
+const STORES_KEY = 'FaultRecordStateStore'
+const CURRENT_VERSION = '0.0.0'
+
+export interface FaultRecordWriteParamItem {
+  key: string
+  id: number
+  val: string
+}
+
+export interface FaultRecordWriteParam {
+  sn: string // aGate SN
+  list: {
+    key: string
+    type: 'pe' | 'agate'
+    sn: string // aGate SN(DSP) / aPower SN
+    paraList: FaultRecordWriteParamItem[]
+  }[]
+}
+
+export const useFaultRecordStateStore = defineSetupStore(
+  STORES_KEY,
+  CURRENT_VERSION,
+  () => {
+    const faultRecordWriteParam = ref<FaultRecordWriteParam>({
+      sn: '',
+      list: [],
+    })
+
+    function $reset() {
+      faultRecordWriteParam.value = {
+        sn: '',
+        list: [],
+      }
+    }
+
+    return {
+      // normal state
+      faultRecordWriteParam,
+
+      // reactive state - ref()
+
+      // reactive getters - computed()
+
+      // normal getters - function()
+
+      // actions - function()
+      $reset,
+    }
+  },
+  {
+    persist: {
+      key: STORES_KEY, // 存储 key
+      storage: localStorage, // 默认是 localStorage,sessionStorage可选
+      debug: true,
+    },
+  },
+)

+ 57 - 30
src/views/fault-record/write/index.vue

@@ -4,12 +4,14 @@
       <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>
+      <NSpin :show="spinSaveConfig">
+        <NButton type="info" :disabled="spinSaveConfig" @click="clickSaveConfig">
+          <template #icon>
+            <span class="i-material-symbols:save-outline" />
+          </template>
+          写入配置
+        </NButton>
+      </NSpin>
       <NDivider />
       <NFormItem content-class="flex flex-col gap-2" path="list" :show-label="false">
         <div
@@ -99,40 +101,30 @@ import {
   NButton,
   NDynamicInput,
   NSelect,
+  NSpin,
 } from 'naive-ui'
 import type { FormRules, FormItemRule } from 'naive-ui'
+import { storeToRefs } from 'pinia'
 import { setFaultRecordParam } from '@/service/faultRecord'
 import type { FaultRecordParamInfo } from '@/service/faultRecord/interface'
+import {
+  useFaultRecordStateStore,
+  type FaultRecordWriteParam as FaultRecordParam,
+  type FaultRecordWriteParamItem as FaultRecordParamItem,
+} from '@/stores/modules/views/faultRecordState'
+
+const faultRecordStateStore = useFaultRecordStateStore()
 
 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 formValue = storeToRefs(faultRecordStateStore).faultRecordWriteParam
 
 const formRules: FormRules = {
   sn: {
@@ -191,7 +183,28 @@ const clickAddSubModel = () => {
     key: crypto.randomUUID(),
     type: 'pe',
     sn: '',
-    paraList: [],
+    paraList: [
+      {
+        key: crypto.randomUUID(),
+        id: 6,
+        val: '010300012D46',
+      },
+      {
+        key: crypto.randomUUID(),
+        id: 6,
+        val: '020300012DDE',
+      },
+      {
+        key: crypto.randomUUID(),
+        id: 6,
+        val: '030300012E12',
+      },
+      {
+        key: crypto.randomUUID(),
+        id: 6,
+        val: '040300012E14',
+      },
+    ],
   })
 }
 
@@ -249,6 +262,8 @@ const transformFaultRecordParam = (para: FaultRecordParam): FaultRecordParamInfo
   }
 }
 
+const spinSaveConfig = ref<boolean>(false)
+
 const clickSaveConfig = () => {
   formRef.value?.validate((errors) => {
     if (errors) {
@@ -256,10 +271,22 @@ const clickSaveConfig = () => {
       return
     }
 
+    spinSaveConfig.value = true
     const res = setFaultRecordParam(transformFaultRecordParam(formValue.value))
-    res.then(() => {
-      message.success('写入成功')
-    })
+    res
+      .then(() => {
+        message.success('写入成功')
+      })
+      .catch((err: unknown) => {
+        if (err instanceof Error) {
+          message.error(err.message)
+        } else {
+          message.error(String(err))
+        }
+      })
+      .finally(() => {
+        spinSaveConfig.value = false
+      })
   })
 }
 </script>

+ 232 - 0
src/views/sim-card-manage/component/simCardDataView.vue

@@ -0,0 +1,232 @@
+<template>
+  <NModal
+    :show="show"
+    :mask-closable="false"
+    draggable
+    preset="card"
+    :title="customTitle"
+    :bordered="false"
+    :closable="true"
+    @close="
+      () => {
+        show = false
+      }
+    "
+    @after-enter="modalAfterEnter"
+    @before-leave="modalBeforeLeave"
+  >
+    <template #header-extra> {{ data.updateTime }} </template>
+    <div ref="simCardMonthlyChart" class="flex-1" />
+    <template #footer> {{ data.month }} </template>
+  </NModal>
+</template>
+
+<script setup lang="ts">
+import type { VNodeChild, CSSProperties } from 'vue'
+import { useMessage, NModal, NSwitch } from 'naive-ui'
+import { Chart, type G2Spec, type Mark, type Composition, type TooltipTitle } from '@antv/g2'
+import type { SimCardMonthlyData, SimCardDailyUsage } from '@/service/simCardManage/interface'
+
+import { usePersonalization } from '@/composable/usePersonalization'
+
+const props = defineProps<{
+  data: SimCardMonthlyData
+}>()
+
+const show = defineModel<boolean>('show', { required: true })
+
+const message = useMessage()
+
+const { isDark, color } = usePersonalization()
+
+const simCardMonthlyChart = ref<HTMLDivElement | null>(null)
+
+interface SimCardMonthlyChartData {
+  day: number
+  value: null | number // 总值
+  increment: null | number // 增量
+}
+
+let simCardMonthlyChartInstance: Chart | null = null
+
+function getDaysInMonth(yearMonth: string): number {
+  const [year, month] = yearMonth.split('-').map(Number)
+
+  // month 是 1~12,Date 的 month 是 0~11
+  return new Date(year, month, 0).getDate()
+}
+
+// 创建Chart
+function initSimCardMonthlyChart() {
+  console.log({ simCardMonthlyChart: simCardMonthlyChart.value })
+  if (!simCardMonthlyChart.value) return
+  console.log({ simCardMonthlyChart })
+
+  // 初始化图表实例
+  const chart = new Chart({
+    container: simCardMonthlyChart.value,
+  })
+
+  const data = props.data
+
+  chart.options({
+    type: 'interval',
+    autoFit: true,
+    theme: { type: isDark.value ? 'dark' : 'light' },
+    data: {
+      type: 'inline',
+      value: data,
+      transform: [
+        {
+          type: 'custom',
+          callback: (data: SimCardMonthlyData): SimCardMonthlyChartData[] => {
+            const chartData: SimCardMonthlyChartData[] = []
+            const daysInMonth = getDaysInMonth(data.month)
+            let maxValue = 0
+            for (let i = 1; i <= daysInMonth; i++) {
+              const value = data.dailyUsage[i]
+              console.log({ value })
+
+              if (value === undefined) {
+                chartData.push({
+                  day: i,
+                  value: null,
+                  increment: null,
+                })
+              } else {
+                chartData.push({
+                  day: i,
+                  value: value.dataUsage / 1024,
+                  increment: (value.dataUsage - maxValue) / 1024,
+                })
+
+                if (value.dataUsage > maxValue) {
+                  maxValue = value.dataUsage
+                }
+              }
+            }
+            console.log({ chartData })
+            return chartData
+          },
+        },
+      ],
+    },
+    encode: {
+      x: 'day',
+      y: 'value',
+    },
+    style: {
+      fill: '#2080f0',
+    },
+    axis: {
+      x: { title: false },
+      y: { title: false },
+    },
+    tooltip: {
+      title: (d: SimCardMonthlyChartData): TooltipTitle => `${d.day}`,
+    },
+  })
+
+  chart.render()
+
+  simCardMonthlyChartInstance = chart
+  console.log({ simCardMonthlyChartInstance })
+}
+
+// 销毁Chart
+function destroySimCardMonthlyChart() {
+  if (!simCardMonthlyChartInstance) return
+  simCardMonthlyChartInstance.destroy()
+  simCardMonthlyChartInstance = null
+}
+
+const modalAfterEnter = () => {
+  initSimCardMonthlyChart()
+}
+
+const modalBeforeLeave = () => {
+  destroySimCardMonthlyChart()
+}
+
+// watch(
+//   () => props.data.dailyUsage,
+//   (v) => {
+//     if (simCardMonthlyChartInstance != null) {
+//       console.log({ data: v })
+//       simCardMonthlyChartInstance.changeData(v)
+//       nextTick(() => {
+//         if (simCardMonthlyChartInstance != null) {
+//           simCardMonthlyChartInstance.render()
+//         }
+//       })
+//     }
+//   },
+// )
+
+function customTitle(): VNodeChild {
+  return h(
+    'div',
+    {
+      class: 'flex gap-4 items-center',
+    },
+    [
+      props.data.simId,
+      h(
+        NSwitch,
+        {
+          railStyle: ({ focused, checked }: { focused: boolean; checked: boolean }) => {
+            const style: CSSProperties = {}
+            if (checked) {
+              // 绿色
+              style.background = '#18a058'
+              if (focused) {
+                style.boxShadow = '0 0 0 2px #18a05840'
+              }
+            } else {
+              // 蓝色
+              style.background = '#2080f0'
+              if (focused) {
+                style.boxShadow = '0 0 0 2px #2080f040'
+              }
+            }
+            return style
+          },
+          onUpdateValue: (value: boolean) => {
+            if (simCardMonthlyChartInstance != null) {
+              if (value) {
+                simCardMonthlyChartInstance.options({
+                  encode: {
+                    x: 'day',
+                    y: 'increment',
+                  },
+                  style: {
+                    fill: '#18a058',
+                  },
+                })
+              } else {
+                simCardMonthlyChartInstance.options({
+                  encode: {
+                    x: 'day',
+                    y: 'value',
+                  },
+                  style: {
+                    fill: '#2080f0',
+                  },
+                })
+              }
+              simCardMonthlyChartInstance.render()
+            }
+          },
+        },
+        { checked: () => '增量', unchecked: () => '总值' },
+      ),
+    ],
+  )
+}
+
+onMounted(() => {})
+
+onUnmounted(() => {
+  destroySimCardMonthlyChart()
+})
+</script>

+ 372 - 0
src/views/sim-card-manage/index.vue

@@ -0,0 +1,372 @@
+<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"
+    >
+      <div class="flex gap-2 h-6 items-center">
+        <!-- <div class="flex items-center"> -->
+        <NForm inline label-placement="left" label-width="auto" :show-feedback="false">
+          <NFormItem label="Status">
+            <NSelect
+              v-model:value="selectStatus"
+              class="w-180px"
+              :options="optionsStatus"
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="GICCID">
+            <NInput
+              v-model:value="searchKeyword"
+              round
+              clearable
+              placeholder="搜索..."
+              style="max-width: 200px"
+              size="small"
+            />
+          </NFormItem>
+        </NForm>
+        <NButton text :focusable="false" style="font-size: 18px" @click="clickSearch">
+          <span class="i-mdi:search" />
+        </NButton>
+        <!-- </div> -->
+        <NButton
+          class="ml-auto"
+          text
+          :focusable="false"
+          style="font-size: 18px"
+          disabled
+          @click="clickExportSiteInfo"
+        >
+          <span class="i-material-symbols:download-rounded" />
+        </NButton>
+      </div>
+      <NDivider style="margin: 16px 0" />
+      <div class="flex flex-1 flex-col min-h-0">
+        <div ref="tableWrapper" class="flex-1 min-h-0 overflow-hidden">
+          <NDataTable
+            ref="dataTableRef"
+            v-model:checked-row-keys="checkedRowKeys"
+            class="max-height-auto"
+            :remote="true"
+            size="small"
+            :columns="tableColumns"
+            :data="tableData"
+            :row-key="tableRowKey"
+            :loading="isRequestLoading"
+            striped
+            :single-line="false"
+            :style="{ height: `${tableHeight}px` }"
+            flex-height
+            :scroll-x="dataTableMaxWidth"
+          />
+        </div>
+        <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>
+    <SimCardDataView v-model:show="showSimCardDataView" :data="simCardData" />
+    <SiteInfoView ref="siteInfoViewRef" />
+  </div>
+</template>
+
+<script setup lang="tsx">
+import { useClipboard } from '@vueuse/core'
+import {
+  useDialog,
+  useMessage,
+  NCard,
+  NButton,
+  NInput,
+  NDivider,
+  NDataTable,
+  NNumberAnimation,
+  NSelect,
+  NForm,
+  NFormItem,
+  NScrollbar,
+} from 'naive-ui'
+import type { PaginationProps, DataTableColumns, SelectOption } from 'naive-ui'
+import { useFullCard } from '@/composable/comp/useFullCard'
+import { getSimCardInfo, getSimCardMonthlyData } from '@/service/simCardManage'
+import type {
+  SimCardInfo,
+  SimCardMonthlyData,
+  SimCardStatus,
+} from '@/service/simCardManage/interface'
+import SimCardDataView from './component/simCardDataView.vue'
+import SiteInfoView from '../site-info-manage/component/siteInfoView.vue'
+
+const dialog = useDialog()
+const message = useMessage()
+
+const mainCardRef = useTemplateRef<InstanceType<typeof NCard>>('mainCardRef')
+const dataTableRef = useTemplateRef<InstanceType<typeof NDataTable>>('dataTableRef')
+const siteInfoViewRef = useTemplateRef<InstanceType<typeof SiteInfoView>>('siteInfoViewRef')
+
+const { height } = useFullCard(mainCardRef)
+
+const searchKeyword = ref('')
+
+const clickSearch = async () => {
+  // 请求查询数据
+  handleListQuery()
+}
+
+const isRequestLoading = ref(false)
+const checkedRowKeys = ref<Array<number | string>>([])
+
+const tableWrapper = ref<HTMLDivElement | null>(null)
+const tableHeight = ref(100)
+
+const cardHeight = computed(() => {
+  return { height: `${height.value === undefined || height.value === 0 ? 480 : height.value}px` }
+})
+
+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 tableRowKey = (rowData: SimCardInfo) => {
+  return rowData.giccid
+}
+
+const showSimCardDataView = ref<boolean>(false)
+const simCardData = ref<SimCardMonthlyData>({
+  simId: '',
+  updateTime: '',
+  month: '',
+  allDataUsage: 0,
+  dailyUsage: {},
+})
+
+function createColumns(): DataTableColumns<SimCardInfo> {
+  return [
+    {
+      title: 'No.',
+      key: '',
+      width: 50,
+      minWidth: 50,
+      fixed: 'left',
+      render(_, index) {
+        const page = tablePagination.page ?? 1
+        const pageSize = tablePagination.pageSize ?? 10
+        const n = (page - 1) * pageSize + index + 1
+        return `${n}`
+      },
+    },
+    {
+      title: 'GICCID',
+      key: 'giccid',
+      width: 200,
+      fixed: 'left',
+      render(rowData, rowIndex) {
+        return h(
+          'div',
+          { class: 'flex gap-1' },
+          {
+            default: () => [
+              h(
+                NButton,
+                {
+                  text: true,
+                  onClick: () => {
+                    getSimCardMonthlyData(rowData.giccid, rowData.month)
+                      .then((resp) => {
+                        simCardData.value = resp
+                        showSimCardDataView.value = true
+                      })
+                      .catch((err: unknown) => {
+                        if (err instanceof Error) {
+                          message.error(err.message)
+                        } else {
+                          message.error(String(err))
+                        }
+                      })
+                  },
+                },
+                { default: () => rowData.giccid },
+              ),
+              h(
+                NButton,
+                {
+                  text: true,
+                  focusable: false,
+                  onClick: () => {
+                    useClipboard()
+                      .copy(rowData.giccid)
+                      .then(() => {
+                        message.success('复制成功')
+                      })
+                      .catch(() => {
+                        message.error('复制失败')
+                      })
+                  },
+                },
+                {
+                  default: () => h('span', { class: 'i-ph:copy-duotone' }),
+                },
+              ),
+            ],
+          },
+        )
+      },
+    },
+    {
+      title: 'SN',
+      key: 'sn',
+      width: 200,
+      render(rowData, rowIndex) {
+        return h(
+          NScrollbar,
+          {
+            style: 'max-height: 120px',
+            trigger: 'none',
+          },
+          {
+            // default: () => rowData.sn.join(',\n'),
+            default: () => [
+              ...rowData.sn.map((sn) => {
+                return h(
+                  NButton,
+                  {
+                    text: true,
+                    onClick: () => siteInfoViewRef.value?.show(sn),
+                  },
+                  {
+                    default: () => sn,
+                  },
+                )
+              }),
+            ],
+          },
+        )
+      },
+    },
+    {
+      title: 'Status',
+      key: 'status',
+      width: 150,
+    },
+    {
+      title: 'Usage',
+      key: 'allDataUsage',
+      minWidth: 100,
+    },
+    {
+      title: 'UpdateTime',
+      key: 'updateTime',
+      width: 300,
+    },
+  ]
+}
+
+const selectStatus = ref<SimCardStatus | undefined>()
+const optionsStatus: SelectOption[] = [
+  { label: 'UNKNOWN', value: 'UNKNOWN' },
+  { label: 'NOT_ACTIVATED', value: 'NOT_ACTIVATED' },
+  { label: 'ACTIVATED', value: 'ACTIVATED' },
+  { label: 'DEACTIVATED', value: 'DEACTIVATED' },
+  { label: 'RETIRED', value: 'RETIRED' },
+]
+
+const tableData = ref<SimCardInfo[]>([])
+const dataTableMaxWidth = ref<number | undefined>(undefined)
+const tableColumns = computed<DataTableColumns<SimCardInfo>>(createColumns)
+const tablePagination = reactive<PaginationProps>({
+  simple: false,
+  page: 1,
+  pageSize: 10,
+  showSizePicker: true,
+  pageSizes: [10, 20, 50, 100],
+  pageSlot: 5,
+  itemCount: 0,
+  showQuickJumper: false,
+  showQuickJumpDropdown: true,
+  onUpdatePage: (page: number) => {
+    tablePagination.page = page
+    handleListQuery()
+  },
+  onUpdatePageSize: (pageSize: number) => {
+    tablePagination.pageSize = pageSize
+    tablePagination.page = 1
+    handleListQuery()
+  },
+})
+
+const clickExportSiteInfo = () => {
+  dialog.warning({
+    title: '确认导出',
+    content: `是否确定导出当前查询条件的${tablePagination.itemCount}条数据?`,
+    positiveText: '确定',
+    negativeText: '取消',
+    maskClosable: false,
+    onPositiveClick: () => {
+      console.log('开始导出')
+    },
+    onNegativeClick: () => {
+      console.log('取消导出')
+    },
+  })
+}
+
+const querySimCardInfoList = async (
+  pageSize: number,
+  pageNum: number,
+  search: string,
+  status: string | undefined,
+) => {
+  const responseList = await getSimCardInfo(pageNum, pageSize, search, status)
+  if (responseList.data != null) {
+    tableData.value = responseList.data
+  } else {
+    tableData.value = []
+  }
+  tablePagination.itemCount = responseList.total
+}
+
+const handleListQuery = async () => {
+  const pageSize = tablePagination.pageSize ?? 10
+  const pageNum = tablePagination.page ?? 1
+
+  await querySimCardInfoList(pageSize, pageNum, searchKeyword.value, selectStatus.value)
+}
+
+onMounted(async () => {
+  // 请求查询数据
+  await handleListQuery()
+
+  nextTick(() => {
+    const observer = new ResizeObserver((entries) => {
+      tableHeight.value = entries[0].contentRect.height
+    })
+
+    if (tableWrapper.value) {
+      observer.observe(tableWrapper.value)
+    }
+  })
+})
+</script>

+ 5 - 216
src/views/test/index.vue

@@ -1,218 +1,7 @@
 <template>
-  <div class="main-wrap flex flex-col gap-y-4 p-4">
-    <NCard ref="mainCardRef" class="h-full" content-class="h-full flex  flex-col" :style="cardHeight">
-      <div class="flex items-center gap-2">
-        <NTooltip :show-arrow="false" placement="bottom-start" trigger="hover">
-          <template #trigger>
-            <NButton text :focusable="false" style="font-size: 18px">
-              <span class="i-ph:info" />
-            </NButton>
-          </template>
-          <div>
-            <span class="font-bold">使用说明</span>
-            <span>搜索</span>
-          </div>
-
-        </NTooltip>
-        <NPopover placement="bottom-start" trigger="manual" :show="showFilterMenu">
-          <template #trigger>
-            <NButton text :focusable="false" style="font-size: 18px" @click="openFilterMenu">
-              <span class="i-mdi:filter-cog-outline" />
-            </NButton>
-          </template>
-          <div>
-            <span>文本过滤条件,每个条件之间是或的关系,每个标签是与的关系。</span>
-            <NDivider style="margin: 8px 0;" />
-            <NDynamicInput v-model:value="filterConfig" :on-create="onCreateFilter">
-              <template #create-button-default>
-                新增过滤条件
-              </template>
-              <template #default="{ value }">
-                <div style="display: flex; align-items: center; width: 100%">
-                  <NCheckbox v-model:checked="value.active" style="margin-right: 12px" />
-                  <NDynamicTags v-model:value="value.tags" />
-                </div>
-              </template>
-            </NDynamicInput>
-            <NDivider style="margin: 8px 0;" />
-            <div class="flex justify-end gap-4">
-              <NButton class="px-3 py-1 border rounded" text :focusable="false" style="font-size: 18px"
-                @click="closeFilterMenu">
-                <span class="i-mdi:close" />
-              </NButton>
-              <NButton class="px-3 py-1 border rounded" text :focusable="false" style="font-size: 18px"
-                @click="submitFilterMenu">
-                <span class="i-mdi:check" />
-              </NButton>
-            </div>
-          </div>
-        </NPopover>
-        <NButton text :focusable="false" style="font-size: 18px" class="transition-colors duration-200"
-          :class="cancelAllFilter ? 'text-green-500' : 'text-gray-400'" @click="() => {
-            cancelAllFilter = !cancelAllFilter
-            console.log({ cancelAllFilter })
-          }">
-          <span class="i-mdi:filter-off-outline" />
-        </NButton>
-        <NButton text :focusable="false" style="font-size: 18px">
-          <span class="i-ph:faders" />
-        </NButton>
-        <NButton text :focusable="false" style="font-size: 18px" @click="openSearch">
-          <span class="i-mdi:search" />
-        </NButton>
-      </div>
-      <NDivider style="margin: 16px 0;" />
-      <LogMonacoEditor class="flex-1" ref="logMonacoEditorRef" v-model="editorTxt" />
-    </NCard>
-  </div>
+  <iframe
+    title="Asynq"
+    src="/fwh-toolbox/api/asynq"
+    style="width: 100%; height: 100vh; border: none"
+  />
 </template>
-
-<script setup lang="ts">
-import { NCard, NButton, NDivider, NTooltip, NPopover, NDynamicInput, NCheckbox, NDynamicTags } from 'naive-ui'
-import { useFullCard } from '@/composable/comp/useFullCard'
-import LogMonacoEditor from '@/utils/monaco/components/LogMonacoEditor.vue'
-
-const mainCardRef = useTemplateRef<InstanceType<typeof NCard>>('mainCardRef')
-const { height } = useFullCard(mainCardRef)
-
-const logMonacoEditorRef = useTemplateRef<InstanceType<typeof LogMonacoEditor>>('logMonacoEditorRef')
-
-// 禁用所有过滤
-const cancelAllFilter = ref(false)
-
-interface FilterConfigType {
-  active: boolean
-  tags: string[]
-}
-
-interface UserConfig {
-  filter: {
-    disable: boolean
-    config: FilterConfigType[]
-  }
-}
-
-const cardHeight = computed(() => {
-  return { height: (height.value === undefined || height.value === 0 ? 480 : height.value) + 'px' }
-})
-
-const filterConfig = ref<FilterConfigType[]>([])
-const onCreateFilter = () => {
-  return {
-    active: true,
-    tags: []
-  }
-}
-
-const userConfig = reactive<UserConfig>({
-  filter: {
-    disable: false,
-    config: []
-  }
-})
-
-const showFilterMenu = ref(false)
-
-const openFilterMenu = () => {
-  showFilterMenu.value = true
-}
-
-const closeFilterMenu = () => {
-  showFilterMenu.value = false
-}
-
-const submitFilterMenu = () => {
-  showFilterMenu.value = false
-}
-
-const openSearch = () => {
-  logMonacoEditorRef.value?.openFind()
-}
-
-const editorTxt = ref<string>(`[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
-[E] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[ERROR] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
-[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
-[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
-[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
-[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
-[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
-[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
-[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
-[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
-[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
-[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
-[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
-[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
-[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
-[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
-[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
-[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
-[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
-[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
-[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
-[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
-[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
-[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
-[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
-`)
-
-// setTimeout(() => {
-//   editorTxt.value = "[I] [20260105,22:12:05.914-0500]"
-// }, 10000);
-
-</script>

+ 218 - 0
src/views/test/index.vue_bak

@@ -0,0 +1,218 @@
+<template>
+  <div class="main-wrap flex flex-col gap-y-4 p-4">
+    <NCard ref="mainCardRef" class="h-full" content-class="h-full flex  flex-col" :style="cardHeight">
+      <div class="flex items-center gap-2">
+        <NTooltip :show-arrow="false" placement="bottom-start" trigger="hover">
+          <template #trigger>
+            <NButton text :focusable="false" style="font-size: 18px">
+              <span class="i-ph:info" />
+            </NButton>
+          </template>
+          <div>
+            <span class="font-bold">使用说明</span>
+            <span>搜索</span>
+          </div>
+
+        </NTooltip>
+        <NPopover placement="bottom-start" trigger="manual" :show="showFilterMenu">
+          <template #trigger>
+            <NButton text :focusable="false" style="font-size: 18px" @click="openFilterMenu">
+              <span class="i-mdi:filter-cog-outline" />
+            </NButton>
+          </template>
+          <div>
+            <span>文本过滤条件,每个条件之间是或的关系,每个标签是与的关系。</span>
+            <NDivider style="margin: 8px 0;" />
+            <NDynamicInput v-model:value="filterConfig" :on-create="onCreateFilter">
+              <template #create-button-default>
+                新增过滤条件
+              </template>
+              <template #default="{ value }">
+                <div style="display: flex; align-items: center; width: 100%">
+                  <NCheckbox v-model:checked="value.active" style="margin-right: 12px" />
+                  <NDynamicTags v-model:value="value.tags" />
+                </div>
+              </template>
+            </NDynamicInput>
+            <NDivider style="margin: 8px 0;" />
+            <div class="flex justify-end gap-4">
+              <NButton class="px-3 py-1 border rounded" text :focusable="false" style="font-size: 18px"
+                @click="closeFilterMenu">
+                <span class="i-mdi:close" />
+              </NButton>
+              <NButton class="px-3 py-1 border rounded" text :focusable="false" style="font-size: 18px"
+                @click="submitFilterMenu">
+                <span class="i-mdi:check" />
+              </NButton>
+            </div>
+          </div>
+        </NPopover>
+        <NButton text :focusable="false" style="font-size: 18px" class="transition-colors duration-200"
+          :class="cancelAllFilter ? 'text-green-500' : 'text-gray-400'" @click="() => {
+            cancelAllFilter = !cancelAllFilter
+            console.log({ cancelAllFilter })
+          }">
+          <span class="i-mdi:filter-off-outline" />
+        </NButton>
+        <NButton text :focusable="false" style="font-size: 18px">
+          <span class="i-ph:faders" />
+        </NButton>
+        <NButton text :focusable="false" style="font-size: 18px" @click="openSearch">
+          <span class="i-mdi:search" />
+        </NButton>
+      </div>
+      <NDivider style="margin: 16px 0;" />
+      <LogMonacoEditor class="flex-1" ref="logMonacoEditorRef" v-model="editorTxt" />
+    </NCard>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { NCard, NButton, NDivider, NTooltip, NPopover, NDynamicInput, NCheckbox, NDynamicTags } from 'naive-ui'
+import { useFullCard } from '@/composable/comp/useFullCard'
+import LogMonacoEditor from '@/utils/monaco/components/LogMonacoEditor.vue'
+
+const mainCardRef = useTemplateRef<InstanceType<typeof NCard>>('mainCardRef')
+const { height } = useFullCard(mainCardRef)
+
+const logMonacoEditorRef = useTemplateRef<InstanceType<typeof LogMonacoEditor>>('logMonacoEditorRef')
+
+// 禁用所有过滤
+const cancelAllFilter = ref(false)
+
+interface FilterConfigType {
+  active: boolean
+  tags: string[]
+}
+
+interface UserConfig {
+  filter: {
+    disable: boolean
+    config: FilterConfigType[]
+  }
+}
+
+const cardHeight = computed(() => {
+  return { height: (height.value === undefined || height.value === 0 ? 480 : height.value) + 'px' }
+})
+
+const filterConfig = ref<FilterConfigType[]>([])
+const onCreateFilter = () => {
+  return {
+    active: true,
+    tags: []
+  }
+}
+
+const userConfig = reactive<UserConfig>({
+  filter: {
+    disable: false,
+    config: []
+  }
+})
+
+const showFilterMenu = ref(false)
+
+const openFilterMenu = () => {
+  showFilterMenu.value = true
+}
+
+const closeFilterMenu = () => {
+  showFilterMenu.value = false
+}
+
+const submitFilterMenu = () => {
+  showFilterMenu.value = false
+}
+
+const openSearch = () => {
+  logMonacoEditorRef.value?.openFind()
+}
+
+const editorTxt = ref<string>(`[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[E] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[ERROR] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+`)
+
+// setTimeout(() => {
+//   editorTxt.value = "[I] [20260105,22:12:05.914-0500]"
+// }, 10000);
+
+</script>

+ 110 - 79
src/views/xlog/component/xlogCommand.vue

@@ -3,9 +3,13 @@
     <div class="flex flex-1 gap-4 h-full overflow-hidden">
       <div class="w-1/3 overflow-auto h-full">
         <NButtonGroup vertical class="w-full">
-          <NButton class="w-full justify-start px-4" v-for="(cmd, index) in cmdList" :key="index"
-            @click="handleListClick(cmd)">
-            {{ index }} - {{ cmd.send }} ({{ cmdTypeMap[cmd.cmdType] ?? "" }})
+          <NButton
+            class="w-full justify-start px-4"
+            v-for="(cmd, index) in cmdList"
+            :key="index"
+            @click="handleListClick(cmd)"
+          >
+            {{ index }} - {{ cmd.send }} ({{ cmdTypeMap[cmd.cmdType] ?? '' }})
           </NButton>
         </NButtonGroup>
       </div>
@@ -13,16 +17,32 @@
     </div>
     <div class="flex items-center space-x-2 mt-auto">
       <NSelect class="w-30" v-model:value="cmdType" :options="cmdTypeOptions" />
-      <NAutoComplete v-model:value="inputCmd" :input-props="{ autocomplete: 'disabled' }" :options="inputCmdOptions"
-        @update:value="handleInputChange" :render-label="inputCmdOptionsRenderLabel">
+      <NAutoComplete
+        v-model:value="inputCmd"
+        :input-props="{ autocomplete: 'disabled' }"
+        :options="inputCmdOptions"
+        @update:value="handleInputChange"
+        :render-label="inputCmdOptionsRenderLabel"
+      >
         <template #prefix>
           <i class="i-ph:terminal-bold" />
         </template>
         <template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }">
-          <NInput :value="slotValue" @input="handleInput" @focus="handleFocus" @blur="handleBlur" clearable />
+          <NInput
+            :value="slotValue"
+            @input="handleInput"
+            @focus="handleFocus"
+            @blur="handleBlur"
+            clearable
+          />
         </template>
       </NAutoComplete>
-      <NButton type="primary" :disabled="disabledSendCmd" :loading="loadingSendCmd" @click="handleSendCmd">
+      <NButton
+        type="primary"
+        :disabled="disabledSendCmd"
+        :loading="loadingSendCmd"
+        @click="handleSendCmd"
+      >
         Send
       </NButton>
     </div>
@@ -33,21 +53,21 @@
 import type { VNodeChild } from 'vue'
 import { NButton, NInput, NLog, NButtonGroup, NSelect, NAutoComplete, NTag } from 'naive-ui'
 import type { AutoCompleteGroupOption, AutoCompleteOption, SelectOption } from 'naive-ui'
-import { sendXlogCmd } from '@/service/xlog';
-import { useXlogViewsStateStore } from '@/stores/modules/views/xlogViewsState';
-import type { XlogCmdListData } from '@/stores/modules/views/xlogViewsState';
+import { sendXlogCmd } from '@/service/xlog'
+import { useXlogViewsStateStore } from '@/stores/modules/views/xlogViewsState'
+import type { XlogCmdListData } from '@/stores/modules/views/xlogViewsState'
 
 const xlogViewsStateStore = useXlogViewsStateStore()
 
 const { uuid, sn } = defineProps<{
-  uuid: string,
+  uuid: string
   sn: string
 }>()
 
 let inputCmdIndex = -1
-let inputCmdTemp = ""
-const inputCmd = ref<string>("")
-const logStr = ref<string>("")
+let inputCmdTemp = ''
+const inputCmd = ref<string>('')
+const logStr = ref<string>('')
 const cmdList = xlogViewsStateStore.getCmdListRef(uuid)
 
 const cmdTypeMap: Record<string, string> = {
@@ -59,84 +79,96 @@ const cmdType = ref('cmd')
 const cmdTypeOptions = Object.entries(cmdTypeMap).map(([key, value]) => {
   return {
     label: value,
-    value: key
+    value: key,
   }
 })
 
 function toBashC(cmd: string): string {
-  return `bash -c "${cmd
-    .replace(/\\/g, '\\\\')   // 先转义反斜杠
-    .replace(/"/g, '\\"')     // 转义双引号
-    .replace(/\$/g, '\\$')    // 防止变量提前展开
-    .replace(/`/g, '\\`')     // 防止命令替换
-    .replace(/\n/g, '\\n')    // 处理换行
-    }"`
+  return `bash -c "${
+    cmd
+      .replace(/\\/g, '\\\\') // 先转义反斜杠
+      .replace(/"/g, '\\"') // 转义双引号
+      .replace(/\$/g, '\\$') // 防止变量提前展开
+      .replace(/`/g, '\\`') // 防止命令替换
+      .replace(/\n/g, '\\n') // 处理换行
+  }"`
 }
 
 // 常用命令
-const commonCmd: { cmd: string, version: string }[] = [
-  { cmd: "head -10 /opt/config/EmuPara/EmuParaDevInfo.json", version: "ALL" },
-  { cmd: "rm -v /opt/app1/configuser/xlog_remote.xml", version: "ALL" },
-  { cmd: "rm -v /home/root/ExportFile/IbgLog/*_UsrRtData*.txt", version: "1.0" },
-  { cmd: "rm -v /home/root/ExportFile/IbgLog/*_AgApRtData*.txt", version: "1.0" },
-  { cmd: "rm -v /home/root/ExportFile/IbgLog/*_devdata.txt", version: "1.0" },
-  { cmd: "rm -v /home/root/ExportFile/IbgLog/*_Power.txt", version: "1.0" },
-  { cmd: "rm1 -v /home/root/ExportFile/IbgLog/*_Power.txt", version: "1.0" },
-  { cmd: "rm2 -v /home/root/ExportFile/IbgLog/*_Power.txt", version: "1.0" },
-  { cmd: "rm3 -v /home/root/ExportFile/IbgLog/*_Power.txt", version: "1.0" },
-  { cmd: "rm4 -v /home/root/ExportFile/IbgLog/*_Power.txt", version: "1.0" },
-  { cmd: "rm5 -v /home/root/ExportFile/IbgLog/*_Power.txt", version: "1.0" },
+const commonCmd: { cmd: string; version: string }[] = [
+  { cmd: 'head -10 /opt/config/EmuPara/EmuParaDevInfo.json', version: 'ALL' },
+  { cmd: 'rm -v /opt/app1/configuser/xlog_remote.xml', version: 'ALL' },
+  { cmd: 'rm -v /home/root/ExportFile/IbgLog/*_UsrRtData*.txt', version: '1.0' },
+  { cmd: 'rm -v /home/root/ExportFile/IbgLog/*_AgApRtData*.txt', version: '1.0' },
+  { cmd: 'rm -v /home/root/ExportFile/IbgLog/*_devdata.txt', version: '1.0' },
+  { cmd: 'rm -v /home/root/ExportFile/IbgLog/*_Power.txt', version: '1.0' },
+  { cmd: 'rm1 -v /home/root/ExportFile/IbgLog/*_Power.txt', version: '1.0' },
+  { cmd: 'rm2 -v /home/root/ExportFile/IbgLog/*_Power.txt', version: '1.0' },
+  { cmd: 'rm3 -v /home/root/ExportFile/IbgLog/*_Power.txt', version: '1.0' },
+  { cmd: 'rm4 -v /home/root/ExportFile/IbgLog/*_Power.txt', version: '1.0' },
+  { cmd: 'rm5 -v /home/root/ExportFile/IbgLog/*_Power.txt', version: '1.0' },
 ]
 
 // 历史命令
 const historyCmd = ref<string[]>([])
 
 // const inputCmdOptions = ref<string[]>(["345", "24324"])
-const inputCmdOptions = computed<Array<AutoCompleteGroupOption | AutoCompleteOption | string>>(() => {
-  return [
-    {
-      type: 'group',
-      label: 'input',
-      key: 'input',
-      children: [{
-        label: inputCmd.value,
-        value: `input__${inputCmd.value}`,
-      } satisfies SelectOption]
-    },
-    {
-      type: 'group',
-      label: 'history',
-      key: 'history',
-      children: Array.from(new Set(cmdList.value.map(item => {
-        return item.send
-      }))).reverse().slice(0, 10).map(item => {
-        return {
-          label: item,
-          value: `history__${item}`,
-        } satisfies SelectOption
-      })
-    },
-    {
-      type: 'group',
-      label: 'common',
-      key: 'common',
-      children: commonCmd.map(item => {
-        return {
-          label: item.cmd,
-          value: `common__${item.cmd}`,
-          version: item.version // 用于显示版本标签
-        } satisfies SelectOption
-      })
-    }
-  ] satisfies AutoCompleteGroupOption[]
-})
+const inputCmdOptions = computed<Array<AutoCompleteGroupOption | AutoCompleteOption | string>>(
+  () => {
+    return [
+      {
+        type: 'group',
+        label: 'input',
+        key: 'input',
+        children: [
+          {
+            label: inputCmd.value,
+            value: `input__${inputCmd.value}`,
+          } satisfies SelectOption,
+        ],
+      },
+      {
+        type: 'group',
+        label: 'history',
+        key: 'history',
+        children: Array.from(
+          new Set(
+            cmdList.value.map((item) => {
+              return item.send
+            }),
+          ),
+        )
+          .reverse()
+          .slice(0, 10)
+          .map((item) => {
+            return {
+              label: item,
+              value: `history__${item}`,
+            } satisfies SelectOption
+          }),
+      },
+      {
+        type: 'group',
+        label: 'common',
+        key: 'common',
+        children: commonCmd.map((item) => {
+          return {
+            label: item.cmd,
+            value: `common__${item.cmd}`,
+            version: item.version, // 用于显示版本标签
+          } satisfies SelectOption
+        }),
+      },
+    ] satisfies AutoCompleteGroupOption[]
+  },
+)
 
 const inputCmdOptionsRenderLabel = (option: SelectOption): VNodeChild => {
   if (option.version !== undefined) {
     return [
       option.label as string,
       ' ',
-      h(NTag, { size: 'small', type: 'info' }, { default: () => option.version })
+      h(NTag, { size: 'small', type: 'info' }, { default: () => option.version }),
     ]
   } else {
     return option.label as string
@@ -156,7 +188,7 @@ const handleSendCmd = async () => {
   cmdList.value.push({
     send: cmdString,
     recv: res,
-    cmdType: cmdType.value
+    cmdType: cmdType.value,
   })
   loadingSendCmd.value = false
   logStr.value = res
@@ -179,10 +211,10 @@ const handleInputUp = (e: KeyboardEvent) => {
   if (inputCmdIndex === -1) {
     inputCmdTemp = inputCmd.value
     inputCmdIndex = cmdList.value.length - 1
-    inputCmd.value = cmdList.value.at(inputCmdIndex)?.send ?? ""
+    inputCmd.value = cmdList.value.at(inputCmdIndex)?.send ?? ''
   } else if (inputCmdIndex > 0) {
     inputCmdIndex = inputCmdIndex - 1
-    inputCmd.value = cmdList.value.at(inputCmdIndex)?.send ?? ""
+    inputCmd.value = cmdList.value.at(inputCmdIndex)?.send ?? ''
   }
 }
 
@@ -195,7 +227,7 @@ const handleInputDown = (e: KeyboardEvent) => {
     inputCmd.value = inputCmdTemp
   } else if (inputCmdIndex < cmdList.value.length - 1) {
     inputCmdIndex = inputCmdIndex + 1
-    inputCmd.value = cmdList.value.at(inputCmdIndex)?.send ?? ""
+    inputCmd.value = cmdList.value.at(inputCmdIndex)?.send ?? ''
   }
 }
 
@@ -210,5 +242,4 @@ onMounted(() => {
     logStr.value = lastCmd.recv
   }
 })
-
-</script>
+</script>