Browse Source

feat:参考fwh_pro完成测试服务器API初版

Jerry Liang 2 months ago
parent
commit
a2d205c0ef

+ 11 - 0
service/main.go

@@ -7,6 +7,7 @@ import (
 	"fwh-toolbox-server/database"
 	"fwh-toolbox-server/repositories"
 	"fwh-toolbox-server/repositories/api/fwh_pro"
+	"fwh-toolbox-server/repositories/api/fwh_test"
 	"fwh-toolbox-server/task"
 	"fwh-toolbox-server/utils/casdoor"
 	"fwh-toolbox-server/utils/minio"
@@ -60,6 +61,9 @@ func main() {
 	// 初始化FwhApi
 	repositories.FwhHttpInit(&cfg.FwhApi)
 
+	// 初始化FwhTestApi(测试环境)
+	repositories.FwhTestHttpInit(&cfg.FwhTestApi)
+
 	// 初始化PlaneApi
 	repositories.PlaneHttpInit(&cfg.PlaneApi)
 
@@ -136,6 +140,13 @@ func main() {
 		// panic(err)
 	}
 
+	// 提前登录FWH测试环境
+	err = fwh_test.Login()
+	if err != nil {
+		zap.L().Error("FWH test login failed.", zap.String("err", err.Error()))
+		// panic(err)
+	}
+
 	// TEST
 	// err = task.CreateTaskSimCardCollect()
 	// if err != nil {

+ 294 - 0
service/repositories/api/fwh_test/fwhHttpClient.go

@@ -0,0 +1,294 @@
+package fwh_test
+
+import (
+	"encoding/json"
+	"fwh-toolbox-server/config"
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/utils/httpClient"
+	"net/http"
+	"net/url"
+	"time"
+)
+
+const (
+	timeout time.Duration = 3 * time.Minute // fwh请求超时3min
+)
+
+var (
+	fwhEnergy     string
+	fwhOss        string
+	fwhUser       string
+	fwhPassword   string
+	fwhHttpClient httpClient.HttpClient
+)
+
+// FwhHttpInit
+func FwhHttpInit(cfg *config.FwhApiConfig) {
+	fwhEnergy = cfg.FwhEnergy
+	fwhOss = cfg.FwhOss
+	fwhUser = cfg.FwhUser
+	fwhPassword = cfg.FwhPassword
+
+	fwhHttpClient = httpClient.HttpClient{}
+	fwhHttpClient.RegisterLogin(reLogin)
+}
+
+// 获取完整EnergyURL
+func getFullEnergyUrl(relativeURL string) string {
+	return "https://" + fwhEnergy + relativeURL
+}
+
+// 获取完整OssURL
+func getFullOssUrl(relativeURL string) string {
+	// return "https://" + fwhOss + relativeURL
+	return "https://" + fwhEnergy + relativeURL
+}
+
+// 仅用户登录或者不需要token的请求
+func FwhHttpGetWithoutToken(url string, params url.Values) (*httpClient.Response, error) {
+	t := timeout
+	opt := httpClient.HttpClientOptions{
+		Timeout: &t,
+		Header: &http.Header{
+			"Accept": {"application/json", "text/plain", "*/*"},
+		},
+	}
+	return fwhHttpClient.HttpGet(url, &params, &opt)
+}
+
+func FwhHttpGet(url string, params url.Values) (*httpClient.Response, error) {
+	t := timeout
+	opt := httpClient.HttpClientOptions{
+		Timeout: &t,
+		Header: &http.Header{
+			"Logintoken": {getLoginToken()},
+			"Accept":     {"application/json", "text/plain", "*/*"},
+		},
+	}
+	return fwhHttpClient.HttpGet(url, &params, &opt)
+}
+
+// 仅用户登录或者不需要token的请求
+func FwhHttpPostWithoutToken(url string, body any) (*httpClient.Response, error) {
+	t := timeout
+	opt := httpClient.HttpClientOptions{
+		Timeout: &t,
+		Header: &http.Header{
+			"Accept": {"application/json", "text/plain", "*/*"},
+		},
+	}
+	return fwhHttpClient.HttpPost(url, body, &opt)
+}
+
+func FwhHttpPost(url string, body any) (*httpClient.Response, error) {
+	t := timeout
+	opt := httpClient.HttpClientOptions{
+		Timeout: &t,
+		Header: &http.Header{
+			"Logintoken": {getLoginToken()},
+			"Accept":     {"application/json", "text/plain", "*/*"},
+		},
+	}
+	return fwhHttpClient.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
+}

+ 101 - 0
service/repositories/api/fwh_test/fwh_emsLog.go

@@ -0,0 +1,101 @@
+package fwh_test
+
+import (
+	"fmt"
+	"fwh-toolbox-server/domain"
+	"net/url"
+	"strconv"
+)
+
+// 召唤EMS日志
+func queryCallEmsLog(sn string, dateStr string, logType int) (*domain.FwhResponse[bool], error) {
+	// 要发送的表单数据
+	// form := url.Values{}
+	// form.Set("gatewayId", sn)                  // SN
+	// form.Set("logEqSn", sn)                    // SN
+	// form.Set("logId", sn)                      // SN
+	// form.Set("logTime", dateStr)               // 日志时间
+	// form.Set("logType", strconv.Itoa(logType)) // 日志类型 4-IBG运行日志 7-电池日志 9-设备系统资源监控日志 10-4G日志 11-AWS日志 12-主进程日志
+	// form.Set("lang", "ZH_CN")
+
+	form := map[string]interface{}{
+		"gatewayId": sn,
+		"logEqSn":   sn,
+		"logId":     sn,
+		"logTime":   dateStr,
+		"logType":   logType,
+		"lang":      "ZH_CN",
+	}
+
+	response, err := FwhHttpPostUnmarshal[bool](getFullEnergyUrl("/hes-gateway/manage/logFileCall"), form)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 召唤EMS日志
+func QueryCallEmsLog(sn string, dateStr string, logType int) error {
+	// 查询告警快照数据
+	callRes, err := queryCallEmsLog(sn, dateStr, logType)
+	if err != nil {
+		return fmt.Errorf("[QueryCallEmsLog] %s", err.Error())
+	}
+
+	if callRes.Code != 200 {
+		return fmt.Errorf("[QueryCallEmsLog] 查询失败 code: %d, message: %s", callRes.Code, callRes.Message)
+	}
+
+	if callRes.Result == nil || !*callRes.Result {
+		return fmt.Errorf("[QueryCallEmsLog] 查询失败 code: %d, result: %v, message: %s", callRes.Code, callRes.Result, callRes.Message)
+	}
+
+	return nil
+}
+
+// 查询EMS日志下载地址
+func queryEmsLogCallRecord(sn string) (*domain.FwhResponse[[]domain.EmsLogRecordInfo], error) {
+	// 构造 query 参数
+	params := url.Values{}
+	params.Add("linkId", sn) // SN
+	params.Add("pageIndex", strconv.Itoa(1))
+	params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
+	params.Add("pageNum", strconv.Itoa(1))
+	params.Add("lang", "ZH_CN")
+
+	response, err := FwhHttpGetUnmarshal[[]domain.EmsLogRecordInfo](getFullEnergyUrl("/hes-gateway/manage/iotDeviceFileRecords"), params)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 查询EMS日志下载地址
+func QueryEmsLogCallRecord(sn string, dateStr string, logType int) (*domain.EmsLogRecordInfo, error) {
+	// 查询告警快照数据
+	callRecordRes, err := queryEmsLogCallRecord(sn)
+	if err != nil {
+		return nil, fmt.Errorf("[QueryEmsLogCallRecord] %s", err.Error())
+	}
+
+	if callRecordRes.Code != 200 {
+		return nil, fmt.Errorf("[QueryEmsLogCallRecord] 查询失败 code: %d, message: %s", callRecordRes.Code, callRecordRes.Message)
+	}
+
+	for _, record := range *callRecordRes.Result {
+		if record.LogTime == dateStr && record.CodeType == logType {
+			return &record, nil
+		}
+	}
+
+	return nil, fmt.Errorf("[QueryEmsLogCallRecord] 没有召唤日志")
+}
+
+// 请求下载EMS日志文件
+func QueryDownloadEmsLogFile(filePath string) ([]byte, error) {
+	response, err := FwhHttpGet(getFullEnergyUrl("/hes-upload/v1/file/download/"+filePath), url.Values{})
+	if err != nil {
+		return nil, err
+	}
+	return response.Body, nil
+}

+ 238 - 0
service/repositories/api/fwh_test/fwh_eventAlarmLog.go

@@ -0,0 +1,238 @@
+package fwh_test
+
+import (
+	"errors"
+	"fmt"
+	"fwh-toolbox-server/domain"
+	"net/url"
+	"strconv"
+	"time"
+
+	"go.uber.org/zap"
+)
+
+type EventAlarmType int
+
+const (
+	All   EventAlarmType = iota // 0
+	Event                       // 1
+	Alarm                       // 2
+)
+
+// 查询事件事件日志
+func queryEventAndAlarmLog(sn string, params url.Values) (*domain.FwhResponse[[]domain.EventAlarmItem], error) {
+	userId := getUserId()
+
+	// 构造 query 参数
+	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))
+	params.Add("pageNum", strconv.Itoa(1))
+	params.Add("land", "ZH_CN")
+
+	response, err := FwhHttpGetUnmarshal[[]domain.EventAlarmItem](getFullEnergyUrl("/hes-gateway/manage/iotDeviceRunLogs"), params)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 查询所有事件告警
+// startTime&endTime 格式 YYYY-MM-DD HH:mm:SS
+func queryAllEventAlarm(sn, startTime, endTime string) (*domain.FwhResponse[[]domain.EventAlarmItem], 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) (*domain.FwhResponse[[]domain.EventAlarmItem], 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) (*domain.FwhResponse[[]domain.EventAlarmItem], 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) (*domain.FwhResponse[[]domain.EventAlarmItem], 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) {
+	// 三次请求尝试
+	for i := 0; i < 3; i++ {
+		eventLogRes, err := queryEventOrAlarmListOnePageExec(sn, startTime, endTime, t)
+		if err != nil {
+			zap.L().Warn("queryEventOrAlarmListOnePageExec failed and retry", zap.String("err", err.Error()))
+			continue
+		}
+
+		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))
+			time.Sleep(time.Second * 5)
+			continue
+		}
+
+		return *eventLogRes.Result, nil
+	}
+
+	return nil, errors.New("retry failed")
+}
+
+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 eventLogRes *domain.FwhResponse[[]domain.EventAlarmItem]
+	var err error
+	switch t {
+	case Event:
+		eventLogRes, err = queryEventLog(sn, startTimeStr, endTimeStr)
+	case Alarm:
+		eventLogRes, err = queryAlarmLog(sn, startTimeStr, endTimeStr, "")
+	default:
+		eventLogRes, err = queryAllEventAlarm(sn, startTimeStr, endTimeStr)
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	return eventLogRes, 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) (*domain.FwhPageResponse[[]domain.AlarmSnapshotInfo], 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")
+
+	response, err := FwhHttpGetUnmarshalPage[[]domain.AlarmSnapshotInfo](getFullEnergyUrl("/hes-gateway/manage/selectAlarmSnapshotList"), params)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+func QueryAlarmSnapshotInfo(sn, logTime string) ([]domain.AlarmSnapshotInfo, error) {
+	var page int = 1
+	var infoList []domain.AlarmSnapshotInfo
+
+	for {
+		// 查询告警快照数据
+		alarmSnapshotRes, err := queryAlarmSnapshotInfo(sn, logTime, page)
+		if err != nil {
+			return nil, fmt.Errorf("[QueryAlarmSnapshotInfo] %s", err.Error())
+		}
+
+		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
+}

+ 211 - 0
service/repositories/api/fwh_test/fwh_faultRecord.go

@@ -0,0 +1,211 @@
+package fwh_test
+
+import (
+	"fmt"
+	"fwh-toolbox-server/domain"
+	"net/url"
+	"strconv"
+	"strings"
+
+	"go.uber.org/zap"
+)
+
+// 查询故障录波日志
+func queryFaultRecordList(sn string, startTime string, endTime string) (*domain.FwhResponse[[]domain.FaultRecordInfo], error) {
+	// 构造 query 参数
+	params := url.Values{}
+	params.Add("deviseSerialNumber", sn)       // SN
+	params.Add("earliestStartTime", startTime) // 时间范围 - 开始时间 format YYYY-MM-DD hh:mm:ss
+	params.Add("earliestEndTime", endTime)     // 时间范围 - 结束时间 format YYYY-MM-DD hh:mm:ss
+	params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
+	params.Add("pageNum", strconv.Itoa(1))
+	params.Add("lang", "ZH_CN")
+	response, err := FwhHttpGetUnmarshal[[]domain.FaultRecordInfo](getFullEnergyUrl("/hes-gateway/manage/faultRecordingLst"), params)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 查询故障录波日志
+func QueryFaultRecordList(sn string, startTime string, endTime string) ([]domain.FaultRecordInfo, error) {
+	faultRecordResp, err := queryFaultRecordList(sn, startTime, endTime)
+	if err != nil {
+		return nil, fmt.Errorf("[QueryFaultRecordList] %s", err.Error())
+	}
+
+	if faultRecordResp.Code != 200 {
+		return nil, fmt.Errorf("[QueryFaultRecordList] 查询失败 code: %d, message: %s", faultRecordResp.Code, faultRecordResp.Message)
+	}
+
+	return *faultRecordResp.Result, nil
+}
+
+// 查询设备可召唤数据列表 - 仅自动录播
+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
+	params.Add("callType", strconv.Itoa(devType))
+	params.Add("lang", "ZH_CN")
+
+	response, err := FwhHttpGetUnmarshal[[]domain.FaultRecordCallableInfo](getFullEnergyUrl("/hes-gateway/manage/getWaveRecordingCatalog"), params)
+	// 此处无数据也会失败,暂时定义失败为无数据,不报错
+	if err != 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) {
+	faultRecordCallableResp, err := queryFaultRecordCallableList(devType, sn)
+	if err != nil {
+		return nil, fmt.Errorf("[QueryFaultRecordCallableList] %s", err.Error())
+	}
+
+	if faultRecordCallableResp.Code != 200 {
+		// 处理未查到数据的情况
+		if faultRecordCallableResp.Code == 400 && faultRecordCallableResp.Message == "故障目录为空" {
+			return []domain.FaultRecordCallableInfo{}, nil
+		}
+		return nil, fmt.Errorf("[QueryFaultRecordCallableList] 查询失败 code: %d, message: %s", faultRecordCallableResp.Code, faultRecordCallableResp.Message)
+	}
+
+	return *faultRecordCallableResp.Result, nil
+}
+
+// 召唤故障录波日志
+func queryCellFaultRecord(devType int, sn string, indexList []int) (*domain.FwhResponse[interface{}], error) {
+	if len(indexList) < 1 {
+		return nil, fmt.Errorf("未选择召唤数据idx")
+	}
+
+	// 将 []int 转为 []string
+	indexListStr := make([]string, len(indexList))
+	for i, v := range indexList {
+		indexListStr[i] = strconv.Itoa(v)
+	}
+
+	// 要发送的 JSON 数据
+	body := map[string]interface{}{
+		"deviceId":    sn,                              // aGate or aPower or aHub SN
+		"index":       strings.Join(indexListStr, ","), // 要召唤数据的idx
+		"callType":    devType,
+		"triggerType": 2,    // 触发类型,目前仅支持2-自动
+		"lang":        "ZH_CN",
+	}
+
+	response, err := FwhHttpPostUnmarshal[interface{}](getFullEnergyUrl("/hes-gateway/manage/callFaultRecording"), body)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 召唤故障录波日志
+func QueryCellFaultRecord(devType int, sn string, indexList []int) error {
+	cellFaultRecordResp, err := queryCellFaultRecord(devType, sn, indexList)
+	if err != nil {
+		return fmt.Errorf("[QueryCellFaultRecord] %s", err.Error())
+	}
+
+	if cellFaultRecordResp.Code != 200 {
+		return fmt.Errorf("[QueryCellFaultRecord] 查询失败 code: %d, message: %s", cellFaultRecordResp.Code, cellFaultRecordResp.Message)
+	}
+
+	return nil
+}
+
+// 查询故障录波召唤记录
+func queryFaultRecordCallRecordList() (*domain.FwhResponse[[]domain.FaultRecordInfo], error) {
+	// 构造 query 参数
+	params := url.Values{}
+	params.Add("pageSize", strconv.Itoa(50))
+	params.Add("pageNum", strconv.Itoa(1))
+	params.Add("lang", "ZH_CN")
+
+	response, err := FwhHttpGetUnmarshal[[]domain.FaultRecordInfo](getFullEnergyUrl("/hes-gateway/manage/lstFaultWaveCall"), params)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 查询故障录波召唤记录
+func QueryFaultRecordCallRecordList() ([]domain.FaultRecordInfo, error) {
+	faultRecordCallRecordResp, err := queryFaultRecordCallRecordList()
+	if err != nil {
+		return nil, fmt.Errorf("[QueryFaultRecordCallRecordList] %s", err.Error())
+	}
+
+	if faultRecordCallRecordResp.Code != 200 {
+		return nil, fmt.Errorf("[QueryFaultRecordCallRecordList] 查询失败 code: %d, message: %s", faultRecordCallRecordResp.Code, faultRecordCallRecordResp.Message)
+	}
+
+	return *faultRecordCallRecordResp.Result, nil
+}
+
+// 查询故障录波子记录
+func queryFaultRecordSubList(id int64) (*domain.FwhResponse[[]domain.FaultRecordSubInfo], error) {
+	// 构造 query 参数
+	params := url.Values{}
+	params.Add("faultId", strconv.FormatInt(id, 10))
+	params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
+	params.Add("pageNum", strconv.Itoa(1))
+	params.Add("lang", "ZH_CN")
+
+	response, err := FwhHttpGetUnmarshal[[]domain.FaultRecordSubInfo](getFullEnergyUrl("/hes-gateway/manage/lstSubFaultWave"), params)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 查询故障录波子记录
+func QueryFaultRecordSubList(id int64) ([]domain.FaultRecordSubInfo, error) {
+	faultRecordSubResp, err := queryFaultRecordSubList(id)
+	if err != nil {
+		return nil, fmt.Errorf("[QueryFaultRecordSubList] %s", err.Error())
+	}
+
+	if faultRecordSubResp.Code != 200 {
+		return nil, fmt.Errorf("[QueryFaultRecordSubList] 查询失败 code: %d, message: %s", faultRecordSubResp.Code, faultRecordSubResp.Message)
+	}
+
+	return *faultRecordSubResp.Result, nil
+}
+
+// 请求下载录波数据
+// return FileName, FileData, Error
+func QueryDownloadFaultRecordData(faultId int64, subRecordId int64) (string, []byte, error) {
+	// 构造 query 参数
+	params := url.Values{}
+	params.Add("faultId", strconv.FormatInt(faultId, 10))
+	params.Add("id", strconv.FormatInt(subRecordId, 10))
+	params.Add("lang", "ZH_CN")
+
+	response, err := FwhHttpGet(getFullEnergyUrl("/hes-gateway/manage/exportSubFaultWave"), params)
+	if err != nil {
+		return "", nil, err
+	}
+
+	// 从 Content-Disposition 提取真实文件名
+	filename := "download.xls"
+	if cd := response.Header.Get("Content-Disposition"); cd != "" {
+		if strings.Contains(cd, "filename=") {
+			name := strings.Split(cd, "filename=")[1]
+			name = strings.Trim(name, "\"")
+			name, _ = url.QueryUnescape(name) // 处理中文名
+			filename = name
+		}
+	}
+
+	return filename, response.Body, nil
+}

+ 163 - 0
service/repositories/api/fwh_test/fwh_login.go

@@ -0,0 +1,163 @@
+package fwh_test
+
+import (
+	"encoding/json"
+)
+
+var loginInfo LoginData = LoginData{
+	UserId: 0,
+	Token:  "",
+}
+
+type LoginPayload struct {
+	LoginName string `json:"loginName"`
+	Password  string `json:"password"`
+	Type      int    `json:"type"`
+	Lang      string `json:"lang"`
+}
+
+type LoginData struct {
+	UserId uint   `json:"userId"`
+	Token  string `json:"token"`
+}
+
+type LoginResponse struct {
+	Code    int       `json:"code"`
+	Msg     string    `json:"msg"`
+	Data    LoginData `json:"data"`
+	Success bool      `json:"success"`
+}
+
+type JwtPayload struct {
+	AdminLevel  int    `json:"adminLevel"`
+	LoginName   string `json:"loginName"`
+	AccountType int    `json:"accountType"`
+	GroupId     int    `json:"groupId"`
+	Iss         string `json:"iss"`
+	UserManage  int    `json:"userManage"`
+	Exp         uint64 `json:"exp"`
+	UserName    string `json:"userName"`
+	Iat         uint64 `json:"iat"`
+	UserId      int    `json:"userId"`
+}
+
+func Login() error {
+	// 要发送的 JSON
+	payload := LoginPayload{
+		LoginName: fwhUser,
+		Password:  fwhPassword,
+		Type:      1,
+		Lang:      "ZH_CN",
+	}
+
+	response, err := FwhHttpPostWithoutToken(getFullOssUrl("/api-user/user/login"), payload)
+	if err != nil {
+		return err
+	}
+	// logger.Std.Printf("Status: %s\nBody: %s", resp.Status, body)
+
+	var loginRes LoginResponse
+	// 反序列化(unmarshal)
+	jsonErr := json.Unmarshal(response.Body, &loginRes)
+	if jsonErr != nil {
+		return jsonErr
+	}
+
+	loginInfo.UserId = loginRes.Data.UserId
+	loginInfo.Token = loginRes.Data.Token
+
+	return 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
+	if loginInfo.Token != "" {
+		// 判断token是否过期
+		// token.payload加密,无法判断过期,改为判断401
+		// expired, err := isTokenExpired(loginInfo.Token)
+		// if err != nil {
+		// 	zap.L().Error("loginInfo.Token 解析出错.", zap.String("token", loginInfo.Token), zap.String("err", err.Error()))
+		// 	return ""
+		// }
+
+		// if expired {
+		// 	zap.L().Info("fwh Token 已过期")
+		// } else {
+		// Token 有效
+		return loginInfo.Token
+		// }
+	}
+
+	// 重新登录
+	Login()
+	return loginInfo.Token
+}
+
+// 弃用: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
+func ClearToken() {
+	loginInfo.UserId = 0
+	loginInfo.Token = ""
+}
+
+// 过期重新登录 - 提供给httpClient
+func reLogin() string {
+	// 重新登录
+	Login()
+	return loginInfo.Token
+}

+ 137 - 0
service/repositories/api/fwh_test/fwh_mqttLog.go

@@ -0,0 +1,137 @@
+package fwh_test
+
+import (
+	"errors"
+	"fmt"
+	"fwh-toolbox-server/domain"
+	"net/url"
+	"strconv"
+	"strings"
+	"time"
+)
+
+type QueryMqttLogParam struct {
+	SN        *string
+	CmdTypes  []domain.MqttCmdType
+	StartTime *time.Time
+	EndTime   *time.Time
+}
+
+// 查询站点信息
+func queryMqttLog(param QueryMqttLogParam) (*domain.FwhPageResponse[[]domain.MqttLogItem], 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")
+
+	response, err := FwhHttpGetUnmarshalPage[[]domain.MqttLogItem](getFullEnergyUrl("/hes-gateway/manage/iotMqttCommlogs"), params)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+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,
+	}
+	mqttLogRes, err := queryMqttLog(param)
+	if err != nil {
+		return nil, err
+	}
+
+	if mqttLogRes.Code != 200 {
+		return nil, fmt.Errorf("[queryMqttLogListOnePage] 查询失败 code: %d, message: %s", mqttLogRes.Code, mqttLogRes.Message)
+	}
+
+	// 判断是否超出可处理数据上限 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
+			}
+
+			time.Sleep(time.Second) // 延时1s
+
+			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
+}

+ 37 - 0
service/repositories/api/fwh_test/fwh_powerFlow.go

@@ -0,0 +1,37 @@
+package fwh_test
+
+import (
+	"fmt"
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/dto"
+	"net/url"
+	"time"
+)
+
+// 查询站点功率流信息
+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 编码
+
+	response, err := FwhHttpGetUnmarshalA[dto.SitePowerFlow](getFullEnergyUrl("/api-energy/power/getFhpPowerByDayOld"), params)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 从fwh_test获取站点功率流
+func QuerySitePowerFlow(site string, day time.Time) (*domain.FwhResponseA[dto.SitePowerFlow], error) {
+	sitePowerFlowRes, err := querySitePowerFlow(site, day)
+	if err != nil {
+		return nil, err
+	}
+
+	if sitePowerFlowRes.Code != 200 {
+		return nil, fmt.Errorf("[queryMqttLogListOnePage] 查询失败 code: %d, message: %s", sitePowerFlowRes.Code, sitePowerFlowRes.Msg)
+	}
+
+	return sitePowerFlowRes, nil
+}

+ 11 - 0
service/repositories/api/fwh_test/fwh_public.go

@@ -0,0 +1,11 @@
+package fwh_test
+
+var REQ_PAGE_SIZE uint64 = 1000
+
+var (
+	America    int = 2
+	Australia  int = 3
+	Canada     int = 5
+	NewZealand int = 6
+	Barbados   int = 51
+)

+ 106 - 0
service/repositories/api/fwh_test/fwh_realtimeAalrm.go

@@ -0,0 +1,106 @@
+package fwh_test
+
+import (
+	"fmt"
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/dto"
+	"net/url"
+	"strconv"
+)
+
+// 查询站点告警信息
+func querySiteAlarmList(sn string) (*domain.FwhPageResponse[[]dto.SiteAlarmList], error) {
+	// 构造 query 参数
+	params := url.Values{}
+	params.Add("gatewayId", sn)
+	params.Add("land", "ZH_CN")
+
+	response, err := FwhHttpGetUnmarshalPage[[]dto.SiteAlarmList](getFullEnergyUrl("/hes-gateway/manage/getEquipmentAlarm"), params)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 从fwh_test获取站点告警列表
+func QuerySiteAlarmList(site string) (*domain.FwhPageResponse[[]dto.SiteAlarmList], error) {
+	siteAlarmListRes, err := querySiteAlarmList(site)
+	if err != nil {
+		return nil, err
+	}
+
+	if siteAlarmListRes.Code != 200 {
+		return nil, fmt.Errorf("[QuerySiteAlarmList] 查询失败 code: %d, message: %s", siteAlarmListRes.Code, siteAlarmListRes.Message)
+	}
+
+	return siteAlarmListRes, nil
+}
+
+// 查询实时告警统计数据
+func queryAlarmStatistics(country domain.CountryType, page int) (*domain.FwhResponseB[domain.AlarmStatisticsData[domain.AlarmStatisticsInfo]], error) {
+	// 构造 query 参数
+	params := url.Values{}
+	if country == domain.CTRY_ALL {
+		params.Add("countryId", "") // 0-ALL
+	} else {
+		params.Add("countryId", strconv.Itoa(int(country))) // 2-美国 3-澳大利亚 5-加拿大 6-新西兰 51-巴巴多斯
+	}
+	params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
+	params.Add("pageNum", strconv.Itoa(page))
+	params.Add("land", "ZH_CN")
+
+	response, err := FwhHttpGetUnmarshalB[domain.AlarmStatisticsData[domain.AlarmStatisticsInfo]](getFullEnergyUrl("/api-inspect/currentAlarm/listCurrentAlarm"), params)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 从fwh_test获取实时告警统计数据
+func QueryAlarmStatistics(country domain.CountryType, page int) (*domain.FwhResponseB[domain.AlarmStatisticsData[domain.AlarmStatisticsInfo]], error) {
+	alarmStatisticsListRes, err := queryAlarmStatistics(country, page)
+	if err != nil {
+		return nil, err
+	}
+
+	if alarmStatisticsListRes.Code != 200 {
+		return nil, fmt.Errorf("[QueryAlarmStatistics] 查询失败 code: %d, message: %s", alarmStatisticsListRes.Code, alarmStatisticsListRes.Msg)
+	}
+
+	return alarmStatisticsListRes, nil
+}
+
+// 查询实时告警码的站点统计数据
+func queryAlarmStatisticsFromCode(country domain.CountryType, alarmCode string, page int) (*domain.FwhResponseB[domain.AlarmStatisticsData[domain.AlarmStatisticsFromCodeInfo]], error) {
+	// 构造 query 参数
+	params := url.Values{}
+	if country == domain.CTRY_ALL {
+		params.Add("countryId", "") // 0-ALL
+	} else {
+		params.Add("countryId", strconv.Itoa(int(country))) // 2-美国 3-澳大利亚 5-加拿大 6-新西兰 51-巴巴多斯
+	}
+	params.Add("alarmCode", alarmCode)
+	params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
+	params.Add("pageNum", strconv.Itoa(page))
+	params.Add("land", "ZH_CN")
+
+	response, err := FwhHttpGetUnmarshalB[domain.AlarmStatisticsData[domain.AlarmStatisticsFromCodeInfo]](getFullEnergyUrl("/api-inspect/currentAlarm/listDeviceByAlarm"), params)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 从fwh_test获取实时告警码的站点统计数据
+func QueryAlarmStatisticsFromCode(country domain.CountryType, alarmCode string, page int) (*domain.FwhResponseB[domain.AlarmStatisticsData[domain.AlarmStatisticsFromCodeInfo]], error) {
+	alarmStatisticsListRes, err := queryAlarmStatisticsFromCode(country, alarmCode, page)
+	if err != nil {
+		return nil, err
+	}
+
+	if alarmStatisticsListRes.Code != 200 {
+		return nil, fmt.Errorf("[QueryAlarmStatisticsFromCode] 查询失败 code: %d, message: %s", alarmStatisticsListRes.Code, alarmStatisticsListRes.Msg)
+	}
+
+	return alarmStatisticsListRes, nil
+}

+ 57 - 0
service/repositories/api/fwh_test/fwh_shutdownList.go

@@ -0,0 +1,57 @@
+package fwh_test
+
+import (
+	"fwh-toolbox-server/domain"
+	"net/url"
+	"strconv"
+)
+
+type ShutdownListData struct {
+	Current int                     `json:"current"` // 当前页
+	Pages   int                     `json:"pages"`   // 总页数
+	Size    int                     `json:"size"`    // 每页最大条数
+	Total   int64                   `json:"total"`   // 总条数
+	Records []domain.ShutdownRecord `json:"records"` // 记录列表
+}
+
+// 查询Shutdown站点列表 one page
+func querySiteShutdownListOnePage(date string, country int, page int) (*domain.FwhResponseB[ShutdownListData], error) {
+	// 构造 query 参数
+	params := url.Values{}
+	if country <= 0 {
+		params.Add("countryId", "") // 0-ALL
+	} else {
+		params.Add("countryId", strconv.Itoa(country)) // 2-美国 3-澳大利亚 5-加拿大 6-新西兰 51-巴巴多斯
+	}
+	params.Add("date", date)
+	params.Add("timeType", strconv.Itoa(1)) // 0-通讯时间;1-设备时间
+	params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
+	params.Add("pageNum", strconv.Itoa(page))
+	params.Add("land", "ZH_CN")
+
+	response, err := FwhHttpGetUnmarshalB[ShutdownListData](getFullEnergyUrl("/api-inspect/fault/listShutdownGateway"), params)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 查询Shutdown站点列表
+func QuerySiteShutdownList(date string, country int) ([]domain.ShutdownRecord, error) {
+	var total int64 = 0
+	var page int = 0
+	var records []domain.ShutdownRecord = []domain.ShutdownRecord{}
+
+	for total == 0 || REQ_PAGE_SIZE*uint64(page) < uint64(total) {
+		page++
+		shutdownListRes, err := querySiteShutdownListOnePage(date, country, page)
+		if err != nil {
+			return nil, err
+		}
+
+		total = int64(shutdownListRes.Data.Total)
+		records = append(records, shutdownListRes.Data.Records...)
+	}
+
+	return records, nil
+}

+ 190 - 0
service/repositories/api/fwh_test/fwh_siteInfo.go

@@ -0,0 +1,190 @@
+package fwh_test
+
+import (
+	"fmt"
+	"fwh-toolbox-server/domain"
+	"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) (*domain.FwhResponse[[]dto.SiteBaseInfo], 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)
+
+	response, err := FwhHttpPostUnmarshal[[]dto.SiteBaseInfo](getFullEnergyUrl("/hes-gateway/manage/selectIotFirmwareGatewayList"), form)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 从fwh_test获取站点基本信息表
+func QuerySiteBaseInfoList(para dto.SiteBaseInfoPara) (*domain.FwhResponse[[]dto.SiteBaseInfo], error) {
+	siteInfoRes, err := querySiteBaseInfoList(para.SiteList, para.PageSize, para.PageNum)
+	if err != nil {
+		return nil, err
+	}
+
+	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) (*domain.FwhPageResponse[[]domain.FwhSiteInfo], 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")
+
+	response, err := FwhHttpGetUnmarshalPage[[]domain.FwhSiteInfo](getFullEnergyUrl("/hes-gateway/manage/selectIotGatewayList"), params)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 从fwh_test获取站点基本信息表
+func QuerySiteInfoList(para dto.SiteInfoPara) (*domain.FwhPageResponse[[]domain.FwhSiteInfo], error) {
+	siteInfoRes, err := querySiteInfoList(para.SN, para.PageSize, para.PageNum)
+	if err != nil {
+		return nil, err
+	}
+
+	if siteInfoRes.Code != 200 {
+		return nil, fmt.Errorf("[QuerySiteInfoList] 查询失败 code: %d, message: %s", siteInfoRes.Code, siteInfoRes.Message)
+	}
+
+	return siteInfoRes, nil
+}
+
+// 查询站点列表(通用)
+func querySiteList(
+	search string,
+	searchFlag int,
+	pageSize uint,
+	pageNum uint,
+) (*domain.FwhPageResponse[[]domain.FwhSiteInfo], error) {
+	// 构造 query 参数
+	params := url.Values{}
+	params.Add("search", search)
+	params.Add("searchFlag", fmt.Sprintf("%d", searchFlag)) // 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")
+
+	response, err := FwhHttpGetUnmarshalPage[[]domain.FwhSiteInfo](getFullEnergyUrl("/hes-gateway/manage/selectIotGatewayList"), params)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 从fwh_test获取站点列表
+func QuerySiteList(
+	search string,
+	searchFlag int,
+	pageSize uint,
+	pageNum uint,
+) (*domain.FwhPageResponse[[]domain.FwhSiteInfo], error) {
+	siteInfoRes, err := querySiteList(search, searchFlag, pageSize, pageNum)
+	if err != nil {
+		return nil, err
+	}
+
+	if siteInfoRes.Code != 200 {
+		return nil, fmt.Errorf("[QuerySiteInfoList] 查询失败 code: %d, message: %s", siteInfoRes.Code, siteInfoRes.Message)
+	}
+
+	return siteInfoRes, nil
+}
+
+// 查询站点工作模式信息
+func querySiteModeInfo(sn string) (*domain.FwhResponse[dto.SiteModeInfo], 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)
+
+	response, err := FwhHttpPostUnmarshal[dto.SiteModeInfo](getFullEnergyUrl("/hes-gateway/manage/tou/getGatewayTouListV2"), form)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 从fwh_test获取站点工作模式信息
+func QuerySiteModeInfo(site string) (*domain.FwhResponse[dto.SiteModeInfo], error) {
+	siteModeInfoRes, err := querySiteModeInfo(site)
+	if err != nil {
+		return nil, err
+	}
+
+	if siteModeInfoRes.Code != 200 {
+		return nil, fmt.Errorf("[QuerySiteModeInfo] 查询失败 code: %d, message: %s", siteModeInfoRes.Code, siteModeInfoRes.Message)
+	}
+
+	return siteModeInfoRes, nil
+}

+ 3 - 0
service/repositories/api/fwh_test/fwh_sttings.go

@@ -0,0 +1,3 @@
+package fwh_test
+
+// TODO 暂不实现

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

@@ -0,0 +1,71 @@
+package fwh_test
+
+import (
+	"fmt"
+	"fwh-toolbox-server/dto"
+	"net/url"
+
+	"go.uber.org/zap"
+)
+
+type SelectTouTemplateListPayload struct {
+	SourceType      *int    `json:"sourceType,omitempty"` // 模板类型 0-系统模板 1-API模板 2-用户推荐
+	CountryId       *int    `json:"countryId,omitempty"`  // 国家ID
+	ProvinceId      *int    `json:"provinceId,omitempty"` // 州ID
+	Status          *int    `json:"status,omitempty"`     // 发表状态 0-未发布 2-已发布
+	StartUpdateTime *string `json:"startUpdateTime"`      // 开始时间(更新时间) 格式:YYYY-MM-DD HH:mm:SS
+	EndUpdateTime   *string `json:"endUpdateTime"`        // 结束时间(更新时间) 格式:YYYY-MM-DD HH:mm:SS
+	Search          string  `json:"search"`
+	PageSize        uint    `json:"pageSize"`
+	PageNum         uint    `json:"pageNum"`
+	Lang            string  `json:"lang"` // 语言 - ZH_CN
+}
+
+// 查询模板列表
+func QueryTouTemplateInfoList(search string, pageSize uint, pageNum uint, sourceType *int, status *int) (*dto.ResPage[interface{}], error) {
+	// 要发送的表单数据
+	form := url.Values{}
+	form.Set("search", search)
+	if sourceType != nil {
+		form.Set("sourceType", fmt.Sprintf("%d", *sourceType))
+	}
+	if status != nil {
+		form.Set("status", fmt.Sprintf("%d", *status))
+	}
+	form.Set("pageSize", fmt.Sprintf("%d", pageSize))
+	form.Set("pageNum", fmt.Sprintf("%d", pageNum))
+	form.Set("lang", "ZH_CN")
+	zap.L().Info("QueryTouTemplateInfoList form", zap.Any("form", form))
+
+	response, err := FwhHttpPostUnmarshalPage[[]interface{}](getFullEnergyUrl("/hes-gateway/manage/selectIotTouTemplateList"), form)
+	if err != nil {
+		return nil, err
+	}
+
+	var data []interface{}
+	if response.Result != nil {
+		data = *response.Result
+	}
+	res := dto.ResPage[interface{}]{
+		Data:     data,
+		PageSize: pageSize,
+		PageNum:  pageNum,
+		Total:    uint(response.Total),
+	}
+
+	return &res, nil
+}
+
+// 查询模板列表
+func QueryTouTemplateDetailInfo(id uint) (*interface{}, error) {
+	// 要发送的表单数据
+	form := url.Values{}
+	form.Set("id", fmt.Sprintf("%d", id))
+	form.Set("lang", "ZH_CN")
+
+	response, err := FwhHttpPostUnmarshal[interface{}](getFullEnergyUrl("/hes-gateway/manage/getIotTouTemplateDetailById"), form)
+	if err != nil {
+		return nil, err
+	}
+	return response.Result, nil
+}

+ 98 - 0
service/repositories/api/fwh_test/fwh_update.go

@@ -0,0 +1,98 @@
+package fwh_test
+
+import (
+	"fmt"
+	"fwh-toolbox-server/domain"
+	"net/url"
+	"strconv"
+)
+
+// 查询系统升级记录
+func querySystemUpdateLog(sn string) (*domain.FwhResponse[[]domain.SystemUpdateRecordInfo], error) {
+	// 要发送的表单数据
+	form := url.Values{}
+	form.Set("optType", "1")  // 1-系统升级记录 2-下载记录(分步升级相关)
+	form.Set("gatewayId", sn) // SN
+	form.Set("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
+	form.Set("pageNum", "1")
+
+	response, err := FwhHttpPostUnmarshal[[]domain.SystemUpdateRecordInfo](getFullEnergyUrl("/hes-gateway/manage/selectIotFirmwareOrderList"), form)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 查询系统升级记录
+func QuerySystemUpdateLog(sn string) ([]domain.SystemUpdateRecordInfo, error) {
+	systemUpdateLog, err := querySystemUpdateLog(sn)
+	if err != nil {
+		return nil, fmt.Errorf("[QuerySystemUpdateLog] %s", err.Error())
+	}
+
+	if systemUpdateLog.Code != 200 {
+		return nil, fmt.Errorf("[QuerySystemUpdateLog] 查询失败 code: %d, message: %s", systemUpdateLog.Code, systemUpdateLog.Message)
+	}
+
+	return *systemUpdateLog.Result, nil
+}
+
+// 查询系统升级记录详细信息
+func querySystemUpdateLogDetail(orderId int64) (*domain.FwhResponse[[]domain.SystemUpdateDetailInfo], error) {
+	// 要发送的表单数据
+	form := url.Values{}
+	form.Set("optType", "1")                            // 1-系统升级记录 2-下载记录(分步升级相关)
+	form.Set("orderId", strconv.FormatInt(orderId, 10)) // 升级单ID
+	form.Set("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
+	form.Set("pageNum", "1")
+
+	response, err := FwhHttpPostUnmarshal[[]domain.SystemUpdateDetailInfo](getFullEnergyUrl("/hes-gateway/manage/selectIotFirmwareOrderDetailList"), form)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 查询系统升级记录详细信息
+func QuerySystemUpdateLogDetail(orderId int64) ([]domain.SystemUpdateDetailInfo, error) {
+	systemUpdateLogDetail, err := querySystemUpdateLogDetail(orderId)
+	if err != nil {
+		return nil, fmt.Errorf("[QuerySystemUpdateLogDetail] %s", err.Error())
+	}
+
+	if systemUpdateLogDetail.Code != 200 {
+		return nil, fmt.Errorf("[QuerySystemUpdateLogDetail] 查询失败 code: %d, message: %s", systemUpdateLogDetail.Code, systemUpdateLogDetail.Message)
+	}
+
+	return *systemUpdateLogDetail.Result, nil
+}
+
+// 查询aHub升级记录
+func queryAHubUpdateLog(sn string) (*domain.FwhPageResponse[[]domain.AHubUpdateRecordInfo], error) {
+	// 构造 query 参数
+	params := url.Values{}
+	params.Add("gatewayId", sn) // SN
+	params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
+	params.Add("pageNum", strconv.Itoa(1))
+	params.Add("lang", "ZH_CN")
+
+	response, err := FwhHttpGetUnmarshalPage[[]domain.AHubUpdateRecordInfo](getFullEnergyUrl("/hes-gateway/manage/listAhubFirmwareOrder"), params)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}
+
+// 查询aHub升级记录
+func QueryAHubUpdateLog(sn string) ([]domain.AHubUpdateRecordInfo, error) {
+	aHubUpdateLog, err := queryAHubUpdateLog(sn)
+	if err != nil {
+		return nil, fmt.Errorf("[QueryAHubUpdateLog] %s", err.Error())
+	}
+
+	if aHubUpdateLog.Code != 200 {
+		return nil, fmt.Errorf("[QueryAHubUpdateLog] 查询失败 code: %d, message: %s", aHubUpdateLog.Code, aHubUpdateLog.Message)
+	}
+
+	return *aHubUpdateLog.Result, nil
+}

+ 52 - 0
service/repositories/repositories.go

@@ -5,6 +5,7 @@ import (
 	"fwh-toolbox-server/domain"
 	"fwh-toolbox-server/repositories/api/acceleronix"
 	"fwh-toolbox-server/repositories/api/fwh_pro"
+	"fwh-toolbox-server/repositories/api/fwh_test"
 	"fwh-toolbox-server/repositories/api/plane_v1"
 	"fwh-toolbox-server/repositories/api/plane_v2"
 	"fwh-toolbox-server/repositories/api/smartHub"
@@ -84,6 +85,57 @@ var QueryAHubUpdateLog = fwh_pro.QueryAHubUpdateLog                 // 查询aHu
 var QueryTouTemplateInfoList = fwh_pro.QueryTouTemplateInfoList // 查询模板列表
 var QueryTouTemplateDetailInfo = fwh_pro.QueryTouTemplateDetailInfo
 
+// ============ fwh_test(测试环境 API,与 fwh_pro 平行)============
+type FwhTestEventAlarmType = fwh_test.EventAlarmType
+
+const (
+	FwhTestEventAndAlarm FwhTestEventAlarmType = fwh_test.All   // 0
+	FwhTestEvent         FwhTestEventAlarmType = fwh_test.Event // 1
+	FwhTestAlarm         FwhTestEventAlarmType = fwh_test.Alarm // 2
+)
+
+var FwhTestHttpInit = fwh_test.FwhHttpInit
+var FwhTestLogin = fwh_test.Login
+
+var FwhTestQuerySiteBaseInfoList = fwh_test.QuerySiteBaseInfoList
+var FwhTestQuerySiteInfoList = fwh_test.QuerySiteInfoList
+var FwhTestQuerySiteList = fwh_test.QuerySiteList
+var FwhTestQuerySiteModeInfo = fwh_test.QuerySiteModeInfo
+var FwhTestQuerySitePowerFlow = fwh_test.QuerySitePowerFlow
+var FwhTestQuerySiteAlarmList = fwh_test.QuerySiteAlarmList
+
+var FwhTestQueryEventOrAlarmListOnTimeRange = fwh_test.QueryEventOrAlarmListOnTimeRange
+var FwhTestQueryAlarmSnapshotInfo = fwh_test.QueryAlarmSnapshotInfo
+
+var FwhTestQueryMqttLogListOnTimeRange = fwh_test.QueryMqttLogListOnTimeRange
+
+var FwhTestQuerySiteShutdownList = fwh_test.QuerySiteShutdownList
+
+var FwhTestQueryAlarmStatistics = fwh_test.QueryAlarmStatistics
+var FwhTestQueryAlarmStatisticsFromCode = fwh_test.QueryAlarmStatisticsFromCode
+
+// fwh_emsLog
+var FwhTestQueryCallEmsLog = fwh_test.QueryCallEmsLog                 // 召唤EMS日志
+var FwhTestQueryEmsLogCallRecord = fwh_test.QueryEmsLogCallRecord     // 查询EMS日志下载地址
+var FwhTestQueryDownloadEmsLogFile = fwh_test.QueryDownloadEmsLogFile // 请求下载EMS日志文件
+
+// fwh_faultRecord
+var FwhTestQueryFaultRecordList = fwh_test.QueryFaultRecordList                     // 查询故障录波日志
+var FwhTestQueryFaultRecordCallableList = fwh_test.QueryFaultRecordCallableList     // 查询设备可召唤数据列表 - 仅自动录播
+var FwhTestQueryCellFaultRecord = fwh_test.QueryCellFaultRecord                     // 召唤故障录波日志
+var FwhTestQueryFaultRecordCallRecordList = fwh_test.QueryFaultRecordCallRecordList // 查询故障录波召唤记录
+var FwhTestQueryFaultRecordSubList = fwh_test.QueryFaultRecordSubList               // 查询故障录波子记录
+var FwhTestQueryDownloadFaultRecordData = fwh_test.QueryDownloadFaultRecordData     // 请求下载录波数据
+
+// fwh_update
+var FwhTestQuerySystemUpdateLog = fwh_test.QuerySystemUpdateLog             // 查询系统升级记录
+var FwhTestQuerySystemUpdateLogDetail = fwh_test.QuerySystemUpdateLogDetail // 查询系统升级记录详细信息
+var FwhTestQueryAHubUpdateLog = fwh_test.QueryAHubUpdateLog                 // 查询aHub升级记录
+
+// fwh_touTemplate
+var FwhTestQueryTouTemplateInfoList = fwh_test.QueryTouTemplateInfoList // 查询模板列表
+var FwhTestQueryTouTemplateDetailInfo = fwh_test.QueryTouTemplateDetailInfo
+
 // SmartHub
 var QuerySmartHubBatchSimCardInfo = smartHub.QueryBatchSimCardInfo // 批量查询SIM卡信息
 var QuerySmartHubSimCardInfo = smartHub.QuerySimCardInfo           // 获取SIM卡详细信息