浏览代码

fix:批量数据下载大部分功能调试完成,可暂时使用

Jerry Liang 11 月之前
父节点
当前提交
b163370ff0

+ 1 - 1
service/domain/batchDownloadDomain.go

@@ -9,7 +9,7 @@ type BatchDownloadInfo struct {
 	Sn          []string             `bson:"sn" json:"sn"`
 	Description string               `bson:"description" json:"description"`
 	TimePeriod  [2]string            `bson:"timePeriod" json:"timePeriod"`
-	Options     BatchDownloadOptions `bson:"options"`
+	Options     BatchDownloadOptions `bson:"options" json:"options"`
 	Progress    Progress             `bson:"progress" json:"progress"`
 	FailReasons []string             `bson:"failReasons" json:"failReasons"`
 }

+ 22 - 3
service/domain/fwhProDomain.go

@@ -1,13 +1,32 @@
 package domain
 
+type FwhResponse[T any] struct {
+	Code    int    `json:"code"` // 401 - invalid token
+	Message string `json:"message"`
+	Result  *T     `json:"result"`
+	Success *bool  `json:"success"`
+}
+
 type FwhPageResponse[T any] struct {
-	Code    int    `json:"code"`
+	Code    int    `json:"code"` // 401 - invalid token
 	Message string `json:"message"`
-	Result  T      `json:"result"`
-	Success bool   `json:"success"`
+	Result  *T     `json:"result"`
+	Success *bool  `json:"success"`
 	Total   uint64 `json:"total"`
 }
 
+type FwhResponseA[T any] struct {
+	Code   int    `json:"code"` // 401 - invalid token
+	Msg    string `json:"msg"`
+	Result *T     `json:"result"`
+}
+
+type FwhResponseB[T any] struct {
+	Code int    `json:"code"` // 401 - invalid token
+	Msg  string `json:"msg"`
+	Data *T     `json:"data"`
+}
+
 type AlarmSnapshotInfo struct {
 	Id           int64  `json:"id"`
 	CodeType     int32  `json:"codeType"` // 作用未知

+ 0 - 37
service/dto/sitesMonitorDTO.go

@@ -21,14 +21,6 @@ type SiteBaseInfo struct {
 	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"`
@@ -60,14 +52,6 @@ type SiteInfo struct {
 	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"`
@@ -86,13 +70,6 @@ type SiteModeInfo struct {
 	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”
@@ -106,12 +83,6 @@ type SitePowerFlow struct {
 	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"`
@@ -125,11 +96,3 @@ type SiteAlarmList struct {
 	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"`
-}

+ 3 - 11
service/jobs/callFwhFile.go

@@ -9,7 +9,6 @@ import (
 	"fwh-toolbox-server/logger"
 	"fwh-toolbox-server/repositories"
 	"path"
-	"regexp"
 	"strconv"
 	"strings"
 	"sync"
@@ -156,7 +155,7 @@ func callFwhFile(mainTaskId primitive.ObjectID, taskId string, info domain.Batch
 						for downloadCount = 0; downloadCount < maxCount; downloadCount++ {
 							dataBytes, err := repositories.QueryDownloadEmsLogFile(recordInfo.FileUrl)
 							if err != nil {
-								zap.L().Error("EMS日志文件下载失败")
+								zap.L().Error("EMS日志文件下载失败", zap.String("err", err.Error()))
 								continue
 							}
 
@@ -166,10 +165,6 @@ func callFwhFile(mainTaskId primitive.ObjectID, taskId string, info domain.Batch
 								baseStr = ""
 							}
 
-							// 使用正则匹配多级后缀,例如 .tar.bz2, .tar.gz, .min.js 等
-							re := regexp.MustCompile(`(\.[a-zA-Z0-9]+)+$`)
-							matchSuffix := re.FindString(baseStr)
-
 							// 加载加州时区(Pacific Time)
 							loc, err := time.LoadLocation("America/Los_Angeles")
 							if err != nil {
@@ -184,11 +179,8 @@ func callFwhFile(mainTaskId primitive.ObjectID, taskId string, info domain.Batch
 								return false, err
 							}
 
-							// 生成文件名 Format "EmsLog_[SN]_[logType]_[logDate]_[cellTimestamp].[Suffix]"
-							fileName := fmt.Sprintf("EmsLog_%s_%s_%s_%s%s", info.Sn, info.FileSupType, strings.ReplaceAll(info.Date, "-", "_"), strconv.FormatInt(callTime.Unix(), 10), matchSuffix)
-
 							// 保存数据到文件服务器
-							objName, err := SaveBatchDownloadEmsLog(info.Sn, dataBytes, fileName, "application/x-bzip2")
+							objName, err := SaveBatchDownloadEmsLog(info.Sn, dataBytes, baseStr, info.FileSupType, info.Date, callTime)
 							if err != nil {
 								zap.L().Error("Ems日志文件保存失败", zap.String("devType", strconv.Itoa(info.DevType)), zap.String("sn", info.Sn), zap.String("err", err.Error()))
 								return false, fmt.Errorf("Ems日志文件保存失败")
@@ -293,7 +285,7 @@ func callFwhFile(mainTaskId primitive.ObjectID, taskId string, info domain.Batch
 								}
 
 								// 保存数据到文件服务器
-								objName, err := SaveBatchDownloadFaultRecord(subRecord.GatewayId, dataBytes, subRecord.FileName+".xls", "application/vnd.ms-excel;charset=gb2312")
+								objName, err := SaveBatchDownloadFaultRecord(subRecord.GatewayId, dataBytes, subRecord.FileName+".xls")
 								if err != nil {
 									zap.L().Error("故障录波记录文件保存失败", zap.String("devType", strconv.Itoa(subRecord.DeviceType)), zap.String("sn", subRecord.GatewayId), zap.String("faultId", strconv.FormatInt(subRecord.FaultId, 10)), zap.String("id", strconv.FormatInt(subRecord.Id, 10)), zap.String("err", err.Error()))
 									return false, fmt.Errorf("故障录波记录文件保存失败")

+ 2 - 4
service/jobs/getAlarmSnapshot.go

@@ -22,8 +22,6 @@ import (
 	"gorm.io/gorm"
 )
 
-type AlarmSnapshotListResponse = domain.FwhPageResponse[[]domain.AlarmSnapshotInfo]
-
 // 获取告警快照数据
 // 从FHW后端请求快照文件后,处理并放入数据库
 // logTime 格式 YYYY-MM-DD
@@ -82,11 +80,11 @@ func GetAlarmSnapshotToDB(ctx context.Context, sn, logTime string) error {
 		}
 
 		// 提取ZoneInfo
-		if len(res.Result) != 1 {
+		if len(*(*res).Result) != 1 {
 			zap.L().Error("站点信息查询失败,未查询待站点数据。", zap.Any("res", res))
 			return fmt.Errorf("站点信息查询失败,未查询待站点数据。")
 		}
-		zoneInfo := res.Result[0].ZoneInfo
+		zoneInfo := (*(*res).Result)[0].ZoneInfo
 
 		snapFileName := filepath.Join(tempDir, info.FileName+".tgz")
 		// 下载快照文件

+ 206 - 3
service/jobs/saveBatchDownloadData.go

@@ -6,9 +6,11 @@ import (
 	"fwh-toolbox-server/domain"
 	"fwh-toolbox-server/dto"
 	"fwh-toolbox-server/utils/echart"
+	"fwh-toolbox-server/utils/mimetype"
 	"fwh-toolbox-server/utils/minio"
 	"os"
 	"path/filepath"
+	"regexp"
 	"strconv"
 	"strings"
 	"time"
@@ -71,6 +73,32 @@ func excelSetCellValue(f *excelize.File, sheetName string, col int, row int, val
 	return f.SetCellValue(sheetName, fmt.Sprintf("%s%d", colName, row), val)
 }
 
+// 判断功率流图文件是否存在
+func ExistsBatchDownloadPowerFlow(sn string, dateStr string) (bool, error) {
+	fileName := fmt.Sprintf("PowerFlow_%s.html", strings.ReplaceAll(dateStr, "-", ""))
+	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subPowerFlowPathName, fileName)
+	exists, err := minio.ExistsObject(objectName)
+	if err != nil {
+		zap.L().Error("检查PowerFlow文件失败", zap.String("fileName", fileName), zap.String("objectName", objectName), zap.String("err", err.Error()))
+		return false, err
+	}
+
+	return exists, nil
+}
+
+// 获取功率流图文件状态信息
+func StatBatchDownloadPowerFlow(sn string, dateStr string) (*minio.ObjectInfo, error) {
+	fileName := fmt.Sprintf("PowerFlow_%s.html", strings.ReplaceAll(dateStr, "-", ""))
+	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subPowerFlowPathName, fileName)
+	status, err := minio.StatObject(objectName)
+	if err != nil {
+		zap.L().Error("获取PowerFlow文件状态失败", zap.String("fileName", fileName), zap.String("objectName", objectName), zap.String("err", err.Error()))
+		return nil, err
+	}
+
+	return &status, nil
+}
+
 // 保存功率流图到文件服务器
 // return objName
 func SaveBatchDownloadPowerFlow(sn string, dateStr string, powerFlowData dto.SitePowerFlow) (string, error) {
@@ -244,6 +272,32 @@ func SaveBatchDownloadPowerFlow(sn string, dateStr string, powerFlowData dto.Sit
 	return objectName, nil
 }
 
+// 判断MqttLog excel文件是否存在
+func ExistsBatchDownloadMqttLog(sn string, cmdType string, dateStr string) (bool, error) {
+	fileName := fmt.Sprintf("MqttLog_%s_%s.xlsx", cmdType, strings.ReplaceAll(dateStr, "-", ""))
+	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subMqttPathName, fileName)
+	exists, err := minio.ExistsObject(objectName)
+	if err != nil {
+		zap.L().Error("检查MqttLog excel文件失败", zap.String("fileName", fileName), zap.String("objectName", objectName), zap.String("err", err.Error()))
+		return false, err
+	}
+
+	return exists, nil
+}
+
+// 获取MqttLog excel文件状态信息
+func StatBatchDownloadMqttLog(sn string, cmdType string, dateStr string) (*minio.ObjectInfo, error) {
+	fileName := fmt.Sprintf("MqttLog_%s_%s.xlsx", cmdType, strings.ReplaceAll(dateStr, "-", ""))
+	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subMqttPathName, fileName)
+	status, err := minio.StatObject(objectName)
+	if err != nil {
+		zap.L().Error("获取MqttLog excel文件状态失败", zap.String("fileName", fileName), zap.String("objectName", objectName), zap.String("err", err.Error()))
+		return nil, err
+	}
+
+	return &status, nil
+}
+
 // 保存MqttLog excel到文件服务器
 // return objName
 func SaveBatchDownloadMqttLog(sn string, cmdType string, dateStr string, data []domain.MqttLogItem) (objectName string, err error) {
@@ -297,6 +351,32 @@ func SaveBatchDownloadMqttLog(sn string, cmdType string, dateStr string, data []
 	return
 }
 
+// 判断EventAlarm excel文件是否存在
+func ExistsBatchDownloadEventAlarm(sn string, dateStr string) (bool, error) {
+	fileName := fmt.Sprintf("EventAlarm_%s.xlsx", strings.ReplaceAll(dateStr, "-", ""))
+	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subEventAlarmPathName, fileName)
+	exists, err := minio.ExistsObject(objectName)
+	if err != nil {
+		zap.L().Error("检查EventAlarm excel文件失败", zap.String("fileName", fileName), zap.String("objectName", objectName), zap.String("err", err.Error()))
+		return false, err
+	}
+
+	return exists, nil
+}
+
+// 获取EventAlarm excel文件状态信息
+func StatBatchDownloadEventAlarm(sn string, dateStr string) (*minio.ObjectInfo, error) {
+	fileName := fmt.Sprintf("EventAlarm_%s.xlsx", strings.ReplaceAll(dateStr, "-", ""))
+	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subEventAlarmPathName, fileName)
+	status, err := minio.StatObject(objectName)
+	if err != nil {
+		zap.L().Error("获取EventAlarm excel文件状态失败", zap.String("fileName", fileName), zap.String("objectName", objectName), zap.String("err", err.Error()))
+		return nil, err
+	}
+
+	return &status, nil
+}
+
 // 保存EventAlarm excel到文件服务器
 // return objName
 func SaveBatchDownloadEventAlarm(sn string, dateStr string, eventAlarmData []domain.EventAlarmItem) (objectName string, err error) {
@@ -365,6 +445,32 @@ func SaveBatchDownloadEventAlarm(sn string, dateStr string, eventAlarmData []dom
 	return
 }
 
+// 判断AlarmSnapshot文件是否存在
+func ExistsBatchDownloadAlarmSnapshot(sn string, dateStr string) (bool, error) {
+	fileName := fmt.Sprintf("EventAlarm_%s.xlsx", strings.ReplaceAll(dateStr, "-", ""))
+	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subAlarmSnapshotPathName, fileName)
+	exists, err := minio.ExistsObject(objectName)
+	if err != nil {
+		zap.L().Error("检查AlarmSnapshot文件失败", zap.String("fileName", fileName), zap.String("objectName", objectName), zap.String("err", err.Error()))
+		return false, err
+	}
+
+	return exists, nil
+}
+
+// 获取AlarmSnapshot文件状态信息
+func StatBatchDownloadAlarmSnapshot(sn string, dateStr string) (*minio.ObjectInfo, error) {
+	fileName := fmt.Sprintf("EventAlarm_%s.xlsx", strings.ReplaceAll(dateStr, "-", ""))
+	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subAlarmSnapshotPathName, fileName)
+	status, err := minio.StatObject(objectName)
+	if err != nil {
+		zap.L().Error("获取AlarmSnapshot文件状态失败", zap.String("fileName", fileName), zap.String("objectName", objectName), zap.String("err", err.Error()))
+		return nil, err
+	}
+
+	return &status, nil
+}
+
 // 保存AlarmSnapshot文件到文件服务器
 // return objName
 func SaveBatchDownloadAlarmSnapshot(sn string, dateStr string, alarmSnapshotData []*domain.SiteAlarmSnapshotData) (objectName string, err error) {
@@ -425,26 +531,123 @@ func SaveBatchDownloadAlarmSnapshot(sn string, dateStr string, alarmSnapshotData
 	return
 }
 
+// 判断EmsLog文件是否存在
+func ExistsBatchDownloadEmsLog(sn string, originalFileName string, fileSupType string, dateStr string, callTime time.Time) (bool, error) {
+	// 使用正则匹配多级后缀,例如 .tar.bz2, .tar.gz, .min.js 等
+	re := regexp.MustCompile(`(\.[a-zA-Z0-9]+)+$`)
+	matchSuffix := re.FindString(originalFileName)
+
+	fileName := fmt.Sprintf("EmsLog_%s_%s_%s_%s%s", sn, fileSupType, strings.ReplaceAll(dateStr, "-", "_"), strconv.FormatInt(callTime.Unix(), 10), matchSuffix)
+	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subEmsLogsPathName, fileName)
+	exists, err := minio.ExistsObject(objectName)
+	if err != nil {
+		zap.L().Error("检查EmsLog文件失败", zap.String("fileName", fileName), zap.String("objectName", objectName), zap.String("err", err.Error()))
+		return false, err
+	}
+
+	return exists, nil
+}
+
+// 获取EmsLog文件状态信息
+func StatBatchDownloadEmsLog(sn string, originalFileName string, fileSupType string, dateStr string, callTime time.Time) (*minio.ObjectInfo, error) {
+	// 使用正则匹配多级后缀,例如 .tar.bz2, .tar.gz, .min.js 等
+	re := regexp.MustCompile(`(\.[a-zA-Z0-9]+)+$`)
+	matchSuffix := re.FindString(originalFileName)
+
+	fileName := fmt.Sprintf("EmsLog_%s_%s_%s_%s%s", sn, fileSupType, strings.ReplaceAll(dateStr, "-", "_"), strconv.FormatInt(callTime.Unix(), 10), matchSuffix)
+	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subEmsLogsPathName, fileName)
+	status, err := minio.StatObject(objectName)
+	if err != nil {
+		zap.L().Error("获取EmsLog文件状态失败", zap.String("fileName", fileName), zap.String("objectName", objectName), zap.String("err", err.Error()))
+		return nil, err
+	}
+
+	return &status, nil
+}
+
 // 保存EmsLog文件到文件服务器
 // return objName
-func SaveBatchDownloadEmsLog(sn string, dataBytes []byte, emsLogFileName string, contentType string) (string, error) {
+func SaveBatchDownloadEmsLog(sn string, dataBytes []byte, originalFileName string, fileSupType string, dateStr string, callTime time.Time) (string, error) {
 	// 将 []byte 封装成 io.Reader
 	dataReader := bytes.NewReader(dataBytes)
+
+	// 使用正则匹配多级后缀,例如 .tar.bz2, .tar.gz, .min.js 等
+	re := regexp.MustCompile(`(\.[a-zA-Z0-9]+)+$`)
+	matchSuffix := re.FindString(originalFileName)
+
+	// 计算Content Type
+	contentType := mimetype.GetContentType(originalFileName)
+
 	// 保存到minio
-	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subEmsLogsPathName, emsLogFileName)
+	fileName := fmt.Sprintf("EmsLog_%s_%s_%s_%s%s", sn, fileSupType, strings.ReplaceAll(dateStr, "-", "_"), strconv.FormatInt(callTime.Unix(), 10), matchSuffix)
+	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subEmsLogsPathName, fileName)
 	return objectName, minio.UploadFromReader(dataReader, int64(len(dataBytes)), objectName, contentType)
 }
 
+// 判断FaultRecord文件是否存在
+func ExistsBatchDownloadFaultRecord(sn string, faultRecordFileName string) (bool, error) {
+	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subFaultRecordPathName, faultRecordFileName)
+	exists, err := minio.ExistsObject(objectName)
+	if err != nil {
+		zap.L().Error("检查FaultRecord文件失败", zap.String("fileName", faultRecordFileName), zap.String("objectName", objectName), zap.String("err", err.Error()))
+		return false, err
+	}
+
+	return exists, nil
+}
+
+// 获取FaultRecord文件状态信息
+func StatBatchDownloadFaultRecord(sn string, faultRecordFileName string) (*minio.ObjectInfo, error) {
+	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subFaultRecordPathName, faultRecordFileName)
+	status, err := minio.StatObject(objectName)
+	if err != nil {
+		zap.L().Error("获取FaultRecord文件状态失败", zap.String("fileName", faultRecordFileName), zap.String("objectName", objectName), zap.String("err", err.Error()))
+		return nil, err
+	}
+
+	return &status, nil
+}
+
 // 保存FaultRecord文件到文件服务器
 // return objName
-func SaveBatchDownloadFaultRecord(sn string, dataBytes []byte, faultRecordFileName string, contentType string) (string, error) {
+func SaveBatchDownloadFaultRecord(sn string, dataBytes []byte, faultRecordFileName string) (string, error) {
 	// 将 []byte 封装成 io.Reader
 	dataReader := bytes.NewReader(dataBytes)
+
+	// 计算Content Type
+	contentType := mimetype.GetContentType(faultRecordFileName)
+
 	// 保存到minio
 	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subFaultRecordPathName, faultRecordFileName)
 	return objectName, minio.UploadFromReader(dataReader, int64(len(dataBytes)), objectName, contentType)
 }
 
+// 判断UpdateLog excel文件是否存在
+func ExistsBatchDownloadUpdateLog(sn string, date time.Time) (bool, error) {
+	fileName := fmt.Sprintf("UpdateLog_%s_%s.xlsx", sn, strings.ReplaceAll(date.Format(time.DateTime), " ", "_"))
+	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subUpdateLogPathName, fileName)
+	exists, err := minio.ExistsObject(objectName)
+	if err != nil {
+		zap.L().Error("检查UpdateLog excel文件失败", zap.String("fileName", fileName), zap.String("objectName", objectName), zap.String("err", err.Error()))
+		return false, err
+	}
+
+	return exists, nil
+}
+
+// 获取UpdateLog excel文件状态信息
+func StatBatchDownloadUpdateLog(sn string, date time.Time) (*minio.ObjectInfo, error) {
+	fileName := fmt.Sprintf("UpdateLog_%s_%s.xlsx", sn, strings.ReplaceAll(date.Format(time.DateTime), " ", "_"))
+	objectName := fmt.Sprintf("%s/%s/%s/%s", mainPathName, sn, subUpdateLogPathName, fileName)
+	status, err := minio.StatObject(objectName)
+	if err != nil {
+		zap.L().Error("获取UpdateLog excel文件状态失败", zap.String("fileName", fileName), zap.String("objectName", objectName), zap.String("err", err.Error()))
+		return nil, err
+	}
+
+	return &status, nil
+}
+
 // 保存UpdateLog excel到文件服务器
 // return objName
 func SaveBatchDownloadUpdateLog(sn string, systemUpdateLogData []domain.BatchDownloadSystemUpdateLogInfo, aHubUpdateLogData []domain.BatchDownloadAHubUpdateLogInfo) (objectName string, err error) {

+ 203 - 1
service/repositories/api/fwh_pro/fwhHttpClient.go

@@ -1,6 +1,8 @@
 package fwh_pro
 
 import (
+	"encoding/json"
+	"fwh-toolbox-server/domain"
 	"fwh-toolbox-server/utils/httpClient"
 	"net/http"
 	"net/url"
@@ -8,7 +10,7 @@ import (
 )
 
 const (
-	timeout time.Duration = 60 // fwh请求超时60s
+	timeout time.Duration = 3 * time.Minute // fwh请求超时3min
 )
 
 // FwhHttpInit
@@ -63,3 +65,203 @@ func FwhHttpPost(url string, body any) (*httpClient.Response, error) {
 	}
 	return httpClient.HttpPost(url, body, &opt)
 }
+
+/************ 实现标准结构体解析 ************/
+
+func FwhHttpGetUnmarshal[T any](url string, params url.Values) (*domain.FwhResponse[T], error) {
+	res, err := FwhHttpGet(url, params)
+	if err != nil {
+		return nil, err
+	}
+
+	var data domain.FwhResponse[T]
+	// 反序列化(unmarshal)
+	jsonErr := json.Unmarshal(res.Body, &data)
+	if jsonErr != nil {
+		return nil, jsonErr
+	}
+
+	if data.Code == 401 {
+		// invalid token
+		reLogin() // 重新登录
+
+		// 重新请求
+		res, err := FwhHttpGet(url, params)
+		if err != nil {
+			return nil, err
+		}
+
+		// 反序列化(unmarshal)
+		jsonErr := json.Unmarshal(res.Body, &data)
+		if jsonErr != nil {
+			return nil, jsonErr
+		}
+	}
+
+	return &data, nil
+}
+
+func FwhHttpGetUnmarshalPage[T any](url string, params url.Values) (*domain.FwhPageResponse[T], error) {
+	res, err := FwhHttpGet(url, params)
+	if err != nil {
+		return nil, err
+	}
+
+	var data domain.FwhPageResponse[T]
+	// 反序列化(unmarshal)
+	jsonErr := json.Unmarshal(res.Body, &data)
+	if jsonErr != nil {
+		return nil, jsonErr
+	}
+
+	if data.Code == 401 {
+		// invalid token
+		reLogin() // 重新登录
+
+		// 重新请求
+		res, err := FwhHttpGet(url, params)
+		if err != nil {
+			return nil, err
+		}
+
+		// 反序列化(unmarshal)
+		jsonErr := json.Unmarshal(res.Body, &data)
+		if jsonErr != nil {
+			return nil, jsonErr
+		}
+	}
+
+	return &data, nil
+}
+
+func FwhHttpGetUnmarshalA[T any](url string, params url.Values) (*domain.FwhResponseA[T], error) {
+	res, err := FwhHttpGet(url, params)
+	if err != nil {
+		return nil, err
+	}
+
+	var data domain.FwhResponseA[T]
+	// 反序列化(unmarshal)
+	jsonErr := json.Unmarshal(res.Body, &data)
+	if jsonErr != nil {
+		return nil, jsonErr
+	}
+
+	if data.Code == 401 {
+		// invalid token
+		reLogin() // 重新登录
+
+		// 重新请求
+		res, err := FwhHttpGet(url, params)
+		if err != nil {
+			return nil, err
+		}
+
+		// 反序列化(unmarshal)
+		jsonErr := json.Unmarshal(res.Body, &data)
+		if jsonErr != nil {
+			return nil, jsonErr
+		}
+	}
+
+	return &data, nil
+}
+
+func FwhHttpGetUnmarshalB[T any](url string, params url.Values) (*domain.FwhResponseB[T], error) {
+	res, err := FwhHttpGet(url, params)
+	if err != nil {
+		return nil, err
+	}
+
+	var data domain.FwhResponseB[T]
+	// 反序列化(unmarshal)
+	jsonErr := json.Unmarshal(res.Body, &data)
+	if jsonErr != nil {
+		return nil, jsonErr
+	}
+
+	if data.Code == 401 {
+		// invalid token
+		reLogin() // 重新登录
+
+		// 重新请求
+		res, err := FwhHttpGet(url, params)
+		if err != nil {
+			return nil, err
+		}
+
+		// 反序列化(unmarshal)
+		jsonErr := json.Unmarshal(res.Body, &data)
+		if jsonErr != nil {
+			return nil, jsonErr
+		}
+	}
+
+	return &data, nil
+}
+
+func FwhHttpPostUnmarshal[T any](url string, body any) (*domain.FwhResponse[T], error) {
+	res, err := FwhHttpPost(url, body)
+	if err != nil {
+		return nil, err
+	}
+
+	var data domain.FwhResponse[T]
+	// 反序列化(unmarshal)
+	jsonErr := json.Unmarshal(res.Body, &data)
+	if jsonErr != nil {
+		return nil, jsonErr
+	}
+
+	if data.Code == 401 {
+		// invalid token
+		reLogin() // 重新登录
+
+		// 重新请求
+		res, err := FwhHttpPost(url, body)
+		if err != nil {
+			return nil, err
+		}
+
+		// 反序列化(unmarshal)
+		jsonErr := json.Unmarshal(res.Body, &data)
+		if jsonErr != nil {
+			return nil, jsonErr
+		}
+	}
+
+	return &data, nil
+}
+
+func FwhHttpPostUnmarshalPage[T any](url string, body any) (*domain.FwhPageResponse[T], error) {
+	res, err := FwhHttpPost(url, body)
+	if err != nil {
+		return nil, err
+	}
+
+	var data domain.FwhPageResponse[T]
+	// 反序列化(unmarshal)
+	jsonErr := json.Unmarshal(res.Body, &data)
+	if jsonErr != nil {
+		return nil, jsonErr
+	}
+
+	if data.Code == 401 {
+		// invalid token
+		reLogin() // 重新登录
+
+		// 重新请求
+		res, err := FwhHttpPost(url, body)
+		if err != nil {
+			return nil, err
+		}
+
+		// 反序列化(unmarshal)
+		jsonErr := json.Unmarshal(res.Body, &data)
+		if jsonErr != nil {
+			return nil, jsonErr
+		}
+	}
+
+	return &data, nil
+}

+ 10 - 25
service/repositories/api/fwh_pro/fwh_emsLog.go

@@ -1,7 +1,6 @@
 package fwh_pro
 
 import (
-	"encoding/json"
 	"fmt"
 	"fwh-toolbox-server/config"
 	"fwh-toolbox-server/domain"
@@ -10,7 +9,7 @@ import (
 )
 
 // 召唤EMS日志
-func queryCallEmsLog(sn string, dateStr string, logType int) ([]byte, error) {
+func queryCallEmsLog(sn string, dateStr string, logType int) (*domain.FwhResponse[bool], error) {
 	// 要发送的表单数据
 	// form := url.Values{}
 	// form.Set("gatewayId", sn)                  // SN
@@ -30,33 +29,26 @@ func queryCallEmsLog(sn string, dateStr string, logType int) ([]byte, error) {
 	}
 
 	url := fmt.Sprintf("https://%s/hes-gateway/manage/logFileCall", config.FWH_ENERGY)
-	response, err := FwhHttpPost(url, form)
+	response, err := FwhHttpPostUnmarshal[bool](url, form)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 召唤EMS日志
 func QueryCallEmsLog(sn string, dateStr string, logType int) error {
 	// 查询告警快照数据
-	resByte, err := queryCallEmsLog(sn, dateStr, logType)
+	callRes, err := queryCallEmsLog(sn, dateStr, logType)
 	if err != nil {
 		return fmt.Errorf("[QueryCallEmsLog] %s", err.Error())
 	}
 
-	var callRes domain.FwhPageResponse[bool]
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &callRes)
-	if jsonErr != nil {
-		return fmt.Errorf("[QueryCallEmsLog] %s", jsonErr)
-	}
-
 	if callRes.Code != 200 {
 		return fmt.Errorf("[QueryCallEmsLog] 查询失败 code: %d, message: %s", callRes.Code, callRes.Message)
 	}
 
-	if !callRes.Result {
+	if callRes.Result == nil || !*callRes.Result {
 		return fmt.Errorf("[QueryCallEmsLog] 查询失败 code: %d, result: %v, message: %s", callRes.Code, callRes.Result, callRes.Message)
 	}
 
@@ -64,7 +56,7 @@ func QueryCallEmsLog(sn string, dateStr string, logType int) error {
 }
 
 // 查询EMS日志下载地址
-func queryEmsLogCallRecord(sn string) ([]byte, error) {
+func queryEmsLogCallRecord(sn string) (*domain.FwhResponse[[]domain.EmsLogRecordInfo], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	params.Add("linkId", sn) // SN
@@ -74,33 +66,26 @@ func queryEmsLogCallRecord(sn string) ([]byte, error) {
 	params.Add("lang", "ZH_CN")
 
 	reqUrl := fmt.Sprintf("https://%s/hes-gateway/manage/iotDeviceFileRecords", config.FWH_ENERGY)
-	response, err := FwhHttpGet(reqUrl, params)
+	response, err := FwhHttpGetUnmarshal[[]domain.EmsLogRecordInfo](reqUrl, params)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 查询EMS日志下载地址
 func QueryEmsLogCallRecord(sn string, dateStr string, logType int) (*domain.EmsLogRecordInfo, error) {
 	// 查询告警快照数据
-	resByte, err := queryEmsLogCallRecord(sn)
+	callRecordRes, err := queryEmsLogCallRecord(sn)
 	if err != nil {
 		return nil, fmt.Errorf("[QueryEmsLogCallRecord] %s", err.Error())
 	}
 
-	var callRecordRes domain.FwhPageResponse[[]domain.EmsLogRecordInfo]
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &callRecordRes)
-	if jsonErr != nil {
-		return nil, fmt.Errorf("[QueryEmsLogCallRecord] %s", jsonErr)
-	}
-
 	if callRecordRes.Code != 200 {
 		return nil, fmt.Errorf("[QueryEmsLogCallRecord] 查询失败 code: %d, message: %s", callRecordRes.Code, callRecordRes.Message)
 	}
 
-	for _, record := range callRecordRes.Result {
+	for _, record := range *callRecordRes.Result {
 		if record.LogTime == dateStr && record.CodeType == logType {
 			return &record, nil
 		}

+ 21 - 49
service/repositories/api/fwh_pro/fwh_eventAlarmLog.go

@@ -1,7 +1,6 @@
 package fwh_pro
 
 import (
-	"encoding/json"
 	"errors"
 	"fmt"
 	"fwh-toolbox-server/config"
@@ -21,21 +20,12 @@ const (
 	Alarm                       // 2
 )
 
-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)
+func queryEventAndAlarmLog(sn string, params url.Values) (*domain.FwhResponse[[]domain.EventAlarmItem], error) {
+	userId := getUserId()
 
 	// 构造 query 参数
-	params.Add("userId", strconv.Itoa(userId))
+	params.Add("userId", strconv.FormatUint(uint64(userId), 10))
 	params.Add("searchName", sn)
 	params.Add("timeType", strconv.Itoa(1)) // 0-通讯时间;1-设备时间
 	params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
@@ -43,16 +33,16 @@ func queryEventAndAlarmLog(sn string, params url.Values) ([]byte, error) {
 	params.Add("land", "ZH_CN")
 
 	reqUrl := fmt.Sprintf("https://%s/hes-gateway/manage/iotDeviceRunLogs", config.FWH_ENERGY)
-	response, err := FwhHttpGet(reqUrl, params)
+	response, err := FwhHttpGetUnmarshal[[]domain.EventAlarmItem](reqUrl, params)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 查询所有事件告警
 // startTime&endTime 格式 YYYY-MM-DD HH:mm:SS
-func queryAllEventAlarm(sn, startTime, endTime string) ([]byte, error) {
+func queryAllEventAlarm(sn, startTime, endTime string) (*domain.FwhResponse[[]domain.EventAlarmItem], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	params.Add("startTime", startTime)
@@ -63,7 +53,7 @@ func queryAllEventAlarm(sn, startTime, endTime string) ([]byte, error) {
 
 // 查询事件日志
 // startTime&endTime 格式 YYYY-MM-DD HH:mm:SS
-func queryEventLog(sn, startTime, endTime string) ([]byte, error) {
+func queryEventLog(sn, startTime, endTime string) (*domain.FwhResponse[[]domain.EventAlarmItem], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	params.Add("logType", strconv.Itoa(0))
@@ -76,7 +66,7 @@ func queryEventLog(sn, startTime, endTime string) ([]byte, error) {
 // 查询告警日志
 // startTime&endTime 格式 YYYY-MM-DD HH:mm:SS
 // alarmCode - 支持多个code,用‘,’隔开,“”代表查询所有
-func queryAlarmLog(sn, startTime, endTime, alarmCode string) ([]byte, error) {
+func queryAlarmLog(sn, startTime, endTime, alarmCode string) (*domain.FwhResponse[[]domain.EventAlarmItem], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	params.Add("logType", strconv.Itoa(1))
@@ -88,7 +78,7 @@ func queryAlarmLog(sn, startTime, endTime, alarmCode string) ([]byte, error) {
 }
 
 // 查询实时告警
-func queryRealtimeAlarm(sn string) ([]byte, error) {
+func queryRealtimeAlarm(sn string) (*domain.FwhResponse[[]domain.EventAlarmItem], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	params.Add("logType", strconv.Itoa(2))
@@ -105,11 +95,6 @@ func queryEventOrAlarmListOnePage(sn string, startTime, endTime time.Time, t Eve
 			continue
 		}
 
-		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)
 			zap.L().Warn("queryEventOrAlarmListOnePage 查询失败 retry", zap.Int("code", eventLogRes.Code), zap.String("msg", eventLogRes.Message))
@@ -117,39 +102,33 @@ func queryEventOrAlarmListOnePage(sn string, startTime, endTime time.Time, t Eve
 			continue
 		}
 
-		return eventLogRes.Result, nil
+		return *eventLogRes.Result, nil
 	}
 
 	return nil, errors.New("retry failed")
 }
 
-func queryEventOrAlarmListOnePageExec(sn string, startTime, endTime time.Time, t EventAlarmType) (*EventAlarmListResponse, error) {
+func queryEventOrAlarmListOnePageExec(sn string, startTime, endTime time.Time, t EventAlarmType) (*domain.FwhResponse[[]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 eventLogRes *domain.FwhResponse[[]domain.EventAlarmItem]
 	var err error
 	switch t {
 	case Event:
-		resByte, err = queryEventLog(sn, startTimeStr, endTimeStr)
+		eventLogRes, err = queryEventLog(sn, startTimeStr, endTimeStr)
 	case Alarm:
-		resByte, err = queryAlarmLog(sn, startTimeStr, endTimeStr, "")
+		eventLogRes, err = queryAlarmLog(sn, startTimeStr, endTimeStr, "")
 	default:
-		resByte, err = queryAllEventAlarm(sn, startTimeStr, endTimeStr)
+		eventLogRes, err = queryAllEventAlarm(sn, startTimeStr, endTimeStr)
 	}
 	if err != nil {
 		return nil, err
 	}
-	var eventLogRes EventAlarmListResponse
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &eventLogRes)
-	if jsonErr != nil {
-		return nil, jsonErr
-	}
 
-	return &eventLogRes, nil
+	return eventLogRes, nil
 }
 
 // 从FWH获取事件或告警log数据
@@ -213,7 +192,7 @@ func QueryEventOrAlarmListOnTimeRange(sn string, startTime, endTime time.Time, t
 
 // 查询快照信息
 // logTime 格式 YYYY-MM-DD
-func queryAlarmSnapshotInfo(sn, logTime string, page int) ([]byte, error) {
+func queryAlarmSnapshotInfo(sn, logTime string, page int) (*domain.FwhPageResponse[[]domain.AlarmSnapshotInfo], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	params.Add("logTime", logTime)
@@ -223,11 +202,11 @@ func queryAlarmSnapshotInfo(sn, logTime string, page int) ([]byte, error) {
 	params.Add("land", "ZH_CN")
 
 	reqUrl := fmt.Sprintf("https://%s/hes-gateway/manage/selectAlarmSnapshotList", config.FWH_ENERGY)
-	response, err := FwhHttpGet(reqUrl, params)
+	response, err := FwhHttpGetUnmarshalPage[[]domain.AlarmSnapshotInfo](reqUrl, params)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 func QueryAlarmSnapshotInfo(sn, logTime string) ([]domain.AlarmSnapshotInfo, error) {
@@ -236,18 +215,11 @@ func QueryAlarmSnapshotInfo(sn, logTime string) ([]domain.AlarmSnapshotInfo, err
 
 	for {
 		// 查询告警快照数据
-		resByte, err := queryAlarmSnapshotInfo(sn, logTime, page)
+		alarmSnapshotRes, 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)
 		}
@@ -256,7 +228,7 @@ func QueryAlarmSnapshotInfo(sn, logTime string) ([]domain.AlarmSnapshotInfo, err
 			return nil, fmt.Errorf("[QueryAlarmSnapshotInfo] 数据超过10000条")
 		}
 
-		infoList = append(infoList, alarmSnapshotRes.Result...)
+		infoList = append(infoList, *alarmSnapshotRes.Result...)
 
 		if alarmSnapshotRes.Total <= uint64(page*int(REQ_PAGE_SIZE)) {
 			break

+ 34 - 62
service/repositories/api/fwh_pro/fwh_faultRecord.go

@@ -1,17 +1,18 @@
 package fwh_pro
 
 import (
-	"encoding/json"
 	"fmt"
 	"fwh-toolbox-server/config"
 	"fwh-toolbox-server/domain"
 	"net/url"
 	"strconv"
 	"strings"
+
+	"go.uber.org/zap"
 )
 
 // 查询故障录波日志
-func queryFaultRecordList(sn string, startTime string, endTime string) ([]byte, error) {
+func queryFaultRecordList(sn string, startTime string, endTime string) (*domain.FwhResponse[[]domain.FaultRecordInfo], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	params.Add("deviseSerialNumber", sn)       // SN
@@ -21,36 +22,29 @@ func queryFaultRecordList(sn string, startTime string, endTime string) ([]byte,
 	params.Add("pageNum", strconv.Itoa(1))
 	params.Add("lang", "ZH_CN")
 	reqUrl := fmt.Sprintf("https://%s/hes-gateway/manage/faultRecordingLst", config.FWH_ENERGY)
-	response, err := FwhHttpGet(reqUrl, params)
+	response, err := FwhHttpGetUnmarshal[[]domain.FaultRecordInfo](reqUrl, params)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 查询故障录波日志
 func QueryFaultRecordList(sn string, startTime string, endTime string) ([]domain.FaultRecordInfo, error) {
-	resByte, err := queryFaultRecordList(sn, startTime, endTime)
+	faultRecordResp, err := queryFaultRecordList(sn, startTime, endTime)
 	if err != nil {
 		return nil, fmt.Errorf("[QueryFaultRecordList] %s", err.Error())
 	}
 
-	var faultRecordResp domain.FwhPageResponse[[]domain.FaultRecordInfo]
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &faultRecordResp)
-	if jsonErr != nil {
-		return nil, fmt.Errorf("[QueryFaultRecordList] %s", jsonErr)
-	}
-
 	if faultRecordResp.Code != 200 {
 		return nil, fmt.Errorf("[QueryFaultRecordList] 查询失败 code: %d, message: %s", faultRecordResp.Code, faultRecordResp.Message)
 	}
 
-	return faultRecordResp.Result, nil
+	return *faultRecordResp.Result, nil
 }
 
 // 查询设备可召唤数据列表 - 仅自动录播
-func queryFaultRecordCallableList(devType int, sn string) ([]byte, error) {
+func queryFaultRecordCallableList(devType int, sn string) (*domain.FwhResponse[[]domain.FaultRecordCallableInfo], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	params.Add("deviceSerial", sn) // aGate or aPower or aHub SN
@@ -58,28 +52,27 @@ func queryFaultRecordCallableList(devType int, sn string) ([]byte, error) {
 	params.Add("lang", "ZH_CN")
 
 	reqUrl := fmt.Sprintf("https://%s/hes-gateway/manage/getWaveRecordingCatalog", config.FWH_ENERGY)
-	response, err := FwhHttpGet(reqUrl, params)
+	response, err := FwhHttpGetUnmarshal[[]domain.FaultRecordCallableInfo](reqUrl, params)
+	// 此处无数据也会失败,暂时定义失败为无数据,不报错
 	if err != nil {
-		return nil, err
-	}
-	return response.Body, nil
+		zap.L().Error("[queryFaultRecordCallableList] Response failed", zap.String("err", err.Error()))
+		// 创造假的空数据
+		return &domain.FwhResponse[[]domain.FaultRecordCallableInfo]{
+			Code:   200,
+			Result: &[]domain.FaultRecordCallableInfo{},
+		}, nil
+	}
+	return response, nil
 }
 
 // 查询设备可召唤数据列表 - 仅自动录播
 // devType: 0-aPower 1-aGate 2-MPPT 3-aHub
 func QueryFaultRecordCallableList(devType int, sn string) ([]domain.FaultRecordCallableInfo, error) {
-	resByte, err := queryFaultRecordCallableList(devType, sn)
+	faultRecordCallableResp, err := queryFaultRecordCallableList(devType, sn)
 	if err != nil {
 		return nil, fmt.Errorf("[QueryFaultRecordCallableList] %s", err.Error())
 	}
 
-	var faultRecordCallableResp domain.FwhPageResponse[[]domain.FaultRecordCallableInfo]
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &faultRecordCallableResp)
-	if jsonErr != nil {
-		return nil, fmt.Errorf("[QueryFaultRecordCallableList] %s", jsonErr)
-	}
-
 	if faultRecordCallableResp.Code != 200 {
 		// 处理未查到数据的情况
 		if faultRecordCallableResp.Code == 400 && faultRecordCallableResp.Message == "故障目录为空" {
@@ -88,11 +81,11 @@ func QueryFaultRecordCallableList(devType int, sn string) ([]domain.FaultRecordC
 		return nil, fmt.Errorf("[QueryFaultRecordCallableList] 查询失败 code: %d, message: %s", faultRecordCallableResp.Code, faultRecordCallableResp.Message)
 	}
 
-	return faultRecordCallableResp.Result, nil
+	return *faultRecordCallableResp.Result, nil
 }
 
 // 召唤故障录波日志
-func queryCellFaultRecord(devType int, sn string, indexList []int) ([]byte, error) {
+func queryCellFaultRecord(devType int, sn string, indexList []int) (*domain.FwhResponse[interface{}], error) {
 	if len(indexList) < 1 {
 		return nil, fmt.Errorf("未选择召唤数据idx")
 	}
@@ -112,27 +105,20 @@ func queryCellFaultRecord(devType int, sn string, indexList []int) ([]byte, erro
 	form.Set("lang", "ZH_CN")
 
 	url := fmt.Sprintf("https://%s/hes-gateway/manage/callFaultRecording", config.FWH_ENERGY)
-	response, err := FwhHttpPost(url, form)
+	response, err := FwhHttpPostUnmarshal[interface{}](url, form)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 召唤故障录波日志
 func QueryCellFaultRecord(devType int, sn string, indexList []int) error {
-	resByte, err := queryCellFaultRecord(devType, sn, indexList)
+	cellFaultRecordResp, err := queryCellFaultRecord(devType, sn, indexList)
 	if err != nil {
 		return fmt.Errorf("[QueryCellFaultRecord] %s", err.Error())
 	}
 
-	var cellFaultRecordResp domain.FwhPageResponse[interface{}]
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &cellFaultRecordResp)
-	if jsonErr != nil {
-		return fmt.Errorf("[QueryCellFaultRecord] %s", jsonErr)
-	}
-
 	if cellFaultRecordResp.Code != 200 {
 		return fmt.Errorf("[QueryCellFaultRecord] 查询失败 code: %d, message: %s", cellFaultRecordResp.Code, cellFaultRecordResp.Message)
 	}
@@ -141,7 +127,7 @@ func QueryCellFaultRecord(devType int, sn string, indexList []int) error {
 }
 
 // 查询故障录波召唤记录
-func queryFaultRecordCallRecordList() ([]byte, error) {
+func queryFaultRecordCallRecordList() (*domain.FwhResponse[[]domain.FaultRecordInfo], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	params.Add("pageSize", strconv.Itoa(50))
@@ -149,36 +135,29 @@ func queryFaultRecordCallRecordList() ([]byte, error) {
 	params.Add("lang", "ZH_CN")
 
 	reqUrl := fmt.Sprintf("https://%s/hes-gateway/manage/lstFaultWaveCall", config.FWH_ENERGY)
-	response, err := FwhHttpGet(reqUrl, params)
+	response, err := FwhHttpGetUnmarshal[[]domain.FaultRecordInfo](reqUrl, params)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 查询故障录波召唤记录
 func QueryFaultRecordCallRecordList() ([]domain.FaultRecordInfo, error) {
-	resByte, err := queryFaultRecordCallRecordList()
+	faultRecordCallRecordResp, err := queryFaultRecordCallRecordList()
 	if err != nil {
 		return nil, fmt.Errorf("[QueryFaultRecordCallRecordList] %s", err.Error())
 	}
 
-	var faultRecordCallRecordResp domain.FwhPageResponse[[]domain.FaultRecordInfo]
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &faultRecordCallRecordResp)
-	if jsonErr != nil {
-		return nil, fmt.Errorf("[QueryFaultRecordCallRecordList] %s", jsonErr)
-	}
-
 	if faultRecordCallRecordResp.Code != 200 {
 		return nil, fmt.Errorf("[QueryFaultRecordCallRecordList] 查询失败 code: %d, message: %s", faultRecordCallRecordResp.Code, faultRecordCallRecordResp.Message)
 	}
 
-	return faultRecordCallRecordResp.Result, nil
+	return *faultRecordCallRecordResp.Result, nil
 }
 
 // 查询故障录波子记录
-func queryFaultRecordSubList(id int64) ([]byte, error) {
+func queryFaultRecordSubList(id int64) (*domain.FwhResponse[[]domain.FaultRecordSubInfo], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	params.Add("faultId", strconv.FormatInt(id, 10))
@@ -187,32 +166,25 @@ func queryFaultRecordSubList(id int64) ([]byte, error) {
 	params.Add("lang", "ZH_CN")
 
 	reqUrl := fmt.Sprintf("https://%s/hes-gateway/manage/lstSubFaultWave", config.FWH_ENERGY)
-	response, err := FwhHttpGet(reqUrl, params)
+	response, err := FwhHttpGetUnmarshal[[]domain.FaultRecordSubInfo](reqUrl, params)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 查询故障录波子记录
 func QueryFaultRecordSubList(id int64) ([]domain.FaultRecordSubInfo, error) {
-	resByte, err := queryFaultRecordSubList(id)
+	faultRecordSubResp, err := queryFaultRecordSubList(id)
 	if err != nil {
 		return nil, fmt.Errorf("[QueryFaultRecordSubList] %s", err.Error())
 	}
 
-	var faultRecordSubResp domain.FwhPageResponse[[]domain.FaultRecordSubInfo]
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &faultRecordSubResp)
-	if jsonErr != nil {
-		return nil, fmt.Errorf("[QueryFaultRecordSubList] %s", jsonErr)
-	}
-
 	if faultRecordSubResp.Code != 200 {
 		return nil, fmt.Errorf("[QueryFaultRecordSubList] 查询失败 code: %d, message: %s", faultRecordSubResp.Code, faultRecordSubResp.Message)
 	}
 
-	return faultRecordSubResp.Result, nil
+	return *faultRecordSubResp.Result, nil
 }
 
 // 请求下载录波数据

+ 53 - 52
service/repositories/api/fwh_pro/fwh_login.go

@@ -1,14 +1,9 @@
 package fwh_pro
 
 import (
-	"encoding/base64"
 	"encoding/json"
 	"fmt"
 	"fwh-toolbox-server/config"
-	"strings"
-	"time"
-
-	"go.uber.org/zap"
 )
 
 var loginInfo LoginData = LoginData{
@@ -77,33 +72,34 @@ func Login() error {
 	return nil
 }
 
-func isTokenExpired(token string) (bool, error) {
-	parts := strings.Split(token, ".")
-	if len(parts) != 3 {
-		return false, fmt.Errorf("token 格式错误")
-	}
-
-	payloadSegment := parts[1]
-	// JWT 使用 base64 URL encoding
-	payloadBytes, err := base64.RawURLEncoding.DecodeString(payloadSegment)
-	if err != nil {
-		return false, err
-	}
-
-	var payload map[string]interface{}
-	if err := json.Unmarshal(payloadBytes, &payload); err != nil {
-		zap.L().Error("token payload解析失败", zap.String("payload", string(payloadBytes)))
-		return false, err
-	}
-
-	expFloat, ok := payload["exp"].(float64)
-	if !ok {
-		return false, fmt.Errorf("token payload 中没有 exp")
-	}
-
-	expTime := time.Unix(int64(expFloat), 0)
-	return time.Now().After(expTime), nil
-}
+// 弃用:token payload被加密
+// func isTokenExpired(token string) (bool, error) {
+// 	parts := strings.Split(token, ".")
+// 	if len(parts) != 3 {
+// 		return false, fmt.Errorf("token 格式错误")
+// 	}
+
+// 	payloadSegment := parts[1]
+// 	// JWT 使用 base64 URL encoding
+// 	payloadBytes, err := base64.RawURLEncoding.DecodeString(payloadSegment)
+// 	if err != nil {
+// 		return false, err
+// 	}
+
+// 	var payload map[string]interface{}
+// 	if err := json.Unmarshal(payloadBytes, &payload); err != nil {
+// 		zap.L().Error("token payload解析失败", zap.String("payload", string(payloadBytes)))
+// 		return false, err
+// 	}
+
+// 	expFloat, ok := payload["exp"].(float64)
+// 	if !ok {
+// 		return false, fmt.Errorf("token payload 中没有 exp")
+// 	}
+
+// 	expTime := time.Unix(int64(expFloat), 0)
+// 	return time.Now().After(expTime), nil
+// }
 
 func getLoginToken() string {
 	// 判断是否已经有token
@@ -129,26 +125,31 @@ func getLoginToken() string {
 	return loginInfo.Token
 }
 
-func getUserId(token string) (int, error) {
-	// JWT 由三部分组成: header.payload.signature
-	parts := strings.Split(token, ".")
-	if len(parts) < 2 {
-		return 0, fmt.Errorf("getUserId: invalid token")
-	}
-
-	// 解码 payload 部分
-	payload, err := base64.RawURLEncoding.DecodeString(parts[1])
-	if err != nil {
-		return 0, fmt.Errorf("getUserId: %s", err.Error())
-	}
-
-	// 解析 JSON
-	var claims JwtPayload
-	if err := json.Unmarshal(payload, &claims); err != nil {
-		return 0, fmt.Errorf("getUserId: %s", err.Error())
-	}
-
-	return claims.UserId, nil
+// 弃用:token payload被加密
+// func getUserId(token string) (int, error) {
+// 	// JWT 由三部分组成: header.payload.signature
+// 	parts := strings.Split(token, ".")
+// 	if len(parts) < 2 {
+// 		return 0, fmt.Errorf("getUserId: invalid token")
+// 	}
+
+// 	// 解码 payload 部分
+// 	payload, err := base64.RawURLEncoding.DecodeString(parts[1])
+// 	if err != nil {
+// 		return 0, fmt.Errorf("getUserId: %s", err.Error())
+// 	}
+
+// 	// 解析 JSON
+// 	var claims JwtPayload
+// 	if err := json.Unmarshal(payload, &claims); err != nil {
+// 		return 0, fmt.Errorf("getUserId: %s", err.Error())
+// 	}
+
+// 	return claims.UserId, nil
+// }
+
+func getUserId() uint {
+	return loginInfo.UserId
 }
 
 // 清空token

+ 7 - 23
service/repositories/api/fwh_pro/fwh_mqttLog.go

@@ -1,7 +1,6 @@
 package fwh_pro
 
 import (
-	"encoding/json"
 	"errors"
 	"fmt"
 	"fwh-toolbox-server/config"
@@ -12,14 +11,6 @@ import (
 	"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
@@ -28,7 +19,7 @@ type QueryMqttLogParam struct {
 }
 
 // 查询站点信息
-func queryMqttLog(param QueryMqttLogParam) ([]byte, error) {
+func queryMqttLog(param QueryMqttLogParam) (*domain.FwhPageResponse[[]domain.MqttLogItem], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	if param.SN != nil {
@@ -55,11 +46,11 @@ func queryMqttLog(param QueryMqttLogParam) ([]byte, error) {
 	params.Add("lang", "ZH_CN")
 
 	reqUrl := fmt.Sprintf("https://%s/hes-gateway/manage/iotMqttCommlogs", config.FWH_ENERGY)
-	response, err := FwhHttpGet(reqUrl, params)
+	response, err := FwhHttpGetUnmarshalPage[[]domain.MqttLogItem](reqUrl, params)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 func queryMqttLogListOnePage(sn string, cmdType domain.MqttCmdType, startTime, endTime time.Time) ([]domain.MqttLogItem, error) {
@@ -69,20 +60,13 @@ func queryMqttLogListOnePage(sn string, cmdType domain.MqttCmdType, startTime, e
 		StartTime: &startTime,
 		EndTime:   &endTime,
 	}
-	resByte, err := queryMqttLog(param)
+	mqttLogRes, 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")
+	if mqttLogRes.Code != 200 {
+		return nil, fmt.Errorf("[queryMqttLogListOnePage] 查询失败 code: %d, message: %s", mqttLogRes.Code, mqttLogRes.Message)
 	}
 
 	// 判断是否超出可处理数据上限 10万条数据
@@ -90,7 +74,7 @@ func queryMqttLogListOnePage(sn string, cmdType domain.MqttCmdType, startTime, e
 		return nil, fmt.Errorf("超出数据请求上限 - Total: %d (>10万)", mqttLogRes.Total)
 	}
 
-	return mqttLogRes.Result, nil
+	return *mqttLogRes.Result, nil
 }
 
 // 从FWH获取MQTT log数据

+ 9 - 17
service/repositories/api/fwh_pro/fwh_powerFlow.go

@@ -1,46 +1,38 @@
 package fwh_pro
 
 import (
-	"encoding/json"
-	"errors"
 	"fmt"
 	"fwh-toolbox-server/config"
+	"fwh-toolbox-server/domain"
 	"fwh-toolbox-server/dto"
 	"net/url"
 	"time"
 )
 
 // 查询站点功率流信息
-func querySitePowerFlow(sn string, day time.Time) ([]byte, error) {
+func querySitePowerFlow(sn string, day time.Time) (*domain.FwhResponseA[dto.SitePowerFlow], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	params.Add("gatewayId", sn)
 	params.Add("dayTime", day.Format(time.DateOnly)) // 会自动 URL 编码
 
 	reqUrl := fmt.Sprintf("https://%s/api-energy/power/getFhpPowerByDayOld", config.FWH_ENERGY)
-	response, err := FwhHttpGet(reqUrl, params)
+	response, err := FwhHttpGetUnmarshalA[dto.SitePowerFlow](reqUrl, params)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 从fwh_pro获取站点功率流
-func QuerySitePowerFlow(site string, day time.Time) (dto.SitePowerFlowResponse, error) {
-	resByte, err := querySitePowerFlow(site, day)
+func QuerySitePowerFlow(site string, day time.Time) (*domain.FwhResponseA[dto.SitePowerFlow], error) {
+	sitePowerFlowRes, 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
+		return nil, err
 	}
 
-	if sitePowerFlowRes.Code == 401 {
-		// TODO 尝试重新登录
-		return dto.SitePowerFlowResponse{}, errors.New("certification expired")
+	if sitePowerFlowRes.Code != 200 {
+		return nil, fmt.Errorf("[queryMqttLogListOnePage] 查询失败 code: %d, message: %s", sitePowerFlowRes.Code, sitePowerFlowRes.Msg)
 	}
 
 	return sitePowerFlowRes, nil

+ 9 - 17
service/repositories/api/fwh_pro/fwh_realtimeAalrm.go

@@ -1,45 +1,37 @@
 package fwh_pro
 
 import (
-	"encoding/json"
-	"errors"
 	"fmt"
 	"fwh-toolbox-server/config"
+	"fwh-toolbox-server/domain"
 	"fwh-toolbox-server/dto"
 	"net/url"
 )
 
 // 查询站点告警信息
-func querySiteAlarmList(sn string) ([]byte, error) {
+func querySiteAlarmList(sn string) (*domain.FwhPageResponse[[]dto.SiteAlarmList], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	params.Add("gatewayId", sn)
 	params.Add("land", "ZH_CN")
 
 	reqUrl := fmt.Sprintf("https://%s/hes-gateway/manage/getEquipmentAlarm", config.FWH_ENERGY)
-	response, err := FwhHttpGet(reqUrl, params)
+	response, err := FwhHttpGetUnmarshalPage[[]dto.SiteAlarmList](reqUrl, params)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 从fwh_pro获取站点告警列表
-func QuerySiteAlarmList(site string) (dto.SiteAlarmListResponse, error) {
-	resByte, err := querySiteAlarmList(site)
+func QuerySiteAlarmList(site string) (*domain.FwhPageResponse[[]dto.SiteAlarmList], error) {
+	siteAlarmListRes, 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
+		return nil, err
 	}
 
-	if siteAlarmListRes.Code == 401 {
-		// TODO 尝试重新登录
-		return dto.SiteAlarmListResponse{}, errors.New("certification expired")
+	if siteAlarmListRes.Code != 200 {
+		return nil, fmt.Errorf("[queryMqttLogListOnePage] 查询失败 code: %d, message: %s", siteAlarmListRes.Code, siteAlarmListRes.Message)
 	}
 
 	return siteAlarmListRes, nil

+ 4 - 18
service/repositories/api/fwh_pro/fwh_shutdownList.go

@@ -1,7 +1,6 @@
 package fwh_pro
 
 import (
-	"encoding/json"
 	"fmt"
 	"fwh-toolbox-server/config"
 	"fwh-toolbox-server/domain"
@@ -9,12 +8,6 @@ import (
 	"strconv"
 )
 
-type ShutdownListResponse struct {
-	Code int              `json:"code"`
-	Msg  string           `json:"msg"`
-	Data ShutdownListData `json:"data"`
-}
-
 type ShutdownListData struct {
 	Current int                     `json:"current"` // 当前页
 	Pages   int                     `json:"pages"`   // 总页数
@@ -24,7 +17,7 @@ type ShutdownListData struct {
 }
 
 // 查询Shutdown站点列表 one page
-func querySiteShutdownListOnePage(date string, country int, page int) ([]byte, error) {
+func querySiteShutdownListOnePage(date string, country int, page int) (*domain.FwhResponseB[ShutdownListData], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	if country <= 0 {
@@ -39,11 +32,11 @@ func querySiteShutdownListOnePage(date string, country int, page int) ([]byte, e
 	params.Add("land", "ZH_CN")
 
 	reqUrl := fmt.Sprintf("https://%s/api-inspect/fault/listShutdownGateway", config.FWH_ENERGY)
-	response, err := FwhHttpGet(reqUrl, params)
+	response, err := FwhHttpGetUnmarshalB[ShutdownListData](reqUrl, params)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 查询Shutdown站点列表
@@ -54,18 +47,11 @@ func QuerySiteShutdownList(date string, country int) ([]domain.ShutdownRecord, e
 
 	for total == 0 || REQ_PAGE_SIZE*uint64(page) < uint64(total) {
 		page++
-		resByte, err := querySiteShutdownListOnePage(date, country, page)
+		shutdownListRes, err := querySiteShutdownListOnePage(date, country, page)
 		if err != nil {
 			return nil, err
 		}
 
-		var shutdownListRes ShutdownListResponse
-		// 反序列化(unmarshal)
-		jsonErr := json.Unmarshal(resByte, &shutdownListRes)
-		if jsonErr != nil {
-			return nil, jsonErr
-		}
-
 		total = int64(shutdownListRes.Data.Total)
 		records = append(records, shutdownListRes.Data.Records...)
 	}

+ 33 - 62
service/repositories/api/fwh_pro/fwh_siteInfo.go

@@ -1,10 +1,9 @@
 package fwh_pro
 
 import (
-	"encoding/json"
-	"errors"
 	"fmt"
 	"fwh-toolbox-server/config"
+	"fwh-toolbox-server/domain"
 	"fwh-toolbox-server/dto"
 	"net/url"
 	"strconv"
@@ -42,7 +41,7 @@ type SiteModeInfoPayload struct {
 }
 
 // 查询站点基本信息
-func querySiteBaseInfoList(sn []string, pageSize uint, pageNum uint) ([]byte, error) {
+func querySiteBaseInfoList(sn []string, pageSize uint, pageNum uint) (*domain.FwhResponse[[]dto.SiteBaseInfo], error) {
 	snList := strings.Join(sn, ",")
 
 	// 要发送的 JSON
@@ -61,36 +60,29 @@ func querySiteBaseInfoList(sn []string, pageSize uint, pageNum uint) ([]byte, er
 	form.Set("lang", payload.Lang)
 
 	url := fmt.Sprintf("https://%s/hes-gateway/manage/selectIotFirmwareGatewayList", config.FWH_ENERGY)
-	response, err := FwhHttpPost(url, form)
+	response, err := FwhHttpPostUnmarshal[[]dto.SiteBaseInfo](url, form)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 从fwh_pro获取站点基本信息表
-func QuerySiteBaseInfoList(para dto.SiteBaseInfoPara) (dto.SiteBaseInfoResponse, error) {
-	resByte, err := querySiteBaseInfoList(para.SiteList, para.PageSize, para.PageNum)
+func QuerySiteBaseInfoList(para dto.SiteBaseInfoPara) (*domain.FwhResponse[[]dto.SiteBaseInfo], error) {
+	siteInfoRes, 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
+		return nil, err
 	}
 
-	if siteInfoRes.Code == 401 {
-		// TODO 尝试重新登录
-		return dto.SiteBaseInfoResponse{}, errors.New("certification expired")
+	if siteInfoRes.Code != 200 {
+		return nil, fmt.Errorf("[QuerySiteBaseInfoList] 查询失败 code: %d, message: %s", siteInfoRes.Code, siteInfoRes.Message)
 	}
 
 	return siteInfoRes, nil
 }
 
 // 查询站点信息
-func querySiteInfoList(sn *string, pageSize uint, pageNum uint) ([]byte, error) {
+func querySiteInfoList(sn *string, pageSize uint, pageNum uint) (*domain.FwhPageResponse[[]dto.SiteInfo], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	if sn != nil {
@@ -102,29 +94,22 @@ func querySiteInfoList(sn *string, pageSize uint, pageNum uint) ([]byte, error)
 	params.Add("lang", "ZH_CN")
 
 	reqUrl := fmt.Sprintf("https://%s/hes-gateway/manage/selectIotGatewayList", config.FWH_ENERGY)
-	response, err := FwhHttpGet(reqUrl, params)
+	response, err := FwhHttpGetUnmarshalPage[[]dto.SiteInfo](reqUrl, params)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 从fwh_pro获取站点基本信息表
-func QuerySiteInfoList(para dto.SiteInfoPara) (dto.SiteInfoResponse, error) {
-	resByte, err := querySiteInfoList(para.SN, para.PageSize, para.PageNum)
+func QuerySiteInfoList(para dto.SiteInfoPara) (*domain.FwhPageResponse[[]dto.SiteInfo], error) {
+	siteInfoRes, 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
+		return nil, err
 	}
 
-	if siteInfoRes.Code == 401 {
-		// TODO 尝试重新登录
-		return dto.SiteInfoResponse{}, errors.New("certification expired")
+	if siteInfoRes.Code != 200 {
+		return nil, fmt.Errorf("[QuerySiteInfoList] 查询失败 code: %d, message: %s", siteInfoRes.Code, siteInfoRes.Message)
 	}
 
 	return siteInfoRes, nil
@@ -136,7 +121,7 @@ func querySiteList(
 	searchFlag int,
 	pageSize uint,
 	pageNum uint,
-) ([]byte, error) {
+) (*domain.FwhPageResponse[[]dto.SiteInfo], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	params.Add("search", search)
@@ -146,11 +131,11 @@ func querySiteList(
 	params.Add("lang", "ZH_CN")
 
 	reqUrl := fmt.Sprintf("https://%s/hes-gateway/manage/selectIotGatewayList", config.FWH_ENERGY)
-	response, err := FwhHttpGet(reqUrl, params)
+	response, err := FwhHttpGetUnmarshalPage[[]dto.SiteInfo](reqUrl, params)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 从fwh_pro获取站点列表
@@ -159,28 +144,21 @@ func QuerySiteList(
 	searchFlag int,
 	pageSize uint,
 	pageNum uint,
-) (dto.SiteInfoResponse, error) {
-	resByte, err := querySiteList(search, searchFlag, pageSize, pageNum)
+) (*domain.FwhPageResponse[[]dto.SiteInfo], error) {
+	siteInfoRes, err := querySiteList(search, searchFlag, pageSize, 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
+		return nil, err
 	}
 
-	if siteInfoRes.Code == 401 {
-		// TODO 尝试重新登录
-		return dto.SiteInfoResponse{}, errors.New("certification expired")
+	if siteInfoRes.Code != 200 {
+		return nil, fmt.Errorf("[QuerySiteInfoList] 查询失败 code: %d, message: %s", siteInfoRes.Code, siteInfoRes.Message)
 	}
 
 	return siteInfoRes, nil
 }
 
 // 查询站点工作模式信息
-func querySiteModeInfo(sn string) ([]byte, error) {
+func querySiteModeInfo(sn string) (*domain.FwhResponse[dto.SiteModeInfo], error) {
 	// 要发送的 JSON
 	payload := SiteModeInfoPayload{
 		GatewayId: sn,
@@ -195,29 +173,22 @@ func querySiteModeInfo(sn string) ([]byte, error) {
 	form.Set("lang", payload.Lang)
 
 	url := fmt.Sprintf("https://%s/hes-gateway/manage/tou/getGatewayTouListV2", config.FWH_ENERGY)
-	response, err := FwhHttpPost(url, form)
+	response, err := FwhHttpPostUnmarshal[dto.SiteModeInfo](url, form)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 从fwh_pro获取站点工作模式信息
-func QuerySiteModeInfo(site string) (dto.SiteModeInfoResponse, error) {
-	resByte, err := querySiteModeInfo(site)
+func QuerySiteModeInfo(site string) (*domain.FwhResponse[dto.SiteModeInfo], error) {
+	siteModeInfoRes, 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
+		return nil, err
 	}
 
-	if siteModeInfoRes.Code == 401 {
-		// TODO 尝试重新登录
-		return dto.SiteModeInfoResponse{}, errors.New("certification expired")
+	if siteModeInfoRes.Code != 200 {
+		return nil, fmt.Errorf("[QuerySiteModeInfo] 查询失败 code: %d, message: %s", siteModeInfoRes.Code, siteModeInfoRes.Message)
 	}
 
 	return siteModeInfoRes, nil

+ 15 - 37
service/repositories/api/fwh_pro/fwh_update.go

@@ -1,7 +1,6 @@
 package fwh_pro
 
 import (
-	"encoding/json"
 	"fmt"
 	"fwh-toolbox-server/config"
 	"fwh-toolbox-server/domain"
@@ -10,7 +9,7 @@ import (
 )
 
 // 查询系统升级记录
-func querySystemUpdateLog(sn string) ([]byte, error) {
+func querySystemUpdateLog(sn string) (*domain.FwhResponse[[]domain.SystemUpdateRecordInfo], error) {
 	// 要发送的表单数据
 	form := url.Values{}
 	form.Set("optType", "1")  // 1-系统升级记录 2-下载记录(分步升级相关)
@@ -19,36 +18,29 @@ func querySystemUpdateLog(sn string) ([]byte, error) {
 	form.Set("pageNum", "1")
 
 	url := fmt.Sprintf("https://%s/hes-gateway/manage/selectIotFirmwareOrderList", config.FWH_ENERGY)
-	response, err := FwhHttpPost(url, form)
+	response, err := FwhHttpPostUnmarshal[[]domain.SystemUpdateRecordInfo](url, form)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 查询系统升级记录
 func QuerySystemUpdateLog(sn string) ([]domain.SystemUpdateRecordInfo, error) {
-	resByte, err := querySystemUpdateLog(sn)
+	systemUpdateLog, err := querySystemUpdateLog(sn)
 	if err != nil {
 		return nil, fmt.Errorf("[QuerySystemUpdateLog] %s", err.Error())
 	}
 
-	var systemUpdateLog domain.FwhPageResponse[[]domain.SystemUpdateRecordInfo]
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &systemUpdateLog)
-	if jsonErr != nil {
-		return nil, fmt.Errorf("[QuerySystemUpdateLog] %s", jsonErr)
-	}
-
 	if systemUpdateLog.Code != 200 {
 		return nil, fmt.Errorf("[QuerySystemUpdateLog] 查询失败 code: %d, message: %s", systemUpdateLog.Code, systemUpdateLog.Message)
 	}
 
-	return systemUpdateLog.Result, nil
+	return *systemUpdateLog.Result, nil
 }
 
 // 查询系统升级记录详细信息
-func querySystemUpdateLogDetail(orderId int64) ([]byte, error) {
+func querySystemUpdateLogDetail(orderId int64) (*domain.FwhResponse[[]domain.SystemUpdateDetailInfo], error) {
 	// 要发送的表单数据
 	form := url.Values{}
 	form.Set("optType", "1")                            // 1-系统升级记录 2-下载记录(分步升级相关)
@@ -57,36 +49,29 @@ func querySystemUpdateLogDetail(orderId int64) ([]byte, error) {
 	form.Set("pageNum", "1")
 
 	url := fmt.Sprintf("https://%s/hes-gateway/manage/selectIotFirmwareOrderDetailList", config.FWH_ENERGY)
-	response, err := FwhHttpPost(url, form)
+	response, err := FwhHttpPostUnmarshal[[]domain.SystemUpdateDetailInfo](url, form)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 查询系统升级记录详细信息
 func QuerySystemUpdateLogDetail(orderId int64) ([]domain.SystemUpdateDetailInfo, error) {
-	resByte, err := querySystemUpdateLogDetail(orderId)
+	systemUpdateLogDetail, err := querySystemUpdateLogDetail(orderId)
 	if err != nil {
 		return nil, fmt.Errorf("[QuerySystemUpdateLogDetail] %s", err.Error())
 	}
 
-	var systemUpdateLogDetail domain.FwhPageResponse[[]domain.SystemUpdateDetailInfo]
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &systemUpdateLogDetail)
-	if jsonErr != nil {
-		return nil, fmt.Errorf("[QuerySystemUpdateLogDetail] %s", jsonErr)
-	}
-
 	if systemUpdateLogDetail.Code != 200 {
 		return nil, fmt.Errorf("[QuerySystemUpdateLogDetail] 查询失败 code: %d, message: %s", systemUpdateLogDetail.Code, systemUpdateLogDetail.Message)
 	}
 
-	return systemUpdateLogDetail.Result, nil
+	return *systemUpdateLogDetail.Result, nil
 }
 
 // 查询aHub升级记录
-func queryAHubUpdateLog(sn string) ([]byte, error) {
+func queryAHubUpdateLog(sn string) (*domain.FwhPageResponse[[]domain.AHubUpdateRecordInfo], error) {
 	// 构造 query 参数
 	params := url.Values{}
 	params.Add("gatewayId", sn) // SN
@@ -95,30 +80,23 @@ func queryAHubUpdateLog(sn string) ([]byte, error) {
 	params.Add("lang", "ZH_CN")
 
 	reqUrl := fmt.Sprintf("https://%s/hes-gateway/manage/listAhubFirmwareOrder", config.FWH_ENERGY)
-	response, err := FwhHttpGet(reqUrl, params)
+	response, err := FwhHttpGetUnmarshalPage[[]domain.AHubUpdateRecordInfo](reqUrl, params)
 	if err != nil {
 		return nil, err
 	}
-	return response.Body, nil
+	return response, nil
 }
 
 // 查询aHub升级记录
 func QueryAHubUpdateLog(sn string) ([]domain.AHubUpdateRecordInfo, error) {
-	resByte, err := queryAHubUpdateLog(sn)
+	aHubUpdateLog, err := queryAHubUpdateLog(sn)
 	if err != nil {
 		return nil, fmt.Errorf("[QueryAHubUpdateLog] %s", err.Error())
 	}
 
-	var aHubUpdateLog domain.FwhPageResponse[[]domain.AHubUpdateRecordInfo]
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &aHubUpdateLog)
-	if jsonErr != nil {
-		return nil, fmt.Errorf("[QueryAHubUpdateLog] %s", jsonErr)
-	}
-
 	if aHubUpdateLog.Code != 200 {
 		return nil, fmt.Errorf("[QueryAHubUpdateLog] 查询失败 code: %d, message: %s", aHubUpdateLog.Code, aHubUpdateLog.Message)
 	}
 
-	return aHubUpdateLog.Result, nil
+	return *aHubUpdateLog.Result, nil
 }

+ 4 - 1
service/repositories/mongo/batchDownloadRepo.go

@@ -39,7 +39,10 @@ func GetBatchDownloadManageList(page, size uint, sn, progress *string) (*dto.Res
 			"failReasons": 1,
 		}).
 		SetSkip(int64((page - 1) * size)).
-		SetLimit(int64(size))
+		SetLimit(int64(size)).
+		SetSort(bson.D{
+			{Key: "_id", Value: -1}, // 倒序
+		})
 
 	cursor, err := collection.Find(ctx, filter, findOptions)
 	if err != nil {

+ 7 - 8
service/scheduler/scheduler.go

@@ -1,8 +1,6 @@
 package scheduler
 
 import (
-	"fwh-toolbox-server/domain"
-	"fwh-toolbox-server/taskQueue"
 	"log"
 	"time"
 
@@ -35,17 +33,18 @@ func Init() *cron.Cron {
 		log.Println("每天 15:35 执行Shutdown统计")
 
 		// 获取当前时间
-		now := time.Now()
+		// now := time.Now()
 
 		// 计算前天(往前两天)
-		beforeYesterday := now.AddDate(0, 0, -2)
+		// beforeYesterday := now.AddDate(0, 0, -2)
 
 		// 格式化为字符串(按 yyyy-MM-dd)
-		beforeYesterdayStr := beforeYesterday.Format(time.DateOnly)
+		// beforeYesterdayStr := beforeYesterday.Format(time.DateOnly)
 
-		taskQueue.EnqueueShutdownStatisticsTask(domain.TaskShutdownStatistics{
-			Date: beforeYesterdayStr,
-		})
+		// TODO 暂停每日任务
+		// taskQueue.EnqueueShutdownStatisticsTask(domain.TaskShutdownStatistics{
+		// 	Date: beforeYesterdayStr,
+		// })
 	})
 
 	c.Start()

+ 5 - 5
service/services/sitesMonitorServices.go

@@ -43,26 +43,26 @@ func UpdateSiteMonitorPageConfig(name string, config domain.MonitorPageConfig) e
 }
 
 // 从fwh_pro获取站点基本信息表
-func QuerySiteBaseInfoList(para dto.SiteBaseInfoPara) (dto.SiteBaseInfoResponse, error) {
+func QuerySiteBaseInfoList(para dto.SiteBaseInfoPara) (*domain.FwhResponse[[]dto.SiteBaseInfo], error) {
 	return repositories.QuerySiteBaseInfoList(para)
 }
 
 // 从fwh_pro获取站点基本信息表
-func QuerySiteInfoList(para dto.SiteInfoPara) (dto.SiteInfoResponse, error) {
+func QuerySiteInfoList(para dto.SiteInfoPara) (*domain.FwhPageResponse[[]dto.SiteInfo], error) {
 	return repositories.QuerySiteInfoList(para)
 }
 
 // 从fwh_pro获取站点工作模式信息
-func QuerySiteModeInfo(site string) (dto.SiteModeInfoResponse, error) {
+func QuerySiteModeInfo(site string) (*domain.FwhResponse[dto.SiteModeInfo], error) {
 	return repositories.QuerySiteModeInfo(site)
 }
 
 // 从fwh_pro获取站点功率流
-func QuerySitePowerFlow(site string, day time.Time) (dto.SitePowerFlowResponse, error) {
+func QuerySitePowerFlow(site string, day time.Time) (*domain.FwhResponseA[dto.SitePowerFlow], error) {
 	return repositories.QuerySitePowerFlow(site, day)
 }
 
 // 从fwh_pro获取站点告警列表
-func QuerySiteAlarmList(site string) (dto.SiteAlarmListResponse, error) {
+func QuerySiteAlarmList(site string) (*domain.FwhPageResponse[[]dto.SiteAlarmList], error) {
 	return repositories.QuerySiteAlarmList(site)
 }

+ 3 - 3
service/taskQueue/taskBatchDownload.go

@@ -139,8 +139,8 @@ var executeTaskBatchDownload = func(ctx context.Context, payload domain.TaskBatc
 				return nil, err
 			}
 
-			if len(siteBaseInfoList.Result) > 0 {
-				aPowerSnList := strings.Split(siteBaseInfoList.Result[0].FhpSn, ",")
+			if len(*siteBaseInfoList.Result) > 0 {
+				aPowerSnList := strings.Split((*siteBaseInfoList.Result)[0].FhpSn, ",")
 				for i := range aPowerSnList {
 					aPowerSn := strings.TrimSpace(aPowerSnList[i])
 
@@ -177,7 +177,7 @@ var executeTaskBatchDownload = func(ctx context.Context, payload domain.TaskBatc
 					return nil, err
 				}
 				dateStr := period.Start.Format(time.DateOnly)
-				objName, err := jobs.SaveBatchDownloadPowerFlow(sn, dateStr, res.Result)
+				objName, err := jobs.SaveBatchDownloadPowerFlow(sn, dateStr, *res.Result)
 				if err != nil {
 					zap.L().Error("站点运行时数据保存失败", zap.String("sn", sn), zap.String("err", err.Error()))
 					return nil, err

+ 17 - 1
service/utils/httpClient/httpClient.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"io"
 	"net/http"
 	"net/url"
@@ -67,7 +68,7 @@ func responseInterceptor(response *Response, retry func(*string) (*Response, err
 	// 判断是否回复code 401
 	var data ResponseData
 	if err := json.Unmarshal(response.Body, &data); err != nil {
-		zap.L().Warn("ResponseData 解析失败", zap.String("body", string(response.Body)), zap.String("err", err.Error()))
+		zap.L().Warn("ResponseData 解析失败", zap.String("Status", response.Status), zap.String("body", string(response.Body)), zap.String("err", err.Error()))
 		return response
 	}
 	if data.Code == 401 {
@@ -89,6 +90,7 @@ func RegisterLogin(login func() string) {
 func HttpGet(url string, params url.Values, opt *HttpClientOptions) (*Response, error) {
 	// 拼接参数到 URL
 	finalURL := url + "?" + params.Encode()
+	zap.L().Debug("HttpGet", zap.String("url", finalURL))
 
 	// 创建请求
 	req, err := http.NewRequest(
@@ -143,6 +145,12 @@ func HttpGet(url string, params url.Values, opt *HttpClientOptions) (*Response,
 		return nil, err
 	}
 
+	// 应答失败,直接返回
+	if response.StatusCode != 200 {
+		zap.L().Warn("[HttpGet] Response failed", zap.String("Status", response.Status), zap.String("Body", string(response.Body)))
+		return response, fmt.Errorf("[HttpGet] Response failed")
+	}
+
 	return responseInterceptor(response, reqFunc), nil
 }
 
@@ -193,6 +201,8 @@ func HttpPost(url string, body any, opt *HttpClientOptions) (*Response, error) {
 		return nil, err
 	}
 
+	zap.L().Debug("HttpPost", zap.String("url", url))
+
 	// 创建请求
 	req, err := http.NewRequest(
 		http.MethodPost,
@@ -248,5 +258,11 @@ func HttpPost(url string, body any, opt *HttpClientOptions) (*Response, error) {
 		return nil, err
 	}
 
+	// 应答失败,直接返回
+	if response.StatusCode != 200 {
+		zap.L().Warn("[HttpPost] Response failed", zap.String("Status", response.Status), zap.String("Body", string(response.Body)))
+		return response, fmt.Errorf("[HttpPost] Response failed")
+	}
+
 	return responseInterceptor(response, reqFunc), nil
 }

+ 146 - 0
service/utils/mimetype/mimetype.go

@@ -0,0 +1,146 @@
+package mimetype
+
+import (
+	"mime"
+	"path/filepath"
+	"strings"
+)
+
+var extraMIMETypes = map[string]string{
+	// --- Text ---
+	".txt":   "text/plain; charset=utf-8",
+	".log":   "text/plain; charset=utf-8",
+	".md":    "text/markdown; charset=utf-8",
+	".csv":   "text/csv; charset=utf-8",
+	".tsv":   "text/tab-separated-values",
+	".css":   "text/css",
+	".htm":   "text/html",
+	".html":  "text/html",
+	".shtml": "text/html",
+	".xml":   "application/xml",
+	".yaml":  "application/x-yaml",
+	".yml":   "application/x-yaml",
+
+	// --- JavaScript / JSON ---
+	".js":     "application/javascript",
+	".mjs":    "application/javascript",
+	".json":   "application/json",
+	".jsonld": "application/ld+json",
+
+	// --- Images ---
+	".jpg":  "image/jpeg",
+	".jpeg": "image/jpeg",
+	".png":  "image/png",
+	".gif":  "image/gif",
+	".svg":  "image/svg+xml",
+	".webp": "image/webp",
+	".bmp":  "image/bmp",
+	".ico":  "image/x-icon",
+	".tif":  "image/tiff",
+	".tiff": "image/tiff",
+	".avif": "image/avif",
+	".heif": "image/heif",
+	".heic": "image/heic",
+
+	// --- Audio ---
+	".mp3":  "audio/mpeg",
+	".wav":  "audio/wav",
+	".aac":  "audio/aac",
+	".flac": "audio/flac",
+	".ogg":  "audio/ogg",
+	".oga":  "audio/ogg",
+	".m4a":  "audio/mp4",
+	".mid":  "audio/midi",
+	".midi": "audio/midi",
+
+	// --- Video ---
+	".mp4":  "video/mp4",
+	".mov":  "video/quicktime",
+	".avi":  "video/x-msvideo",
+	".wmv":  "video/x-ms-wmv",
+	".flv":  "video/x-flv",
+	".mkv":  "video/x-matroska",
+	".webm": "video/webm",
+	".mpeg": "video/mpeg",
+	".mpg":  "video/mpeg",
+
+	// --- Archives ---
+	".zip": "application/zip",
+	".rar": "application/vnd.rar",
+	".tar": "application/x-tar",
+	".gz":  "application/gzip",
+	".tgz": "application/gzip",
+	".bz":  "application/x-bzip",
+	".bz2": "application/x-bzip2",
+	".7z":  "application/x-7z-compressed",
+
+	// --- Documents ---
+	".pdf":  "application/pdf",
+	".doc":  "application/msword",
+	".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+	".xls":  "application/vnd.ms-excel",
+	".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+	".ppt":  "application/vnd.ms-powerpoint",
+	".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+
+	// --- Code / Config / Dev ---
+	".go":   "text/plain",
+	".py":   "text/x-python",
+	".java": "text/x-java-source",
+	".c":    "text/x-c",
+	".cpp":  "text/x-c++",
+	".h":    "text/x-c",
+	".hpp":  "text/x-c++",
+	".rs":   "text/plain",
+	".php":  "text/x-php",
+	".sql":  "application/sql",
+	".env":  "text/plain",
+	".ini":  "text/plain",
+
+	// --- Fonts ---
+	".woff":  "font/woff",
+	".woff2": "font/woff2",
+	".ttf":   "font/ttf",
+	".otf":   "font/otf",
+	".eot":   "application/vnd.ms-fontobject",
+
+	// --- Vector / CAD ---
+	".psd": "image/vnd.adobe.photoshop",
+	".ai":  "application/postscript",
+	".eps": "application/postscript",
+
+	// --- Binary / Program ---
+	".exe": "application/vnd.microsoft.portable-executable",
+	".bin": "application/octet-stream",
+	".dll": "application/x-msdownload",
+	".apk": "application/vnd.android.package-archive",
+	".dmg": "application/x-apple-diskimage",
+	".iso": "application/x-iso9660-image",
+
+	// --- Misc ---
+	".rtf":  "application/rtf",
+	".wasm": "application/wasm",
+	".swf":  "application/x-shockwave-flash",
+	".map":  "application/json",
+}
+
+// GetContentType 根据文件名/扩展名获取 Content-Type
+func GetContentType(filename string) string {
+	ext := strings.ToLower(filepath.Ext(filename))
+	if ext == "" {
+		return "application/octet-stream"
+	}
+
+	// 1. 标准库优先
+	if ct := mime.TypeByExtension(ext); ct != "" {
+		return ct
+	}
+
+	// 2. 补充表
+	if ct, ok := extraMIMETypes[ext]; ok {
+		return ct
+	}
+
+	// 3. 默认值
+	return "application/octet-stream"
+}

+ 38 - 1
service/utils/minio/minio.go

@@ -18,6 +18,8 @@ import (
 var Client *minio.Client
 var BucketName string
 
+type ObjectInfo = minio.ObjectInfo
+
 func InitClient(cfg *config.Config) {
 	var err error
 	Client, err = minio.New(cfg.MinIoEndpoint, &minio.Options{
@@ -72,7 +74,22 @@ func DownloadFile(objectName, localPath string) error {
 	return Client.FGetObject(ctx, BucketName, objectName, localPath, minio.GetObjectOptions{})
 }
 
-func ListFiles() ([]string, error) {
+func ListPathOrFiles(prefix string, recursive bool) ([]string, error) {
+	ctx := context.Background()
+	var result []string
+	for obj := range Client.ListObjects(ctx, BucketName, minio.ListObjectsOptions{
+		Prefix:    prefix,    // 遍历 prefix 目录
+		Recursive: recursive, // true - 递归所有文件, false - 只列出当前层级
+	}) {
+		if obj.Err != nil {
+			return nil, obj.Err
+		}
+		result = append(result, obj.Key)
+	}
+	return result, nil
+}
+
+func ListAllFiles() ([]string, error) {
 	ctx := context.Background()
 	var result []string
 	for obj := range Client.ListObjects(ctx, BucketName, minio.ListObjectsOptions{Recursive: true}) {
@@ -94,6 +111,12 @@ func GetObject(objectName string) (*minio.Object, error) {
 	return Client.GetObject(ctx, BucketName, objectName, minio.GetObjectOptions{})
 }
 
+// StatObject 会验证对象是否存在,您是否拥有访问该对象的权限,并返回有关该对象的信息。
+func StatObject(objectName string) (minio.ObjectInfo, error) {
+	ctx := context.Background()
+	return Client.StatObject(ctx, BucketName, objectName, minio.StatObjectOptions{})
+}
+
 // 生成临时下载链接
 func GetPresignedURL(objectName string, expire time.Duration) (*url.URL, error) {
 	ctx := context.Background()
@@ -112,3 +135,17 @@ func UploadFromReader(file io.Reader, size int64, objectName, contentType string
 	zap.L().Info(fmt.Sprintf("✅ Uploaded %s (%d bytes)", info.Key, info.Size))
 	return nil
 }
+
+// 判断文件是否存在
+func ExistsObject(objectName string) (bool, error) {
+	_, err := StatObject(objectName)
+	if err != nil {
+		// 对象不存在的判断(最常见)
+		if minio.ToErrorResponse(err).Code == "NoSuchKey" {
+			log.Printf("Object(%s) does NOT exist\n", objectName)
+			return false, nil
+		}
+		return false, err
+	}
+	return true, nil
+}

+ 65 - 8
src/views/batch-download/index.vue

@@ -50,6 +50,11 @@
           <NPagination v-bind="tablePagination" :prefix="paginationPrefix" />
         </div>
       </div>
+      <NDrawer v-model:show="drawerShow" :width="502" placement='right'>
+        <NDrawerContent :title="drawerTitle">
+          <pre class="whitespace-pre-wrap">{{ drawerContext }}</pre>
+        </NDrawerContent>
+      </NDrawer>
     </NCard>
     <NDropdown placement="bottom-start" trigger="manual" v-bind="dropdownOptions" :show="showDropdown" />
   </div>
@@ -57,7 +62,7 @@
 
 <script setup lang="tsx">
 import { useRouter } from 'vue-router'
-import { NButton, NDataTable, NForm, NNumberAnimation, NInput, NEllipsis } from 'naive-ui'
+import { NButton, NDataTable, NForm, NNumberAnimation, NInput, NEllipsis, NDrawer, NDrawerContent } from 'naive-ui'
 import type { DataTableColumns, PaginationProps, DropdownProps, EllipsisProps } from 'naive-ui'
 import { useDataTable } from '@/composable/comp/useDataTable'
 import { useResettableReactive } from '@/composable/useResettable'
@@ -85,6 +90,10 @@ const checkedRowKeys = ref<Array<number | string>>([])
 const contextmenu = ref<BatchDownloadInfo | undefined>(undefined)
 const isSelectMultiple = ref(false)
 
+const drawerShow = ref(false)
+const drawerTitle = ref<string>("")
+const drawerContext = ref<string>("")
+
 type FilterForm = BatchDownloadFilter
 
 const [form, , resetForm] = useResettableReactive<FilterForm>({
@@ -227,18 +236,32 @@ function createColumns(): DataTableColumns<BatchDownloadInfo> {
           })
       },
     },
-    {
-      title: '时间范围',
-      key: 'timeRange',
-      width: 100,
-    },
     {
       title: '进度',
       key: 'progress',
+      width: 100,
     },
     {
       title: '任务结果',
-      key: 'result'
+      key: 'failReasons',
+      render(rowData, rowIndex) {
+        return h(NEllipsis, {
+          lineClamp: 3,
+          tooltip: {
+            placement: "left-start",   // ⬅️ Tooltip 显示在右侧
+            style: {
+              maxHeight: "200px",       // ⬅️ 多行时滚动
+              overflowY: "auto",
+              whiteSpace: "pre-line",   // ⬅️ \n 正常换行
+              maxWidth: "480px"
+            }
+          }
+        } as EllipsisProps,
+          {
+            default: () =>
+              h("span", { style: { whiteSpace: "pre-line" } }, rowData.failReasons.join("\n"))
+          })
+      },
     },
     {
       title: '操作',
@@ -247,11 +270,19 @@ function createColumns(): DataTableColumns<BatchDownloadInfo> {
       fixed: 'right',
       render(row) {
         return h('div', { class: 'flex justify-center gap-4 p-x-4' }, [
+          h(NButton, {
+            type: "info",
+            strong: true,
+            size: 'small',
+            onClick: () => itemView(row)
+          }, {
+            default: () => '详情'
+          }),
           h(NButton, {
             type: "success",
             strong: true,
             size: 'small',
-            disabled: row.progress === "pending" || row.progress === "running",
+            disabled: row.progress !== "complete",
             onClick: () => itemDownload(row.id)
           }, {
             default: () => '下载'
@@ -308,6 +339,32 @@ const tablePagination = reactive<PaginationProps>({
   },
 })
 
+const itemView = (info: BatchDownloadInfo) => {
+  console.log({ info })
+  const idStr = `ID: ${info.id}`
+  const snStr = "SN:\n" + info.sn.map(sn => `\t${sn}`).join("\n")
+  const descStr = `Description: ${info.description}`
+  const periodStr = `Period: ${info.timePeriod[0]} - ${info.timePeriod[1]}`
+
+  const powerFlowStr = `\tPower Flow: ${info.options.powerFlow ? '✔️' : '❌'}`
+  const mqttLogsStr = `\tMQTT Logs: [${info.options.mqttLogs.join(', ')}]`
+  const eventAlarmStr = `\tEvent & Alarm: ${info.options.eventAlarm ? '✔️' : '❌'}`
+  const alarmSnapshotStr = `\tAlarm Snapshot: ${info.options.alarmSnapshot ? '✔️' : '❌'}`
+  const emsLogsStr = `\tEMS Logs: [${info.options.emsLogs.join(', ')}]`
+  const faultRecordStr = `\tFault Record: ${info.options.faultRecord ? '✔️' : '❌'}`
+  const updateLogStr = `\tUpdate Log: ${info.options.updateLog ? '✔️' : '❌'}`
+  const settingsStr = `\tSettings: ${info.options.settings ? '✔️' : '❌'}`
+
+  const optionStr = "Options:\n" + powerFlowStr + '\n' + mqttLogsStr + '\n' + eventAlarmStr + '\n' + alarmSnapshotStr + '\n' + emsLogsStr + '\n' + faultRecordStr + '\n' + updateLogStr + '\n' + settingsStr
+  const progressStr = `Progress: ${info.progress}`
+  const failReasonsStr = "Fail Reasons:\n" + info.failReasons.map(fail => `\t${fail}`).join("\n")
+  const contextStr = idStr + "\n" + snStr + "\n" + descStr + "\n" + periodStr + "\n" + optionStr + "\n" + progressStr + "\n" + failReasonsStr
+
+  drawerTitle.value = "任务详情"
+  drawerContext.value = contextStr
+  drawerShow.value = true
+}
+
 const itemDownload = (id: string) => {
   // TODO
 }

+ 4 - 2
src/views/sites-analysis/component/siteRuntimeChart.vue

@@ -432,7 +432,8 @@ const getChildrenChartOpt = (xRange: [number, number]): (Mark | Composition)[] =
             x: false,
             y: {
               tickCount: 0,
-              position: 'bottom'
+              position: 'bottom',
+              title: key,
             }
           },
           scale: {
@@ -462,7 +463,8 @@ const getChildrenChartOpt = (xRange: [number, number]): (Mark | Composition)[] =
             x: false,
             y: {
               tickCount: 0,
-              position: 'bottom'
+              position: 'bottom',
+              title: key,
             }
           },
           scale: {