| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- package fwhQueue
- import (
- "context"
- "encoding/json"
- "fmt"
- "fwh-toolbox-server/domain"
- "fwh-toolbox-server/jobs"
- "fwh-toolbox-server/repositories"
- "path"
- "strconv"
- "strings"
- "time"
- "github.com/hibiken/asynq"
- "go.mongodb.org/mongo-driver/bson/primitive"
- "go.uber.org/zap"
- )
- const maxCallRetry = 3
- // handleCallFile 是文件下载子任务的 Asynq handler
- // Asynq 本身会做重试(MaxRetry=3),所以 handler 内部不再做外层重试循环
- // 返回 error 会触发 Asynq 重试,返回 nil 表示成功(结果已通过 finishItem 上报)
- func handleCallFile(ctx context.Context, task *asynq.Task) error {
- var payload callFilePayload
- if err := json.Unmarshal(task.Payload(), &payload); err != nil {
- // payload 解析失败无法定位 item,只能记日志,不重试
- zap.L().Error("[CallFile] unmarshal payload 失败", zap.String("err", err.Error()))
- return nil
- }
- info := payload.TaskInfo
- mainTaskId := payload.MainTaskId
- mainObjId, _ := primitive.ObjectIDFromHex(mainTaskId)
- var stageErr *syncItemError
- switch info.FileType {
- case jobs.FileTypeEmsLog:
- stageErr = handleEmsLogDownload(ctx, mainTaskId, info)
- case jobs.FileTypeFaultRecord:
- stageErr = handleFaultRecordDownload(ctx, mainTaskId, info)
- default:
- stageErr = &syncItemError{stage: domain.StageBuild, reason: fmt.Sprintf("unknown file type: %s", info.FileType)}
- }
- if stageErr != nil {
- // 判断是否还有重试机会
- retryCount, _ := asynq.GetRetryCount(ctx)
- maxRetry, _ := asynq.GetMaxRetry(ctx)
- if retryCount >= maxRetry-1 {
- // 最后一次重试仍失败:记 failed item(带 stage),推进完成计数
- finishItem(mainObjId, payload.ItemKey, domain.ItemFailed, stageErr.stage, stageErr.reason)
- return nil // 不再重试
- }
- return stageErr // 触发 Asynq 重试
- }
- // 成功
- finishItem(mainObjId, payload.ItemKey, domain.ItemSuccess, "", "")
- return nil
- }
- // ==================== EmsLog 下载 ====================
- func handleEmsLogDownload(ctx context.Context, mainTaskId string, info domain.BatchDownloadFileTaskInfo) *syncItemError {
- // 1. 召唤 EmsLog
- if err := callEmsLog(info.Sn, info.Date, info.FileSupType); err != nil {
- return &syncItemError{stage: domain.StageCall, reason: fmt.Sprintf("[EmsLog] 召唤失败(sn=%s, date=%s, type=%s): %s", info.Sn, info.Date, info.FileSupType, err.Error())}
- }
- // 2. 轮询等待召唤结果
- recordInfo, sErr := pollEmsLogResult(ctx, info.Sn, info.Date, info.FileSupType)
- if sErr != nil {
- return sErr
- }
- // 3. 下载并保存文件
- mainObjId, _ := primitive.ObjectIDFromHex(mainTaskId)
- return downloadAndSaveEmsLog(ctx, mainObjId, info, recordInfo)
- }
- func pollEmsLogResult(ctx context.Context, sn, date, fileSupType string) (*domain.EmsLogRecordInfo, *syncItemError) {
- ticker := time.NewTicker(time.Minute)
- defer ticker.Stop()
- timeout := time.After(30 * time.Minute)
- for {
- select {
- case <-ctx.Done():
- return nil, &syncItemError{stage: domain.StagePoll, reason: ctx.Err().Error()}
- case <-timeout:
- return nil, &syncItemError{stage: domain.StagePoll, reason: fmt.Sprintf("[EmsLog] 轮询超时(sn=%s, date=%s)", sn, date)}
- case <-ticker.C:
- for attempt := 0; attempt < maxCallRetry; attempt++ {
- recordInfo, err := getEmsLogUrl(sn, date, fileSupType)
- if err != nil {
- continue
- }
- if recordInfo.Status == -1 {
- return nil, &syncItemError{stage: domain.StagePoll, reason: fmt.Sprintf("[EmsLog] 召唤失败(sn=%s)", sn)}
- }
- if recordInfo.Status == 1 {
- return recordInfo, nil
- }
- // status == 0: 还在处理中
- break
- }
- }
- }
- }
- func downloadAndSaveEmsLog(ctx context.Context, mainTaskId primitive.ObjectID, info domain.BatchDownloadFileTaskInfo, recordInfo *domain.EmsLogRecordInfo) *syncItemError {
- var dataBytes []byte
- var err error
- for attempt := 0; attempt < maxCallRetry; attempt++ {
- dataBytes, err = repositories.QueryDownloadEmsLogFile(recordInfo.FileUrl)
- if err == nil {
- break
- }
- zap.L().Warn("[EmsLog] 文件下载重试", zap.String("sn", info.Sn), zap.Int("attempt", attempt))
- }
- if err != nil {
- return &syncItemError{stage: domain.StageDownload, reason: fmt.Sprintf("[EmsLog] 文件下载失败(sn=%s): %s", info.Sn, err.Error())}
- }
- // 提取文件名
- baseStr := path.Base(recordInfo.FileUrl)
- if baseStr == "." || baseStr == "/" {
- baseStr = ""
- }
- // 解析召唤时间
- loc, err := time.LoadLocation("America/Los_Angeles")
- if err != nil {
- return &syncItemError{stage: domain.StageSave, reason: fmt.Sprintf("[EmsLog] 时区加载失败: %s", err.Error())}
- }
- callTime, err := time.ParseInLocation(time.DateTime, recordInfo.CallTime, loc)
- if err != nil {
- return &syncItemError{stage: domain.StageSave, reason: fmt.Sprintf("[EmsLog] CallTime解析失败: %s", err.Error())}
- }
- // 保存到 MinIO
- objName, err := jobs.SaveBatchDownloadEmsLog(info.Sn, dataBytes, baseStr, info.FileSupType, info.Date, callTime)
- if err != nil {
- return &syncItemError{stage: domain.StageSave, reason: fmt.Sprintf("[EmsLog] 文件保存失败(sn=%s): %s", info.Sn, err.Error())}
- }
- // 写入路径记录
- if err := repositories.InsertBatchDownloadPathEmsLogs(mainTaskId, info.Sn, info.Date, info.FileSupType, objName); err != nil {
- return &syncItemError{stage: domain.StageSave, reason: fmt.Sprintf("[EmsLog] 路径写入失败(sn=%s): %s", info.Sn, err.Error())}
- }
- return nil
- }
- // ==================== FaultRecord 下载 ====================
- func handleFaultRecordDownload(ctx context.Context, mainTaskId string, info domain.BatchDownloadFileTaskInfo) *syncItemError {
- // 1. 解析参数
- timeRange := strings.Split(info.Date, ",")
- if len(timeRange) != 2 {
- return &syncItemError{stage: domain.StageBuild, reason: fmt.Sprintf("[FaultRecord] timeRange 格式错误: %s", info.Date)}
- }
- // 2. 查询故障录波列表,判断是否存在满足时间范围的数据
- existingRecords, err := repositories.QueryFaultRecordList(info.Sn, timeRange[0], timeRange[1])
- if err != nil {
- zap.L().Error("[FaultRecord] 查询已有录波列表失败", zap.String("sn", info.Sn), zap.String("err", err.Error()))
- return &syncItemError{stage: domain.StageQuery, reason: fmt.Sprintf("[FaultRecord] 查询已有录波列表失败(sn=%s): %s", info.Sn, err.Error())}
- } else {
- // 判断列表中是否存在 earliestFaultTime 在时间范围内的记录
- faultID, err := checkFaultRecordInTimeRange(existingRecords, info.DevType, info.Sn, timeRange[0], timeRange[1])
- if err != nil {
- zap.L().Error("[FaultRecord] 判断时间范围失败", zap.String("sn", info.Sn), zap.String("err", err.Error()))
- return &syncItemError{stage: domain.StageQuery, reason: fmt.Sprintf("[FaultRecord] 判断时间范围失败(sn=%s): %s", info.Sn, err.Error())}
- }
- if faultID >= 0 {
- // 故障录波文件存在,直接下载所有子记录文件
- mainObjId, _ := primitive.ObjectIDFromHex(mainTaskId)
- return downloadAndSaveFaultRecords(ctx, mainObjId, faultID)
- }
- zap.L().Info("[FaultRecord] 无满足时间范围的录波数据,需要召唤",
- zap.String("sn", info.Sn),
- zap.String("startTime", timeRange[0]),
- zap.String("endTime", timeRange[1]),
- )
- }
- indexList, err := parseIndexList(info.FileSupType)
- if err != nil {
- return &syncItemError{stage: domain.StageBuild, reason: fmt.Sprintf("[FaultRecord] indexList 解析错误: %s", err.Error())}
- }
- // 3. 召唤故障录波
- if err := callFaultRecord(info.DevType, info.Sn, timeRange[0], timeRange[1], indexList); err != nil {
- return &syncItemError{stage: domain.StageCall, reason: fmt.Sprintf("[FaultRecord] 召唤失败(sn=%s): %s", info.Sn, err.Error())}
- }
- // 4. 轮询等待召唤结果
- faultId, sErr := pollFaultRecordResult(ctx, info.DevType, info.Sn, timeRange[0], timeRange[1])
- if sErr != nil {
- return sErr
- }
- // 5. 下载所有子记录文件
- mainObjId, _ := primitive.ObjectIDFromHex(mainTaskId)
- return downloadAndSaveFaultRecords(ctx, mainObjId, faultId)
- }
- // checkFaultRecordInTimeRange 检查故障录波列表中是否存在 earliestFaultTime 满足时间范围的记录,返回FaultID和错误信息,不在范围内FaultID = -1
- func checkFaultRecordInTimeRange(records []domain.FaultRecordInfo, devType int, sn string, startTime string, endTime string) (int64, error) {
- start, err := time.Parse(time.DateTime, startTime)
- if err != nil {
- zap.L().Error("[checkFaultRecordInTimeRange] 解析startTime失败",
- zap.String("startTime", startTime),
- zap.String("err", err.Error()),
- )
- return -1, err // 解析失败,保守处理:继续召唤
- }
- end, err := time.Parse(time.DateTime, endTime)
- if err != nil {
- zap.L().Error("[checkFaultRecordInTimeRange] 解析endTime失败",
- zap.String("endTime", endTime),
- zap.String("err", err.Error()),
- )
- return -1, err
- }
- for _, record := range records {
- // 匹配设备类型和 SN
- if record.DeviceType != devType {
- continue
- }
- var recordSn string
- switch devType {
- case 0: // aPower
- if record.ApowerId != nil {
- recordSn = *record.ApowerId
- }
- case 1: // aGate
- recordSn = record.GatewayId
- case 2: // MPPT
- if record.ApowerId != nil {
- recordSn = *record.ApowerId
- }
- case 3: // aHub
- if record.PowerlinkId != nil {
- recordSn = *record.PowerlinkId
- }
- }
- if recordSn != sn {
- continue
- }
- // 解析 earliestFaultTime 并判断是否在时间范围内
- faultTime, err := time.Parse(time.DateTime, record.EarliestFaultTime)
- if err != nil {
- // 尝试时间戳格式
- continue
- }
- inRange := (faultTime.Equal(start) || faultTime.After(start)) &&
- (faultTime.Equal(end) || faultTime.Before(end))
- if inRange {
- return record.Id, nil
- }
- }
- return -1, nil
- }
- func pollFaultRecordResult(ctx context.Context, devType int, sn, startTime, endTime string) (int64, *syncItemError) {
- ticker := time.NewTicker(time.Minute)
- defer ticker.Stop()
- timeout := time.After(30 * time.Minute)
- for {
- select {
- case <-ctx.Done():
- return 0, &syncItemError{stage: domain.StagePoll, reason: ctx.Err().Error()}
- case <-timeout:
- return 0, &syncItemError{stage: domain.StagePoll, reason: fmt.Sprintf("[FaultRecord] 轮询超时(sn=%s)", sn)}
- case <-ticker.C:
- for attempt := 0; attempt < maxCallRetry; attempt++ {
- status, faultId, reason, err := getFaultRecordUrl(devType, sn, startTime, endTime)
- if err != nil {
- continue
- }
- if status == -1 {
- return 0, &syncItemError{stage: domain.StagePoll, reason: fmt.Sprintf("[FaultRecord] 召唤失败(sn=%s): %s", sn, reason)}
- }
- if status == 2 {
- return faultId, nil
- }
- // status == 0: 还在处理中
- break
- }
- }
- }
- }
- func downloadAndSaveFaultRecords(ctx context.Context, mainTaskId primitive.ObjectID, faultId int64) *syncItemError {
- subRecordList, err := repositories.QueryFaultRecordSubList(faultId)
- if err != nil {
- return &syncItemError{stage: domain.StageDownload, reason: fmt.Sprintf("[FaultRecord] 子记录查询失败(faultId=%d): %s", faultId, err.Error())}
- }
- for _, subRecord := range subRecordList {
- var dataBytes []byte
- var downloadErr error
- for attempt := 0; attempt < maxCallRetry; attempt++ {
- _, dataBytes, downloadErr = repositories.QueryDownloadFaultRecordData(faultId, subRecord.Id)
- if downloadErr == nil {
- break
- }
- zap.L().Warn("[FaultRecord] 文件下载重试",
- zap.String("sn", subRecord.GatewayId),
- zap.Int64("subId", subRecord.Id),
- zap.Int("attempt", attempt),
- )
- }
- if downloadErr != nil {
- return &syncItemError{stage: domain.StageDownload, reason: fmt.Sprintf("[FaultRecord] 文件下载失败(sn=%s, id=%d): %s", subRecord.GatewayId, subRecord.Id, downloadErr.Error())}
- }
- // 保存到 MinIO
- objName, err := jobs.SaveBatchDownloadFaultRecord(subRecord.GatewayId, dataBytes, subRecord.FileName+".xls")
- if err != nil {
- return &syncItemError{stage: domain.StageSave, reason: fmt.Sprintf("[FaultRecord] 文件保存失败(sn=%s): %s", subRecord.GatewayId, err.Error())}
- }
- // 写入路径记录
- if err := repositories.InsertBatchDownloadPathFaultRecord(mainTaskId, subRecord.GatewayId, subRecord.FileName, objName); err != nil {
- return &syncItemError{stage: domain.StageSave, reason: fmt.Sprintf("[FaultRecord] 路径写入失败(sn=%s): %s", subRecord.GatewayId, err.Error())}
- }
- }
- return nil
- }
- // ==================== 辅助函数(复用原有逻辑) ====================
- // EMS日志类型map
- var mapEmsLogType = map[string]int{
- "EmsRunLog": 4,
- "EmsBattLog": 7,
- "EmsMonitorLog": 9,
- "Ems4gLog": 10,
- "EmsAwsLog": 11,
- "EmsMainLog": 12,
- }
- func callEmsLog(sn string, date string, logType string) error {
- logTypeNum, ok := mapEmsLogType[logType]
- if !ok {
- return fmt.Errorf("logType不存在: %s", logType)
- }
- return repositories.QueryCallEmsLog(sn, date, logTypeNum)
- }
- func getEmsLogUrl(sn string, date string, logType string) (*domain.EmsLogRecordInfo, error) {
- logTypeNum, ok := mapEmsLogType[logType]
- if !ok {
- return nil, fmt.Errorf("logType不存在: %s", logType)
- }
- return repositories.QueryEmsLogCallRecord(sn, date, logTypeNum)
- }
- func callFaultRecord(devType int, sn string, startTime string, endTime string, indexList []int) error {
- return repositories.QueryCellFaultRecord(devType, sn, indexList)
- }
- func getFaultRecordUrl(devType int, sn string, startTime string, endTime string) (int, int64, string, error) {
- recordList, err := repositories.QueryFaultRecordCallRecordList()
- if err != nil {
- return 0, 0, "", err
- }
- for _, record := range recordList {
- if record.DeviceType == devType {
- var devSn string
- switch devType {
- case 0:
- devSn = *record.ApowerId
- case 1:
- devSn = record.GatewayId
- case 2:
- devSn = *record.ApowerId
- case 3:
- devSn = *record.PowerlinkId
- default:
- continue
- }
- if devSn == sn {
- if *record.Status == -1 {
- return *record.Status, record.Id, *record.FaultReason, nil
- }
- return *record.Status, record.Id, "", nil
- }
- }
- }
- return 0, 0, "", fmt.Errorf("未找到召唤匹配记录(devType=%d, sn=%s)", devType, sn)
- }
- func parseIndexList(fileSupType string) ([]int, error) {
- indexStrList := strings.Split(fileSupType, ",")
- indexList := make([]int, 0, len(indexStrList))
- for _, s := range indexStrList {
- idx, err := strconv.Atoi(s)
- if err != nil {
- return nil, fmt.Errorf("无法解析 index '%s': %w", s, err)
- }
- indexList = append(indexList, idx)
- }
- return indexList, nil
- }
|