Ver Fonte

faet: 完成站点分析框架,实现Timeline图

Jerry Liang há 1 ano atrás
pai
commit
2cb7becde4
59 ficheiros alterados com 3310 adições e 1597 exclusões
  1. 1 1
      service/.env
  2. 37 3
      service/README.md
  3. 8 0
      service/cache/cache.go
  4. 2 0
      service/cmd/deploy.sh
  5. 62 0
      service/controllers/public.go
  6. 113 135
      service/controllers/sitesAnalysisControllers.go
  7. 139 407
      service/controllers/sitesMonitorControllers.go
  8. 33 33
      service/controllers/xlogControllers.go
  9. 6 0
      service/domain/alarmSnapshotModelDomain.go
  10. 42 0
      service/domain/fwhProDomain.go
  11. 359 0
      service/domain/fwhProMqttDomain.go
  12. 77 0
      service/domain/sitesAnalysisDomain.go
  13. 46 0
      service/domain/sitesMonitorDomain.go
  14. 6 0
      service/domain/xlogDomain.go
  15. 1 2
      service/dto/public.go
  16. 14 0
      service/dto/sitesAnalysisDTO.go
  17. 135 0
      service/dto/sitesMonitorDTO.go
  18. 0 149
      service/ext/api/fwh_pro/fwh_eventAlarmLog.go
  19. 0 52
      service/ext/api/fwh_pro/fwh_powerFlow.go
  20. 0 52
      service/ext/api/fwh_pro/fwh_realtimeAalrm.go
  21. 0 183
      service/ext/api/fwh_pro/fwh_siteInfo.go
  22. 181 0
      service/jobs/analysisSiteData.go
  23. 16 81
      service/jobs/getAlarmSnapshot.go
  24. 9 0
      service/main.go
  25. 4 8
      service/networkEvents/evioEvents.go
  26. 60 0
      service/repositories/api/fwh_pro/fwhHttpClient.go
  27. 235 0
      service/repositories/api/fwh_pro/fwh_eventAlarmLog.go
  28. 1 25
      service/repositories/api/fwh_pro/fwh_login.go
  29. 147 0
      service/repositories/api/fwh_pro/fwh_mqttLog.go
  30. 40 0
      service/repositories/api/fwh_pro/fwh_powerFlow.go
  31. 0 0
      service/repositories/api/fwh_pro/fwh_public.go
  32. 39 0
      service/repositories/api/fwh_pro/fwh_realtimeAalrm.go
  33. 159 0
      service/repositories/api/fwh_pro/fwh_siteInfo.go
  34. 275 0
      service/repositories/mongo/sitesAnalysisRepo.go
  35. 175 0
      service/repositories/mongo/sitesMonitorRepo.go
  36. 46 0
      service/repositories/mongo/xlogRepo.go
  37. 1 1
      service/repositories/mysql/fwh_pro_db/fwhDbRepo.go
  38. 29 0
      service/repositories/pgsql/alarmSnapshot/siteAlarmSnapshotData.go
  39. 43 0
      service/repositories/pgsql/alarmSnapshot/siteAlarmSnapshotFileRecord.go
  40. 64 0
      service/repositories/repositories.go
  41. 0 57
      service/routes/routes.go
  42. 9 105
      service/routes/sitesAnalysisRoutes.go
  43. 12 155
      service/routes/sitesMonitorRoutes.go
  44. 2 33
      service/routes/xlogRoutes.go
  45. 97 0
      service/services/sitesAnalysisServices.go
  46. 68 0
      service/services/sitesMonitorServices.go
  47. 20 0
      service/services/xlogServices.go
  48. 55 10
      service/taskQueue/taskQueue.go
  49. 121 11
      service/taskQueue/taskSiteAnalysis.go
  50. 0 9
      service/types/fwhPublic.go
  51. 0 38
      service/types/siteAnalysis.go
  52. 157 0
      service/utils/httpClient/httpClient.go
  53. 9 2
      src/service/sitesAnalysis/index.ts
  54. 14 5
      src/service/sitesAnalysis/interface.ts
  55. 29 19
      src/views/sites-analysis/ModalData.vue
  56. 76 18
      src/views/sites-analysis/component/siteTimeline.vue
  57. 24 0
      src/views/sites-analysis/index.vue
  58. 11 2
      src/views/sites-analysis/manage.vue
  59. 1 1
      src/views/sites-monitor/manage.vue

+ 1 - 1
service/.env

@@ -3,4 +3,4 @@ APP_PORT="8301"
 MYSQL_DSN="admin:V1hdkLzRoleI0YG6If24@tcp(34.211.183.235:3308)/home_energy_storage_server?charset=utf8mb4&parseTime=True&loc=Local"
 POSTGRES_DSN="host=localhost user=fwh_toolbox password=fwh_toolbox dbname=fwh_toolbox_db port=5432 sslmode=disable"
 MONGO_DSN="mongodb://127.0.0.1:27017/FwhToolboxDbDev"
-REDIS_ADDR="localhost"
+REDIS_ADDR="localhost:6379"

+ 37 - 3
service/README.md

@@ -27,9 +27,13 @@ Go 后端服务,整合了:
 ├── routes/
 │    └── routes.go      # Fiber 路由 & swagger
 ├── controllers/        # 控制器
-├── models/             # GORM 模型
-├── services/           # 业务逻辑
-├── middleware/         # 中间件
+├── models/             # GORM 模型,数据结构定义(实体、DTO)
+├── services/           # 业务逻辑(核心)
+├── middleware/         # 中间件 (鉴权 / 日志 / 限流 / 恢复 panic)
+├── repositories/       # 数据访问层(DB/Cache/外部 API)
+├── dto/                # 请求/响应 DTO (Data Transfer Object)
+├── domain/             # 领域对象 Domain Model
+├── utils/              # 工具函数
 ├── pkg/                # 自定义通用包
 ├── docs/               # Swagger 文档
 ├── .env                # 环境变量
@@ -37,6 +41,36 @@ Go 后端服务,整合了:
 └── go.sum
 ```
 
+### ♻️ 项目标准访问流
+
+Notes:
+- 带*为可选历程,可跳过
+- 一般情况下不允许跳层级访问
+
+Client (HTTP / gRPC / MQ)
+    │
+    ▼
+Route (Fiber 路由)
+    │
+    ▼
+Middleware* (鉴权 / 日志 / 限流 / 恢复 panic)
+    │
+    ▼
+Controller / Handler (解析请求,调用 service)
+    │
+    ▼
+Service (业务逻辑) ────────────────────────────────────────────┐
+    │                                                         ▼
+    │                                          Task (实时任务) / Scheduler (定时任务)
+    │                                                          │
+    │                                                          ▼
+    │                                                        Jobs* (繁琐事务)
+    ▼                                                         
+Repository / DAO (数据库 / 缓存 / API) ◀───────────────────────┘
+    │
+    ▼
+Database / Cache (Redis) / 外部服务 (FWH-API)
+
 ## ✅ 项目特点 & 核心依赖
 
 - 配置: Viper(加载 .env、支持环境变量)

+ 8 - 0
service/cache/cache.go

@@ -2,6 +2,7 @@ package cache
 
 import (
 	"context"
+	"fmt"
 	"time"
 
 	"github.com/go-redis/redis/v8"
@@ -14,6 +15,13 @@ func Init(addr string) {
 		Addr: addr,
 		DB:   12,
 	})
+
+	ctx := context.Background()
+	if err := C.Do(ctx, "PING").Err(); err != nil {
+		fmt.Println(addr)
+		errStr := fmt.Sprintf("Redis 无法访问: %s", err.Error())
+		panic(errStr)
+	}
 }
 
 func GetOrSet(ctx context.Context, key string, ttl time.Duration, fetch func() (interface{}, error)) (interface{}, error) {

+ 2 - 0
service/cmd/deploy.sh

@@ -29,6 +29,8 @@ ExecStart=$APP_PATH
 Restart=always
 User=root
 WorkingDirectory=$APP_DIR
+CPUQuota=200%
+MemoryLimit=2G
 
 [Install]
 WantedBy=multi-user.target

+ 62 - 0
service/controllers/public.go

@@ -0,0 +1,62 @@
+package controllers
+
+import (
+	"time"
+
+	"github.com/gofiber/fiber/v2"
+	"github.com/google/uuid"
+)
+
+const traceIdHeader string = "X-Trace-Id"
+
+type Response[T any] struct {
+	Code      int    `json:"code"`
+	Message   string `json:"message"`
+	Data      T      `json:"data"`
+	TraceID   string `json:"traceId"`
+	Timestamp int64  `json:"timestamp"`
+}
+
+type ResponseNoData struct {
+	Code      int    `json:"code"`
+	Message   string `json:"message"`
+	TraceID   string `json:"traceId"`
+	Timestamp int64  `json:"timestamp"`
+}
+
+func JSONResponse[T any](c *fiber.Ctx, code int, message string, data T) error {
+	traceId := c.Get(traceIdHeader) // 从请求头取
+	if traceId == "" {
+		traceId = uuid.New().String() // 没有就生成
+	}
+
+	// 设置到响应头,让前端也能拿到
+	c.Set(traceIdHeader, traceId)
+
+	resp := Response[T]{
+		Code:      code,
+		Message:   message,
+		Data:      data,
+		TraceID:   traceId,
+		Timestamp: time.Now().UnixMilli(),
+	}
+	return c.JSON(resp)
+}
+
+func JSONResponseNoData(c *fiber.Ctx, code int, message string) error {
+	traceId := c.Get(traceIdHeader) // 从请求头取
+	if traceId == "" {
+		traceId = uuid.New().String() // 没有就生成
+	}
+
+	// 设置到响应头,让前端也能拿到
+	c.Set(traceIdHeader, traceId)
+
+	resp := ResponseNoData{
+		Code:      code,
+		Message:   message,
+		TraceID:   traceId,
+		Timestamp: time.Now().UnixMilli(),
+	}
+	return c.JSON(resp)
+}

+ 113 - 135
service/controllers/sitesAnalysisControllers.go

@@ -1,186 +1,164 @@
+/*
+ * Sites Analysis API List
+ * /sitesAnalysis/manage/analysisList - GET
+ * /sitesAnalysis/manage/config - POST 创建新配置
+ * /sitesAnalysis/manage/config/{id} - GET & POST & DELETE 获取、更新、删除配置
+ * /sitesAnalysis/data/{id} - GET 获取指定ID的分析数据
+ */
 package controllers
 
 import (
-	"context"
-	"fwh-toolbox-server/database"
-	taskqueue "fwh-toolbox-server/taskQueue"
-	"fwh-toolbox-server/types"
-	"time"
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/services"
+	"strconv"
 
-	"go.mongodb.org/mongo-driver/bson"
+	"github.com/gofiber/fiber/v2"
 	"go.mongodb.org/mongo-driver/bson/primitive"
-	"go.mongodb.org/mongo-driver/mongo/options"
 )
 
-type SiteAnalysisInfo = types.SiteAnalysisInfo
-type SiteAnalysisOptions = types.SiteAnalysisOptions
-type SiteAnalysisConfig = types.SiteAnalysisConfig
-type SiteAnalysisConfigWrite = types.SiteAnalysisConfigWrite
-
-// 从数据库获取站点分析列表
-func GetSiteAnalysisManageList(page, size uint, sn, progress *string) (*ResPage[SiteAnalysisInfo], error) {
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	collection := database.GetCollection("SiteAnalysisConfig")
-
-	filter := bson.M{}
-	// 条件
-	if sn != nil {
-		filter["sn"] = *sn
-	}
-	if progress != nil {
-		filter["progress"] = *progress
+// 获取站点分析管理名称表
+// /sitesAnalysis/manage/analysisList
+func GetSitesAnalysisList(c *fiber.Ctx) error {
+	pageInt, _ := strconv.Atoi(c.Query("pageNum", "1"))
+	sizeInt, _ := strconv.Atoi(c.Query("pageSize", "20"))
+
+	if pageInt <= 0 || sizeInt <= 0 {
+		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+			"code":    400,
+			"message": "Invalid request para",
+		})
 	}
 
-	findOptions := options.Find().
-		SetProjection(bson.M{
-			"_id":         1,
-			"sn":          1,
-			"description": 1,
-			"item":        1,
-			"progress":    1,
-		}).
-		SetSkip(int64((page - 1) * size)).
-		SetLimit(int64(size))
+	sn := c.Query("sn", "")
+	progress := c.Query("progress", "")
 
-	cursor, err := collection.Find(ctx, filter, findOptions)
-	if err != nil {
-		return nil, err
+	var snP *string = nil
+	if sn != "" {
+		snP = &sn
 	}
-	defer cursor.Close(ctx)
 
-	var configList []SiteAnalysisInfo
-	for cursor.Next(ctx) {
-		var cfg SiteAnalysisInfo
-		if err := cursor.Decode(&cfg); err != nil {
-			return nil, err
-		}
-		configList = append(configList, cfg)
+	var progressP *string = nil
+	if progress != "" {
+		progressP = &progress
 	}
 
-	// 查询总数
-	total, err := collection.CountDocuments(ctx, filter)
+	listPage, err := services.GetSiteAnalysisManageList(uint(pageInt), uint(sizeInt), snP, progressP)
 	if err != nil {
-		return nil, err
+		return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
 	}
+	return JSONResponse(c, 0, "success", listPage)
+}
 
-	res := ResPage[SiteAnalysisInfo]{
-		Data:     configList,
-		PageSize: size,
-		PageNum:  page,
-		Total:    uint(total),
+// 创建一个分析配置
+// /sitesAnalysis/manage/config
+func CreateSitesAnalysis(c *fiber.Ctx) error {
+	var body domain.SiteAnalysisConfigWrite
+	if err := c.BodyParser(&body); err != nil {
+		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+			"code":    400,
+			"message": "Invalid request body",
+		})
 	}
 
-	return &res, nil
+	if err := services.InsertSiteAnalysisConfig(body); err != nil {
+		return JSONResponseNoData(c, 500, "Database write failed. (MongoDb)")
+	}
+	return JSONResponse(c, 0, "success", body)
 }
 
-// 从数据库获取一条站点分析配置
-func GetSiteAnalysisConfig(id primitive.ObjectID) (*SitesMonitorConfig, error) {
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
+// 获取单个分析配置
+// /sitesAnalysis/manage/config/:id
+func GetOneSiteAnalysisConfig(c *fiber.Ctx) error {
+	id := c.Params("id")
 
-	collection := database.GetCollection("SiteMonitorConfig")
-	var cfg SitesMonitorConfig
-	err := collection.FindOne(ctx, bson.M{"_id": id}).Decode(&cfg)
+	objID, err := primitive.ObjectIDFromHex(id)
 	if err != nil {
-		return nil, err
+		return JSONResponseNoData(c, 400, "Invalid request para :id")
 	}
 
-	return &cfg, nil
-}
-
-// 插入一条站点分析配置到数据库
-func InsertSiteAnalysisConfig(writeConfig SiteAnalysisConfigWrite) error {
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	config := SiteAnalysisConfig{
-		SiteAnalysisConfigWrite: writeConfig,
-		Item:                    []string{},
-		Progress:                "pending",
+	config, err := services.GetSiteAnalysisConfig(objID)
+	if err != nil {
+		return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
 	}
+	return JSONResponse(c, 0, "success", config)
+}
 
-	// 生成Item标签
-	if writeConfig.EnableTimeline {
-		config.Item = append(config.Item, "时间线")
+// 更新一个分析配置
+// /sitesAnalysis/manage/config/:id
+func UpdateOneSiteAnalysisConfig(c *fiber.Ctx) error {
+	id := c.Params("id")
+	var body domain.SiteAnalysisConfigWrite
+	if err := c.BodyParser(&body); err != nil {
+		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+			"code":    400,
+			"message": "Invalid request body",
+		})
 	}
 
-	collection := database.GetCollection("SiteMonitorConfig")
-
-	res, err := collection.InsertOne(ctx, config)
+	objID, err := primitive.ObjectIDFromHex(id)
 	if err != nil {
-		return err
+		return JSONResponseNoData(c, 400, "Invalid request para :id")
 	}
 
-	taskqueue.EnqueueSiteAnalysisTask(taskqueue.TaskSiteAnalysis{
-		Id:         res.InsertedID.(primitive.ObjectID),
-		Sn:         writeConfig.Sn,
-		TimePeriod: writeConfig.TimePeriod,
-		SiteAnalysisOptions: SiteAnalysisOptions{
-			EnableTimeline: writeConfig.EnableTimeline,
-		},
-	})
-
-	return nil
+	if err := services.UpdateSiteAnalysisConfig(objID, body); err != nil {
+		return JSONResponseNoData(c, 500, "Database write failed. (MongoDb)")
+	}
+	return JSONResponse(c, 0, "success", body)
 }
 
-// 更新一条站点分析配置到数据库
-func UpdateSiteAnalysisConfig(id primitive.ObjectID, writeConfig SiteAnalysisConfigWrite) error {
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	config := SiteAnalysisConfig{
-		SiteAnalysisConfigWrite: writeConfig,
-		Item:                    []string{},
-		Progress:                "pending",
+// 更新一个分析配置
+// /sitesAnalysis/manage/description/:id
+func UpdateOneSiteAnalysisDescription(c *fiber.Ctx) error {
+	id := c.Params("id")
+	var body string
+	if err := c.BodyParser(&body); err != nil {
+		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+			"code":    400,
+			"message": "Invalid request body",
+		})
 	}
 
-	// 生成Item标签
-	if writeConfig.EnableTimeline {
-		config.Item = append(config.Item, "时间线")
+	objID, err := primitive.ObjectIDFromHex(id)
+	if err != nil {
+		return JSONResponseNoData(c, 400, "Invalid request para :id")
 	}
 
-	collection := database.GetCollection("SiteMonitorConfig")
-
-	// _, err := collection.InsertOne(ctx, config)
-	filter := bson.M{"_id": id} // 按ID匹配
-	update := bson.M{
-		"$set": config, // 如果存在就更新所有字段
+	if err := services.UpdateSiteAnalysisDescription(objID, body); err != nil {
+		return JSONResponseNoData(c, 500, "Database write failed. (MongoDb)")
 	}
+	return JSONResponse(c, 0, "success", body)
+}
 
-	opts := options.Update().SetUpsert(true)
+// 删除一个分析数据
+// /sitesAnalysis/manage/config/:id
+func DeleteOneSiteAnalysis(c *fiber.Ctx) error {
+	id := c.Params("id")
 
-	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	objID, err := primitive.ObjectIDFromHex(id)
 	if err != nil {
-		return err
+		return JSONResponseNoData(c, 400, "Invalid request para :id")
 	}
 
-	taskqueue.EnqueueSiteAnalysisTask(taskqueue.TaskSiteAnalysis{
-		Id:         writeConfig.Id,
-		Sn:         writeConfig.Sn,
-		TimePeriod: writeConfig.TimePeriod,
-		SiteAnalysisOptions: SiteAnalysisOptions{
-			EnableTimeline: writeConfig.EnableTimeline,
-		},
-	})
-
-	return nil
+	err = services.DeleteSiteAnalysisConfig(objID)
+	if err != nil {
+		return JSONResponseNoData(c, 500, "Database delete failed. (MongoDb)")
+	}
+	return JSONResponseNoData(c, 0, "success")
 }
 
-// 删除一条站点分析配置
-func DeleteSiteAnalysisConfig(id primitive.ObjectID) error {
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
+// 获取指定ID的分析数据
+// /sitesAnalysis/data/:id
+func GetOneSiteAnalysisData(c *fiber.Ctx) error {
+	id := c.Params("id")
 
-	filter := bson.M{"_id": id} // 删除条件
-	collection := database.GetCollection("SiteMonitorConfig")
-	_, err := collection.DeleteOne(ctx, filter)
+	objID, err := primitive.ObjectIDFromHex(id)
 	if err != nil {
-		return err
+		return JSONResponseNoData(c, 400, "Invalid request para :id")
 	}
 
-	// TODO 需删除相关数据
-
-	return nil
+	data, err := services.GetSiteAnalysisData(objID)
+	if err != nil {
+		return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
+	}
+	return JSONResponse(c, 0, "success", data)
 }

+ 139 - 407
service/controllers/sitesMonitorControllers.go

@@ -1,461 +1,193 @@
 package controllers
 
 import (
-	"context"
-	"encoding/json"
-	"errors"
-	"fwh-toolbox-server/database"
-	"fwh-toolbox-server/ext/api/fwh_pro"
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/dto"
+	"fwh-toolbox-server/services"
 	"time"
 
-	"go.mongodb.org/mongo-driver/bson"
-	"go.mongodb.org/mongo-driver/mongo"
-	"go.mongodb.org/mongo-driver/mongo/options"
+	"github.com/gofiber/fiber/v2"
+	"go.uber.org/zap"
 )
 
-type StateConfig struct {
-	Enable bool `bson:"enable" json:"enable"`
-}
-
-type SocConfig struct {
-	Enable bool `bson:"enable" json:"enable"`
-}
-
-type APowerConfig struct {
-	Enable bool `bson:"enable" json:"enable"`
-}
-
-type AlarmConfig struct {
-	Enable bool `bson:"enable" json:"enable"`
-}
-
-type VersionConfig struct {
-	Enable bool `bson:"enable" json:"enable"`
-}
-
-type TariffsConfig struct {
-	Enable bool `bson:"enable" json:"enable"`
-}
-
-type OthersConfig struct {
-	Enable bool `bson:"enable" json:"enable"`
-}
-
-type MonitorPageConfig struct {
-	State   StateConfig   `bson:"state" json:"state"`
-	Soc     SocConfig     `bson:"soc" json:"soc"`
-	APower  APowerConfig  `bson:"aPower" json:"aPower"`
-	Alarm   AlarmConfig   `bson:"alarm" json:"alarm"`
-	Version VersionConfig `bson:"version" json:"version"`
-	Tariffs TariffsConfig `bson:"tariffs" json:"tariffs"`
-	Others  OthersConfig  `bson:"others" json:"others"`
-}
-
-type SitesMonitorConfig struct {
-	Name        string             `bson:"name" json:"name"`
-	Description string             `bson:"description" json:"description"`
-	SiteList    []string           `bson:"siteList" json:"siteList"`
-	PageConfig  *MonitorPageConfig `bson:"pageConfig,omitempty" json:"pageConfig,omitempty"`
-}
-
-type SiteBaseInfoPara struct {
-	SiteList []string `json:"siteList"`
-	PageSize uint     `json:"pageSize"`
-	PageNum  uint     `json:"pageNum"`
-}
-
-type SiteBaseInfo struct {
-	Id               string  `json:"id"`
-	Name             *string `json:"name"`
-	FhpSn            string  `json:"fhpSn"`
-	ActiveStatus     int     `json:"activeStatus"`
-	ActiveTime       string  `json:"activeTime"`
-	Country          string  `json:"country"`
-	Province         string  `json:"province"`
-	City             string  `json:"city"`
-	OnOffStatus      int     `json:"onOffStatus"`
-	RealSysHdVersion string  `json:"realSysHdVersion"`
-	RunStatus        int     `json:"runStatus"`
-	Version          string  `json:"version"`
-}
-
-type SiteBaseInfoResponse struct {
-	Code    int            `json:"code"`
-	Message string         `json:"message"`
-	Result  []SiteBaseInfo `json:"result"`
-	Success bool           `json:"success"`
-	Total   uint           `json:"total"`
-}
-
-type SiteInfoPara struct {
-	SN       *string `json:"sn"`
-	PageSize uint    `json:"pageSize"`
-	PageNum  uint    `json:"pageNum"`
-}
-
-type SiteInfo struct {
-	Id               string  `json:"id"`
-	Name             *string `json:"name"`
-	FhpSn            string  `json:"fhpSn"`            // aPower SN 多设备用,分割
-	ActiveStatus     int     `json:"activeStatus"`     // 激活状态 0-未激活 1-激活
-	ActiveTime       string  `json:"activeTime"`       // 激活时间(北京时间)
-	LocalActiveTime  string  `json:"localActiveTime"`  // 激活时间(站点当地时间)
-	Country          string  `json:"country"`          // 国家
-	Province         string  `json:"province"`         // 州/省
-	City             string  `json:"city"`             // 城市
-	RealSysHdVersion string  `json:"realSysHdVersion"` // 系统版本信息
-	Version          string  `json:"version"`          // 设备固件版本
-	InstallerName    string  `json:"installerName"`    // 安装商名称
-	Longitude        string  `json:"longitude"`        // 经度
-	Latitude         string  `json:"latitude"`         // 纬度
-	ICCID            string  `json:"iccId"`
-	IMEI             string  `json:"imei"`
-	FirmUpgradeTime  string  `json:"firmUpgradeTime"` // 固件最后更新时间 YYYY-MM-DD HH:mm:SS
-	CreateTime       string  `json:"createTime"`      // 设备首次登录时间 YYYY-MM-DD HH:mm:SS
-	TimeZone         float32 `json:"timeZone"`        // 时区数值
-	ZoneInfo         string  `json:"zoneInfo"`        // 时区信息 America/Los_Angeles
-	GenEn            int     `json:"genEn"`           // 发电机是否使能 0-不使能 1-使能
-	Remark           *string `json:"remark"`          // 备注信息
-}
-
-type SiteInfoResponse struct {
-	Code    int        `json:"code"`
-	Message string     `json:"message"`
-	Result  []SiteInfo `json:"result"`
-	Success bool       `json:"success"`
-	Total   uint       `json:"total"`
-}
-
-type WorkModeInfo struct {
-	Id                   uint     `json:"id"`
-	OldIndex             uint     `json:"oldIndex"`
-	Name                 string   `json:"name"`
-	Soc                  float32  `json:"soc"`
-	MaxSoc               float32  `json:"maxSoc"`
-	MinSoc               float32  `json:"minSoc"`
-	DischargeDepthSoc    *float32 `json:"dischargeDepthSoc"`
-	WorkMode             uint     `json:"workMode"`
-	EnergyIncentivesType uint     `json:"energyIncentivesType"`
-	ElectricityType      uint     `json:"electricityType"`
-}
-
-type SiteModeInfo struct {
-	CurrendId uint           `json:"currendId"`
-	List      []WorkModeInfo `json:"list"`
-}
-
-type SiteModeInfoResponse struct {
-	Code    int          `json:"code"`
-	Msg     string       `json:"msg"`
-	Result  SiteModeInfo `json:"result"`
-	Success bool         `json:"success"`
-}
-
-type SitePowerFlow struct {
-	Index           uint      `json:"index"`           // 非当天为0,当天为实时数量
-	DeviceTimeArray []string  `json:"deviceTimeArray"` // 设备本地时间,格式 “2025-08-18 00:00:00”
-	PowerFhpArray   []float32 `json:"powerFhpArray"`
-	PowerGenArray   []float32 `json:"powerGenArray"`
-	PowerLoadArray  []float32 `json:"powerLoadArray"`
-	PowerSunArray   []float32 `json:"powerSunArray"`
-	PowerUtiArray   []float32 `json:"powerUtiArray"`
-	SocArray        []float32 `json:"socArray"`
-	RunStatusArray  []uint    `json:"runStatusArray"` // 运行状态,0:待机;1:充电;2:放电;3:故障
-	KwhTotalArray   []float32 `json:"kwhTotalArray"`  // 总可用电量,应该是根据SOC推算出来的。
-}
-
-type SitePowerFlowResponse struct {
-	Code   int           `json:"code"`
-	Msg    string        `json:"msg"`
-	Result SitePowerFlow `json:"result"`
-}
-
-type SiteAlarmList struct {
-	Id              uint   `json:"id"`
-	GatewayId       string `json:"gatewayId"`
-	AlarmEqSn       string `json:"alarmEqSn"`
-	AlarmCode       string `json:"alarmCode"`
-	CommonAlarmCode string `json:"commonAlarmCode"`
-	Level           int    `json:"level"`
-	Time            string `json:"time"`
-	LogName         string `json:"logName"`
-	CommonLogName   string `json:"commonLogName"`
-	LogNameEn       string `json:"logNameEn"`
-	CommonLogNameEn string `json:"commonLogNameEn"`
-}
-
-type SiteAlarmListResponse struct {
-	Code    int             `json:"code"`
-	Message string          `json:"message"`
-	Result  []SiteAlarmList `json:"result"`
-	Success bool            `json:"success"`
-	Total   uint            `json:"total"`
-}
-
-// 从数据库获取站点监控配置列表
-func GetSiteMonitorConfig() ([]SitesMonitorConfig, error) {
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	collection := database.GetCollection("SiteMonitorConfig")
-	cursor, err := collection.Find(ctx, bson.M{}) // 空条件,查全部
+// 获取站点监控管理名称表
+// /sitesMonitor/manage/nameList
+func GetSitesMonitorNameList(c *fiber.Ctx) error {
+	names, err := services.GetSiteMonitorNameList()
 	if err != nil {
-		return nil, err
-	}
-	defer cursor.Close(ctx)
-
-	var configList []SitesMonitorConfig
-	for cursor.Next(ctx) {
-		var cfg SitesMonitorConfig
-		if err := cursor.Decode(&cfg); err != nil {
-			return nil, err
-		}
-		configList = append(configList, cfg)
+		zap.L().Error("查询站点监控管理名称表错误。", zap.String("err", err.Error()))
+		return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
 	}
-
-	return configList, nil
+	return JSONResponse(c, 0, "success", names)
 }
 
-// 从数据库获取监控名列表
-func GetSiteMonitorNameList() ([]string, error) {
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	collection := database.GetCollection("SiteMonitorConfig")
-	cursor, err := collection.Find(
-		ctx,
-		bson.M{}, // 空条件,查全部
-		options.Find().SetProjection(bson.M{
-			"name": 1, // 只要 name 字段
-			"_id":  0, // 不要 _id 字段(默认会返回)
-		}),
-	)
+// 获取单个监控配置
+// /sitesMonitor/manage/config/:name
+func GetSitesMonitorConfig(c *fiber.Ctx) error {
+	name := c.Params("name")
+	configList, err := services.GetOneSiteMonitorConfig(name)
 	if err != nil {
-		return nil, err
-	}
-	defer cursor.Close(ctx)
-
-	var nameList []string
-	for cursor.Next(ctx) {
-		var result struct {
-			Name string `bson:"name"`
-		}
-		if err := cursor.Decode(&result); err != nil {
-			return nil, err
-		}
-		nameList = append(nameList, result.Name)
-	}
-
-	return nameList, nil
-}
-
-// 从数据库获取一条站点监控配置
-func GetOneSiteMonitorConfig(name string) ([]SitesMonitorConfig, error) {
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	collection := database.GetCollection("SiteMonitorConfig")
-	var cfg SitesMonitorConfig
-	err := collection.FindOne(ctx, bson.M{"name": name}).Decode(&cfg)
-	if err == mongo.ErrNoDocuments {
-		return []SitesMonitorConfig{}, nil
-	} else if err != nil {
-		return nil, err
+		zap.L().Error("查询单个监控配置错误。", zap.String("err", err.Error()))
+		return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
+	} else if len(configList) != 1 {
+		return JSONResponseNoData(c, 204, "User not found")
+	}
+	return JSONResponse(c, 0, "success", configList[0])
+}
+
+// 更新一个监控配置
+// /sitesMonitor/manage/config/:name
+func UpdateSitesMonitorConfig(c *fiber.Ctx) error {
+	name := c.Params("name")
+	var body domain.SitesMonitorConfig
+	if err := c.BodyParser(&body); err != nil {
+		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+			"code":    400,
+			"message": "Invalid request body",
+		})
+	}
+	err := services.UpdateSiteMonitorConfig(name, body)
+	if err != nil {
+		zap.L().Error("更新一个监控配置错误。", zap.String("err", err.Error()))
+		return JSONResponseNoData(c, 500, "Database write failed. (MongoDb)")
 	}
-
-	var configList []SitesMonitorConfig
-	configList = append(configList, cfg)
-
-	return configList, nil
+	return JSONResponse(c, 0, "success", body)
 }
 
-// 插入一条站点监控配置到数据库
-func InsertSiteMonitorConfig(config SitesMonitorConfig) error {
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	collection := database.GetCollection("SiteMonitorConfig")
-
-	// _, err := collection.InsertOne(ctx, config)
-	filter := bson.M{"name": config.Name} // 按用户名匹配
-	update := bson.M{
-		"$set": config, // 如果存在就更新所有字段
+// 更新一个监控页面配置
+// /sitesMonitor/manage/pageConfig/:name
+func UpdateSitesMonitorPageConfig(c *fiber.Ctx) error {
+	name := c.Params("name")
+	var body domain.MonitorPageConfig
+	if err := c.BodyParser(&body); err != nil {
+		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+			"code":    400,
+			"message": "Invalid request body",
+		})
 	}
-
-	opts := options.Update().SetUpsert(true)
-
-	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	err := services.UpdateSiteMonitorPageConfig(name, body)
 	if err != nil {
-		return err
+		zap.L().Error("更新一个监控页面配置错误。", zap.String("err", err.Error()))
+		return JSONResponseNoData(c, 500, "Database write failed. (MongoDb)")
 	}
-
-	return nil
+	return JSONResponse(c, 0, "success", body)
 }
 
-// 更新一条站点监控配置到数据库
-func UpdateSiteMonitorConfig(name string, config SitesMonitorConfig) error {
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	collection := database.GetCollection("SiteMonitorConfig")
-
-	// _, err := collection.InsertOne(ctx, config)
-	filter := bson.M{"name": name} // 按用户名匹配
-	update := bson.M{
-		"$set": config, // 如果存在就更新所有字段
-	}
-
-	opts := options.Update().SetUpsert(true)
-
-	_, err := collection.UpdateOne(ctx, filter, update, opts)
+// 删除一个监控
+// /sitesMonitor/manage/config/:name
+func DeleteSitesMonitorConfig(c *fiber.Ctx) error {
+	name := c.Params("name")
+	err := services.DeleteSiteMonitorConfig(name)
 	if err != nil {
-		return err
+		zap.L().Error("删除一个监控错误。", zap.String("err", err.Error()))
+		return JSONResponseNoData(c, 500, "Database delete failed. (MongoDb)")
 	}
-
-	return nil
+	return JSONResponseNoData(c, 0, "success")
 }
 
-// 删除一条站点监控配置
-func DeleteSiteMonitorConfig(name string) error {
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	filter := bson.M{"name": name} // 删除条件
-	collection := database.GetCollection("SiteMonitorConfig")
-	_, err := collection.DeleteOne(ctx, filter)
+// 获取所有监控配置
+// /sitesMonitor/manage/config
+func GetSitesMonitorAllConfig(c *fiber.Ctx) error {
+	configList, err := services.GetSiteMonitorConfig()
 	if err != nil {
-		return err
+		zap.L().Error("查询所有监控配置错误。", zap.String("err", err.Error()))
+		return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
 	}
-
-	return nil
+	return JSONResponse(c, 0, "success", configList)
 }
 
-// 更新一条站点监控页面配置到数据库
-func UpdateSiteMonitorPageConfig(name string, config MonitorPageConfig) error {
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	collection := database.GetCollection("SiteMonitorConfig")
-
-	// _, err := collection.InsertOne(ctx, config)
-	filter := bson.M{"name": name} // 按用户名匹配
-	update := bson.M{
-		"$set": bson.M{
-			"pageConfig": config, // 更新页面配置字段
-		},
+// 创建一个监控配置
+// /sitesMonitor/manage/config
+func CreateSitesMonitorConfig(c *fiber.Ctx) error {
+	var body domain.SitesMonitorConfig
+	if err := c.BodyParser(&body); err != nil {
+		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+			"code":    400,
+			"message": "Invalid request body",
+		})
 	}
-
-	opts := options.Update().SetUpsert(true)
-
-	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	err := services.InsertSiteMonitorConfig(body)
 	if err != nil {
-		return err
+		zap.L().Error("创建一个监控配置错误。", zap.String("err", err.Error()))
+		return JSONResponseNoData(c, 500, "Database write failed. (MongoDb)")
 	}
-
-	return nil
+	return JSONResponse(c, 0, "success", body)
 }
 
-// 从fwh_pro获取站点基本信息表
-func QuerySiteBaseInfoList(para SiteBaseInfoPara) (SiteBaseInfoResponse, error) {
-	resByte, err := fwh_pro.QuerySiteBaseInfoList(para.SiteList, para.PageSize, para.PageNum)
-	if err != nil {
-		return SiteBaseInfoResponse{}, err
-	}
-	var siteInfoRes SiteBaseInfoResponse
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &siteInfoRes)
-	if jsonErr != nil {
-		return SiteBaseInfoResponse{}, jsonErr
+// 批量获取站点信息表
+// /sitesMonitor/site/infoList
+func GetSiteInfoList(c *fiber.Ctx) error {
+	var body dto.SiteBaseInfoPara
+	if err := c.BodyParser(&body); err != nil {
+		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+			"code":    400,
+			"message": "Invalid request body",
+		})
 	}
-
-	if siteInfoRes.Code == 401 {
-		// TODO 尝试重新登录
-		return SiteBaseInfoResponse{}, errors.New("certification expired")
+	infoList, err := services.QuerySiteBaseInfoList(body)
+	if err != nil {
+		zap.L().Error("查询批量站点信息表错误。", zap.String("err", err.Error()))
+		return JSONResponseNoData(c, 500, "Database write failed. (MongoDb)")
 	}
-
-	return siteInfoRes, nil
+	return JSONResponse(c, 0, "success", infoList)
 }
 
-// 从fwh_pro获取站点基本信息表
-func QuerySiteInfoList(para SiteInfoPara) (SiteInfoResponse, error) {
-	resByte, err := fwh_pro.QuerySiteInfoList(para.SN, para.PageSize, para.PageNum)
+// 获取单个站点信息
+// /sitesMonitor/site/:site/info
+func GetOneSiteInfo(c *fiber.Ctx) error {
+	site := c.Params("site")
+	siteInfo, err := services.QuerySiteBaseInfoList(dto.SiteBaseInfoPara{
+		SiteList: []string{site},
+		PageSize: 10,
+		PageNum:  1,
+	})
 	if err != nil {
-		return SiteInfoResponse{}, err
-	}
-	var siteInfoRes SiteInfoResponse
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &siteInfoRes)
-	if jsonErr != nil {
-		return SiteInfoResponse{}, jsonErr
+		zap.L().Error("查询单个站点信息错误。", zap.String("err", err.Error()))
+		return JSONResponseNoData(c, 500, "fwh_pro query failed.")
 	}
-
-	if siteInfoRes.Code == 401 {
-		// TODO 尝试重新登录
-		return SiteInfoResponse{}, errors.New("certification expired")
-	}
-
-	return siteInfoRes, nil
+	return JSONResponse(c, 0, "success", siteInfo)
 }
 
-// 从fwh_pro获取站点工作模式信息
-func QuerySiteModeInfo(site string) (SiteModeInfoResponse, error) {
-	resByte, err := fwh_pro.QuerySiteModeInfo(site)
+// 获取单个站点工作模式信息
+// /sitesMonitor/site/:site/modeInfo
+func GetOneSiteModeInfo(c *fiber.Ctx) error {
+	site := c.Params("site")
+	siteModeInfo, err := services.QuerySiteModeInfo(site)
 	if err != nil {
-		return SiteModeInfoResponse{}, err
+		zap.L().Error("查询单个站点工作模式信息错误。", zap.String("err", err.Error()))
+		return JSONResponseNoData(c, 500, "fwh_pro query failed.")
 	}
-	var siteModeInfoRes SiteModeInfoResponse
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &siteModeInfoRes)
-	if jsonErr != nil {
-		return SiteModeInfoResponse{}, jsonErr
-	}
-
-	if siteModeInfoRes.Code == 401 {
-		// TODO 尝试重新登录
-		return SiteModeInfoResponse{}, errors.New("certification expired")
-	}
-
-	return siteModeInfoRes, nil
+	return JSONResponse(c, 0, "success", siteModeInfo)
 }
 
-// 从fwh_pro获取站点功率流
-func QuerySitePowerFlow(site string, day time.Time) (SitePowerFlowResponse, error) {
-	resByte, err := fwh_pro.QuerySitePowerFlow(site, day)
+// 获取单个站点功率流
+// /sitesMonitor/site/:site/powerFlow/:day
+func GetOneSitePowerFlow(c *fiber.Ctx) error {
+	site := c.Params("site")
+	day := c.Params("day")
+	dayTime, err := time.Parse(time.DateOnly, day)
 	if err != nil {
-		return SitePowerFlowResponse{}, err
-	}
-	var sitePowerFlowRes SitePowerFlowResponse
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &sitePowerFlowRes)
-	if jsonErr != nil {
-		return SitePowerFlowResponse{}, jsonErr
+		zap.L().Error("查询单个站点功率流参数错误。(day)", zap.String("err", err.Error()))
+		return JSONResponseNoData(c, 500, "day parament error.")
 	}
 
-	if sitePowerFlowRes.Code == 401 {
-		// TODO 尝试重新登录
-		return SitePowerFlowResponse{}, errors.New("certification expired")
+	sitePowerFlow, err := services.QuerySitePowerFlow(site, dayTime)
+	if err != nil {
+		zap.L().Error("查询单个站点功率流错误。", zap.String("err", err.Error()))
+		return JSONResponseNoData(c, 500, "fwh_pro query failed.")
 	}
-
-	return sitePowerFlowRes, nil
+	return JSONResponse(c, 0, "success", sitePowerFlow)
 }
 
-// 从fwh_pro获取站点告警列表
-func QuerySiteAlarmList(site string) (SiteAlarmListResponse, error) {
-	resByte, err := fwh_pro.QuerySiteAlarmList(site)
-	if err != nil {
-		return SiteAlarmListResponse{}, err
-	}
-	var siteAlarmListRes SiteAlarmListResponse
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &siteAlarmListRes)
-	if jsonErr != nil {
-		return SiteAlarmListResponse{}, jsonErr
-	}
+// 获取单个站点告警列表
+// /sitesMonitor/site/:site/alarmList
+func GetOneSiteAlarmList(c *fiber.Ctx) error {
+	site := c.Params("site")
 
-	if siteAlarmListRes.Code == 401 {
-		// TODO 尝试重新登录
-		return SiteAlarmListResponse{}, errors.New("certification expired")
+	siteAlarmList, err := services.QuerySiteAlarmList(site)
+	if err != nil {
+		zap.L().Error("查询单个站点告警列表错误。", zap.String("err", err.Error()))
+		return JSONResponseNoData(c, 500, "fwh_pro query failed.")
 	}
-
-	return siteAlarmListRes, nil
+	return JSONResponse(c, 0, "success", siteAlarmList)
 }

+ 33 - 33
service/controllers/xlogControllers.go

@@ -2,45 +2,45 @@ package controllers
 
 import (
 	"fwh-toolbox-server/logger"
-	"fwh-toolbox-server/networkEvents"
-)
-
-type XlogManageInfo = networkEvents.XlogSiteInfo
-type XlogManagePage = ResPage[XlogManageInfo]
+	"fwh-toolbox-server/services"
+	"strconv"
 
-func GetXlogManageList(page, size uint) XlogManagePage {
-	infoList := networkEvents.GetConnectList()
-	infoListLen := uint(len(infoList))
-	resPage := XlogManagePage{
-		Data:     []XlogManageInfo{},
-		PageNum:  page,
-		PageSize: size,
-		Total:    infoListLen,
-	}
+	"github.com/gofiber/fiber/v2"
+)
 
-	if infoListLen == 0 {
-		return resPage
+// 获取站点分析管理名称表
+// /xlog/manage/siteList
+func GetXlogManageSiteList(c *fiber.Ctx) error {
+	pageInt, _ := strconv.Atoi(c.Query("pageNum", "1"))
+	sizeInt, _ := strconv.Atoi(c.Query("pageSize", "20"))
+
+	if pageInt <= 0 || sizeInt <= 0 {
+		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+			"code":    400,
+			"message": "Invalid request para",
+		})
 	}
 
-	if page < 1 {
-		return resPage
-	}
+	listPage := services.GetXlogManageList(uint(pageInt), uint(sizeInt))
+	return JSONResponse(c, 0, "success", listPage)
+}
 
-	startIndex := (page - 1) * size
-	endIndex := page * size
-	if endIndex > infoListLen {
-		endIndex = infoListLen
+// 发送命令
+// /xlog/terminal/:uuid
+func PostXlogTerminalSend(c *fiber.Ctx) error {
+	uuid := c.Params("uuid")
+	logger.Std.Println(uuid)
+	var body string
+	if err := c.BodyParser(&body); err != nil {
+		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+			"code":    400,
+			"message": "Invalid request body",
+		})
 	}
 
-	if endIndex <= startIndex {
-		return resPage
+	data, err := services.SendXlogCmd(uuid, body)
+	if err != nil {
+		return JSONResponseNoData(c, 500, err.Error())
 	}
-
-	resPage.Data = infoList[startIndex:endIndex]
-	return resPage
-}
-
-func SendXlogCmd(uuid, cmd string) (string, error) {
-	logger.Std.Printf("Send xlog cmd: %s - %s\n", uuid, cmd)
-	return networkEvents.SendXlogCmdDate(uuid, []byte(cmd))
+	return JSONResponse(c, 0, "success", data)
 }

+ 6 - 0
service/domain/alarmSnapshotModelDomain.go

@@ -0,0 +1,6 @@
+package domain
+
+import "fwh-toolbox-server/models/model_pgsql/model_alarm_snapshot"
+
+type SiteAlarmSnapshotFileRecord = model_alarm_snapshot.SiteAlarmSnapshotFileRecord
+type SiteAlarmSnapshotData = model_alarm_snapshot.SiteAlarmSnapshotData

+ 42 - 0
service/domain/fwhProDomain.go

@@ -0,0 +1,42 @@
+package domain
+
+type FwhPageResponse[T any] struct {
+	Code    int    `json:"code"`
+	Message string `json:"message"`
+	Result  []T    `json:"result"`
+	Success bool   `json:"success"`
+	Total   uint64 `json:"total"`
+}
+
+type AlarmSnapshotInfo struct {
+	Id           int64  `json:"id"`
+	CodeType     int32  `json:"codeType"` // 作用未知
+	FileName     string `json:"fileName"`
+	FileUrl      string `json:"fileUrl"`
+	LinkId       string `json:"linkId"`  // site SN
+	LogTime      string `json:"logTime"` // format YYYY-MM-DD
+	SerialNumber string `json:"serialNumber"`
+	StartTime    string `json:"startTime"`    // format YYYY-MM-DD HH:mm:SS
+	EndTime      string `json:"endTime"`      // format YYYY-MM-DD HH:mm:SS
+	Status       int    `json:"status"`       // 1-完成
+	CompleteTime string `json:"completeTime"` // format YYYY-MM-DD HH:mm:SS
+}
+
+type EventAlarmItem struct {
+	ID            *string `json:"id"`        // UUID or null
+	CmdType       string  `json:"cmdType"`   // 命令说明
+	CmdName       string  `json:"cmdName"`   // 命令类型 exp:201...
+	GatewayId     string  `json:"gatewayId"` // SN
+	GatewayType   *string `json:"gatewayType"`
+	GatewayStatus *string `json:"gatewayStatus"`
+	ActiveStatus  *string `json:"activeStatus"` // 激活状态
+	ActiveTime    *string `json:"activeTime"`   // 激活时间
+	AlarmCode     string  `json:"alarmCode"`    // 告警码,事件则为0
+	LogName       string  `json:"logName"`
+	LogType       string  `json:"logType"`   // 日志类型 0-事件 1-告警
+	LogData       string  `json:"logData"`   // JSON数据
+	LogTime       string  `json:"logTime"`   // 设备时间(日志上传时间,告警未结束为开始时间,结束为结束时间)
+	Time          string  `json:"time"`      // UTC时间
+	StartTime     string  `json:"startTime"` // 事件或告警的开始时间
+	EndTime       *string `json:"endTime"`   // 告警结束时间,事件没有结束时间为null,告警枚举结束时间则未结束
+}

+ 359 - 0
service/domain/fwhProMqttDomain.go

@@ -0,0 +1,359 @@
+package domain
+
+// 定义枚举类型
+type MqttCmdType string
+
+const (
+	MQTT_LOG_0    MqttCmdType = "0"    // 设备连接中断
+	MQTT_LOG_101  MqttCmdType = "101"  // 登录上行命令
+	MQTT_LOG_102  MqttCmdType = "102"  // 登录应答下行命令
+	MQTT_LOG_103  MqttCmdType = "103"  // 设备绑定解绑上行命令
+	MQTT_LOG_104  MqttCmdType = "104"  // 设备绑定解绑应答下行命令
+	MQTT_LOG_105  MqttCmdType = "105"  // 设备心跳上行数据
+	MQTT_LOG_106  MqttCmdType = "106"  // 应答云平台心跳下行数据
+	MQTT_LOG_107  MqttCmdType = "107"  // 设备参数更新上行数据
+	MQTT_LOG_108  MqttCmdType = "108"  // 应答设备参数更新下行数据
+	MQTT_LOG_109  MqttCmdType = "109"  // 云平台更新设备参数下行数据
+	MQTT_LOG_110  MqttCmdType = "110"  // 应答云平台更新设备参数上行数据
+	MQTT_LOG_201  MqttCmdType = "201"  // 用户实时数据 (运行数据,默认数据间隔5min)
+	MQTT_LOG_202  MqttCmdType = "202"  // 厂商实时数据上传通知
+	MQTT_LOG_203  MqttCmdType = "203"  // 实时数据召唤
+	MQTT_LOG_204  MqttCmdType = "204"  // 实时数据召唤应答
+	MQTT_LOG_205  MqttCmdType = "205"  // 当前告警上传
+	MQTT_LOG_206  MqttCmdType = "206"  // 当前告警上传应答
+	MQTT_LOG_207  MqttCmdType = "207"  // 告警日志上传
+	MQTT_LOG_208  MqttCmdType = "208"  // 告警日志上传应答
+	MQTT_LOG_209  MqttCmdType = "209"  // 用户实时数据离线后补传命令
+	MQTT_LOG_210  MqttCmdType = "210"  // 用户实时数据离线后补传应答
+	MQTT_LOG_211  MqttCmdType = "211"  // 设备实时数据召唤下行数据
+	MQTT_LOG_212  MqttCmdType = "212"  // 设备实时数据召唤上行数据
+	MQTT_LOG_213  MqttCmdType = "213"  // 告警日志文件上传通知
+	MQTT_LOG_214  MqttCmdType = "214"  // 当前告警集合上行数据
+	MQTT_LOG_215  MqttCmdType = "215"  // 应答当前告警集合下行数据
+	MQTT_LOG_216  MqttCmdType = "216"  // 当前告警集合查询下行数据
+	MQTT_LOG_217  MqttCmdType = "217"  // 应答当前告警集合查询上行数据
+	MQTT_LOG_218  MqttCmdType = "218"  // 告警监控快照文件上传通知
+	MQTT_LOG_219  MqttCmdType = "219"  // 应答告警监控快照文件上传
+	MQTT_LOG_220  MqttCmdType = "220"  // 系统资源监控数据上行数据
+	MQTT_LOG_221  MqttCmdType = "221"  // 设备数据主动上传
+	MQTT_LOG_223  MqttCmdType = "223"  // 设备数据补传通知上行数据
+	MQTT_LOG_224  MqttCmdType = "224"  // 应答设备数据补传下行数据
+	MQTT_LOG_225  MqttCmdType = "225"  // 继电器切断次数上行数据
+	MQTT_LOG_227  MqttCmdType = "227"  // 设备实时上传数据
+	MQTT_LOG_229  MqttCmdType = "229"  // 实时数据补传上行
+	MQTT_LOG_230  MqttCmdType = "230"  // 实时数据补传下行
+	MQTT_LOG_235  MqttCmdType = "235"  // sunrun数据上报使能设置
+	MQTT_LOG_236  MqttCmdType = "236"  // sunrun数据上报使能设置应答
+	MQTT_LOG_231  MqttCmdType = "231"  // 电池数据主动上传
+	MQTT_LOG_233  MqttCmdType = "233"  // 实时数据补传通知上行数据
+	MQTT_LOG_234  MqttCmdType = "234"  // 应答实时数据补传下行数据
+	MQTT_LOG_301  MqttCmdType = "301"  // 参数设置
+	MQTT_LOG_302  MqttCmdType = "302"  // 参数设置应答
+	MQTT_LOG_303  MqttCmdType = "303"  // 参数查询
+	MQTT_LOG_304  MqttCmdType = "304"  // 参数查询应答
+	MQTT_LOG_305  MqttCmdType = "305"  // 预警模式
+	MQTT_LOG_306  MqttCmdType = "306"  // 预警模式应答
+	MQTT_LOG_307  MqttCmdType = "307"  // 设备端解除预警模式
+	MQTT_LOG_308  MqttCmdType = "308"  // 设备端解除预警模式应答
+	MQTT_LOG_309  MqttCmdType = "309"  // 设备事件推送
+	MQTT_LOG_310  MqttCmdType = "310"  // 设备事件推送应答
+	MQTT_LOG_311  MqttCmdType = "311"  // 设备控制指令设置查询
+	MQTT_LOG_312  MqttCmdType = "312"  // 设备控制指令设置查询应答
+	MQTT_LOG_313  MqttCmdType = "313"  // TOU参数配置查询指令
+	MQTT_LOG_314  MqttCmdType = "314"  // TOU参数配置查询指令应答
+	MQTT_LOG_315  MqttCmdType = "315"  // 系统设置下行数据
+	MQTT_LOG_316  MqttCmdType = "316"  // 系统设置上行数据
+	MQTT_LOG_317  MqttCmdType = "317"  // 全局参数配置指令
+	MQTT_LOG_318  MqttCmdType = "318"  // 全局参数配置指令应答
+	MQTT_LOG_319  MqttCmdType = "319"  // TOU策略配置查询下行数据
+	MQTT_LOG_320  MqttCmdType = "320"  // 应答TOU策略配置查询上行数据
+	MQTT_LOG_321  MqttCmdType = "321"  // Wi-Fi设置查询下行数据
+	MQTT_LOG_322  MqttCmdType = "322"  // 应答Wi-Fi设置查询上行数据
+	MQTT_LOG_323  MqttCmdType = "323"  // 发电机参数设置查询下行数据
+	MQTT_LOG_324  MqttCmdType = "324"  // 应答发电机参数设置查询上行数据
+	MQTT_LOG_325  MqttCmdType = "325"  // 光伏参数设置查询下行数据
+	MQTT_LOG_326  MqttCmdType = "326"  // 应答光伏参数设置查询上行数据
+	MQTT_LOG_327  MqttCmdType = "327"  // aPower灯开关控制下行数据
+	MQTT_LOG_328  MqttCmdType = "328"  // 应答aPower灯开关控制上行数据
+	MQTT_LOG_329  MqttCmdType = "329"  // 自动编址下行数据
+	MQTT_LOG_330  MqttCmdType = "330"  // 应答自动编址上行数据
+	MQTT_LOG_331  MqttCmdType = "331"  // 自动编址结果查询下行数据
+	MQTT_LOG_332  MqttCmdType = "332"  // 应答自动编址结果查询上行数据
+	MQTT_LOG_333  MqttCmdType = "333"  // 调试模式下行数据
+	MQTT_LOG_334  MqttCmdType = "334"  // 应答调试模式上行数据
+	MQTT_LOG_335  MqttCmdType = "335"  // Wi-Fi热点扫描下行数据
+	MQTT_LOG_336  MqttCmdType = "336"  // 应答Wi-Fi热点扫描上行数据
+	MQTT_LOG_337  MqttCmdType = "337"  // Wi-Fi热点账号查询设置下行数据
+	MQTT_LOG_338  MqttCmdType = "338"  // 应答Wi-Fi热点账号查询设置上行数据
+	MQTT_LOG_339  MqttCmdType = "339"  // 连接状态查询下行数据
+	MQTT_LOG_340  MqttCmdType = "340"  // 应答连接状态查询上行数据
+	MQTT_LOG_341  MqttCmdType = "341"  // 网终开关查询设置下行数据
+	MQTT_LOG_342  MqttCmdType = "342"  // 应答网终开关查询设置上行数据
+	MQTT_LOG_343  MqttCmdType = "343"  // 电池控制下行数据
+	MQTT_LOG_344  MqttCmdType = "344"  // 应答电池控制上行数据
+	MQTT_LOG_345  MqttCmdType = "345"  // 电网设置下行数据
+	MQTT_LOG_346  MqttCmdType = "346"  // 应答电网设置上行数据
+	MQTT_LOG_347  MqttCmdType = "347"  // 电网事件结果通知上行数据
+	MQTT_LOG_348  MqttCmdType = "348"  // 应答电网事件结果下行数据
+	MQTT_LOG_349  MqttCmdType = "349"  // 主动离网数据下行
+	MQTT_LOG_350  MqttCmdType = "350"  // 应答主动离网数据上行
+	MQTT_LOG_351  MqttCmdType = "351"  // 主动离网结果通知上行数据
+	MQTT_LOG_352  MqttCmdType = "352"  // 应答主动离网结果通知下行数据
+	MQTT_LOG_353  MqttCmdType = "353"  // 系统参数查询设置下行数据
+	MQTT_LOG_354  MqttCmdType = "354"  // 系统参数查询设置上行数据
+	MQTT_LOG_355  MqttCmdType = "355"  // 能量调度策略查询
+	MQTT_LOG_356  MqttCmdType = "356"  // 能量调度策略查询应答
+	MQTT_LOG_357  MqttCmdType = "357"  // 能量调度策略增加和删除设置
+	MQTT_LOG_358  MqttCmdType = "358"  // 能量调度策略增加和删除设置应答
+	MQTT_LOG_359  MqttCmdType = "359"  // 系统参数查询设置
+	MQTT_LOG_360  MqttCmdType = "360"  // 系统参数查询设置应答
+	MQTT_LOG_361  MqttCmdType = "361"  // 网络通道检测设置
+	MQTT_LOG_362  MqttCmdType = "362"  // 网络通道检测设置应答
+	MQTT_LOG_363  MqttCmdType = "363"  // 网络通道检测结果查询
+	MQTT_LOG_364  MqttCmdType = "364"  // 网络通道检测结果查询应答
+	MQTT_LOG_365  MqttCmdType = "365"  // Swell注册/注销设置
+	MQTT_LOG_366  MqttCmdType = "366"  // Swell注册/注销设置应答
+	MQTT_LOG_367  MqttCmdType = "367"  // Swell应用程序状态查询
+	MQTT_LOG_368  MqttCmdType = "368"  // Swell应用程序状态查询应答
+	MQTT_LOG_369  MqttCmdType = "369"  // Modbus参数权限的查询设置
+	MQTT_LOG_370  MqttCmdType = "370"  // modbus参数权限的查询设置应答
+	MQTT_LOG_371  MqttCmdType = "371"  // 同步点表配置请求
+	MQTT_LOG_372  MqttCmdType = "372"  // 同步点表配置请求应答
+	MQTT_LOG_373  MqttCmdType = "373"  // 点表配置表增删查命令
+	MQTT_LOG_374  MqttCmdType = "374"  // 点表配置表增删查命令应答
+	MQTT_LOG_375  MqttCmdType = "375"  // 点表响应设置命令
+	MQTT_LOG_376  MqttCmdType = "376"  // 点表响应设置应答
+	MQTT_LOG_377  MqttCmdType = "377"  // 故障录波实时数据获取
+	MQTT_LOG_378  MqttCmdType = "378"  // 故障录波信息应答
+	MQTT_LOG_379  MqttCmdType = "379"  // 故障录波信息上传
+	MQTT_LOG_380  MqttCmdType = "380"  // 故障录波信息上传应答
+	MQTT_LOG_381  MqttCmdType = "381"  // 设备休眠配置命令
+	MQTT_LOG_382  MqttCmdType = "382"  // 设备休眠命令应答
+	MQTT_LOG_383  MqttCmdType = "383"  // 休眠/退出休眠结果上报
+	MQTT_LOG_384  MqttCmdType = "384"  // 休眠/退出休眠结果上报应答
+	MQTT_LOG_385  MqttCmdType = "385"  // 设备退出休眠/取消充电休眠配置命令
+	MQTT_LOG_386  MqttCmdType = "386"  // 设备退出休眠/取消充电休眠配置命令应答
+	MQTT_LOG_387  MqttCmdType = "387"  // 智能负载开关查询设置
+	MQTT_LOG_388  MqttCmdType = "388"  // 智能负载开关查询设置应答
+	MQTT_LOG_389  MqttCmdType = "389"  // 智能负载开关实时数据查询命令
+	MQTT_LOG_390  MqttCmdType = "390"  // 智能负载开关实时数据查询应答
+	MQTT_LOG_391  MqttCmdType = "391"  // aPbox2.0查询设置
+	MQTT_LOG_392  MqttCmdType = "392"  // aPbox2.0查询设置应答
+	MQTT_LOG_3100 MqttCmdType = "3100" // 完成MSA安装设置命令
+	MQTT_LOG_3101 MqttCmdType = "3101" // 完成MSA安装设置应答
+	MQTT_LOG_3104 MqttCmdType = "3104" // CT转向设置查询
+	MQTT_LOG_3105 MqttCmdType = "3105" // CT转向设置查询应答
+	MQTT_LOG_3106 MqttCmdType = "3106" // 设备编址完成
+	MQTT_LOG_3107 MqttCmdType = "3107" // 设备编址完成应答
+	MQTT_LOG_393  MqttCmdType = "393"  // CT检测设置查询
+	MQTT_LOG_394  MqttCmdType = "394"  // CT检测设置查询应答
+	MQTT_LOG_395  MqttCmdType = "395"  // CT检测数据/结果上传
+	MQTT_LOG_396  MqttCmdType = "396"  // CT检测数据/结果应答
+	MQTT_LOG_401  MqttCmdType = "401"  // 日功率曲线电量召唤
+	MQTT_LOG_402  MqttCmdType = "402"  // 日功率曲线电量召唤应答
+	MQTT_LOG_403  MqttCmdType = "403"  // 电量历史报表召唤(月,年)
+	MQTT_LOG_404  MqttCmdType = "404"  // 电量历史报表召唤应答
+	MQTT_LOG_405  MqttCmdType = "405"  // 备电日志上传
+	MQTT_LOG_406  MqttCmdType = "406"  // 备电日志上传应答
+	MQTT_LOG_407  MqttCmdType = "407"  // 历史日功率曲线电量报表上传通知
+	MQTT_LOG_408  MqttCmdType = "408"  // 历史日功率曲线电量报表上传应答
+	MQTT_LOG_409  MqttCmdType = "409"  // 电量历史报表上传通知(月,年)
+	MQTT_LOG_410  MqttCmdType = "410"  // 电量历史报表上传应答
+	MQTT_LOG_411  MqttCmdType = "411"  // 日电量统计上传上行数据
+	MQTT_LOG_412  MqttCmdType = "412"  // 应答日电量统计上传下行数据
+	MQTT_LOG_413  MqttCmdType = "413"  // 设备模式变化时上传
+	MQTT_LOG_414  MqttCmdType = "414"  // 应答设备模式变化下行数据
+	MQTT_LOG_501  MqttCmdType = "501"  // IBG设备安装信息主动上传
+	MQTT_LOG_502  MqttCmdType = "502"  // IBG设备安装信息主动上传应答
+	MQTT_LOG_503  MqttCmdType = "503"  // IBG设备安装信息召唤
+	MQTT_LOG_504  MqttCmdType = "504"  // IBG设备安装信息上报
+	MQTT_LOG_505  MqttCmdType = "505"  // 设备升级指令
+	MQTT_LOG_506  MqttCmdType = "506"  // 设备升级应答
+	MQTT_LOG_507  MqttCmdType = "507"  // 设备升级结果
+	MQTT_LOG_508  MqttCmdType = "508"  // 设备升级结果应答
+	MQTT_LOG_509  MqttCmdType = "509"  // 设备安装信息主动上传上行数据
+	MQTT_LOG_510  MqttCmdType = "510"  // 设备安装信息主动上传下行数据
+	MQTT_LOG_511  MqttCmdType = "511"  // 设备升级文件下载信息下发
+	MQTT_LOG_512  MqttCmdType = "512"  // 设备升级文件下载信息响应上行数据
+	MQTT_LOG_513  MqttCmdType = "513"  // 设备升级文件下载完成上行数据
+	MQTT_LOG_514  MqttCmdType = "514"  // 应答设备升级文件下载完成下行数据
+	MQTT_LOG_515  MqttCmdType = "515"  // 特殊升级启动信息上报上行数据
+	MQTT_LOG_516  MqttCmdType = "516"  // 特殊升级启动信息上报应答下行数据
+	MQTT_LOG_517  MqttCmdType = "517"  // 设备编址后参数更新上行数据
+	MQTT_LOG_518  MqttCmdType = "518"  // 应答设备编址后参数下行数据
+	MQTT_LOG_523  MqttCmdType = "523"  // 设备升级进度上报上行数据
+	MQTT_LOG_524  MqttCmdType = "524"  // aHub升级记录结果上行数据
+	MQTT_LOG_525  MqttCmdType = "525"  // aHub升级记录结果应答下行数据
+	MQTT_LOG_526  MqttCmdType = "526"  // aHub连接状态检测上行数据
+	MQTT_LOG_601  MqttCmdType = "601"  // 调试参数下发
+	MQTT_LOG_602  MqttCmdType = "602"  // 调试参数下发结果
+	MQTT_LOG_603  MqttCmdType = "603"  // 设备日志召唤
+	MQTT_LOG_604  MqttCmdType = "604"  // 设备日志召唤应答
+	MQTT_LOG_605  MqttCmdType = "605"  // 设备日志上传结果
+	MQTT_LOG_606  MqttCmdType = "606"  // 设备日志上传结果应答
+	MQTT_LOG_607  MqttCmdType = "607"  // 故障录波召唤下行数据
+	MQTT_LOG_608  MqttCmdType = "608"  // 故障录波召唤应答上行数据
+	MQTT_LOG_609  MqttCmdType = "609"  // 故障录波文件上传结果
+	MQTT_LOG_610  MqttCmdType = "610"  // 故障录波文件上传结果应答
+	MQTT_LOG_611  MqttCmdType = "611"  // 模块日志召唤
+	MQTT_LOG_612  MqttCmdType = "612"  // 模块日志召唤应答
+	MQTT_LOG_613  MqttCmdType = "613"  // 模块日志文件上传结果
+	MQTT_LOG_614  MqttCmdType = "614"  // 模块日志文件上传结果应答
+	MQTT_LOG_701  MqttCmdType = "701"  // 设备数据主动上传通知上行数据
+	MQTT_LOG_702  MqttCmdType = "702"  // 应答设备数据补传下行数据
+	MQTT_LOG_703  MqttCmdType = "703"  // 设备数据主动上传通知上行数据
+	MQTT_LOG_704  MqttCmdType = "704"  // 应答设备数据补传下行数据
+	MQTT_LOG_705  MqttCmdType = "705"  // 设备数据主动上传通知下行数据
+	MQTT_LOG_706  MqttCmdType = "706"  // 应答设备数据补传下行数据
+	MQTT_LOG_801  MqttCmdType = "801"  // sunnova数据周期上传
+	MQTT_LOG_802  MqttCmdType = "802"  // sunnova数据主动上传使能按钮
+	MQTT_LOG_803  MqttCmdType = "803"  // 应答sunnova数据设置查询
+)
+
+type MqttLogItem struct {
+	CmdType       string  `json:"cmdType"`
+	CmdName       string  `json:"cmdName"`
+	GatewayId     string  `json:"gatewayId"` // SN
+	GatewayType   *string `json:"gatewayType"`
+	GatewayStatus *string `json:"gatewayStatus"`
+	ActiveStatus  *string `json:"activeStatus"` // 激活状态
+	LogData       string  `json:"logData"`      // JSON数据
+	LogTime       string  `json:"logTime"`      // 设备时间
+	Time          string  `json:"time"`         // UTC时间
+}
+
+/********* LogData struct 定义 *********/
+
+type MqttLogContent[T any] struct {
+	CmdType   string `json:"cmdType"` // 日志类型 MqttCmdType
+	EquipNo   string `json:"equipNo"` // SN
+	Type      int    `json:"type"`    // 设备类型 0-设备网关IBG
+	TimeStamp int64  `json:"timeStamp"`
+	DataArea  T      `json:"dataArea"` // 业务数据
+}
+
+// MQTT_LOG_0
+type MqttLogType_0 struct {
+	Lwt string `json:"lwt"` // 遗愿 "Lost Connect!"
+}
+
+// MQTT_LOG_101
+type MqttLogType_101 struct {
+	IBG_SN       string `json:"IBG_SN"`     // SN
+	DeviceType   int    `json:"deviceType"` // 设备类型 0-设备网关IBG
+	TimeStamp    int64  `json:"timeStamp"`
+	ProtocolVer  string `json:"protocolVer"`  // MQTT协议版本
+	SysHdVersion int    `json:"SysHdVersion"` // 系统硬件版本 100 = 1.0
+}
+
+// MQTT_LOG_201
+type MqttLogType_201 struct {
+	ReportType        int        `json:"report_type"`               // 上报类型 0-周期 1-召唤 2-事件变化
+	Mode              int        `json:"mode"`                      // EMS当前工作模式 0-初始状态 2-自发自用 3-TOU(平衡) 4-TOU(经济) 5-并网状态下的仅备电状态 6-风暴等待模式 7-离网,电池备电中 8-离网,发电机备电中 9-VPP调度 10-紧急停止(EPO) 11-故障进入安全模式 13-调试 16-升级状态 25-apower手动关机 26-主动离网(Go Off-Grid  NSC+ULG支持)27-v2L模式(ULG支持) 28-newhome模式 29-msa安装未完成 更多模式信息请查阅文档
+	ModeName          int        `json:"name"`                      // 调度模式名称
+	RunStatus         int        `json:"run_status"`                // EMS当前运行状态 0-待机 1-充电 2-放电 3-故障
+	SlaverState       int        `json:"slaver_stat"`               // EMS_SLAVE当前运行状态 0-初始化 1-参数获取;2:监听;3:待机;4:市电并网运行;5:离网运行;6:油机并网运行 离网油机接入运行;7:故障
+	GridState         int        `json:"elecnet_state"`             // 市电网状态 0-正常 1-故障
+	FhpSn             []string   `json:"fhpSn"`                     // FHP设备编号
+	APowerStatus      []int      `json:"infi_status"`               // FHP状态 0-正常 1-告警 2-故障 3-离线;4-关机 5-自检(apowerS支持)
+	PeState           []int      `json:"pe_stat"`                   // PE当前运行状态 -1-离线 0-初始化 1-参数初始化 2-监听 3-待机 4-预充检测 5-预充 6-起机延时 7-逆变继电器吸合 8-运行 9-故障 注:老版本中10 表示离线;
+	BmsState          []int      `json:"bms_work"`                  // BMS工作状态 0-停机 1-初始化 2-自检 3-待机 4-预充 5-运行 6-充电 7-放电 8-充电保护 9-放电保护 10-充放电保护 11-故障 12-异常充电准备 13-异常充电 14-异常充电停止 15-工装模式 16-补电模式 17-补电故障
+	BmsHeatState      []int      `json:"bms_heat_state"`            // 取值为0~15:Bit0	加热膜1状态,0:未使能;1:使能 Bit1	加热膜2状态,0:未使能;1:使能 Bit2	加热膜3状态,0:未使能;1:使能 Bit3	加热膜4状态,0:未使能;1:使能
+	P_uti             float32    `json:"p_uti"`                     // 市电供电功率 单位kW,供电为正(uti→home),馈电为负(home→uti)
+	P_sun             float32    `json:"p_sun"`                     // 太阳能发电功率 单位kW,供电为正(sun→home),无馈电
+	P_gen             float32    `json:"p_gen"`                     // 发电机发电功率 单位kW,供电为正(gen→home),无馈电
+	P_fhp             float32    `json:"p_fhp"`                     // FHP发电功率 单位kW,放电为正(fhp→home),充电为负(home→fhp)
+	P_load            float32    `json:"p_load"`                    // 负荷功率 单位kW,耗电为正(home)
+	Kwh_uti_in        float32    `json:"kwh_uti_in"`                // 市电口输入电量 单位kWh,日统计电量,截止到当前时间点的电量,下同
+	Kwh_uti_out       float32    `json:"kwh_uti_out"`               // 市电口输出电量 单位kWh,馈入电网电量
+	Kwh_sun           float32    `json:"kwh_sun"`                   // 太阳能供电电量 单位kWh
+	Kwh_gen           float32    `json:"kwh_gen"`                   // 发电机供电电量 单位kWh
+	Kwh_fhp_di        float32    `json:"kwh_fhp_di"`                // FHP放电电量 单位kWh
+	Kwh_fhp_chg       float32    `json:"kwh_fhp_chg"`               // FHP充电电量 单位kWh
+	Kwh_load          float32    `json:"kwh_load"`                  // 负荷用电量 单位kWh
+	Soc               float32    `json:"soc"`                       //系统SOC 单位%
+	FhpSoc            []float32  `json:"fhpSoc"`                    // FHP电池SOC 单位%
+	FhpPower          []float32  `json:"fhpPower"`                  // FHP有功功率 单位kw
+	T_amb             float32    `json:"t_amb"`                     // 环境温度 单位℃
+	MainSwitchState   [3]int     `json:"main_sw"`                   // 主开关状态 a1-备电主开关 a2-发电机主开关 a3-光伏主开关 (0-开关断开 1-开关闭合)
+	ProLoadState      [3]int     `json:"pro_load"`                  // 编程负载状态 0-断开 1-闭合
+	DoState           [4]int     `json:"do"`                        // 干接点继电器输出状态 0-断开 1-闭合
+	DiState           [4]int     `json:"di"`                        // 输入干接点状态 0-断开 1-闭合
+	Signal_4G         int        `json:"signal"`                    // 4G信号强度
+	Signal_WIFI       int        `json:"wifiSignal"`                // wifi信号强度
+	ConnType          int        `json:"connType"`                  // 网络连接方式 0-未接入网络 1-eth0 2-eth1 3-wifi 4-4G
+	GenState          int        `json:"genStat"`                   // 发电机状态 0-不使能 1-停机 2-启动 3-运行 4-退出 5-故障 6-演习中
+	RemoteSolarEn     int        `json:"remoteSolarEn"`             // 远端光伏接入使能(接入数量) 0-不使能 1-使能数量1 2-使能数量2 (默认0)
+	SolarPower        int        `json:"solarPower"`                // 近端光伏功率 单位:0.1KW
+	RemoteSolar1Power int        `json:"remoteSolar1Power"`         // 远端光伏1功率 单位:0.1KW 负载侧远端光伏1或市电侧远端光伏功率
+	RemoteSolar2Power int        `json:"remoteSolar2Power"`         // 远端光伏2功率 负载侧远端光伏2单位:0.1KW
+	BattHTemp         float32    `json:"sinHTemp"`                  // 最高单体温度 单位:0.1℃
+	BattLTemp         float32    `json:"sinLTemp"`                  // 最低单体温度 单位:0.1℃
+	BlackStartRelay   *int       `json:"blackStartRelay,omitempty"` // 黑启动继电器状态 0-断开 1-闭合
+	GridPhaseConSet   int        `json:"gridPhaseConSet"`           // 市电相制设置 00-split 01-单相三角形接法 02-三相三角形接法
+	MpptAllPower      *float32   `json:"mpptAllPower,omitempty"`    // MPPT光伏总功率 单位KW
+	MpptActPower      *[]float32 `json:"mpptActPower,omitempty"`    // MPPT光伏有功功率 单位KW
+	MpptSta           *[]int     `json:"mpptSta"`                   // MPPT工作状态 0-初始状态 1-升级状态 2-待机状态 3-启机状态 4-运行状态 5-故障状态 6-自检状态 7~13-预留 14-离线 15-休眠
+}
+
+// MQTT_LOG_357
+type MqttLogType_357 struct {
+	Opt       int           `json:"opt"` // 操作类型 1-增加/编辑 2-删除 3-修改当前工作序列
+	Sn        string        `json:"sn"`
+	Data      *ScheduleData `json:"data,omitempty"`       // opt=1时存在,更新的工作策略表
+	Id        []int         `json:"id,omitempty"`         // opt=2时存在,删除的ID,不允许删除当前使用的工作序列
+	CurrentId *int          `json:"current_id,omitempty"` // opt=3时存在,模式策略切换
+}
+
+type ScheduleData struct {
+	Name            string            `json:"name"`             // 参数表名称 可选,默认空字符串,自发自用或者TOU等
+	ID              uint              `json:"id"`               // 参数表ID 唯一标识此工作序列(ID从128后开始)【后台生成】
+	StartTime       string            `json:"startTime"`        // 模式开始时间 eg.2024-03-05 13:41:00 设备时区
+	Duration        int               `json:"duration"`         // 模式持续时间 单位秒,默认0,表示持续运行不退出
+	IntervalTime    int               `json:"intervalTime"`     // 间隔时间 单位秒,默认0,无间隔
+	StartEvent      int               `json:"startEvent"`       // 模式开始事件 事件ID,默认0,表示无此事件
+	EndEvent        int               `json:"endEvent"`         // 模式结束事件 事件ID,默认0,表示无此事件
+	NextMode        int               `json:"nextmode"`         // 模式退出后要执行的模式ID 默认为2,系统预留的自发自用
+	ElectricityType int               `json:"electricity_type"` // 电费类型 1:分时电价;2:阶梯电价;3:固定电价;4:分时&阶梯 5:BB套餐;6:peak demand
+	SchedulingType  int               `json:"scheduling_type"`  // 调度类型 work_mode 工作模式 1:tou 2: 自发自用 3:仅备电
+	ReservedSoc     uint              `json:"reserved_soc"`     // 备电预留SOC 备电预留SOC,可选,默认20%,与策略表项中的min_discharge_soc相比,取较大值
+	Loop            int               `json:"loop"`             // 模式执行次数 0:不限执行次数 1:执行一次,可选,默认0
+	Priority        int               `json:"priority"`         // 优先级 模式优先级,可选,默认为0,与TOU、自发自用一样具有相同优先级;0 优先级最低,数字越大优先级越高
+	Env             uint              `json:"env"`              // 使用环境 模式使用条件,可选,默认为0。bit0~1:00,不涉及,01,未安装光伏,10,已安装光伏; bit2~bit3:00,不涉及,01,不允许馈网,10,允许馈网,其他预留
+	List            []ScheduleSection `json:"list"`             // 策略表
+	YearStrategy    []int             `json:"yearStra"`         // 年策略表方案 [1,1,1......],共366个点,表示1年366天,数值代表策略ID
+}
+
+type ScheduleSection struct {
+	ID      int                   `json:"id"`      // 策略ID
+	Season  string                `json:"season"`  // 季节 eg.summer
+	DayType string                `json:"dayType"` // 日类型 weekdays or weekends
+	List    []ScheduleTimeSection `json:"list"`    // 时间段数据
+}
+
+type ScheduleTimeSection struct {
+	StartTime        string   `json:"start_time"`                   // 开始时间 06:30
+	EndTime          string   `json:"end_time"`                     // 结束时间 07:30
+	Flag             int      `json:"flag"`                         // 峰平谷 0-谷 1-平 2-峰 3-尖峰 4-低谷
+	Name             string   `json:"name"`                         // 名称 eg.On-Peak
+	Describe         string   `json:"describe"`                     // 描述 eg.光伏优先供载
+	SolarPriority    *[3]uint `json:"solar_priority,omitempty"`     // 光伏电能优先级 [0]-市电, [1]-负载, [2]-FHP;优先级为0/1/2/3,可相同,若相同则表明需要按比例或者功率分配,0为不涉及,数字越大优先级越高
+	GridDischargeMax *int     `json:"grid_discharge_max,omitempty"` // 电池最大允许馈网功率 可选,默认不能馈网;0:不可馈网,-1:不限制
+	GridChargeMax    *int     `json:"grid_charge_max,omitempty"`    // 电网允许最大充电功率 可选,默认不允许;0:不允许,-1:不限制
+	GCAO_max         *int     `json:"GCAO_max,omitempty"`           // 低于预留SOC电网充电使能 可选;0:不允许,-1:不限制;默认未设置(-3);如果使能之后没有设置值,则为2.5kw*N(用-2标识)
+	ChargePower      *int     `json:"charge_power,omitempty"`       // 可选,按单台功率计算,默认未设置-1
+	DischargePower   *int     `json:"discharge_power,omitempty"`    // 电池放电功率 可选,按单台功率计算,默认未设置-1
+	ChargeMax        *int     `json:"charge_max,omitempty"`         // 电池最大允许充电功率 可选,按单台功率计算,-1:无限制,默认-1
+	DischargeMax     *int     `json:"discharge_max,omitempty"`      // 电池最大允许放电功率 可选,电池需要放电时可设置,按单台功率计算,-1:无限制,默认-1
+	LoadPriority     *[3]uint `json:"load_priority,omitempty"`      // 负载用电优先级 可选,[0]-市电, [1]-光伏, [2]-FHP;优先级为0/1/2/3,可相同,若相同则表明需要按比例或者功率分配,0为不涉及,数字越大优先级
+	GridFeedMax      *int     `json:"grid_feed_max,omitempty"`      // 电网允许最大馈网功率 0:不允许,-1:不限制,-2:未设置,可选,默认-2(aPbox功率算在这里吗?不算)
+	GridMax          *int     `json:"grid_max,omitempty"`           // 电网允许最大取电功率 0-不允许,-1-不限制,可选,默认不限制
+	MinDischargeSoc  *int     `json:"min_discharge_soc,omitempty"`  // 放电截止SOC 预留SOC,供停电后使用,可选,默认20%
+	MaxChargeSoc     *int     `json:"max_charge_soc,omitempty"`     // 充电截止SOC 预留SOC,供停电后使用,可选,默认100%
+	HeatEnable       *int     `json:"heat_enable,omitempty"`        // 充电加热使能 0-未设置(默认) 1-不使能 2-使能
+	RampTime         *int     `json:"ramp_time,omitempty"`          // 爬坡时间 0~300,默认值0,BB套餐默认300
+	PowerOffAPower   *[]int   `json:"poweroff_apower,omitempty"`    // 主动关闭APower 0-不控制(默认) 1-断开 2-闭合
+	OffGrid          *int     `json:"off_grid,omitempty"`           // 主动断开市电继电器 0-不控制(默认) 1-断开 2-闭合
+	SolarCutoff      *int     `json:"solar_cutoff,omitempty"`       // 主动断开光伏继电器 0-不控制(默认) 1-断开 2-闭合
+	SchedulingType   int      `json:"scheduling_type"`              // 调度类型 1-TOU 2-自发自用 3-仅备电
+}

+ 77 - 0
service/domain/sitesAnalysisDomain.go

@@ -0,0 +1,77 @@
+package domain
+
+import (
+	"go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type Progress string // 'pending' | 'running' | 'error' | 'complete'
+
+// Progress枚举
+const (
+	ProgressPending  Progress = "pending"
+	ProgressRunning  Progress = "running"
+	ProgressError    Progress = "error"
+	ProgressComplete Progress = "complete"
+)
+
+type SiteAnalysisInfo struct {
+	Id          primitive.ObjectID `bson:"_id" json:"id"`
+	Sn          string             `bson:"sn" json:"sn"`
+	Description string             `bson:"description" json:"description"`
+	Item        []string           `bson:"item" json:"item"`
+	Progress    Progress           `bson:"progress" json:"progress"`
+}
+
+type SiteAnalysisOptions struct {
+	EnableTimeline bool `bson:"enableTimeline" json:"enableTimeline"`
+	// EnableRuntime  bool `bson:"enableRuntime" json:"enableRuntime"`
+}
+
+type SiteAnalysisConfigWrite struct {
+	Id          primitive.ObjectID  `bson:"_id,omitempty" json:"id,omitempty"`
+	Sn          string              `bson:"sn" json:"sn"`
+	Description string              `bson:"description" json:"description"`
+	TimePeriod  [2]string           `bson:"timePeriod" json:"timePeriod"`
+	Options     SiteAnalysisOptions `bson:"options"`
+}
+
+type SiteAnalysisConfig struct {
+	Id          primitive.ObjectID  `bson:"_id,omitempty" json:"id,omitempty"`
+	Sn          string              `bson:"sn" json:"sn"`
+	Description string              `bson:"description" json:"description"`
+	TimePeriod  [2]string           `bson:"timePeriod" json:"timePeriod"`
+	Options     SiteAnalysisOptions `bson:"options"`
+	Item        []string            `bson:"item" json:"item"`
+	Progress    Progress            `bson:"progress" json:"progress"`
+	Data        SiteAnalysisData    `bson:"data" json:"data"`
+}
+
+type TaskSiteAnalysis struct {
+	Id         primitive.ObjectID  `bson:"_id" json:"id"`
+	Sn         string              `bson:"sn" json:"sn"`
+	TimePeriod [2]string           `bson:"timePeriod" json:"timePeriod"`
+	Options    SiteAnalysisOptions `bson:"options"`
+}
+
+type SiteAnalysisData struct {
+	Timeline []TimelineItem `bson:"timeline" json:"timeline"`
+}
+
+/************** Timeline **************/
+
+type TimelineType string // 'offline' 'online' '' ...
+
+const (
+	TimelineOffline    TimelineType = "offline"
+	TimelineOnline     TimelineType = "online"
+	TimelineModeChange TimelineType = "mode_change"
+	TimelineEvent      TimelineType = "event"
+	TimelineAlarm      TimelineType = "alarm"
+)
+
+type TimelineItem struct {
+	Time string       `bson:"time" json:"time"` // YYYY-MM-DD HH:mm:SS
+	Type TimelineType `bson:"type" json:"type"`
+	Info *string      `bson:"info" json:"info"` // 时间点的说明信息
+	Data *string      `bson:"data" json:"data"` // 时间点的详细数据 JSON string
+}

+ 46 - 0
service/domain/sitesMonitorDomain.go

@@ -0,0 +1,46 @@
+package domain
+
+type StateConfig struct {
+	Enable bool `bson:"enable" json:"enable"`
+}
+
+type SocConfig struct {
+	Enable bool `bson:"enable" json:"enable"`
+}
+
+type APowerConfig struct {
+	Enable bool `bson:"enable" json:"enable"`
+}
+
+type AlarmConfig struct {
+	Enable bool `bson:"enable" json:"enable"`
+}
+
+type VersionConfig struct {
+	Enable bool `bson:"enable" json:"enable"`
+}
+
+type TariffsConfig struct {
+	Enable bool `bson:"enable" json:"enable"`
+}
+
+type OthersConfig struct {
+	Enable bool `bson:"enable" json:"enable"`
+}
+
+type MonitorPageConfig struct {
+	State   StateConfig   `bson:"state" json:"state"`
+	Soc     SocConfig     `bson:"soc" json:"soc"`
+	APower  APowerConfig  `bson:"aPower" json:"aPower"`
+	Alarm   AlarmConfig   `bson:"alarm" json:"alarm"`
+	Version VersionConfig `bson:"version" json:"version"`
+	Tariffs TariffsConfig `bson:"tariffs" json:"tariffs"`
+	Others  OthersConfig  `bson:"others" json:"others"`
+}
+
+type SitesMonitorConfig struct {
+	Name        string             `bson:"name" json:"name"`
+	Description string             `bson:"description" json:"description"`
+	SiteList    []string           `bson:"siteList" json:"siteList"`
+	PageConfig  *MonitorPageConfig `bson:"pageConfig,omitempty" json:"pageConfig,omitempty"`
+}

+ 6 - 0
service/domain/xlogDomain.go

@@ -0,0 +1,6 @@
+package domain
+
+type XlogSiteInfo struct {
+	Sn   string `json:"sn"`
+	Uuid string `json:"uuid"`
+}

+ 1 - 2
service/controllers/controllers.go → service/dto/public.go

@@ -1,6 +1,5 @@
-package controllers
+package dto
 
-// Public
 type ResPage[T any] struct {
 	Data     []T  `json:"data"`
 	PageSize uint `json:"pageSize"`

+ 14 - 0
service/dto/sitesAnalysisDTO.go

@@ -0,0 +1,14 @@
+package dto
+
+import (
+	"fwh-toolbox-server/domain"
+
+	"go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type SiteAnalysisDataInfo struct {
+	Id       primitive.ObjectID      `bson:"_id,omitempty" json:"id,omitempty"`
+	Sn       string                  `bson:"sn" json:"sn"`
+	Progress domain.Progress         `bson:"progress" json:"progress"`
+	Data     domain.SiteAnalysisData `bson:"data" json:"data"`
+}

+ 135 - 0
service/dto/sitesMonitorDTO.go

@@ -0,0 +1,135 @@
+package dto
+
+type SiteBaseInfoPara struct {
+	SiteList []string `json:"siteList"`
+	PageSize uint     `json:"pageSize"`
+	PageNum  uint     `json:"pageNum"`
+}
+
+type SiteBaseInfo struct {
+	Id               string  `json:"id"`
+	Name             *string `json:"name"`
+	FhpSn            string  `json:"fhpSn"`
+	ActiveStatus     int     `json:"activeStatus"`
+	ActiveTime       string  `json:"activeTime"`
+	Country          string  `json:"country"`
+	Province         string  `json:"province"`
+	City             string  `json:"city"`
+	OnOffStatus      int     `json:"onOffStatus"`
+	RealSysHdVersion string  `json:"realSysHdVersion"`
+	RunStatus        int     `json:"runStatus"`
+	Version          string  `json:"version"`
+}
+
+type SiteBaseInfoResponse struct {
+	Code    int            `json:"code"`
+	Message string         `json:"message"`
+	Result  []SiteBaseInfo `json:"result"`
+	Success bool           `json:"success"`
+	Total   uint           `json:"total"`
+}
+
+type SiteInfoPara struct {
+	SN       *string `json:"sn"`
+	PageSize uint    `json:"pageSize"`
+	PageNum  uint    `json:"pageNum"`
+}
+
+type SiteInfo struct {
+	Id               string  `json:"id"`
+	Name             *string `json:"name"`
+	FhpSn            string  `json:"fhpSn"`            // aPower SN 多设备用,分割
+	ActiveStatus     int     `json:"activeStatus"`     // 激活状态 0-未激活 1-激活
+	ActiveTime       string  `json:"activeTime"`       // 激活时间(北京时间)
+	LocalActiveTime  string  `json:"localActiveTime"`  // 激活时间(站点当地时间)
+	Country          string  `json:"country"`          // 国家
+	Province         string  `json:"province"`         // 州/省
+	City             string  `json:"city"`             // 城市
+	RealSysHdVersion string  `json:"realSysHdVersion"` // 系统版本信息
+	Version          string  `json:"version"`          // 设备固件版本
+	InstallerName    string  `json:"installerName"`    // 安装商名称
+	Longitude        string  `json:"longitude"`        // 经度
+	Latitude         string  `json:"latitude"`         // 纬度
+	ICCID            string  `json:"iccId"`
+	IMEI             string  `json:"imei"`
+	FirmUpgradeTime  string  `json:"firmUpgradeTime"` // 固件最后更新时间 YYYY-MM-DD HH:mm:SS
+	CreateTime       string  `json:"createTime"`      // 设备首次登录时间 YYYY-MM-DD HH:mm:SS
+	TimeZone         float32 `json:"timeZone"`        // 时区数值
+	ZoneInfo         string  `json:"zoneInfo"`        // 时区信息 America/Los_Angeles
+	GenEn            int     `json:"genEn"`           // 发电机是否使能 0-不使能 1-使能
+	Remark           *string `json:"remark"`          // 备注信息
+}
+
+type SiteInfoResponse struct {
+	Code    int        `json:"code"`
+	Message string     `json:"message"`
+	Result  []SiteInfo `json:"result"`
+	Success bool       `json:"success"`
+	Total   uint       `json:"total"`
+}
+
+type WorkModeInfo struct {
+	Id                   uint     `json:"id"`
+	OldIndex             uint     `json:"oldIndex"`
+	Name                 string   `json:"name"`
+	Soc                  float32  `json:"soc"`
+	MaxSoc               float32  `json:"maxSoc"`
+	MinSoc               float32  `json:"minSoc"`
+	DischargeDepthSoc    *float32 `json:"dischargeDepthSoc"`
+	WorkMode             uint     `json:"workMode"`
+	EnergyIncentivesType uint     `json:"energyIncentivesType"`
+	ElectricityType      uint     `json:"electricityType"`
+}
+
+type SiteModeInfo struct {
+	CurrendId uint           `json:"currendId"`
+	List      []WorkModeInfo `json:"list"`
+}
+
+type SiteModeInfoResponse struct {
+	Code    int          `json:"code"`
+	Msg     string       `json:"msg"`
+	Result  SiteModeInfo `json:"result"`
+	Success bool         `json:"success"`
+}
+
+type SitePowerFlow struct {
+	Index           uint      `json:"index"`           // 非当天为0,当天为实时数量
+	DeviceTimeArray []string  `json:"deviceTimeArray"` // 设备本地时间,格式 “2025-08-18 00:00:00”
+	PowerFhpArray   []float32 `json:"powerFhpArray"`
+	PowerGenArray   []float32 `json:"powerGenArray"`
+	PowerLoadArray  []float32 `json:"powerLoadArray"`
+	PowerSunArray   []float32 `json:"powerSunArray"`
+	PowerUtiArray   []float32 `json:"powerUtiArray"`
+	SocArray        []float32 `json:"socArray"`
+	RunStatusArray  []uint    `json:"runStatusArray"` // 运行状态,0:待机;1:充电;2:放电;3:故障
+	KwhTotalArray   []float32 `json:"kwhTotalArray"`  // 总可用电量,应该是根据SOC推算出来的。
+}
+
+type SitePowerFlowResponse struct {
+	Code   int           `json:"code"`
+	Msg    string        `json:"msg"`
+	Result SitePowerFlow `json:"result"`
+}
+
+type SiteAlarmList struct {
+	Id              uint   `json:"id"`
+	GatewayId       string `json:"gatewayId"`
+	AlarmEqSn       string `json:"alarmEqSn"`
+	AlarmCode       string `json:"alarmCode"`
+	CommonAlarmCode string `json:"commonAlarmCode"`
+	Level           int    `json:"level"`
+	Time            string `json:"time"`
+	LogName         string `json:"logName"`
+	CommonLogName   string `json:"commonLogName"`
+	LogNameEn       string `json:"logNameEn"`
+	CommonLogNameEn string `json:"commonLogNameEn"`
+}
+
+type SiteAlarmListResponse struct {
+	Code    int             `json:"code"`
+	Message string          `json:"message"`
+	Result  []SiteAlarmList `json:"result"`
+	Success bool            `json:"success"`
+	Total   uint            `json:"total"`
+}

+ 0 - 149
service/ext/api/fwh_pro/fwh_eventAlarmLog.go

@@ -1,149 +0,0 @@
-package fwh_pro
-
-import (
-	"io"
-	"net/http"
-	"net/url"
-	"strconv"
-	"time"
-
-	"go.uber.org/zap"
-)
-
-// 查询事件事件日志
-func queryEventAndAlarmLog(sn string, params url.Values) ([]byte, error) {
-	baseURL := "https://energy.franklinwh.com/hes-gateway/manage/iotDeviceRunLogs"
-
-	token := getLoginToken()
-	userId, _ := getUserId(token)
-
-	// 构造 query 参数
-	params.Add("userId", strconv.Itoa(userId))
-	params.Add("searchName", sn)
-	params.Add("timeType", strconv.Itoa(1)) // 0-通讯时间;1-设备时间
-	params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
-	params.Add("pageNum", strconv.Itoa(1))
-	params.Add("land", "ZH_CN")
-
-	// 拼接到 URL
-	finalURL := baseURL + "?" + params.Encode()
-
-	// 创建请求
-	req, err := http.NewRequest(
-		http.MethodGet,
-		finalURL,
-		nil)
-	if err != nil {
-		zap.L().Error("NewRequest failed.", zap.String("err", err.Error()))
-		return nil, err
-	}
-
-	// 设置 Content-Type 和 Authorization
-	req.Header.Set("Content-Type", "application/json")
-	req.Header.Set("Logintoken", token)
-
-	// 发送请求
-	client := &http.Client{Timeout: 30 * time.Second}
-	resp, err := client.Do(req)
-	if err != nil {
-		zap.L().Error("Do Send failed.", zap.String("err", err.Error()))
-		return nil, err
-	}
-	defer resp.Body.Close()
-
-	body, _ := io.ReadAll(resp.Body)
-	// logger.Std.Printf("Status: %s\nBody: %s\n", resp.Status, body)
-	return body, nil
-}
-
-// 查询快照信息
-// logTime 格式 YYYY-MM-DD
-func QueryAlarmSnapshotInfo(sn, logTime string, page int) ([]byte, error) {
-	baseURL := "https://energy.franklinwh.com/hes-gateway/manage/selectAlarmSnapshotList"
-
-	token := getLoginToken()
-
-	// 构造 query 参数
-	params := url.Values{}
-	params.Add("logTime", logTime)
-	params.Add("linkId", sn)
-	params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
-	params.Add("pageNum", strconv.Itoa(page))
-	params.Add("land", "ZH_CN")
-
-	// 拼接到 URL
-	finalURL := baseURL + "?" + params.Encode()
-
-	// 创建请求
-	req, err := http.NewRequest(
-		http.MethodGet,
-		finalURL,
-		nil)
-	if err != nil {
-		zap.L().Error("NewRequest failed.", zap.String("err", err.Error()))
-		return nil, err
-	}
-
-	// 设置 Content-Type 和 Authorization
-	req.Header.Set("Content-Type", "application/json")
-	req.Header.Set("Logintoken", token)
-
-	// 发送请求
-	client := &http.Client{Timeout: 30 * time.Second}
-	resp, err := client.Do(req)
-	if err != nil {
-		zap.L().Error("Do send failed.", zap.String("err", err.Error()))
-		return nil, err
-	}
-	defer resp.Body.Close()
-
-	body, _ := io.ReadAll(resp.Body)
-	// logger.Std.Printf("Status: %s\nBody: %s\n", resp.Status, body)
-	return body, nil
-}
-
-// 查询所有事件告警
-// startTime&endTime 格式 YYYY-MM-DD HH:mm:SS
-func QueryAllEventAlarm(sn, startTime, endTime string) ([]byte, error) {
-	// 构造 query 参数
-	params := url.Values{}
-	params.Add("startTime", startTime)
-	params.Add("endTime", endTime)
-
-	return queryEventAndAlarmLog(sn, params)
-}
-
-// 查询事件日志
-// startTime&endTime 格式 YYYY-MM-DD HH:mm:SS
-func QueryEventLog(sn, startTime, endTime string) ([]byte, error) {
-	// 构造 query 参数
-	params := url.Values{}
-	params.Add("logType", strconv.Itoa(0))
-	params.Add("startTime", startTime)
-	params.Add("endTime", endTime)
-
-	return queryEventAndAlarmLog(sn, params)
-}
-
-// 查询告警日志
-// startTime&endTime 格式 YYYY-MM-DD HH:mm:SS
-// alarmCode - 支持多个code,用‘,’隔开,“”代表查询所有
-func QueryAlarmLog(sn, startTime, endTime, alarmCode string) ([]byte, error) {
-	// 构造 query 参数
-	params := url.Values{}
-	params.Add("logType", strconv.Itoa(1))
-	params.Add("alarmCode", alarmCode)
-	params.Add("startTime", startTime)
-	params.Add("endTime", endTime)
-
-	return queryEventAndAlarmLog(sn, params)
-}
-
-// 查询实时告警
-func QueryRealtimeAlarm(sn string) ([]byte, error) {
-	// 构造 query 参数
-	params := url.Values{}
-	params.Add("logType", strconv.Itoa(2))
-
-	return queryEventAndAlarmLog(sn, params)
-}

+ 0 - 52
service/ext/api/fwh_pro/fwh_powerFlow.go

@@ -1,52 +0,0 @@
-package fwh_pro
-
-import (
-	"io"
-	"net/http"
-	"net/url"
-	"time"
-
-	"go.uber.org/zap"
-)
-
-// 查询站点功率流信息
-func QuerySitePowerFlow(sn string, day time.Time) ([]byte, error) {
-	baseURL := "https://energy.franklinwh.com/api-energy/power/getFhpPowerByDayOld"
-
-	token := getLoginToken()
-
-	// 构造 query 参数
-	params := url.Values{}
-	params.Add("gatewayId", sn)
-	params.Add("dayTime", day.Format(time.DateOnly)) // 会自动 URL 编码
-
-	// 拼接到 URL
-	finalURL := baseURL + "?" + params.Encode()
-
-	// 创建请求
-	req, err := http.NewRequest(
-		http.MethodGet,
-		finalURL,
-		nil)
-	if err != nil {
-		zap.L().Error("NewRequest failed.", zap.String("err", err.Error()))
-		return nil, err
-	}
-
-	// 设置 Content-Type 和 Authorization
-	req.Header.Set("Content-Type", "application/json")
-	req.Header.Set("Logintoken", token)
-
-	// 发送请求
-	client := &http.Client{Timeout: 30 * time.Second}
-	resp, err := client.Do(req)
-	if err != nil {
-		zap.L().Error("Do Send failed.", zap.String("err", err.Error()))
-		return nil, err
-	}
-	defer resp.Body.Close()
-
-	body, _ := io.ReadAll(resp.Body)
-	// logger.Std.Printf("Status: %s\nBody: %s\n", resp.Status, body)
-	return body, nil
-}

+ 0 - 52
service/ext/api/fwh_pro/fwh_realtimeAalrm.go

@@ -1,52 +0,0 @@
-package fwh_pro
-
-import (
-	"io"
-	"net/http"
-	"net/url"
-	"time"
-
-	"go.uber.org/zap"
-)
-
-// 查询站点告警信息
-func QuerySiteAlarmList(sn string) ([]byte, error) {
-	baseURL := "https://energy.franklinwh.com/hes-gateway/manage/getEquipmentAlarm"
-
-	token := getLoginToken()
-
-	// 构造 query 参数
-	params := url.Values{}
-	params.Add("gatewayId", sn)
-	params.Add("land", "ZH_CN")
-
-	// 拼接到 URL
-	finalURL := baseURL + "?" + params.Encode()
-
-	// 创建请求
-	req, err := http.NewRequest(
-		http.MethodGet,
-		finalURL,
-		nil)
-	if err != nil {
-		zap.L().Error("NewRequest failed.", zap.String("err", err.Error()))
-		return nil, err
-	}
-
-	// 设置 Content-Type 和 Authorization
-	req.Header.Set("Content-Type", "application/json")
-	req.Header.Set("Logintoken", token)
-
-	// 发送请求
-	client := &http.Client{Timeout: 30 * time.Second}
-	resp, err := client.Do(req)
-	if err != nil {
-		zap.L().Error("Do Send failed.", zap.String("err", err.Error()))
-		return nil, err
-	}
-	defer resp.Body.Close()
-
-	body, _ := io.ReadAll(resp.Body)
-	// logger.Std.Printf("Status: %s\nBody: %s\n", resp.Status, body)
-	return body, nil
-}

+ 0 - 183
service/ext/api/fwh_pro/fwh_siteInfo.go

@@ -1,183 +0,0 @@
-package fwh_pro
-
-import (
-	"fmt"
-	"io"
-	"net/http"
-	"net/url"
-	"strconv"
-	"strings"
-	"time"
-
-	"go.uber.org/zap"
-)
-
-type SiteBaseInfoListPayload struct {
-	Search        *string `json:"search,omitempty"`  // 只能搜索单条SN
-	Serials       *string `json:"serials,omitempty"` // 可搜索多条SN,SN之间以‘,’隔开
-	CountryId     *int    `json:"countryId,omitempty"`
-	ProvinceId    *int    `json:"provinceId,omitempty"`
-	CityId        *int    `json:"cityId,omitempty"`
-	NetworkStatus *int    `json:"networkStatus,omitempty"`
-	DeviceStatus  *int    `json:"deviceStatus,omitempty"`
-	OnOffStatus   *int    `json:"onOffStatus,omitempty"`
-	PageSize      uint    `json:"pageSize"`
-	PageNum       uint    `json:"pageNum"`
-	Lang          string  `json:"lang"`
-}
-
-type SiteInfoListPayload struct {
-	Search     *string `json:"search,omitempty"` // 只能搜索单条SN
-	CountryId  *int    `json:"countryId,omitempty"`
-	ProvinceId *int    `json:"provinceId,omitempty"`
-	CityId     *int    `json:"cityId,omitempty"`
-	PageSize   uint    `json:"pageSize"`
-	PageNum    uint    `json:"pageNum"`
-	Lang       string  `json:"lang"`
-}
-
-type SiteModeInfoPayload struct {
-	GatewayId string `json:"gatewayId"`
-	ShowType  int    `json:"showType"`
-	Lang      string `json:"lang"`
-}
-
-// 查询站点基本信息
-func QuerySiteBaseInfoList(sn []string, pageSize uint, pageNum uint) ([]byte, error) {
-	snList := strings.Join(sn, ",")
-
-	token := getLoginToken()
-
-	// 要发送的 JSON
-	payload := SiteBaseInfoListPayload{
-		Serials:  &snList,
-		PageSize: pageSize,
-		PageNum:  pageNum,
-		Lang:     "ZH_CN",
-	}
-
-	// 要发送的表单数据
-	form := url.Values{}
-	form.Set("serials", *payload.Serials) // *string 转 string
-	form.Set("pageSize", fmt.Sprintf("%d", payload.PageSize))
-	form.Set("pageNum", fmt.Sprintf("%d", payload.PageNum))
-	form.Set("lang", payload.Lang)
-
-	// 创建请求
-	req, err := http.NewRequest(
-		http.MethodPost,
-		"https://energy.franklinwh.com/hes-gateway/manage/selectIotFirmwareGatewayList",
-		strings.NewReader(form.Encode()))
-	if err != nil {
-		zap.L().Error("NewRequest failed.", zap.String("err", err.Error()))
-		return nil, err
-	}
-
-	// 设置 Content-Type 和 Authorization
-	req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
-	req.Header.Set("Logintoken", token)
-
-	// 发送请求
-	client := &http.Client{Timeout: 30 * time.Second}
-	resp, err := client.Do(req)
-	if err != nil {
-		zap.L().Error("Do Send failed.", zap.String("err", err.Error()))
-		return nil, err
-	}
-	defer resp.Body.Close()
-
-	body, _ := io.ReadAll(resp.Body)
-	// logger.Std.Printf("Status: %s\nBody: %s\n", resp.Status, body)
-	return body, nil
-}
-
-// 查询站点信息
-func QuerySiteInfoList(sn *string, pageSize uint, pageNum uint) ([]byte, error) {
-	token := getLoginToken()
-
-	// 构造 query 参数
-	params := url.Values{}
-	if sn != nil {
-		params.Add("search", *sn)
-	}
-	params.Add("searchFlag", fmt.Sprintf("%d", 0)) // 0-全部 1-aGate名称 11-aGate序列号 2-aPower序列号 7-EMS_MAIN版本 8-EMS_DSP版本 10-aPower子版本号 13-协议版本 14-硬件版本 16-备注
-	params.Add("pageSize", fmt.Sprintf("%d", pageSize))
-	params.Add("pageNum", fmt.Sprintf("%d", pageNum))
-	params.Add("lang", "ZH_CN")
-
-	// 拼接到 URL
-	baseURL := "https://energy.franklinwh.com/hes-gateway/manage/selectIotGatewayList"
-	finalURL := baseURL + "?" + params.Encode()
-
-	// 创建请求
-	req, err := http.NewRequest(
-		http.MethodGet,
-		finalURL,
-		nil)
-	if err != nil {
-		zap.L().Error("NewRequest failed.", zap.String("err", err.Error()))
-		return nil, err
-	}
-
-	// 设置 Content-Type 和 Authorization
-	req.Header.Set("Content-Type", "application/json")
-	req.Header.Set("Logintoken", token)
-
-	// 发送请求
-	client := &http.Client{Timeout: 30 * time.Second}
-	resp, err := client.Do(req)
-	if err != nil {
-		zap.L().Error("Do Send failed.", zap.String("err", err.Error()))
-		return nil, err
-	}
-	defer resp.Body.Close()
-
-	body, _ := io.ReadAll(resp.Body)
-	// logger.Std.Printf("Status: %s\nBody: %s\n", resp.Status, body)
-	return body, nil
-}
-
-// 查询站点工作模式信息
-func QuerySiteModeInfo(sn string) ([]byte, error) {
-	token := getLoginToken()
-
-	// 要发送的 JSON
-	payload := SiteModeInfoPayload{
-		GatewayId: sn,
-		ShowType:  1,
-		Lang:      "ZH_CN",
-	}
-
-	// 要发送的表单数据
-	form := url.Values{}
-	form.Set("gatewayId", payload.GatewayId)
-	form.Set("showType", strconv.Itoa(payload.ShowType))
-	form.Set("lang", payload.Lang)
-
-	// 创建请求
-	req, err := http.NewRequest(
-		http.MethodPost,
-		"https://energy.franklinwh.com/hes-gateway/manage/tou/getGatewayTouListV2",
-		strings.NewReader(form.Encode()))
-	if err != nil {
-		zap.L().Error("NewRequest failed.", zap.String("err", err.Error()))
-		return nil, err
-	}
-
-	// 设置 Content-Type 和 Authorization
-	req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
-	req.Header.Set("Logintoken", token)
-
-	// 发送请求
-	client := &http.Client{Timeout: 30 * time.Second}
-	resp, err := client.Do(req)
-	if err != nil {
-		zap.L().Error("Do Send failed.", zap.String("err", err.Error()))
-		return nil, err
-	}
-	defer resp.Body.Close()
-
-	body, _ := io.ReadAll(resp.Body)
-	// logger.Std.Printf("Status: %s\nBody: %s\n", resp.Status, body)
-	return body, nil
-}

+ 181 - 0
service/jobs/analysisSiteData.go

@@ -0,0 +1,181 @@
+package jobs
+
+import (
+	"encoding/json"
+	"fmt"
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/repositories"
+	"sort"
+	"strconv"
+	"time"
+
+	"go.mongodb.org/mongo-driver/bson/primitive"
+	"go.uber.org/zap"
+)
+
+type SiteDataSet struct {
+	Log_0    []domain.MqttLogItem
+	Log_101  []domain.MqttLogItem
+	Log_357  []domain.MqttLogItem
+	EventLog []domain.EventAlarmItem
+	AlarmLog []domain.EventAlarmItem
+}
+
+func AnalysisSiteTimeline(id primitive.ObjectID, data SiteDataSet) error {
+	var timelineItemList []domain.TimelineItem
+
+	// 分析offline
+	for _, log := range data.Log_0 {
+		_, err := time.Parse(time.DateTime, log.LogTime)
+		if err != nil {
+			zap.L().Error("0日志时间格式错误", zap.String("time", log.LogTime))
+			return err
+		}
+		timelineItem := domain.TimelineItem{
+			Time: log.LogTime,
+			Type: domain.TimelineOffline,
+		}
+		timelineItemList = append(timelineItemList, timelineItem)
+	}
+
+	// 分析online
+	for _, log := range data.Log_101 {
+		_, err := time.Parse(time.DateTime, log.LogTime)
+		if err != nil {
+			zap.L().Error("101日志时间格式错误", zap.String("time", log.LogTime))
+			return err
+		}
+		timelineItem := domain.TimelineItem{
+			Time: log.LogTime,
+			Type: domain.TimelineOnline,
+		}
+		timelineItemList = append(timelineItemList, timelineItem)
+	}
+
+	// 分析套餐&模式日志
+	scheduleDataList := make(map[uint]domain.ScheduleData)
+	for _, log := range data.Log_357 {
+		_, err := time.Parse(time.DateTime, log.LogTime)
+		if err != nil {
+			zap.L().Error("357日志时间格式错误", zap.String("time", log.LogTime))
+			return err
+		}
+
+		var log_357 domain.MqttLogType_357
+		err = json.Unmarshal([]byte(log.LogData), &log_357)
+		if err != nil {
+			zap.L().Error("357日志JSON数据格式错误", zap.String("data", log.LogData))
+			return err
+		}
+
+		// 判断操作类型 1-增加/编辑 2-删除 3-修改当前工作序列
+		opt := log_357.Opt
+		var modeInfo *string
+		var modeData *string
+		switch opt {
+		case 1:
+			// 记录策略ID对应的策略信息,用于关联切换的模式
+			if log_357.Data != nil {
+				scheduleDataList[log_357.Data.ID] = *log_357.Data
+			}
+		case 2:
+			// 删除不做处理
+		case 3:
+			if log_357.CurrentId != nil && *log_357.CurrentId >= 0 {
+				val, ok := scheduleDataList[uint(*log_357.CurrentId)]
+				if ok {
+					modeInfo = &val.Name
+					// 转成 JSON 字符串
+					jsonModeData, err := json.Marshal(val)
+					if err != nil {
+						fmt.Println("序列化失败:", err)
+						zap.L().Error("357日志策略信息JSON数据序列化失败", zap.String("err", err.Error()), zap.Any("data", val))
+					} else {
+						dataStr := string(jsonModeData)
+						modeData = &dataStr
+					}
+				} else {
+					infoStr := fmt.Sprintf("未知模式(%d)", log_357.CurrentId)
+					modeInfo = &infoStr
+				}
+			}
+		default:
+			zap.L().Error("357日志不支持的操作类型", zap.String("opt", strconv.Itoa(log_357.Opt)))
+			return err
+		}
+
+		timelineItem := domain.TimelineItem{
+			Time: log.LogTime,
+			Type: domain.TimelineModeChange,
+			Info: modeInfo,
+			Data: modeData,
+		}
+		timelineItemList = append(timelineItemList, timelineItem)
+	}
+
+	// 分析Event
+	for _, log := range data.EventLog {
+		_, err := time.Parse(time.DateTime, log.LogTime)
+		if err != nil {
+			zap.L().Error("Event日志时间格式错误", zap.String("time", log.LogTime))
+			return err
+		}
+		timelineItem := domain.TimelineItem{
+			Time: log.LogTime,
+			Type: domain.TimelineEvent,
+			Info: &log.LogName,
+		}
+		timelineItemList = append(timelineItemList, timelineItem)
+	}
+
+	// 分析Alarm
+	for _, log := range data.AlarmLog {
+		_, err := time.Parse(time.DateTime, log.LogTime)
+		if err != nil {
+			zap.L().Error("Alarm日志时间格式错误", zap.String("time", log.LogTime))
+			return err
+		}
+		alarmCode := []byte(log.AlarmCode)
+		var alarmName string
+		if len(alarmCode) < 4 {
+			alarmName = fmt.Sprintf("#? %s", log.LogName)
+		} else {
+			if alarmCode[1] == '0' {
+				alarmName = log.LogName
+			} else {
+				alarmName = fmt.Sprintf("#%s %s", string(alarmCode[1]), log.LogName)
+			}
+
+		}
+		timelineItem := domain.TimelineItem{
+			Time: log.LogTime,
+			Type: domain.TimelineAlarm,
+			Info: &alarmName,
+		}
+		timelineItemList = append(timelineItemList, timelineItem)
+	}
+
+	// 数据时间排序
+	timelineSlice(&timelineItemList)
+
+	// 最后将时间线数据存入数据库
+	if err := repositories.InsertSiteAnalysisTimeline(id, timelineItemList); err != nil {
+		zap.L().Error("插入站点分析timeline数据失败", zap.String("id", id.String()))
+		return err
+	}
+
+	return nil
+}
+
+func timelineSlice(list *[]domain.TimelineItem) {
+	if list == nil {
+		return
+	}
+
+	// 排序
+	sort.Slice(*list, func(i, j int) bool {
+		ti, _ := time.Parse(time.DateTime, (*list)[i].Time)
+		tj, _ := time.Parse(time.DateTime, (*list)[j].Time)
+		return ti.Before(tj)
+	})
+}

+ 16 - 81
service/jobs/getAlarmSnapshot.go

@@ -5,13 +5,11 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"fwh-toolbox-server/controllers"
-	"fwh-toolbox-server/ext/api/fwh_pro"
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/dto"
 	function "fwh-toolbox-server/function/SnapFile"
 	"fwh-toolbox-server/logger"
-	"fwh-toolbox-server/models/model_pgsql/model_alarm_snapshot"
-	"fwh-toolbox-server/models/model_pgsql/query_alarm_snapshot"
-	"fwh-toolbox-server/types"
+	"fwh-toolbox-server/repositories"
 	"fwh-toolbox-server/utils/files"
 	"os"
 	"path/filepath"
@@ -22,67 +20,19 @@ import (
 
 	"go.uber.org/zap"
 	"gorm.io/gorm"
-	"gorm.io/gorm/clause"
 )
 
-type AlarmSnapshotInfo struct {
-	Id           int64  `json:"id"`
-	CodeType     int32  `json:"codeType"` // 作用未知
-	FileName     string `json:"fileName"`
-	FileUrl      string `json:"fileUrl"`
-	LinkId       string `json:"linkId"`  // site SN
-	LogTime      string `json:"logTime"` // format YYYY-MM-DD
-	SerialNumber string `json:"serialNumber"`
-	StartTime    string `json:"startTime"`    // format YYYY-MM-DD HH:mm:SS
-	EndTime      string `json:"endTime"`      // format YYYY-MM-DD HH:mm:SS
-	Status       int    `json:"status"`       // 1-完成
-	CompleteTime string `json:"completeTime"` // format YYYY-MM-DD HH:mm:SS
-}
-
-type AlarmSnapshotListResponse = types.FwhPageResponse[AlarmSnapshotInfo]
-
-type FileRecord = model_alarm_snapshot.SiteAlarmSnapshotFileRecord
-type SnapshotData = model_alarm_snapshot.SiteAlarmSnapshotData
-
-// 解析
-func handleAlarmSnapshot(sn string, logTime string, page int) (uint64, []AlarmSnapshotInfo, error) {
-	// 查询告警快照数据
-	resByte, err := fwh_pro.QueryAlarmSnapshotInfo(sn, logTime, page)
-	if err != nil {
-		return 0, nil, fmt.Errorf("[handleAlarmSnapshot] %s", err.Error())
-	}
-
-	var alarmSnapshotRes AlarmSnapshotListResponse
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &alarmSnapshotRes)
-	if jsonErr != nil {
-		return 0, nil, fmt.Errorf("[handleAlarmSnapshot] %s", jsonErr)
-	}
-
-	return alarmSnapshotRes.Total, alarmSnapshotRes.Result, nil
-}
+type AlarmSnapshotListResponse = domain.FwhPageResponse[domain.AlarmSnapshotInfo]
 
 // 获取告警快照
 // 从FHW后端请求快照文件后,处理并放入数据库
 // logTime 格式 YYYY-MM-DD
 func GetAlarmSnapshot(ctx context.Context, sn, logTime string) error {
 	// 请求快照信息列表
-	var page int = 1
-	snapshotList := make([]AlarmSnapshotInfo, 0)
-	total, dataList, err := handleAlarmSnapshot(sn, logTime, page)
+	snapshotList, err := repositories.QueryAlarmSnapshotInfo(sn, logTime)
 	if err != nil {
 		return fmt.Errorf("[GetAlarmSnapshot] %s", err.Error())
 	}
-	snapshotList = append(snapshotList, dataList...)
-
-	for uint64(len(snapshotList)) < total {
-		page = page + 1
-		_, dataList, err = handleAlarmSnapshot(sn, logTime, page)
-		if err != nil {
-			return fmt.Errorf("[GetAlarmSnapshot] %s", err.Error())
-		}
-		snapshotList = append(snapshotList, dataList...)
-	}
 
 	// 创建临时目录存储下载和解压数据
 	tempDir, err := os.MkdirTemp("", "fwhtb-alarmSnapshot-*")
@@ -93,12 +43,8 @@ func GetAlarmSnapshot(ctx context.Context, sn, logTime string) error {
 	defer os.RemoveAll(tempDir) // 程序结束时清理临时目录
 
 	for _, info := range snapshotList {
-		fileRecordModel := query_alarm_snapshot.SiteAlarmSnapshotFileRecord
-
 		// 判断文件是否已经导入
-		_, err := fileRecordModel.WithContext(ctx).
-			Where(fileRecordModel.FileName.Eq(info.FileName)).
-			First()
+		_, err := repositories.QuerySnapshotFileRecord(ctx, info.FileName)
 
 		if errors.Is(err, gorm.ErrRecordNotFound) {
 			// 不存在 - 继续运行
@@ -112,7 +58,7 @@ func GetAlarmSnapshot(ctx context.Context, sn, logTime string) error {
 		}
 
 		// 获取站点信息,获取设备时区信息
-		res, err := controllers.QuerySiteInfoList(controllers.SiteInfoPara{
+		res, err := repositories.QuerySiteInfoList(dto.SiteInfoPara{
 			SN:       &info.LinkId,
 			PageSize: 1,
 			PageNum:  1,
@@ -174,7 +120,7 @@ func GetAlarmSnapshot(ctx context.Context, sn, logTime string) error {
 			return err
 		}
 
-		fileRecord := FileRecord{
+		fileRecord := domain.SiteAlarmSnapshotFileRecord{
 			CodeType:     info.CodeType,
 			FileName:     info.FileName,
 			FileURL:      info.FileUrl,
@@ -187,11 +133,8 @@ func GetAlarmSnapshot(ctx context.Context, sn, logTime string) error {
 		}
 
 		// 创建文件解析记录(冲突时覆盖)
-		err = fileRecordModel.WithContext(ctx).Clauses(clause.OnConflict{
-			UpdateAll: true,
-		}).Create(&fileRecord)
+		err = repositories.InsertSnapshotFileRecord(ctx, &fileRecord)
 		if err != nil {
-			zap.L().Error("文件解析记录数据库创建失败", zap.String("err", err.Error()))
 			return err
 		}
 
@@ -216,7 +159,7 @@ func GetAlarmSnapshot(ctx context.Context, sn, logTime string) error {
 }
 
 // handleAlarmSnapshotFiles 遍历文件列表并处理符合格式的文件
-func handleAlarmSnapshotFiles(ctx context.Context, record FileRecord, fileList []string, zoneInfo string) error {
+func handleAlarmSnapshotFiles(ctx context.Context, record domain.SiteAlarmSnapshotFileRecord, fileList []string, zoneInfo string) error {
 	// 正则规则
 	reDigits10 := regexp.MustCompile(`^\d{10}$`)
 	reZip := regexp.MustCompile(`^\d{8}_\d{6}_\d{3}\.zip$`)
@@ -246,7 +189,7 @@ func handleAlarmSnapshotFiles(ctx context.Context, record FileRecord, fileList [
 	return nil
 }
 
-func parserOldAlarmSnapshotFile(ctx context.Context, record FileRecord, filePath string, zoneInfo string) error {
+func parserOldAlarmSnapshotFile(ctx context.Context, record domain.SiteAlarmSnapshotFileRecord, filePath string, zoneInfo string) error {
 	// 获取文件名(去掉路径)
 	name := filepath.Base(filePath)
 
@@ -274,26 +217,22 @@ func parserOldAlarmSnapshotFile(ctx context.Context, record FileRecord, filePath
 	t := time.Unix(tsInt, 0) // 秒级
 
 	// 存入数据库
-	siteAlarmSnapshotData := SnapshotData{
+	siteAlarmSnapshotData := domain.SiteAlarmSnapshotData{
 		Sn:           record.Sn,
 		FileRecordID: record.ID,
 		LogTime:      t,
 		JSONData:     string(js),
 		ZoneInfo:     zoneInfo,
 	}
-	SnapshotDataModel := query_alarm_snapshot.SiteAlarmSnapshotData
-	err = SnapshotDataModel.WithContext(ctx).Clauses(clause.OnConflict{
-		UpdateAll: true,
-	}).Create(&siteAlarmSnapshotData)
+	err = repositories.InsertSnapshotData(ctx, &siteAlarmSnapshotData)
 	if err != nil {
-		zap.L().Error("文件解析记录数据库创建失败", zap.String("err", err.Error()))
 		return err
 	}
 
 	return nil
 }
 
-func parserNewAlarmSnapshotFile(ctx context.Context, record FileRecord, filePath string, zoneInfo string) error {
+func parserNewAlarmSnapshotFile(ctx context.Context, record domain.SiteAlarmSnapshotFileRecord, filePath string, zoneInfo string) error {
 	// 获取文件名(去掉路径)
 	name := filepath.Base(filePath)
 	// 生成文件夹路径
@@ -331,19 +270,15 @@ func parserNewAlarmSnapshotFile(ctx context.Context, record FileRecord, filePath
 		}
 
 		// 存入数据库
-		siteAlarmSnapshotData := SnapshotData{
+		siteAlarmSnapshotData := domain.SiteAlarmSnapshotData{
 			Sn:           record.Sn,
 			FileRecordID: record.ID,
 			LogTime:      time.Unix(int64(d.TimeStamp), 0),
 			JSONData:     string(dataStr),
 			ZoneInfo:     zoneInfo,
 		}
-		SnapshotDataModel := query_alarm_snapshot.SiteAlarmSnapshotData
-		err = SnapshotDataModel.WithContext(ctx).Clauses(clause.OnConflict{
-			UpdateAll: true,
-		}).Create(&siteAlarmSnapshotData)
+		err = repositories.InsertSnapshotData(ctx, &siteAlarmSnapshotData)
 		if err != nil {
-			zap.L().Error("文件解析记录数据库创建失败", zap.String("err", err.Error()))
 			return err
 		}
 	}

+ 9 - 0
service/main.go

@@ -4,6 +4,7 @@ import (
 	"fwh-toolbox-server/cache"
 	"fwh-toolbox-server/config"
 	"fwh-toolbox-server/database"
+	"fwh-toolbox-server/taskQueue"
 
 	// "fwh-toolbox-server/database"
 	_ "fwh-toolbox-server/docs" // swag 自动生成文档
@@ -31,14 +32,22 @@ func main() {
 	}
 	defer logger.Log.Sync() // 退出前同步zap缓冲区
 
+	// 初始化数据库
 	err = database.Init(cfg)
 	if err != nil {
 		zap.L().Error("database init failed.", zap.String("err", err.Error()))
 		panic(err)
 	}
+
+	// 初始化缓存
 	cache.Init(cfg.RedisAddr)
+
+	// 初始化计划任务
 	scheduler.Init()
 
+	// 初始化任务队列
+	taskQueue.InitTaskQueue()
+
 	app := fiber.New()
 	app.Use(func(c *fiber.Ctx) error {
 		logger.Log.Info("请求", zap.String("path", c.Path()))

+ 4 - 8
service/networkEvents/evioEvents.go

@@ -3,6 +3,7 @@ package networkEvents
 import (
 	"context"
 	"fmt"
+	"fwh-toolbox-server/domain"
 	"fwh-toolbox-server/logger"
 	"fwh-toolbox-server/utils/xlog"
 	"sync"
@@ -12,11 +13,6 @@ import (
 	"go.uber.org/zap"
 )
 
-type XlogSiteInfo struct {
-	Sn   string `json:"sn"`
-	Uuid string `json:"uuid"`
-}
-
 var connMap sync.Map // key - string UUID, value - evio.Conn
 
 type ConnectContext = xlog.ConnectContext
@@ -111,12 +107,12 @@ func EventsData(c evio.Conn, in []byte) (out []byte, action evio.Action) {
 	return
 }
 
-func GetConnectList() []XlogSiteInfo {
-	infoList := make([]XlogSiteInfo, 0)
+func GetConnectList() []domain.XlogSiteInfo {
+	infoList := make([]domain.XlogSiteInfo, 0)
 	connMap.Range(func(key, value any) bool {
 		conn := value.(evio.Conn)
 		ctx := conn.Context().(*ConnectContext)
-		infoList = append(infoList, XlogSiteInfo{
+		infoList = append(infoList, domain.XlogSiteInfo{
 			Sn:   ctx.SN,
 			Uuid: key.(string),
 		})

+ 60 - 0
service/repositories/api/fwh_pro/fwhHttpClient.go

@@ -0,0 +1,60 @@
+package fwh_pro
+
+import (
+	"fwh-toolbox-server/utils/httpClient"
+	"net/http"
+	"net/url"
+	"time"
+)
+
+const (
+	timeout time.Duration = 30 // fwh请求超时30s
+)
+
+// 仅用户登录或者不需要token的请求
+func FwhHttpGetWithoutToken(url string, params url.Values) ([]byte, error) {
+	t := timeout
+	opt := httpClient.HttpClientOptions{
+		Timeout: &t,
+		Header: &http.Header{
+			"Accept": {"application/json", "text/plain", "*/*"},
+		},
+	}
+	return httpClient.HttpGet(url, params, &opt)
+}
+
+func FwhHttpGet(url string, params url.Values) ([]byte, error) {
+	t := timeout
+	opt := httpClient.HttpClientOptions{
+		Timeout: &t,
+		Header: &http.Header{
+			"Logintoken": {getLoginToken()},
+			"Accept":     {"application/json", "text/plain", "*/*"},
+		},
+	}
+	return httpClient.HttpGet(url, params, &opt)
+}
+
+// 仅用户登录或者不需要token的请求
+func FwhHttpPostWithoutToken(url string, body any) ([]byte, error) {
+	t := timeout
+	opt := httpClient.HttpClientOptions{
+		Timeout: &t,
+		Header: &http.Header{
+			"Accept": {"application/json", "text/plain", "*/*"},
+		},
+	}
+	return httpClient.HttpPost(url, body, &opt)
+}
+
+func FwhHttpPost(url string, body any) ([]byte, error) {
+	t := timeout
+	opt := httpClient.HttpClientOptions{
+		Timeout: &t,
+		Header: &http.Header{
+			"Logintoken": {getLoginToken()},
+			"Accept":     {"application/json", "text/plain", "*/*"},
+		},
+	}
+	return httpClient.HttpPost(url, body, &opt)
+}

+ 235 - 0
service/repositories/api/fwh_pro/fwh_eventAlarmLog.go

@@ -0,0 +1,235 @@
+package fwh_pro
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"fwh-toolbox-server/domain"
+	"net/url"
+	"strconv"
+	"time"
+)
+
+type EventAlarmType int
+
+const (
+	Event EventAlarmType = iota // 0
+	Alarm                       // 1
+)
+
+type EventAlarmListResponse struct {
+	Code    int                     `json:"code"`
+	Message string                  `json:"message"`
+	Result  []domain.EventAlarmItem `json:"result"`
+	Success bool                    `json:"success"`
+	Total   uint                    `json:"total"`
+}
+
+// 查询事件事件日志
+func queryEventAndAlarmLog(sn string, params url.Values) ([]byte, error) {
+	token := getLoginToken()
+	userId, _ := getUserId(token)
+
+	// 构造 query 参数
+	params.Add("userId", strconv.Itoa(userId))
+	params.Add("searchName", sn)
+	params.Add("timeType", strconv.Itoa(1)) // 0-通讯时间;1-设备时间
+	params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
+	params.Add("pageNum", strconv.Itoa(1))
+	params.Add("land", "ZH_CN")
+
+	return FwhHttpGet("https://energy.franklinwh.com/hes-gateway/manage/iotDeviceRunLogs", params)
+}
+
+// 查询所有事件告警
+// startTime&endTime 格式 YYYY-MM-DD HH:mm:SS
+func queryAllEventAlarm(sn, startTime, endTime string) ([]byte, error) {
+	// 构造 query 参数
+	params := url.Values{}
+	params.Add("startTime", startTime)
+	params.Add("endTime", endTime)
+
+	return queryEventAndAlarmLog(sn, params)
+}
+
+// 查询事件日志
+// startTime&endTime 格式 YYYY-MM-DD HH:mm:SS
+func queryEventLog(sn, startTime, endTime string) ([]byte, error) {
+	// 构造 query 参数
+	params := url.Values{}
+	params.Add("logType", strconv.Itoa(0))
+	params.Add("startTime", startTime)
+	params.Add("endTime", endTime)
+
+	return queryEventAndAlarmLog(sn, params)
+}
+
+// 查询告警日志
+// startTime&endTime 格式 YYYY-MM-DD HH:mm:SS
+// alarmCode - 支持多个code,用‘,’隔开,“”代表查询所有
+func queryAlarmLog(sn, startTime, endTime, alarmCode string) ([]byte, error) {
+	// 构造 query 参数
+	params := url.Values{}
+	params.Add("logType", strconv.Itoa(1))
+	params.Add("alarmCode", alarmCode)
+	params.Add("startTime", startTime)
+	params.Add("endTime", endTime)
+
+	return queryEventAndAlarmLog(sn, params)
+}
+
+// 查询实时告警
+func queryRealtimeAlarm(sn string) ([]byte, error) {
+	// 构造 query 参数
+	params := url.Values{}
+	params.Add("logType", strconv.Itoa(2))
+
+	return queryEventAndAlarmLog(sn, params)
+}
+
+func queryEventOrAlarmListOnePage(sn string, startTime, endTime time.Time, t EventAlarmType) ([]domain.EventAlarmItem, error) {
+	startTimeStr := startTime.Format(time.DateTime)
+	endTimeStr := endTime.Format(time.DateTime)
+	// TEST
+	fmt.Printf("startTime = %s\n", startTimeStr)
+	fmt.Printf("endTime = %s\n", endTimeStr)
+	var resByte []byte
+	var err error
+	switch t {
+	case Event:
+		resByte, err = queryEventLog(sn, startTimeStr, endTimeStr)
+	case Alarm:
+		resByte, err = queryAlarmLog(sn, startTimeStr, endTimeStr, "")
+	}
+	if err != nil {
+		return nil, err
+	}
+	var eventLogRes EventAlarmListResponse
+	// 反序列化(unmarshal)
+	jsonErr := json.Unmarshal(resByte, &eventLogRes)
+	if jsonErr != nil {
+		return nil, jsonErr
+	}
+
+	if eventLogRes.Code == 401 {
+		// TODO 尝试重新登录
+		return nil, errors.New("certification expired")
+	}
+
+	if eventLogRes.Code != 200 {
+		return nil, fmt.Errorf("查询失败 code: %d, message: %s", eventLogRes.Code, eventLogRes.Message)
+	}
+
+	return eventLogRes.Result, nil
+}
+
+// 从FWH获取事件或告警log数据
+func QueryEventOrAlarmListOnTimeRange(sn string, startTime, endTime time.Time, t EventAlarmType) ([]domain.EventAlarmItem, error) {
+	// 保证 startTime <= endTime
+	if startTime.After(endTime) {
+		return nil, errors.New("startTime > endTime")
+	}
+
+	// 按天拆分时间段
+	var periods []struct{ Start, End time.Time }
+	currTime := startTime
+	for currTime.Before(endTime) {
+		// 当天结束时间
+		dayEnd := time.Date(currTime.Year(), currTime.Month(), currTime.Day(), 23, 59, 59, 0, currTime.Location())
+
+		// 如果当天结束大于 endTime,使用 endTime
+		if dayEnd.After(endTime) {
+			dayEnd = endTime
+		}
+
+		periods = append(periods, struct{ Start, End time.Time }{Start: currTime, End: dayEnd})
+
+		// 下一天开始
+		currTime = dayEnd.Add(time.Second).Truncate(24 * time.Hour)
+		currTime = time.Date(currTime.Year(), currTime.Month(), currTime.Day(), 0, 0, 0, 0, currTime.Location())
+	}
+
+	// 按天查询
+	var eventLogList []domain.EventAlarmItem
+	for _, period := range periods {
+		var eventLogRes []domain.EventAlarmItem = nil
+		periodEndTime := period.End
+		// 检查数据是否超过1000(单次查询上限)
+		for {
+			var err error
+			eventLogRes, err = queryEventOrAlarmListOnePage(sn, period.Start, periodEndTime, t)
+			if err != nil {
+				return nil, err
+			}
+
+			eventLogList = append(eventLogList, eventLogRes...)
+
+			// 判断是否未查完
+			if len(eventLogRes) < int(REQ_PAGE_SIZE) {
+				// 请求完成,退出
+				break
+			}
+
+			// 跳转时间段,再次请求
+			// 提取最后一条数据的设备时间
+			periodEndTime, err = time.Parse(time.DateTime, eventLogRes[len(eventLogRes)-1].LogTime)
+			if err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	return eventLogList, nil
+}
+
+// 查询快照信息
+// logTime 格式 YYYY-MM-DD
+func queryAlarmSnapshotInfo(sn, logTime string, page int) ([]byte, error) {
+	// 构造 query 参数
+	params := url.Values{}
+	params.Add("logTime", logTime)
+	params.Add("linkId", sn)
+	params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
+	params.Add("pageNum", strconv.Itoa(page))
+	params.Add("land", "ZH_CN")
+
+	return FwhHttpGet("https://energy.franklinwh.com/hes-gateway/manage/selectAlarmSnapshotList", params)
+}
+
+func QueryAlarmSnapshotInfo(sn, logTime string) ([]domain.AlarmSnapshotInfo, error) {
+	var page int = 1
+	var infoList []domain.AlarmSnapshotInfo
+
+	for {
+		// 查询告警快照数据
+		resByte, err := queryAlarmSnapshotInfo(sn, logTime, page)
+		if err != nil {
+			return nil, fmt.Errorf("[QueryAlarmSnapshotInfo] %s", err.Error())
+		}
+
+		var alarmSnapshotRes domain.FwhPageResponse[domain.AlarmSnapshotInfo]
+		// 反序列化(unmarshal)
+		jsonErr := json.Unmarshal(resByte, &alarmSnapshotRes)
+		if jsonErr != nil {
+			return nil, fmt.Errorf("[QueryAlarmSnapshotInfo] %s", jsonErr)
+		}
+
+		if alarmSnapshotRes.Code != 200 {
+			return nil, fmt.Errorf("[QueryAlarmSnapshotInfo] 查询失败 code: %d, message: %s", alarmSnapshotRes.Code, alarmSnapshotRes.Message)
+		}
+
+		if alarmSnapshotRes.Total > 10000 {
+			return nil, fmt.Errorf("[QueryAlarmSnapshotInfo] 数据超过10000条")
+		}
+
+		infoList = append(infoList, alarmSnapshotRes.Result...)
+
+		if alarmSnapshotRes.Total <= uint64(page*int(REQ_PAGE_SIZE)) {
+			break
+		}
+
+		page++
+	}
+
+	return infoList, nil
+}

+ 1 - 25
service/ext/api/fwh_pro/fwh_login.go → service/repositories/api/fwh_pro/fwh_login.go

@@ -1,12 +1,9 @@
 package fwh_pro
 
 import (
-	"bytes"
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
-	"io"
-	"net/http"
 	"strings"
 	"time"
 
@@ -59,31 +56,10 @@ func Login() error {
 		Lang:      "ZH_CN",
 	}
 
-	jsonData, _ := json.Marshal(payload)
-
-	// 创建请求
-	req, err := http.NewRequest(
-		http.MethodPost,
-		"https://oss.franklinwh.com/api-user/user/login",
-		bytes.NewBuffer(jsonData))
+	body, err := FwhHttpPostWithoutToken("https://oss.franklinwh.com/api-user/user/login", payload)
 	if err != nil {
-		zap.L().Error("NewRequest failed.", zap.String("err", err.Error()))
 		return err
 	}
-
-	// 设置 Content-Type 和 Authorization
-	req.Header.Set("Content-Type", "application/json")
-
-	// 发送请求
-	client := &http.Client{Timeout: 30 * time.Second}
-	resp, err := client.Do(req)
-	if err != nil {
-		zap.L().Error("Do Send failed.", zap.String("err", err.Error()))
-		return err
-	}
-	defer resp.Body.Close()
-
-	body, _ := io.ReadAll(resp.Body)
 	// logger.Std.Printf("Status: %s\nBody: %s", resp.Status, body)
 
 	var loginRes LoginResponse

+ 147 - 0
service/repositories/api/fwh_pro/fwh_mqttLog.go

@@ -0,0 +1,147 @@
+package fwh_pro
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"fwh-toolbox-server/domain"
+	"net/url"
+	"strconv"
+	"strings"
+	"time"
+)
+
+type MqttLogListResponse struct {
+	Code    int                  `json:"code"`
+	Message string               `json:"message"`
+	Result  []domain.MqttLogItem `json:"result"`
+	Success bool                 `json:"success"`
+	Total   uint                 `json:"total"`
+}
+
+type QueryMqttLogParam struct {
+	SN        *string
+	CmdTypes  []domain.MqttCmdType
+	StartTime *time.Time
+	EndTime   *time.Time
+}
+
+// 查询站点信息
+func queryMqttLog(param QueryMqttLogParam) ([]byte, error) {
+	// 构造 query 参数
+	params := url.Values{}
+	if param.SN != nil {
+		params.Add("searchName", *param.SN)
+	}
+	if len(param.CmdTypes) > 0 {
+		cmdTypesStr := make([]string, len(param.CmdTypes))
+		for i, v := range param.CmdTypes {
+			cmdTypesStr[i] = string(v) // 转成 string
+		}
+		params.Add("cmdTypes", strings.Join(cmdTypesStr, ","))
+	}
+	if param.StartTime != nil {
+		params.Add("startTime", param.StartTime.Format(time.DateTime))
+	}
+	if param.EndTime != nil {
+		params.Add("endTime", param.EndTime.Format(time.DateTime))
+	}
+	if param.StartTime != nil || param.EndTime != nil {
+		params.Add("timeType", "1") // 0-通讯时间 1-设备时间时
+	}
+	params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
+	params.Add("pageNum", strconv.Itoa(1))
+	params.Add("lang", "ZH_CN")
+
+	return FwhHttpGet("https://energy.franklinwh.com/hes-gateway/manage/iotMqttCommlogs", params)
+}
+
+func queryMqttLogListOnePage(sn string, cmdType domain.MqttCmdType, startTime, endTime time.Time) ([]domain.MqttLogItem, error) {
+	param := QueryMqttLogParam{
+		SN:        &sn,
+		CmdTypes:  []domain.MqttCmdType{cmdType},
+		StartTime: &startTime,
+		EndTime:   &endTime,
+	}
+	resByte, err := queryMqttLog(param)
+	if err != nil {
+		return nil, err
+	}
+	var mqttLogRes MqttLogListResponse
+	// 反序列化(unmarshal)
+	jsonErr := json.Unmarshal(resByte, &mqttLogRes)
+	if jsonErr != nil {
+		return nil, jsonErr
+	}
+
+	if mqttLogRes.Code == 401 {
+		// TODO 尝试重新登录
+		return nil, errors.New("certification expired")
+	}
+
+	// 判断是否超出可处理数据上限 10万条数据
+	if mqttLogRes.Total > 10*10000 {
+		return nil, fmt.Errorf("超出数据请求上限 - Total: %d (>10万)", mqttLogRes.Total)
+	}
+
+	return mqttLogRes.Result, nil
+}
+
+// 从FWH获取MQTT log数据
+func QueryMqttLogListOnTimeRange(sn string, cmdType domain.MqttCmdType, startTime, endTime time.Time) ([]domain.MqttLogItem, error) {
+	// 保证 startTime <= endTime
+	if startTime.After(endTime) {
+		return nil, errors.New("startTime > endTime")
+	}
+
+	// 按天拆分时间段
+	var periods []struct{ Start, End time.Time }
+	currTime := startTime
+	for currTime.Before(endTime) {
+		// 当天结束时间
+		dayEnd := time.Date(currTime.Year(), currTime.Month(), currTime.Day(), 23, 59, 59, 0, currTime.Location())
+
+		// 如果当天结束大于 endTime,使用 endTime
+		if dayEnd.After(endTime) {
+			dayEnd = endTime
+		}
+
+		periods = append(periods, struct{ Start, End time.Time }{Start: currTime, End: dayEnd})
+
+		// 下一天开始
+		currTime = dayEnd.Add(time.Second).Truncate(24 * time.Hour)
+		currTime = time.Date(currTime.Year(), currTime.Month(), currTime.Day(), 0, 0, 0, 0, currTime.Location())
+	}
+
+	// 按天查询
+	var mqttLogList []domain.MqttLogItem
+	for _, period := range periods {
+		var mqttLogRes []domain.MqttLogItem = nil
+		periodEndTime := period.End
+		// 检查数据是否超过1000(单次查询上限)
+		for {
+			var err error
+			mqttLogRes, err = queryMqttLogListOnePage(sn, cmdType, period.Start, periodEndTime)
+			if err != nil {
+				return nil, err
+			}
+
+			mqttLogList = append(mqttLogList, mqttLogRes...)
+
+			// 判断是否未查完
+			if len(mqttLogRes) < int(REQ_PAGE_SIZE) {
+				// 请求完成,退出
+				break
+			}
+
+			// 跳转时间段,再次请求
+			// 提取最后一条数据的设备时间
+			periodEndTime, err = time.Parse(time.DateTime, mqttLogRes[len(mqttLogRes)-1].LogTime)
+			if err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	return mqttLogList, nil
+}

+ 40 - 0
service/repositories/api/fwh_pro/fwh_powerFlow.go

@@ -0,0 +1,40 @@
+package fwh_pro
+
+import (
+	"encoding/json"
+	"errors"
+	"fwh-toolbox-server/dto"
+	"net/url"
+	"time"
+)
+
+// 查询站点功率流信息
+func querySitePowerFlow(sn string, day time.Time) ([]byte, error) {
+	// 构造 query 参数
+	params := url.Values{}
+	params.Add("gatewayId", sn)
+	params.Add("dayTime", day.Format(time.DateOnly)) // 会自动 URL 编码
+
+	return FwhHttpGet("https://energy.franklinwh.com/api-energy/power/getFhpPowerByDayOld", params)
+}
+
+// 从fwh_pro获取站点功率流
+func QuerySitePowerFlow(site string, day time.Time) (dto.SitePowerFlowResponse, error) {
+	resByte, err := querySitePowerFlow(site, day)
+	if err != nil {
+		return dto.SitePowerFlowResponse{}, err
+	}
+	var sitePowerFlowRes dto.SitePowerFlowResponse
+	// 反序列化(unmarshal)
+	jsonErr := json.Unmarshal(resByte, &sitePowerFlowRes)
+	if jsonErr != nil {
+		return dto.SitePowerFlowResponse{}, jsonErr
+	}
+
+	if sitePowerFlowRes.Code == 401 {
+		// TODO 尝试重新登录
+		return dto.SitePowerFlowResponse{}, errors.New("certification expired")
+	}
+
+	return sitePowerFlowRes, nil
+}

+ 0 - 0
service/ext/api/fwh_pro/fwh_public.go → service/repositories/api/fwh_pro/fwh_public.go


+ 39 - 0
service/repositories/api/fwh_pro/fwh_realtimeAalrm.go

@@ -0,0 +1,39 @@
+package fwh_pro
+
+import (
+	"encoding/json"
+	"errors"
+	"fwh-toolbox-server/dto"
+	"net/url"
+)
+
+// 查询站点告警信息
+func querySiteAlarmList(sn string) ([]byte, error) {
+	// 构造 query 参数
+	params := url.Values{}
+	params.Add("gatewayId", sn)
+	params.Add("land", "ZH_CN")
+
+	return FwhHttpGet("https://energy.franklinwh.com/hes-gateway/manage/getEquipmentAlarm", params)
+}
+
+// 从fwh_pro获取站点告警列表
+func QuerySiteAlarmList(site string) (dto.SiteAlarmListResponse, error) {
+	resByte, err := querySiteAlarmList(site)
+	if err != nil {
+		return dto.SiteAlarmListResponse{}, err
+	}
+	var siteAlarmListRes dto.SiteAlarmListResponse
+	// 反序列化(unmarshal)
+	jsonErr := json.Unmarshal(resByte, &siteAlarmListRes)
+	if jsonErr != nil {
+		return dto.SiteAlarmListResponse{}, jsonErr
+	}
+
+	if siteAlarmListRes.Code == 401 {
+		// TODO 尝试重新登录
+		return dto.SiteAlarmListResponse{}, errors.New("certification expired")
+	}
+
+	return siteAlarmListRes, nil
+}

+ 159 - 0
service/repositories/api/fwh_pro/fwh_siteInfo.go

@@ -0,0 +1,159 @@
+package fwh_pro
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"fwh-toolbox-server/dto"
+	"net/url"
+	"strconv"
+	"strings"
+)
+
+type SiteBaseInfoListPayload struct {
+	Search        *string `json:"search,omitempty"`  // 只能搜索单条SN
+	Serials       *string `json:"serials,omitempty"` // 可搜索多条SN,SN之间以‘,’隔开
+	CountryId     *int    `json:"countryId,omitempty"`
+	ProvinceId    *int    `json:"provinceId,omitempty"`
+	CityId        *int    `json:"cityId,omitempty"`
+	NetworkStatus *int    `json:"networkStatus,omitempty"`
+	DeviceStatus  *int    `json:"deviceStatus,omitempty"`
+	OnOffStatus   *int    `json:"onOffStatus,omitempty"`
+	PageSize      uint    `json:"pageSize"`
+	PageNum       uint    `json:"pageNum"`
+	Lang          string  `json:"lang"`
+}
+
+type SiteInfoListPayload struct {
+	Search     *string `json:"search,omitempty"` // 只能搜索单条SN
+	CountryId  *int    `json:"countryId,omitempty"`
+	ProvinceId *int    `json:"provinceId,omitempty"`
+	CityId     *int    `json:"cityId,omitempty"`
+	PageSize   uint    `json:"pageSize"`
+	PageNum    uint    `json:"pageNum"`
+	Lang       string  `json:"lang"`
+}
+
+type SiteModeInfoPayload struct {
+	GatewayId string `json:"gatewayId"`
+	ShowType  int    `json:"showType"`
+	Lang      string `json:"lang"`
+}
+
+// 查询站点基本信息
+func querySiteBaseInfoList(sn []string, pageSize uint, pageNum uint) ([]byte, error) {
+	snList := strings.Join(sn, ",")
+
+	// 要发送的 JSON
+	payload := SiteBaseInfoListPayload{
+		Serials:  &snList,
+		PageSize: pageSize,
+		PageNum:  pageNum,
+		Lang:     "ZH_CN",
+	}
+
+	// 要发送的表单数据
+	form := url.Values{}
+	form.Set("serials", *payload.Serials) // *string 转 string
+	form.Set("pageSize", fmt.Sprintf("%d", payload.PageSize))
+	form.Set("pageNum", fmt.Sprintf("%d", payload.PageNum))
+	form.Set("lang", payload.Lang)
+
+	return FwhHttpPost("https://energy.franklinwh.com/hes-gateway/manage/selectIotFirmwareGatewayList", form)
+}
+
+// 从fwh_pro获取站点基本信息表
+func QuerySiteBaseInfoList(para dto.SiteBaseInfoPara) (dto.SiteBaseInfoResponse, error) {
+	resByte, err := querySiteBaseInfoList(para.SiteList, para.PageSize, para.PageNum)
+	if err != nil {
+		return dto.SiteBaseInfoResponse{}, err
+	}
+	var siteInfoRes dto.SiteBaseInfoResponse
+	// 反序列化(unmarshal)
+	jsonErr := json.Unmarshal(resByte, &siteInfoRes)
+	if jsonErr != nil {
+		return dto.SiteBaseInfoResponse{}, jsonErr
+	}
+
+	if siteInfoRes.Code == 401 {
+		// TODO 尝试重新登录
+		return dto.SiteBaseInfoResponse{}, errors.New("certification expired")
+	}
+
+	return siteInfoRes, nil
+}
+
+// 查询站点信息
+func querySiteInfoList(sn *string, pageSize uint, pageNum uint) ([]byte, error) {
+	// 构造 query 参数
+	params := url.Values{}
+	if sn != nil {
+		params.Add("search", *sn)
+	}
+	params.Add("searchFlag", fmt.Sprintf("%d", 0)) // 0-全部 1-aGate名称 11-aGate序列号 2-aPower序列号 7-EMS_MAIN版本 8-EMS_DSP版本 10-aPower子版本号 13-协议版本 14-硬件版本 16-备注
+	params.Add("pageSize", fmt.Sprintf("%d", pageSize))
+	params.Add("pageNum", fmt.Sprintf("%d", pageNum))
+	params.Add("lang", "ZH_CN")
+
+	return FwhHttpGet("https://energy.franklinwh.com/hes-gateway/manage/selectIotGatewayList", params)
+}
+
+// 从fwh_pro获取站点基本信息表
+func QuerySiteInfoList(para dto.SiteInfoPara) (dto.SiteInfoResponse, error) {
+	resByte, err := querySiteInfoList(para.SN, para.PageSize, para.PageNum)
+	if err != nil {
+		return dto.SiteInfoResponse{}, err
+	}
+	var siteInfoRes dto.SiteInfoResponse
+	// 反序列化(unmarshal)
+	jsonErr := json.Unmarshal(resByte, &siteInfoRes)
+	if jsonErr != nil {
+		return dto.SiteInfoResponse{}, jsonErr
+	}
+
+	if siteInfoRes.Code == 401 {
+		// TODO 尝试重新登录
+		return dto.SiteInfoResponse{}, errors.New("certification expired")
+	}
+
+	return siteInfoRes, nil
+}
+
+// 查询站点工作模式信息
+func querySiteModeInfo(sn string) ([]byte, error) {
+	// 要发送的 JSON
+	payload := SiteModeInfoPayload{
+		GatewayId: sn,
+		ShowType:  1,
+		Lang:      "ZH_CN",
+	}
+
+	// 要发送的表单数据
+	form := url.Values{}
+	form.Set("gatewayId", payload.GatewayId)
+	form.Set("showType", strconv.Itoa(payload.ShowType))
+	form.Set("lang", payload.Lang)
+
+	return FwhHttpPost("https://energy.franklinwh.com/hes-gateway/manage/tou/getGatewayTouListV2", form)
+}
+
+// 从fwh_pro获取站点工作模式信息
+func QuerySiteModeInfo(site string) (dto.SiteModeInfoResponse, error) {
+	resByte, err := querySiteModeInfo(site)
+	if err != nil {
+		return dto.SiteModeInfoResponse{}, err
+	}
+	var siteModeInfoRes dto.SiteModeInfoResponse
+	// 反序列化(unmarshal)
+	jsonErr := json.Unmarshal(resByte, &siteModeInfoRes)
+	if jsonErr != nil {
+		return dto.SiteModeInfoResponse{}, jsonErr
+	}
+
+	if siteModeInfoRes.Code == 401 {
+		// TODO 尝试重新登录
+		return dto.SiteModeInfoResponse{}, errors.New("certification expired")
+	}
+
+	return siteModeInfoRes, nil
+}

+ 275 - 0
service/repositories/mongo/sitesAnalysisRepo.go

@@ -0,0 +1,275 @@
+package mongo
+
+import (
+	"context"
+	"fwh-toolbox-server/database"
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/dto"
+	"time"
+
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/bson/primitive"
+	"go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 从数据库获取站点分析列表
+func GetSiteAnalysisManageList(page, size uint, sn, progress *string) (*dto.ResPage[domain.SiteAnalysisInfo], error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection("SiteAnalysisConfig")
+
+	filter := bson.M{}
+	// 条件
+	if sn != nil {
+		filter["sn"] = *sn
+	}
+	if progress != nil {
+		filter["progress"] = *progress
+	}
+
+	findOptions := options.Find().
+		SetProjection(bson.M{
+			"_id":         1,
+			"sn":          1,
+			"description": 1,
+			"item":        1,
+			"progress":    1,
+		}).
+		SetSkip(int64((page - 1) * size)).
+		SetLimit(int64(size))
+
+	cursor, err := collection.Find(ctx, filter, findOptions)
+	if err != nil {
+		return nil, err
+	}
+	defer cursor.Close(ctx)
+
+	var configList []domain.SiteAnalysisInfo
+	for cursor.Next(ctx) {
+		var cfg domain.SiteAnalysisInfo
+		if err := cursor.Decode(&cfg); err != nil {
+			return nil, err
+		}
+		configList = append(configList, cfg)
+	}
+
+	// 查询总数
+	total, err := collection.CountDocuments(ctx, filter)
+	if err != nil {
+		return nil, err
+	}
+
+	res := dto.ResPage[domain.SiteAnalysisInfo]{
+		Data:     configList,
+		PageSize: size,
+		PageNum:  page,
+		Total:    uint(total),
+	}
+
+	return &res, nil
+}
+
+// 从数据库获取一条站点分析配置
+func GetSiteAnalysisConfig(id primitive.ObjectID) (*domain.SiteAnalysisConfig, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection("SiteAnalysisConfig")
+	var cfg domain.SiteAnalysisConfig
+	err := collection.FindOne(ctx, bson.M{"_id": id}).Decode(&cfg)
+	if err != nil {
+		return nil, err
+	}
+
+	return &cfg, nil
+}
+
+// 插入一条站点分析配置到数据库
+func InsertSiteAnalysisConfig(writeConfig domain.SiteAnalysisConfigWrite) (primitive.ObjectID, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	config := domain.SiteAnalysisConfig{
+		Id:          writeConfig.Id,
+		Sn:          writeConfig.Sn,
+		Description: writeConfig.Description,
+		TimePeriod:  writeConfig.TimePeriod,
+		Options:     writeConfig.Options,
+		Item:        []string{},
+		Progress:    domain.ProgressPending,
+	}
+
+	// 生成Item标签
+	if writeConfig.Options.EnableTimeline {
+		config.Item = append(config.Item, "时间线")
+	}
+
+	collection := database.GetCollection("SiteAnalysisConfig")
+
+	res, err := collection.InsertOne(ctx, config)
+	if err != nil {
+		return primitive.ObjectID{}, err
+	}
+
+	return res.InsertedID.(primitive.ObjectID), nil
+}
+
+// 更新一条站点分析配置到数据库
+func UpdateSiteAnalysisConfig(id primitive.ObjectID, writeConfig domain.SiteAnalysisConfigWrite) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	config := domain.SiteAnalysisConfig{
+		Id:          writeConfig.Id,
+		Sn:          writeConfig.Sn,
+		Description: writeConfig.Description,
+		Options:     writeConfig.Options,
+		Item:        []string{},
+		Progress:    domain.ProgressPending,
+	}
+
+	// 生成Item标签
+	if writeConfig.Options.EnableTimeline {
+		config.Item = append(config.Item, "时间线")
+	}
+
+	collection := database.GetCollection("SiteAnalysisConfig")
+
+	// _, err := collection.InsertOne(ctx, config)
+	filter := bson.M{"_id": id} // 按ID匹配
+	update := bson.M{
+		// 如果存在就更新全部字段
+		"$set": config,
+	}
+
+	opts := options.Update().SetUpsert(true)
+
+	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// 更新一条站点分析描述到数据库
+func UpdateSiteAnalysisDescription(id primitive.ObjectID, desc string) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection("SiteAnalysisConfig")
+
+	// _, err := collection.InsertOne(ctx, config)
+	filter := bson.M{"_id": id} // 按ID匹配
+	update := bson.M{
+		// 如果存在就更新全部字段
+		"$set": bson.M{
+			"description": desc,
+		},
+	}
+
+	opts := options.Update().SetUpsert(true)
+
+	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// 更新一条站点分析结果到数据库
+func UpdateSiteAnalysisResult(id primitive.ObjectID, result domain.Progress) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection("SiteAnalysisConfig")
+
+	// _, err := collection.InsertOne(ctx, config)
+	filter := bson.M{"_id": id} // 按ID匹配
+	update := bson.M{
+		// 如果存在就更新非progress字段
+		"$set": bson.M{
+			"progress": result,
+		},
+	}
+
+	opts := options.Update().SetUpsert(true)
+
+	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// 删除一条站点分析配置
+func DeleteSiteAnalysisConfig(id primitive.ObjectID) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	filter := bson.M{"_id": id} // 删除条件
+	collection := database.GetCollection("SiteAnalysisConfig")
+	_, err := collection.DeleteOne(ctx, filter)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// 插入完整分析结果数据
+func InsertSiteAnalysisData(id primitive.ObjectID, data domain.SiteAnalysisData) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection("SiteAnalysisConfig")
+
+	// _, err := collection.InsertOne(ctx, config)
+	filter := bson.M{"_id": id} // 按ID匹配
+	update := bson.M{
+		// 如果存在就更新非progress字段
+		"$set": bson.M{
+			"data": data,
+		},
+	}
+
+	opts := options.Update().SetUpsert(true)
+
+	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// 插入分析结果timeline数据
+func InsertSiteAnalysisTimeline(id primitive.ObjectID, timeline []domain.TimelineItem) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection("SiteAnalysisConfig")
+
+	// _, err := collection.InsertOne(ctx, config)
+	filter := bson.M{"_id": id} // 按ID匹配
+	update := bson.M{
+		// 如果存在就更新非progress字段
+		"$set": bson.M{
+			"data": bson.M{
+				"timeline": timeline,
+			},
+		},
+	}
+
+	opts := options.Update().SetUpsert(true)
+
+	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 175 - 0
service/repositories/mongo/sitesMonitorRepo.go

@@ -0,0 +1,175 @@
+package mongo
+
+import (
+	"context"
+	"fwh-toolbox-server/database"
+	"fwh-toolbox-server/domain"
+	"time"
+
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo"
+	"go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 从数据库获取站点监控配置列表
+func GetSiteMonitorConfig() ([]domain.SitesMonitorConfig, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection("SiteMonitorConfig")
+	cursor, err := collection.Find(ctx, bson.M{}) // 空条件,查全部
+	if err != nil {
+		return nil, err
+	}
+	defer cursor.Close(ctx)
+
+	var configList []domain.SitesMonitorConfig
+	for cursor.Next(ctx) {
+		var cfg domain.SitesMonitorConfig
+		if err := cursor.Decode(&cfg); err != nil {
+			return nil, err
+		}
+		configList = append(configList, cfg)
+	}
+
+	return configList, nil
+}
+
+// 从数据库获取监控名列表
+func GetSiteMonitorNameList() ([]string, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection("SiteMonitorConfig")
+	cursor, err := collection.Find(
+		ctx,
+		bson.M{}, // 空条件,查全部
+		options.Find().SetProjection(bson.M{
+			"name": 1, // 只要 name 字段
+			"_id":  0, // 不要 _id 字段(默认会返回)
+		}),
+	)
+	if err != nil {
+		return nil, err
+	}
+	defer cursor.Close(ctx)
+
+	var nameList []string
+	for cursor.Next(ctx) {
+		var result struct {
+			Name string `bson:"name"`
+		}
+		if err := cursor.Decode(&result); err != nil {
+			return nil, err
+		}
+		nameList = append(nameList, result.Name)
+	}
+
+	return nameList, nil
+}
+
+// 从数据库获取一条站点监控配置
+func GetOneSiteMonitorConfig(name string) ([]domain.SitesMonitorConfig, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection("SiteMonitorConfig")
+	var cfg domain.SitesMonitorConfig
+	err := collection.FindOne(ctx, bson.M{"name": name}).Decode(&cfg)
+	if err == mongo.ErrNoDocuments {
+		return []domain.SitesMonitorConfig{}, nil
+	} else if err != nil {
+		return nil, err
+	}
+
+	var configList []domain.SitesMonitorConfig
+	configList = append(configList, cfg)
+
+	return configList, nil
+}
+
+// 插入一条站点监控配置到数据库
+func InsertSiteMonitorConfig(config domain.SitesMonitorConfig) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection("SiteMonitorConfig")
+
+	// _, err := collection.InsertOne(ctx, config)
+	filter := bson.M{"name": config.Name} // 按用户名匹配
+	update := bson.M{
+		"$set": config, // 如果存在就更新所有字段
+	}
+
+	opts := options.Update().SetUpsert(true)
+
+	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// 更新一条站点监控配置到数据库
+func UpdateSiteMonitorConfig(name string, config domain.SitesMonitorConfig) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection("SiteMonitorConfig")
+
+	// _, err := collection.InsertOne(ctx, config)
+	filter := bson.M{"name": name} // 按用户名匹配
+	update := bson.M{
+		"$set": config, // 如果存在就更新所有字段
+	}
+
+	opts := options.Update().SetUpsert(true)
+
+	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// 删除一条站点监控配置
+func DeleteSiteMonitorConfig(name string) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	filter := bson.M{"name": name} // 删除条件
+	collection := database.GetCollection("SiteMonitorConfig")
+	_, err := collection.DeleteOne(ctx, filter)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// 更新一条站点监控页面配置到数据库
+func UpdateSiteMonitorPageConfig(name string, config domain.MonitorPageConfig) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection("SiteMonitorConfig")
+
+	// _, err := collection.InsertOne(ctx, config)
+	filter := bson.M{"name": name} // 按用户名匹配
+	update := bson.M{
+		"$set": bson.M{
+			"pageConfig": config, // 更新页面配置字段
+		},
+	}
+
+	opts := options.Update().SetUpsert(true)
+
+	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 46 - 0
service/repositories/mongo/xlogRepo.go

@@ -0,0 +1,46 @@
+package mongo
+
+import (
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/dto"
+	"fwh-toolbox-server/networkEvents"
+)
+
+type XlogManageInfo = domain.XlogSiteInfo
+type XlogManagePage = dto.ResPage[XlogManageInfo]
+
+func GetXlogManageList(page, size uint) XlogManagePage {
+	infoList := networkEvents.GetConnectList()
+	infoListLen := uint(len(infoList))
+	resPage := XlogManagePage{
+		Data:     []XlogManageInfo{},
+		PageNum:  page,
+		PageSize: size,
+		Total:    infoListLen,
+	}
+
+	if infoListLen == 0 {
+		return resPage
+	}
+
+	if page < 1 {
+		return resPage
+	}
+
+	startIndex := (page - 1) * size
+	endIndex := page * size
+	if endIndex > infoListLen {
+		endIndex = infoListLen
+	}
+
+	if endIndex <= startIndex {
+		return resPage
+	}
+
+	resPage.Data = infoList[startIndex:endIndex]
+	return resPage
+}
+
+func SendXlogCmd(uuid, cmd string) (string, error) {
+	return networkEvents.SendXlogCmdDate(uuid, []byte(cmd))
+}

+ 1 - 1
service/controllers/mysql_controllers.go → service/repositories/mysql/fwh_pro_db/fwhDbRepo.go

@@ -1,4 +1,4 @@
-package controllers
+package fwh_pro_db
 
 import (
 	"context"

+ 29 - 0
service/repositories/pgsql/alarmSnapshot/siteAlarmSnapshotData.go

@@ -0,0 +1,29 @@
+package alarmSnapshot
+
+import (
+	"context"
+	"fwh-toolbox-server/models/model_pgsql/model_alarm_snapshot"
+	"fwh-toolbox-server/models/model_pgsql/query_alarm_snapshot"
+	"time"
+
+	"go.uber.org/zap"
+	"gorm.io/gorm/clause"
+)
+
+type SnapshotData = model_alarm_snapshot.SiteAlarmSnapshotData
+
+func InsertSnapshotData(c context.Context, data *SnapshotData) error {
+	ctx, cancel := context.WithTimeout(c, 5*time.Second)
+	defer cancel()
+
+	SnapshotDataModel := query_alarm_snapshot.SiteAlarmSnapshotData
+	err := SnapshotDataModel.WithContext(ctx).Clauses(clause.OnConflict{
+		UpdateAll: true,
+	}).Create(data)
+	if err != nil {
+		zap.L().Error("文件解析记录数据库创建失败", zap.String("err", err.Error()))
+		return err
+	}
+
+	return nil
+}

+ 43 - 0
service/repositories/pgsql/alarmSnapshot/siteAlarmSnapshotFileRecord.go

@@ -0,0 +1,43 @@
+package alarmSnapshot
+
+import (
+	"context"
+	"fwh-toolbox-server/models/model_pgsql/model_alarm_snapshot"
+	"fwh-toolbox-server/models/model_pgsql/query_alarm_snapshot"
+	"time"
+
+	"go.uber.org/zap"
+	"gorm.io/gorm/clause"
+)
+
+type FileRecord = model_alarm_snapshot.SiteAlarmSnapshotFileRecord
+
+// 查询文件解析记录
+func QuerySnapshotFileRecord(c context.Context, fileName string) (*FileRecord, error) {
+	ctx, cancel := context.WithTimeout(c, 5*time.Second)
+	defer cancel()
+
+	fileRecordModel := query_alarm_snapshot.SiteAlarmSnapshotFileRecord
+	data, err := fileRecordModel.WithContext(ctx).
+		Where(fileRecordModel.FileName.Eq(fileName)).
+		First()
+
+	return data, err
+}
+
+// 插入文件解析记录(冲突时覆盖)
+func InsertSnapshotFileRecord(c context.Context, data *FileRecord) error {
+	ctx, cancel := context.WithTimeout(c, 5*time.Second)
+	defer cancel()
+
+	fileRecordModel := query_alarm_snapshot.SiteAlarmSnapshotFileRecord
+	err := fileRecordModel.WithContext(ctx).Clauses(clause.OnConflict{
+		UpdateAll: true,
+	}).Create(data)
+	if err != nil {
+		zap.L().Error("文件解析记录数据库创建失败", zap.String("err", err.Error()))
+		return err
+	}
+
+	return nil
+}

+ 64 - 0
service/repositories/repositories.go

@@ -0,0 +1,64 @@
+package repositories
+
+import (
+	"fwh-toolbox-server/repositories/api/fwh_pro"
+	"fwh-toolbox-server/repositories/mongo"
+	"fwh-toolbox-server/repositories/pgsql/alarmSnapshot"
+)
+
+// 聚合包
+
+/*************** API ***************/
+
+// fwh_pro
+type EventAlarmType = fwh_pro.EventAlarmType
+
+const (
+	Event EventAlarmType = fwh_pro.Event // 0
+	Alarm EventAlarmType = fwh_pro.Alarm // 1
+)
+
+var QuerySiteBaseInfoList = fwh_pro.QuerySiteBaseInfoList
+var QuerySiteInfoList = fwh_pro.QuerySiteInfoList
+var QuerySiteModeInfo = fwh_pro.QuerySiteModeInfo
+var QuerySitePowerFlow = fwh_pro.QuerySitePowerFlow
+var QuerySiteAlarmList = fwh_pro.QuerySiteAlarmList
+
+var QueryEventOrAlarmListOnTimeRange = fwh_pro.QueryEventOrAlarmListOnTimeRange
+var QueryAlarmSnapshotInfo = fwh_pro.QueryAlarmSnapshotInfo
+
+var QueryMqttLogListOnTimeRange = fwh_pro.QueryMqttLogListOnTimeRange
+
+/*************** MONGO ***************/
+// Xlog
+var GetXlogManageList = mongo.GetXlogManageList
+var SendXlogCmd = mongo.SendXlogCmd
+
+// SiteMonitor
+var GetSiteMonitorConfig = mongo.GetSiteMonitorConfig
+var GetSiteMonitorNameList = mongo.GetSiteMonitorNameList
+var GetOneSiteMonitorConfig = mongo.GetOneSiteMonitorConfig
+var InsertSiteMonitorConfig = mongo.InsertSiteMonitorConfig
+var UpdateSiteMonitorConfig = mongo.UpdateSiteMonitorConfig
+var DeleteSiteMonitorConfig = mongo.DeleteSiteMonitorConfig
+var UpdateSiteMonitorPageConfig = mongo.UpdateSiteMonitorPageConfig
+
+// SiteAnalysis
+var GetSiteAnalysisManageList = mongo.GetSiteAnalysisManageList
+var GetSiteAnalysisConfig = mongo.GetSiteAnalysisConfig
+var InsertSiteAnalysisConfig = mongo.InsertSiteAnalysisConfig
+var UpdateSiteAnalysisConfig = mongo.UpdateSiteAnalysisConfig
+var UpdateSiteAnalysisDescription = mongo.UpdateSiteAnalysisDescription
+var UpdateSiteAnalysisResult = mongo.UpdateSiteAnalysisResult
+var DeleteSiteAnalysisConfig = mongo.DeleteSiteAnalysisConfig
+var InsertSiteAnalysisData = mongo.InsertSiteAnalysisData
+var InsertSiteAnalysisTimeline = mongo.InsertSiteAnalysisTimeline
+
+/*************** PGSQL ***************/
+
+// Alarm Snapshot
+var QuerySnapshotFileRecord = alarmSnapshot.QuerySnapshotFileRecord
+var InsertSnapshotFileRecord = alarmSnapshot.InsertSnapshotFileRecord
+var InsertSnapshotData = alarmSnapshot.InsertSnapshotData
+
+/*************** MYSQL ***************/

+ 0 - 57
service/routes/routes.go

@@ -1,69 +1,12 @@
 package routes
 
 import (
-	// "fwh-toolbox-server/controllers"
 	_ "fwh-toolbox-server/docs"
-	"time"
 
 	"github.com/gofiber/fiber/v2"
 	"github.com/gofiber/swagger"
-	"github.com/google/uuid"
 )
 
-const traceIdHeader string = "X-Trace-Id"
-
-type Response[T any] struct {
-	Code      int    `json:"code"`
-	Message   string `json:"message"`
-	Data      T      `json:"data"`
-	TraceID   string `json:"traceId"`
-	Timestamp int64  `json:"timestamp"`
-}
-
-type ResponseNoData struct {
-	Code      int    `json:"code"`
-	Message   string `json:"message"`
-	TraceID   string `json:"traceId"`
-	Timestamp int64  `json:"timestamp"`
-}
-
-func JSONResponse[T any](c *fiber.Ctx, code int, message string, data T) error {
-	traceId := c.Get(traceIdHeader) // 从请求头取
-	if traceId == "" {
-		traceId = uuid.New().String() // 没有就生成
-	}
-
-	// 设置到响应头,让前端也能拿到
-	c.Set(traceIdHeader, traceId)
-
-	resp := Response[T]{
-		Code:      code,
-		Message:   message,
-		Data:      data,
-		TraceID:   traceId,
-		Timestamp: time.Now().UnixMilli(),
-	}
-	return c.JSON(resp)
-}
-
-func JSONResponseNoData(c *fiber.Ctx, code int, message string) error {
-	traceId := c.Get(traceIdHeader) // 从请求头取
-	if traceId == "" {
-		traceId = uuid.New().String() // 没有就生成
-	}
-
-	// 设置到响应头,让前端也能拿到
-	c.Set(traceIdHeader, traceId)
-
-	resp := ResponseNoData{
-		Code:      code,
-		Message:   message,
-		TraceID:   traceId,
-		Timestamp: time.Now().UnixMilli(),
-	}
-	return c.JSON(resp)
-}
-
 func Setup(app *fiber.App) {
 	api := app.Group("/api")
 	// api.Get("/keyAccounts/sunrun/state", controllers.GetUsers)

+ 9 - 105
service/routes/sitesAnalysisRoutes.go

@@ -9,125 +9,29 @@ package routes
 
 import (
 	"fwh-toolbox-server/controllers"
-	"strconv"
 
 	"github.com/gofiber/fiber/v2"
-	"go.mongodb.org/mongo-driver/bson/primitive"
 )
 
 func SitesAnalysisSetup(api fiber.Router) {
 	// 获取站点分析管理名称表
-	api.Get("/sitesAnalysis/manage/analysisList", func(c *fiber.Ctx) error {
-		pageInt, _ := strconv.Atoi(c.Query("pageNum", "1"))
-		sizeInt, _ := strconv.Atoi(c.Query("pageSize", "20"))
-
-		if pageInt <= 0 || sizeInt <= 0 {
-			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
-				"code":    400,
-				"message": "Invalid request para",
-			})
-		}
-
-		sn := c.Query("sn", "")
-		progress := c.Query("progress", "")
-
-		var snP *string = nil
-		if sn != "" {
-			snP = &sn
-		}
-
-		var progressP *string = nil
-		if progress != "" {
-			progressP = &progress
-		}
-
-		listPage, err := controllers.GetSiteAnalysisManageList(uint(pageInt), uint(sizeInt), snP, progressP)
-		if err != nil {
-			return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
-		}
-		return JSONResponse(c, 0, "success", listPage)
-	})
+	api.Get("/sitesAnalysis/manage/analysisList", controllers.GetSitesAnalysisList)
 
 	// 创建一个分析配置
-	api.Post("/sitesAnalysis/manage/config", func(c *fiber.Ctx) error {
-		var body controllers.SiteAnalysisConfigWrite
-		if err := c.BodyParser(&body); err != nil {
-			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
-				"code":    400,
-				"message": "Invalid request body",
-			})
-		}
-
-		if err := controllers.InsertSiteAnalysisConfig(body); err != nil {
-			return JSONResponseNoData(c, 500, "Database write failed. (MongoDb)")
-		}
-		return JSONResponse(c, 0, "success", body)
-	})
+	api.Post("/sitesAnalysis/manage/config", controllers.CreateSitesAnalysis)
 
 	// 获取单个分析配置
-	api.Get("/sitesAnalysis/manage/config/:id", func(c *fiber.Ctx) error {
-		id := c.Params("id")
-
-		objID, err := primitive.ObjectIDFromHex(id)
-		if err != nil {
-			return JSONResponseNoData(c, 400, "Invalid request para :id")
-		}
-
-		config, err := controllers.GetSiteAnalysisConfig(objID)
-		if err != nil {
-			return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
-		}
-		return JSONResponse(c, 0, "success", config)
-	})
+	api.Get("/sitesAnalysis/manage/config/:id", controllers.GetOneSiteAnalysisConfig)
 
 	// 更新一个分析配置
-	api.Post("/sitesAnalysis/manage/config/:id", func(c *fiber.Ctx) error {
-		id := c.Params("id")
-		var body controllers.SiteAnalysisConfigWrite
-		if err := c.BodyParser(&body); err != nil {
-			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
-				"code":    400,
-				"message": "Invalid request body",
-			})
-		}
-
-		objID, err := primitive.ObjectIDFromHex(id)
-		if err != nil {
-			return JSONResponseNoData(c, 400, "Invalid request para :id")
-		}
-
-		if err := controllers.UpdateSiteAnalysisConfig(objID, body); err != nil {
-			return JSONResponseNoData(c, 500, "Database write failed. (MongoDb)")
-		}
-		return JSONResponse(c, 0, "success", body)
-	})
-
-	// 删除一个监控
-	api.Delete("/sitesAnalysis/manage/config/:id", func(c *fiber.Ctx) error {
-		id := c.Params("id")
+	api.Post("/sitesAnalysis/manage/config/:id", controllers.UpdateOneSiteAnalysisConfig)
 
-		objID, err := primitive.ObjectIDFromHex(id)
-		if err != nil {
-			return JSONResponseNoData(c, 400, "Invalid request para :id")
-		}
+	// 更新一个分析说明
+	api.Post("/sitesAnalysis/manage/description/:id", controllers.UpdateOneSiteAnalysisDescription)
 
-		err = controllers.DeleteSiteAnalysisConfig(objID)
-		if err != nil {
-			return JSONResponseNoData(c, 500, "Database delete failed. (MongoDb)")
-		}
-		return JSONResponseNoData(c, 0, "success")
-	})
+	// 删除一个分析数据
+	api.Delete("/sitesAnalysis/manage/config/:id", controllers.DeleteOneSiteAnalysis)
 
 	// 获取指定ID的分析数据
-	// api.Get("/sitesAnalysis/data/:id", func(c *fiber.Ctx) error {
-	// 	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
-	// 	if err != nil {
-	// 		return JSONResponseNoData(c, 400, "Invalid request id")
-	// 	}
-	// 	data, err := controllers.GetSiteAnalysisData(id)
-	// 	if err != nil {
-	// 		return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
-	// 	}
-	// 	return JSONResponse(c, 0, "success", data)
-	// })
+	api.Get("/sitesAnalysis/data/:id", controllers.GetOneSiteAnalysisData)
 }

+ 12 - 155
service/routes/sitesMonitorRoutes.go

@@ -14,187 +14,44 @@ package routes
 
 import (
 	"fwh-toolbox-server/controllers"
-	"time"
 
 	"github.com/gofiber/fiber/v2"
-	"go.uber.org/zap"
 )
 
-// 在sitesMonitorControllers.go中定义
-// type SitesMonitorConfig struct {
-// 	Name        string   `json:"name"`
-// 	Description string   `json:"description"`
-// 	SiteList    []string `json:"siteList"`
-// }
-
 func SitesMonitorSetup(api fiber.Router) {
 	// 获取站点监控管理名称表
-	api.Get("/sitesMonitor/manage/nameList", func(c *fiber.Ctx) error {
-		names, err := controllers.GetSiteMonitorNameList()
-		if err != nil {
-			zap.L().Error("查询站点监控管理名称表错误。", zap.String("err", err.Error()))
-			return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
-		}
-		return JSONResponse(c, 0, "success", names)
-	})
+	api.Get("/sitesMonitor/manage/nameList", controllers.GetSitesMonitorNameList)
 
 	// 获取单个监控配置
-	api.Get("/sitesMonitor/manage/config/:name", func(c *fiber.Ctx) error {
-		name := c.Params("name")
-		configList, err := controllers.GetOneSiteMonitorConfig(name)
-		if err != nil {
-			zap.L().Error("查询单个监控配置错误。", zap.String("err", err.Error()))
-			return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
-		} else if len(configList) != 1 {
-			return JSONResponseNoData(c, 204, "User not found")
-		}
-		return JSONResponse(c, 0, "success", configList[0])
-	})
+	api.Get("/sitesMonitor/manage/config/:name", controllers.GetSitesMonitorConfig)
 
 	// 更新一个监控配置
-	api.Post("/sitesMonitor/manage/config/:name", func(c *fiber.Ctx) error {
-		name := c.Params("name")
-		var body controllers.SitesMonitorConfig
-		if err := c.BodyParser(&body); err != nil {
-			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
-				"code":    400,
-				"message": "Invalid request body",
-			})
-		}
-		err := controllers.UpdateSiteMonitorConfig(name, body)
-		if err != nil {
-			zap.L().Error("更新一个监控配置错误。", zap.String("err", err.Error()))
-			return JSONResponseNoData(c, 500, "Database write failed. (MongoDb)")
-		}
-		return JSONResponse(c, 0, "success", body)
-	})
+	api.Post("/sitesMonitor/manage/config/:name", controllers.UpdateSitesMonitorConfig)
 
 	// 更新一个监控页面配置
-	api.Post("/sitesMonitor/manage/pageConfig/:name", func(c *fiber.Ctx) error {
-		name := c.Params("name")
-		var body controllers.MonitorPageConfig
-		if err := c.BodyParser(&body); err != nil {
-			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
-				"code":    400,
-				"message": "Invalid request body",
-			})
-		}
-		err := controllers.UpdateSiteMonitorPageConfig(name, body)
-		if err != nil {
-			zap.L().Error("更新一个监控页面配置错误。", zap.String("err", err.Error()))
-			return JSONResponseNoData(c, 500, "Database write failed. (MongoDb)")
-		}
-		return JSONResponse(c, 0, "success", body)
-	})
+	api.Post("/sitesMonitor/manage/pageConfig/:name", controllers.UpdateSitesMonitorPageConfig)
 
 	// 删除一个监控
-	api.Delete("/sitesMonitor/manage/config/:name", func(c *fiber.Ctx) error {
-		name := c.Params("name")
-		err := controllers.DeleteSiteMonitorConfig(name)
-		if err != nil {
-			zap.L().Error("删除一个监控错误。", zap.String("err", err.Error()))
-			return JSONResponseNoData(c, 500, "Database delete failed. (MongoDb)")
-		}
-		return JSONResponseNoData(c, 0, "success")
-	})
+	api.Delete("/sitesMonitor/manage/config/:name", controllers.DeleteSitesMonitorConfig)
 
 	// 获取所有监控配置
-	api.Get("/sitesMonitor/manage/config", func(c *fiber.Ctx) error {
-		configList, err := controllers.GetSiteMonitorConfig()
-		if err != nil {
-			zap.L().Error("查询所有监控配置错误。", zap.String("err", err.Error()))
-			return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
-		}
-		return JSONResponse(c, 0, "success", configList)
-	})
+	api.Get("/sitesMonitor/manage/config", controllers.GetSitesMonitorAllConfig)
 
 	// 创建一个监控配置
-	api.Post("/sitesMonitor/manage/config", func(c *fiber.Ctx) error {
-		var body controllers.SitesMonitorConfig
-		if err := c.BodyParser(&body); err != nil {
-			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
-				"code":    400,
-				"message": "Invalid request body",
-			})
-		}
-		err := controllers.InsertSiteMonitorConfig(body)
-		if err != nil {
-			zap.L().Error("创建一个监控配置错误。", zap.String("err", err.Error()))
-			return JSONResponseNoData(c, 500, "Database write failed. (MongoDb)")
-		}
-		return JSONResponse(c, 0, "success", body)
-	})
+	api.Post("/sitesMonitor/manage/config", controllers.CreateSitesMonitorConfig)
 
 	// 批量获取站点信息表
-	api.Post("/sitesMonitor/site/infoList", func(c *fiber.Ctx) error {
-		var body controllers.SiteBaseInfoPara
-		if err := c.BodyParser(&body); err != nil {
-			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
-				"code":    400,
-				"message": "Invalid request body",
-			})
-		}
-		infoList, err := controllers.QuerySiteBaseInfoList(body)
-		if err != nil {
-			zap.L().Error("查询批量站点信息表错误。", zap.String("err", err.Error()))
-			return JSONResponseNoData(c, 500, "Database write failed. (MongoDb)")
-		}
-		return JSONResponse(c, 0, "success", infoList)
-	})
+	api.Post("/sitesMonitor/site/infoList", controllers.GetSiteInfoList)
 
 	// 获取单个站点信息
-	api.Get("/sitesMonitor/site/:site/info", func(c *fiber.Ctx) error {
-		site := c.Params("site")
-		siteInfo, err := controllers.QuerySiteBaseInfoList(controllers.SiteBaseInfoPara{
-			SiteList: []string{site},
-			PageSize: 10,
-			PageNum:  1,
-		})
-		if err != nil {
-			zap.L().Error("查询单个站点信息错误。", zap.String("err", err.Error()))
-			return JSONResponseNoData(c, 500, "fwh_pro query failed.")
-		}
-		return JSONResponse(c, 0, "success", siteInfo)
-	})
+	api.Get("/sitesMonitor/site/:site/info", controllers.GetOneSiteInfo)
 
 	// 获取单个站点工作模式信息
-	api.Get("/sitesMonitor/site/:site/modeInfo", func(c *fiber.Ctx) error {
-		site := c.Params("site")
-		siteModeInfo, err := controllers.QuerySiteModeInfo(site)
-		if err != nil {
-			zap.L().Error("查询单个站点工作模式信息错误。", zap.String("err", err.Error()))
-			return JSONResponseNoData(c, 500, "fwh_pro query failed.")
-		}
-		return JSONResponse(c, 0, "success", siteModeInfo)
-	})
+	api.Get("/sitesMonitor/site/:site/modeInfo", controllers.GetOneSiteModeInfo)
 
 	// 获取单个站点功率流
-	api.Get("/sitesMonitor/site/:site/powerFlow/:day", func(c *fiber.Ctx) error {
-		site := c.Params("site")
-		day := c.Params("day")
-		dayTime, err := time.Parse(time.DateOnly, day)
-		if err != nil {
-			zap.L().Error("查询单个站点功率流参数错误。(day)", zap.String("err", err.Error()))
-			return JSONResponseNoData(c, 500, "day parament error.")
-		}
-
-		sitePowerFlow, err := controllers.QuerySitePowerFlow(site, dayTime)
-		if err != nil {
-			zap.L().Error("查询单个站点功率流错误。", zap.String("err", err.Error()))
-			return JSONResponseNoData(c, 500, "fwh_pro query failed.")
-		}
-		return JSONResponse(c, 0, "success", sitePowerFlow)
-	})
+	api.Get("/sitesMonitor/site/:site/powerFlow/:day", controllers.GetOneSitePowerFlow)
 
 	// 获取单个站点告警列表
-	api.Get("/sitesMonitor/site/:site/alarmList", func(c *fiber.Ctx) error {
-		site := c.Params("site")
-
-		siteAlarmList, err := controllers.QuerySiteAlarmList(site)
-		if err != nil {
-			zap.L().Error("查询单个站点告警列表错误。", zap.String("err", err.Error()))
-			return JSONResponseNoData(c, 500, "fwh_pro query failed.")
-		}
-		return JSONResponse(c, 0, "success", siteAlarmList)
-	})
+	api.Get("/sitesMonitor/site/:site/alarmList", controllers.GetOneSiteAlarmList)
 }

+ 2 - 33
service/routes/xlogRoutes.go

@@ -7,45 +7,14 @@ package routes
 
 import (
 	"fwh-toolbox-server/controllers"
-	"fwh-toolbox-server/logger"
-	"strconv"
 
 	"github.com/gofiber/fiber/v2"
 )
 
 func XlogSetup(api fiber.Router) {
 	// 获取站点分析管理名称表
-	api.Get("/xlog/manage/siteList", func(c *fiber.Ctx) error {
-		pageInt, _ := strconv.Atoi(c.Query("pageNum", "1"))
-		sizeInt, _ := strconv.Atoi(c.Query("pageSize", "20"))
-
-		if pageInt <= 0 || sizeInt <= 0 {
-			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
-				"code":    400,
-				"message": "Invalid request para",
-			})
-		}
-
-		listPage := controllers.GetXlogManageList(uint(pageInt), uint(sizeInt))
-		return JSONResponse(c, 0, "success", listPage)
-	})
+	api.Get("/xlog/manage/siteList", controllers.GetXlogManageSiteList)
 
 	// 发送命令
-	api.Post("/xlog/terminal/:uuid", func(c *fiber.Ctx) error {
-		uuid := c.Params("uuid")
-		logger.Std.Println(uuid)
-		var body string
-		if err := c.BodyParser(&body); err != nil {
-			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
-				"code":    400,
-				"message": "Invalid request body",
-			})
-		}
-
-		data, err := controllers.SendXlogCmd(uuid, body)
-		if err != nil {
-			return JSONResponseNoData(c, 500, err.Error())
-		}
-		return JSONResponse(c, 0, "success", data)
-	})
+	api.Post("/xlog/terminal/:uuid", controllers.PostXlogTerminalSend)
 }

+ 97 - 0
service/services/sitesAnalysisServices.go

@@ -0,0 +1,97 @@
+package services
+
+import (
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/dto"
+	"fwh-toolbox-server/repositories"
+	"fwh-toolbox-server/taskQueue"
+
+	"go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// 从数据库获取站点分析列表
+func GetSiteAnalysisManageList(page, size uint, sn, progress *string) (*dto.ResPage[domain.SiteAnalysisInfo], error) {
+	return repositories.GetSiteAnalysisManageList(page, size, sn, progress)
+}
+
+// 从数据库获取一条站点分析配置
+func GetSiteAnalysisConfig(id primitive.ObjectID) (*domain.SiteAnalysisConfig, error) {
+	return repositories.GetSiteAnalysisConfig(id)
+}
+
+// 插入一条站点分析配置到数据库
+func InsertSiteAnalysisConfig(writeConfig domain.SiteAnalysisConfigWrite) error {
+	id, err := repositories.InsertSiteAnalysisConfig(writeConfig)
+	if err != nil {
+		return err
+	}
+
+	taskQueue.EnqueueSiteAnalysisTask(domain.TaskSiteAnalysis{
+		Id:         id,
+		Sn:         writeConfig.Sn,
+		TimePeriod: writeConfig.TimePeriod,
+		Options: domain.SiteAnalysisOptions{
+			EnableTimeline: writeConfig.Options.EnableTimeline,
+		},
+	})
+
+	return nil
+}
+
+// 更新一条站点分析配置到数据库
+func UpdateSiteAnalysisConfig(id primitive.ObjectID, writeConfig domain.SiteAnalysisConfigWrite) error {
+	err := repositories.UpdateSiteAnalysisConfig(id, writeConfig)
+	if err != nil {
+		return err
+	}
+
+	taskQueue.EnqueueSiteAnalysisTask(domain.TaskSiteAnalysis{
+		Id:         writeConfig.Id,
+		Sn:         writeConfig.Sn,
+		TimePeriod: writeConfig.TimePeriod,
+		Options: domain.SiteAnalysisOptions{
+			EnableTimeline: writeConfig.Options.EnableTimeline,
+		},
+	})
+
+	return nil
+}
+
+// 更新一条站点分析配置到数据库
+func UpdateSiteAnalysisDescription(id primitive.ObjectID, desc string) error {
+	err := repositories.UpdateSiteAnalysisDescription(id, desc)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// 删除一条站点分析配置
+func DeleteSiteAnalysisConfig(id primitive.ObjectID) error {
+	err := repositories.DeleteSiteAnalysisConfig(id)
+	if err != nil {
+		return err
+	}
+
+	// TODO 需删除相关数据
+
+	return nil
+}
+
+// 从数据库获取站点分析数据
+func GetSiteAnalysisData(id primitive.ObjectID) (*dto.SiteAnalysisDataInfo, error) {
+	cfg, err := repositories.GetSiteAnalysisConfig(id)
+	if err != nil {
+		return nil, err
+	}
+
+	data := dto.SiteAnalysisDataInfo{
+		Id:       cfg.Id,
+		Sn:       cfg.Sn,
+		Progress: cfg.Progress,
+		Data:     cfg.Data,
+	}
+
+	return &data, nil
+}

+ 68 - 0
service/services/sitesMonitorServices.go

@@ -0,0 +1,68 @@
+package services
+
+import (
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/dto"
+	"fwh-toolbox-server/repositories"
+	"time"
+)
+
+// 从数据库获取站点监控配置列表
+func GetSiteMonitorConfig() ([]domain.SitesMonitorConfig, error) {
+	return repositories.GetSiteMonitorConfig()
+}
+
+// 从数据库获取监控名列表
+func GetSiteMonitorNameList() ([]string, error) {
+	return repositories.GetSiteMonitorNameList()
+}
+
+// 从数据库获取一条站点监控配置
+func GetOneSiteMonitorConfig(name string) ([]domain.SitesMonitorConfig, error) {
+	return repositories.GetOneSiteMonitorConfig(name)
+}
+
+// 插入一条站点监控配置到数据库
+func InsertSiteMonitorConfig(config domain.SitesMonitorConfig) error {
+	return repositories.InsertSiteMonitorConfig(config)
+}
+
+// 更新一条站点监控配置到数据库
+func UpdateSiteMonitorConfig(name string, config domain.SitesMonitorConfig) error {
+	return repositories.UpdateSiteMonitorConfig(name, config)
+}
+
+// 删除一条站点监控配置
+func DeleteSiteMonitorConfig(name string) error {
+	return repositories.DeleteSiteMonitorConfig(name)
+}
+
+// 更新一条站点监控页面配置到数据库
+func UpdateSiteMonitorPageConfig(name string, config domain.MonitorPageConfig) error {
+	return repositories.UpdateSiteMonitorPageConfig(name, config)
+}
+
+// 从fwh_pro获取站点基本信息表
+func QuerySiteBaseInfoList(para dto.SiteBaseInfoPara) (dto.SiteBaseInfoResponse, error) {
+	return repositories.QuerySiteBaseInfoList(para)
+}
+
+// 从fwh_pro获取站点基本信息表
+func QuerySiteInfoList(para dto.SiteInfoPara) (dto.SiteInfoResponse, error) {
+	return repositories.QuerySiteInfoList(para)
+}
+
+// 从fwh_pro获取站点工作模式信息
+func QuerySiteModeInfo(site string) (dto.SiteModeInfoResponse, error) {
+	return repositories.QuerySiteModeInfo(site)
+}
+
+// 从fwh_pro获取站点功率流
+func QuerySitePowerFlow(site string, day time.Time) (dto.SitePowerFlowResponse, error) {
+	return repositories.QuerySitePowerFlow(site, day)
+}
+
+// 从fwh_pro获取站点告警列表
+func QuerySiteAlarmList(site string) (dto.SiteAlarmListResponse, error) {
+	return repositories.QuerySiteAlarmList(site)
+}

+ 20 - 0
service/services/xlogServices.go

@@ -0,0 +1,20 @@
+package services
+
+import (
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/dto"
+	"fwh-toolbox-server/logger"
+	"fwh-toolbox-server/repositories"
+)
+
+type XlogManageInfo = domain.XlogSiteInfo
+type XlogManagePage = dto.ResPage[XlogManageInfo]
+
+func GetXlogManageList(page, size uint) XlogManagePage {
+	return repositories.GetXlogManageList(page, size)
+}
+
+func SendXlogCmd(uuid, cmd string) (string, error) {
+	logger.Std.Printf("Send xlog cmd: %s - %s\n", uuid, cmd)
+	return repositories.SendXlogCmd(uuid, cmd)
+}

+ 55 - 10
service/taskQueue/taskQueue.go

@@ -1,11 +1,12 @@
-package taskqueue
+package taskQueue
 
 import (
 	"context"
 	"encoding/json"
+	"fmt"
 	"fwh-toolbox-server/cache"
+	"fwh-toolbox-server/domain"
 	"fwh-toolbox-server/logger"
-	"log"
 	"time"
 
 	"github.com/go-redis/redis/v8"
@@ -19,6 +20,13 @@ var (
 	consumerID = "worker-taskQueue"
 )
 
+type TaskStreamInfo struct {
+	Ctx        context.Context
+	StreamName string
+	GroupName  string
+	ID         string
+}
+
 type TaskPayloadBase struct {
 	Type string `json:"type"`
 }
@@ -29,6 +37,9 @@ type TaskPayload[T any] struct {
 }
 
 func InitTaskQueue() {
+	// 刷新redis指针
+	rdb = cache.C
+
 	// 确保消费组存在(忽略已存在错误)
 	_ = rdb.XGroupCreateMkStream(ctx, streamName, groupName, "$").Err()
 
@@ -64,38 +75,72 @@ func startTaskQueueWorker() {
 				// 先反序列化成 map 确定 type
 				var base TaskPayloadBase
 				if err := json.Unmarshal([]byte(rawPayload), &base); err != nil {
-					logger.Std.Println("unmarshal base error:", err)
+					ackTask(TaskStreamInfo{
+						Ctx:        ctx,
+						StreamName: streamName,
+						GroupName:  groupName,
+						ID:         id,
+					}, false, fmt.Errorf("unmarshal base error - %s", err.Error()))
 					continue
 				}
 
 				switch base.Type {
 				case TaskTypeSiteAnalysis:
-					var payload TaskPayload[TaskSiteAnalysis]
+					var payload TaskPayload[domain.TaskSiteAnalysis]
 					if err := json.Unmarshal([]byte(rawPayload), &payload); err != nil {
-						log.Println("unmarshal error:", err)
+						ackTask(TaskStreamInfo{
+							Ctx:        ctx,
+							StreamName: streamName,
+							GroupName:  groupName,
+							ID:         id,
+						}, false, fmt.Errorf("unmarshal payload error - %s", err.Error()))
 						continue
 					}
 					logger.Std.Printf("Processing task %s: %+v\n", id, payload)
-					handleTaskSiteAnalysis(ctx, payload.Payload)
+					if err := handleTaskSiteAnalysis(ctx, payload.Payload); err != nil {
+						ackTask(TaskStreamInfo{
+							Ctx:        ctx,
+							StreamName: streamName,
+							GroupName:  groupName,
+							ID:         id,
+						}, false, fmt.Errorf("handle task SiteAnalysis error - %s", err.Error()))
+						continue
+					}
 				default:
 					logger.Std.Println("Unknown task type:", base.Type)
 				}
 
-				// 确认任务完成
-				if err := rdb.XAck(ctx, streamName, groupName, id).Err(); err != nil {
-					logger.Std.Println("XAck error:", err)
-				}
+				// 任务完成
+				ackTask(TaskStreamInfo{
+					Ctx:        ctx,
+					StreamName: streamName,
+					GroupName:  groupName,
+					ID:         id,
+				}, true, nil)
 			}
 		}
 	}
 }
 
+func ackTask(streamInfo TaskStreamInfo, success bool, err error) {
+	if success {
+		// 确认任务完成
+		if err := rdb.XAck(streamInfo.Ctx, streamInfo.StreamName, streamInfo.GroupName, streamInfo.ID).Err(); err != nil {
+			logger.Std.Println("XAck error:", err)
+		}
+	} else {
+		// 任务失败
+		logger.Std.Println("Task queue execution failed, error:", err)
+	}
+}
+
 func EnqueueTask[T any](payload TaskPayload[T]) error {
 	data, err := json.Marshal(payload)
 	if err != nil {
 		return err
 	}
 
+	fmt.Println(rdb)
 	if _, err := rdb.XAdd(ctx, &redis.XAddArgs{
 		Stream: streamName,
 		Values: map[string]interface{}{"payload": string(data)},

+ 121 - 11
service/taskQueue/taskSiteAnalysis.go

@@ -1,35 +1,145 @@
-package taskqueue
+package taskQueue
 
 import (
 	"context"
+	"fmt"
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/jobs"
 	"fwh-toolbox-server/logger"
-	"fwh-toolbox-server/types"
+	"fwh-toolbox-server/repositories"
 	"time"
+
+	"go.uber.org/zap"
 )
 
 var (
 	TaskTypeSiteAnalysis = "task_SiteAnalysis"
 )
 
-type TaskSiteAnalysis = types.TaskSiteAnalysis
+// type TaskSiteAnalysis = types.TaskSiteAnalysis
+
+func handleTaskSiteAnalysis(ctx context.Context, payload domain.TaskSiteAnalysis) error {
+	// 更新运行状态
+	fmt.Println("更新任务运行状态")
+	if repoErr := repositories.UpdateSiteAnalysisResult(payload.Id, domain.ProgressRunning); repoErr != nil {
+		zap.L().Error("更新站点分析结果状态失败(Running)", zap.String("id", payload.Id.String()))
+		return repoErr
+	}
+
+	fmt.Printf("开始执行任务 %v\n", payload)
+	err := executeTaskSiteAnalysis(ctx, payload)
+	// 记录任务执行结果
+	if err != nil {
+		fmt.Printf("任务执行失败 %s\n", err.Error())
+		if repoErr := repositories.UpdateSiteAnalysisResult(payload.Id, domain.ProgressError); repoErr != nil {
+			zap.L().Error("更新站点分析结果状态失败(Error)", zap.String("id", payload.Id.String()))
+			return repoErr
+		}
+	} else {
+		fmt.Println("任务执行成功")
+		if repoErr := repositories.UpdateSiteAnalysisResult(payload.Id, domain.ProgressComplete); repoErr != nil {
+			zap.L().Error("更新站点分析结果状态失败(Complete)", zap.String("id", payload.Id.String()))
+			return repoErr
+		}
+	}
+
+	return err
+}
+
+var executeTaskSiteAnalysis = func(ctx context.Context, payload domain.TaskSiteAnalysis) error {
+	// 转换时间范围,只取用日期部分
+	startTime, err := time.Parse(time.DateOnly, payload.TimePeriod[0])
+	if err != nil {
+		zap.L().Error("StartTime 格式错误", zap.String("time", payload.TimePeriod[0]))
+		return err
+	}
+	endTime, err := time.Parse(time.DateOnly, payload.TimePeriod[1])
+	if err != nil {
+		zap.L().Error("EndTime 格式错误", zap.String("time", payload.TimePeriod[1]))
+		return err
+	}
+
+	// 将 endTime 调整到当天的 23:59:59
+	endTime = endTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
+
+	if payload.Options.EnableTimeline {
+		// 时间线展示功能
+		// Site 时间线生成逻辑:
+		// 1、分析MQTT日志0和101,获取离线上线状态
+		// 2、分析事件日志,获取站点的事件触发记录
+		// 3、分析告警日志,获取站点的告警开始-结束触发记录
+		// 4、分析MQTT日志357,获取站点模式切换记录
+		// 最终处理:获取事件时间点的MQTT日志201数据,关联到事件点。
+		// if payload.SiteAnalysisOptions.EnableTimeline
+
+		// 查询MQTT日志0和101
+		fmt.Println("查询MQTT日志0")
+		logList_0, err := repositories.QueryMqttLogListOnTimeRange(payload.Sn, domain.MQTT_LOG_0, startTime, endTime)
+		if err != nil {
+			zap.L().Error("查询MQTT日志0失败")
+			return err
+		}
+		fmt.Println("查询MQTT日志101")
+		logList_101, err := repositories.QueryMqttLogListOnTimeRange(payload.Sn, domain.MQTT_LOG_101, startTime, endTime)
+		if err != nil {
+			zap.L().Error("查询MQTT日志101失败")
+			return err
+		}
 
-func handleTaskSiteAnalysis(ctx context.Context, payload TaskSiteAnalysis) {
-	// TODO 执行任务(严格顺序)
-	// logger.Std.Printf("Processing task %s: %v\n", id, payload)
-	time.Sleep(3 * time.Second) // 模拟耗时
+		// 查询站点的事件触发记录
+		fmt.Println("查询站点的事件触发记录")
+		eventLog, err := repositories.QueryEventOrAlarmListOnTimeRange(payload.Sn, startTime, endTime, repositories.Event)
+		if err != nil {
+			zap.L().Error("查询事件日志失败")
+			return err
+		}
 
-	if payload.SiteAnalysisOptions.EnableTimeline
+		// 查询站点的告警开始-结束触发记录
+		fmt.Println("查询站点的告警开始-结束触发记录")
+		alarmLog, err := repositories.QueryEventOrAlarmListOnTimeRange(payload.Sn, startTime, endTime, repositories.Alarm)
+		if err != nil {
+			zap.L().Error("查询事件日志失败")
+			return err
+		}
 
+		// 查询MQTT日志357
+		fmt.Println("查询MQTT日志357")
+		logList_357, err := repositories.QueryMqttLogListOnTimeRange(payload.Sn, domain.MQTT_LOG_357, startTime, endTime)
+		if err != nil {
+			zap.L().Error("查询MQTT日志357失败")
+			return err
+		}
+
+		// 分析数据,并将结果写入数据库
+		fmt.Println("分析数据,并将结果写入数据库")
+		err = jobs.AnalysisSiteTimeline(payload.Id, jobs.SiteDataSet{
+			Log_0:    logList_0,
+			Log_101:  logList_101,
+			Log_357:  logList_357,
+			EventLog: eventLog,
+			AlarmLog: alarmLog,
+		})
+		if err != nil {
+			zap.L().Error("站点时间线数据分析失败", zap.String("sn", payload.Sn))
+			return err
+		}
+	}
+
+	// if payload.Options.EnableRuntime {
+	// 	// 实时数据展示
+	// }
+
+	return nil
 }
 
-func EnqueueSiteAnalysisTask(payload TaskSiteAnalysis) error {
-	p := TaskPayload[TaskSiteAnalysis]{
+func EnqueueSiteAnalysisTask(payload domain.TaskSiteAnalysis) error {
+	p := TaskPayload[domain.TaskSiteAnalysis]{
 		TaskPayloadBase: TaskPayloadBase{
 			Type: TaskTypeSiteAnalysis,
 		},
 		Payload: payload,
 	}
-	if err := EnqueueTask[TaskSiteAnalysis](p); err != nil {
+	if err := EnqueueTask[domain.TaskSiteAnalysis](p); err != nil {
 		logger.Std.Printf("EnqueueSiteAnalysisTask - %s : %s\n", payload.Id, err.Error())
 		return err
 	}

+ 0 - 9
service/types/fwhPublic.go

@@ -1,9 +0,0 @@
-package types
-
-type FwhPageResponse[T any] struct {
-	Code    int    `json:"code"`
-	Message string `json:"message"`
-	Result  []T    `json:"result"`
-	Success bool   `json:"success"`
-	Total   uint64 `json:"total"`
-}

+ 0 - 38
service/types/siteAnalysis.go

@@ -1,38 +0,0 @@
-package types
-
-import "go.mongodb.org/mongo-driver/bson/primitive"
-
-type Progress string // 'pending' | 'running' | 'error' | 'complete'
-
-type SiteAnalysisInfo struct {
-	Id          primitive.ObjectID `bson:"_id" json:"id"`
-	Sn          string             `bson:"sn" json:"sn"`
-	Description string             `bson:"description" json:"description"`
-	Item        []string           `bson:"item" json:"item"`
-	Progress    Progress           `bson:"progress" json:"progress"`
-}
-
-type SiteAnalysisOptions struct {
-	EnableTimeline bool `bson:"enableTimeline" json:"enableTimeline"`
-}
-
-type SiteAnalysisConfigWrite struct {
-	Id          primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
-	Sn          string             `bson:"sn" json:"sn"`
-	Description string             `bson:"description" json:"description"`
-	TimePeriod  [2]string          `bson:"timePeriod" json:"timePeriod"`
-	SiteAnalysisOptions
-}
-
-type SiteAnalysisConfig struct {
-	SiteAnalysisConfigWrite
-	Item     []string `bson:"item" json:"item"`
-	Progress Progress `bson:"progress" json:"progress"`
-}
-
-type TaskSiteAnalysis struct {
-	Id         primitive.ObjectID `bson:"_id" json:"id"`
-	Sn         string             `bson:"sn" json:"sn"`
-	TimePeriod [2]string          `bson:"timePeriod" json:"timePeriod"`
-	SiteAnalysisOptions
-}

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

@@ -0,0 +1,157 @@
+package httpClient
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+
+	"go.uber.org/zap"
+)
+
+const (
+	DefaultTimeout time.Duration = 5 // 默认超时时间5s
+)
+
+type HttpClientOptions struct {
+	Timeout *time.Duration // 单位:s
+	Header  *http.Header
+}
+
+// PostBody 包含转换后的 Reader 和推荐的 Content-Type
+type PostBody struct {
+	Reader      io.Reader
+	ContentType string
+}
+
+func HttpGet(url string, params url.Values, opt *HttpClientOptions) ([]byte, error) {
+	// 拼接参数到 URL
+	finalURL := url + "?" + params.Encode()
+
+	// 创建请求
+	req, err := http.NewRequest(
+		http.MethodGet,
+		finalURL,
+		nil)
+	if err != nil {
+		zap.L().Error("NewRequest failed.", zap.String("err", err.Error()))
+		return nil, err
+	}
+
+	// 设置 Header
+	if opt != nil && opt.Header != nil {
+		for key, values := range *opt.Header {
+			for _, v := range values {
+				req.Header.Add(key, v)
+			}
+		}
+	}
+
+	// 设置 Timeout
+	timeout := DefaultTimeout
+	if opt != nil && opt.Timeout != nil {
+		timeout = *opt.Timeout
+	}
+
+	// 发送请求
+	client := &http.Client{Timeout: timeout * time.Second}
+	resp, err := client.Do(req)
+	if err != nil {
+		zap.L().Error("Do Send failed.", zap.String("err", err.Error()))
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	return io.ReadAll(resp.Body)
+}
+
+// toPostBody 将任意 body 转换为 http POST 可用的 io.Reader
+func toPostBody(body any) (*PostBody, error) {
+	if body == nil {
+		return &PostBody{Reader: nil, ContentType: ""}, nil
+	}
+
+	switch v := body.(type) {
+	case io.Reader:
+		// 直接传 io.Reader
+		return &PostBody{Reader: v}, nil
+
+	case string:
+		// 字符串默认 text/plain
+		return &PostBody{Reader: strings.NewReader(v), ContentType: "text/plain"}, nil
+
+	case []byte:
+		// []byte 默认 application/octet-stream
+		return &PostBody{Reader: bytes.NewReader(v), ContentType: "application/octet-stream"}, nil
+
+	case url.Values:
+		// 表单类型 application/x-www-form-urlencoded
+		return &PostBody{
+			Reader:      strings.NewReader(v.Encode()),
+			ContentType: "application/x-www-form-urlencoded; charset=UTF-8",
+		}, nil
+
+	default:
+		// 尝试序列化为 JSON
+		jsonData, err := json.Marshal(v)
+		if err != nil {
+			return nil, errors.New("unsupported body type and failed to marshal to JSON")
+		}
+		return &PostBody{
+			Reader:      bytes.NewReader(jsonData),
+			ContentType: "application/json",
+		}, nil
+	}
+}
+
+func HttpPost(url string, body any, opt *HttpClientOptions) ([]byte, error) {
+	// 转换body数据
+	payload, err := toPostBody(body)
+	if err != nil {
+		zap.L().Error("to PostBody failed.", zap.String("err", err.Error()))
+		return nil, err
+	}
+
+	// 创建请求
+	req, err := http.NewRequest(
+		http.MethodPost,
+		"https://oss.franklinwh.com/api-user/user/login",
+		payload.Reader)
+	if err != nil {
+		zap.L().Error("NewRequest failed.", zap.String("err", err.Error()))
+		return nil, err
+	}
+
+	// 设置 Content-Type
+	req.Header.Set("Content-Type", payload.ContentType)
+
+	// 设置 Header
+	if opt != nil && opt.Header != nil {
+		for key, values := range *opt.Header {
+			for _, v := range values {
+				req.Header.Add(key, v)
+			}
+		}
+	}
+
+	// 设置 Timeout
+	timeout := DefaultTimeout
+	if opt != nil && opt.Timeout != nil {
+		timeout = *opt.Timeout
+	}
+
+	// 发送请求
+	client := &http.Client{Timeout: timeout * time.Second}
+	resp, err := client.Do(req)
+	if err != nil {
+		zap.L().Error("Do Send failed.", zap.String("err", err.Error()))
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	return io.ReadAll(resp.Body)
+}

+ 9 - 2
src/service/sitesAnalysis/index.ts

@@ -16,6 +16,7 @@ const API_List = {
   manageList: '/sitesAnalysis/manage/analysisList',
   manageConfig: '/sitesAnalysis/manage/config',
   manageConfigWithID: '/sitesAnalysis/manage/config/{id}',
+  manageConfigDescriptionWithID: '/sitesAnalysis/manage/description/{id}',
   analysisData: '/sitesAnalysis/data/{id}',
 }
 
@@ -29,8 +30,8 @@ export const getManageList = async (
     params: {
       pageSize,
       pageNum,
-      sn: filter ? filter.sn : undefined,
-      progress: filter ? filter.progress : undefined,
+      sn: filter ? (filter.sn ?? '') : '',
+      progress: filter ? (filter.progress ?? '') : '',
     },
   })
   return res
@@ -55,6 +56,12 @@ export const updateManageConfig = async (id: string, config: AnalysisConfig) =>
   const res = await http.post(url, config)
 }
 
+// POST 更新指定id配置
+export const updateManageConfigDescription = async (id: string, description: string) => {
+  const url = API_List.manageConfigDescriptionWithID.replace('{id}', encodeURIComponent(id))
+  const res = await http.post(url, description)
+}
+
 // DELETE 删除指定id配置
 export const deleteManageConfig = async (id: string) => {
   const url = API_List.manageConfigWithID.replace('{id}', encodeURIComponent(id))

+ 14 - 5
src/service/sitesAnalysis/interface.ts

@@ -5,15 +5,17 @@ export interface AnalysisListFilter {
   progress?: 'pending' | 'running' | 'error' | 'complete'
 }
 
+export interface AnalysisConfigOptions {
+  enableTimeline: boolean
+}
+
 export interface AnalysisConfig {
   id: string
   sn: string
   description: string
   timePeriod: [string, string] | null
 
-  enableEventAndAlarm: boolean
-  enableCloudLog: boolean
-  enableSiteLog: boolean
+  options: AnalysisConfigOptions
 }
 
 export interface AnalysisItemInfo {
@@ -27,11 +29,18 @@ export interface AnalysisItemInfo {
 export interface AnalysisItemInfoPage extends ResPage<AnalysisItemInfo[]> {}
 
 export interface AnalysisData {
-  // TODO 待补充
+  timeline: TimelineItem[]
 }
 
 export interface AnalysisResultData {
-  result: boolean
+  code: number
   message: string
   data?: AnalysisData
 }
+
+export interface TimelineItem {
+  time: Date
+  type: string
+  info?: string
+  data?: string
+}

+ 29 - 19
src/views/sites-analysis/ModalData.vue

@@ -9,24 +9,13 @@
         <NInput type="textarea" v-model:value="form.description" />
       </NFormItem>
       <NFormItem label="时间" path="timePeriod">
-        <NDatePicker v-model:formatted-value="form.timePeriod" value-format="yyyy.MM.dd HH:mm:ss" type="datetimerange"
-          clearable />
+        <NDatePicker v-model:formatted-value="form.timePeriod" value-format="yyyy-MM-dd" type="daterange" clearable />
       </NFormItem>
 
-      <NCollapse :trigger-areas="['arrow']">
-        <NCollapseItem :disable="!form.enableEventAndAlarm" title="事件&告警">
+      <NCollapse :trigger-areas="['arrow']" class="p-4">
+        <NCollapseItem :disable="!form.options.enableTimeline" title="时间线">
           <template #header-extra>
-            <NSwitch v-model:value="form.enableEventAndAlarm" size="small" />
-          </template>
-        </NCollapseItem>
-        <NCollapseItem :disable="!form.enableCloudLog" title="云通讯日志">
-          <template #header-extra>
-            <NSwitch v-model:value="form.enableCloudLog" size="small" />
-          </template>
-        </NCollapseItem>
-        <NCollapseItem :disable="!form.enableSiteLog" title="设备日志">
-          <template #header-extra>
-            <NSwitch v-model:value="form.enableSiteLog" size="small" />
+            <NSwitch v-model:value="form.options.enableTimeline" size="small" />
           </template>
         </NCollapseItem>
       </NCollapse>
@@ -74,9 +63,9 @@ const [form, setUserForm, reset, updateInitialState] = useResettableReactive<Ana
   description: '',
   timePeriod: null,
 
-  enableEventAndAlarm: false,
-  enableCloudLog: false,
-  enableSiteLog: false
+  options: {
+    enableTimeline: true
+  }
 })
 
 const handleAllowInput = (value: string) => {
@@ -87,7 +76,28 @@ const handleAllowInput = (value: string) => {
 
 const rules: FormRules = {
   sn: { required: true, message: '请输入站点SN', trigger: ['input'] },
-  timePeriod: { required: true, message: '请选择时间范围', trigger: ['input'] },
+  timePeriod: [
+    // { required: true, message: '请选择时间范围', trigger: ['change', 'blur'] },
+    {
+      validator: (rule, value: [string, string] | null) => {
+        // ✅ 先判断是否有值
+        if (!value || !value[0] || !value[1]) {
+          return Promise.reject('请选择时间范围')
+        }
+        // ✅ 再判断时间先后
+        const [start, end] = value
+        if (!start || !end) return false
+        // 将字符串转成 Date 再比较
+        const startTime = new Date(start.replace(/\./g, '-')).getTime()
+        const endTime = new Date(end.replace(/\./g, '-')).getTime()
+        if (endTime < startTime) {
+          return Promise.reject('结束时间必须晚于开始时间')
+        }
+        return Promise.resolve()
+      },
+      trigger: ['change', 'blur']
+    }
+  ],
 }
 
 const handleSubmitClick = () => {

+ 76 - 18
src/views/sites-analysis/component/siteTimeline.vue

@@ -1,12 +1,8 @@
 <template>
   <div class="flex flex-col gap-y-2 p-4">
-    <NCard content-class="flex gap-4">
-
-    </NCard>
     <NCard>
       <NTimeline>
-        <component v-for="item in timeLine" :is="NTimelineItem"
-          :key="item.time + ' - ' + item.type + ' - ' + item.title" v-bind="item.props" v-slots="item.slots" />
+        <component v-for="comp in timelineComponents" :is="comp.component" />
       </NTimeline>
     </NCard>
   </div>
@@ -16,9 +12,17 @@
 import { ref } from 'vue';
 import { NCard, NTimeline, NTimelineItem } from 'naive-ui'
 import type { TimelineItemProps, TimelineItemSlots } from 'naive-ui'
+import { getAnalysisData } from '@/service/sitesAnalysis';
+
+const props = defineProps<{
+  id: string
+}>()
+
+type TimelineItemType = 'info' | 'event' | 'alarm'
+type NTimelineItemType = TimelineItemProps['type']
 
 interface TimelineItemConfig {
-  type: 'info' | 'alarm' | 'error'
+  type: TimelineItemType
   time: string // format YYYY-MM-DD HH:mm
   title: string
   content: string
@@ -26,14 +30,19 @@ interface TimelineItemConfig {
   slots?: TimelineItemSlots
 }
 
+interface TimelineComponent {
+  component: VNode,
+  config: TimelineItemConfig
+}
+
 // type 的优先级映射
 const typeOrder: Record<TimelineItemConfig['type'], number> = {
   info: 1,
-  alarm: 2,
-  error: 3
+  event: 2,
+  alarm: 3
 }
 
-// TimeLine排序
+// TimeLine排序 (暂时不需要,已在后台排序)
 const sortTimeLine = (list: TimelineItemConfig[]): TimelineItemConfig[] => {
   return [...list].sort((a, b) => {
     // 先比较时间
@@ -47,17 +56,66 @@ const sortTimeLine = (list: TimelineItemConfig[]): TimelineItemConfig[] => {
 
 const timeLine = ref<TimelineItemConfig[]>([])
 
+const timelineComponents = computed<TimelineComponent[]>(() => {
+  console.log({ timeLine })
+  return timeLine.value.map(item => {
+    const component = item.slots !== undefined
+      ? h(NTimelineItem, { ...item.props, key: item.time + '-' + item.type + '-' + item.title }, item.slots)
+      : h(NTimelineItem, { ...item.props, key: item.time + '-' + item.type + '-' + item.title })
+    return {
+      component,
+      config: item
+    }
+  })
+});
+
 onMounted(async () => {
   // 获取Timeline数据
-  const data: TimeLineItemConfig[] = [
-    {
-      type: 'info',
-      time: '2025-09-19 11:02',
-      title: 'test',
-      content: 'test',
-      component: NTimelineItem
-    }
-  ]
+  const analysisData = await getAnalysisData(props.id)
+  console.log({ analysisData })
+  if (analysisData.data !== undefined) {
+    timeLine.value = analysisData.data.timeline.map<TimelineItemConfig>(item => {
+      let type: TimelineItemType = "info"
+      let timelineType: NTimelineItemType = undefined
+      let title = ""
+      switch (item.type) {
+        case 'offline':
+          title = "离线"
+          break
+        case 'online':
+          title = "上线"
+          timelineType = "info"
+          break
+        case 'mode_change':
+          title = item.info ?? "未知"
+          break
+        case 'event':
+          type = 'event'
+          title = item.info ?? "未知"
+          timelineType = "warning"
+          break
+        case 'alarm':
+          type = 'alarm'
+          title = item.info ?? "未知"
+          timelineType = "error"
+          break
+        default:
+          title = "未知"
+          break
+      }
+      return {
+        type,
+        time: item.time.toString(),
+        title,
+        content: "",
+        props: {
+          time: item.time.toString(),
+          title,
+          type: timelineType,
+        }
+      }
+    })
+  }
 })
 
 </script>

+ 24 - 0
src/views/sites-analysis/index.vue

@@ -0,0 +1,24 @@
+<template>
+  <div class="flex flex-col gap-y-2 p-4">
+    <NTabs type="line" animated>
+      <NTabPane name="timeline" tab="时间线">
+        <SiteTimeline :id="analysisID" />
+      </NTabPane>
+    </NTabs>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useRoute } from 'vue-router'
+import { NTabs, NTabPane } from 'naive-ui'
+import SiteTimeline from './component/siteTimeline.vue'
+
+defineOptions({
+  name: 'SiteAnalysis',
+})
+
+const route = useRoute()
+
+// 获取 name
+const analysisID = route.params.name as string
+</script>

+ 11 - 2
src/views/sites-analysis/manage.vue

@@ -328,7 +328,11 @@ interface AnalysisListFilter {
 
 const querySitesAnalysisList = async (pageSize: number, pageNum: number, filter?: AnalysisListFilter) => {
   const res = await getManageList(pageSize, pageNum, filter)
-  tableData.value = res.data
+  if (res.data === null) {
+    tableData.value = []
+  } else {
+    tableData.value = res.data
+  }
 }
 
 const createSiteAnalysisConfig = async (data: AnalysisConfig) => {
@@ -346,7 +350,7 @@ const deleteSiteFromList = async (id: string) => {
   handleListQuery()
 }
 
-const handleListQuery = () => {
+const handleListQuery = async () => {
   const pageSize = tablePagination.pageSize ?? 10
   const page = tablePagination.page ?? 1
 
@@ -359,4 +363,9 @@ const handleListQuery = () => {
   querySitesAnalysisList(pageSize, page, filter)
 }
 
+onMounted(async () => {
+  // 请求查询数据
+  await handleListQuery()
+})
+
 </script>

+ 1 - 1
src/views/sites-monitor/manage.vue

@@ -174,7 +174,7 @@ onMounted(async () => {
       </div>
       <NGrid cols="2 600:4 900:6">
         <NGridItem v-for="name in monitorList" :key="name">
-          <div class="flex">
+          <div class="flex p-y-4">
             <RouterLink :to="`/sites-monitor/${name}`">
               <NButton class="!w-[120px]" block secondary :type="`/sites-monitor/${name}`.includes(
                 router.currentRoute.value.fullPath,