| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551 |
- package fwhQueue
- import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "fwh-toolbox-server/cache"
- "fwh-toolbox-server/domain"
- "fwh-toolbox-server/dto"
- "fwh-toolbox-server/jobs"
- "fwh-toolbox-server/repositories"
- "strconv"
- "strings"
- "time"
- "github.com/hibiken/asynq"
- "go.uber.org/zap"
- )
- // handleBatchDownload 是 BatchDownload 主任务的 Asynq handler
- func handleBatchDownload(ctx context.Context, task *asynq.Task) error {
- var payload domain.TaskBatchDownload
- if err := json.Unmarshal(task.Payload(), &payload); err != nil {
- return fmt.Errorf("unmarshal BatchDownload payload: %w", err)
- }
- mainTaskId := payload.Id.Hex()
- // 更新运行状态
- if err := repositories.UpdateBatchDownloadResult(payload.Id, domain.ProgressRunning, []string{}); err != nil {
- zap.L().Error("[BatchDownload] 更新状态失败(Running)", zap.String("id", mainTaskId))
- return err
- }
- // 执行主逻辑
- reasons, err := executeBatchDownload(ctx, payload)
- // 记录结果
- if err != nil {
- zap.L().Error("[BatchDownload] 任务执行失败", zap.String("id", mainTaskId), zap.String("err", err.Error()))
- repositories.UpdateBatchDownloadResult(payload.Id, domain.ProgressError, []string{err.Error()})
- return err
- }
- if len(reasons) == 0 {
- zap.L().Info("[BatchDownload] 任务执行成功", zap.String("id", mainTaskId))
- repositories.UpdateBatchDownloadResult(payload.Id, domain.ProgressComplete, []string{})
- } else {
- zap.L().Warn("[BatchDownload] 任务部分失败", zap.String("id", mainTaskId), zap.Strings("reasons", reasons))
- repositories.UpdateBatchDownloadResult(payload.Id, domain.ProgressFailed, reasons)
- }
- return nil // 返回 nil 让 Asynq 标记任务为 completed,不触发重试
- }
- // executeBatchDownload 执行批量下载逻辑
- func executeBatchDownload(ctx context.Context, payload domain.TaskBatchDownload) ([]string, error) {
- mainTaskId := payload.Id.Hex()
- // 解析时间范围
- startTime, err := time.Parse(time.DateOnly, payload.TimePeriod[0])
- if err != nil {
- return nil, fmt.Errorf("startTime 格式错误: %w", err)
- }
- endTime, err := time.Parse(time.DateOnly, payload.TimePeriod[1])
- if err != nil {
- return nil, fmt.Errorf("endTime 格式错误: %w", err)
- }
- endTime = endTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
- if startTime.After(endTime) {
- return nil, errors.New("startTime > endTime")
- }
- // 按天拆分时间段
- periods := splitTimePeriods(startTime, endTime)
- // ==================== 阶段1:创建文件下载子任务 ====================
- callFileTasks := buildCallFileTasks(payload, periods, startTime, endTime)
- totalSubTasks := len(callFileTasks)
- if totalSubTasks > 0 {
- // 初始化 Redis 计数器(待完成数)
- counterKey := callFileCounterPrefix + mainTaskId
- failuresKey := callFileFailuresPrefix + mainTaskId
- cache.Set(ctx, counterKey, totalSubTasks, 2*time.Hour)
- cache.Delete(ctx, failuresKey)
- // 入队所有子任务
- for i, cfTask := range callFileTasks {
- taskPayload, _ := json.Marshal(cfTask)
- subTaskID := fmt.Sprintf("callfile-%s-%d", mainTaskId, i)
- t := asynq.NewTask(TaskTypeCallFile, taskPayload,
- asynq.MaxRetry(3),
- asynq.Timeout(35*time.Minute),
- asynq.Queue(QueueDownload),
- asynq.TaskID(subTaskID),
- )
- if _, err := TaskClient.Enqueue(t); err != nil {
- zap.L().Error("[BatchDownload] 子任务入队失败",
- zap.String("id", mainTaskId),
- zap.Int("index", i),
- zap.String("err", err.Error()),
- )
- }
- }
- }
- // ==================== 阶段2:同步处理其他数据任务 ====================
- if err := executeSyncTasks(ctx, payload, periods); err != nil {
- return nil, err
- }
- // ==================== 阶段3:等待子任务完成 ====================
- if totalSubTasks > 0 {
- failReasons, err := waitForCallFileTasks(ctx, mainTaskId)
- if err != nil {
- return nil, err
- }
- return failReasons, nil
- }
- return nil, nil
- }
- // callFilePayload 是传递给子任务的 payload 结构
- type callFilePayload struct {
- MainTaskId string `json:"mainTaskId"`
- TaskInfo domain.BatchDownloadFileTaskInfo `json:"taskInfo"`
- }
- // buildCallFileTasks 构建所有需要异步下载的文件任务列表
- func buildCallFileTasks(payload domain.TaskBatchDownload, periods []timePeriod, startTime, endTime time.Time) []callFilePayload {
- mainTaskId := payload.Id.Hex()
- var tasks []callFilePayload
- for _, sn := range payload.Sn {
- // EmsLog 任务
- for _, period := range periods {
- dateStr := period.Start.Format(time.DateOnly)
- for _, logType := range payload.Options.EmsLogs {
- tasks = append(tasks, callFilePayload{
- MainTaskId: mainTaskId,
- TaskInfo: domain.BatchDownloadFileTaskInfo{
- DevType: -1,
- Sn: sn,
- Date: dateStr,
- FileType: jobs.FileTypeEmsLog,
- FileSupType: logType,
- },
- })
- }
- }
- // FaultRecord 任务
- if payload.Options.FaultRecord {
- frTasks := buildFaultRecordTasks(mainTaskId, sn, startTime, endTime)
- tasks = append(tasks, frTasks...)
- }
- }
- return tasks
- }
- // buildFaultRecordTasks 为单个 SN 构建故障录波下载任务
- func buildFaultRecordTasks(mainTaskId string, sn string, startTime, endTime time.Time) []callFilePayload {
- var tasks []callFilePayload
- // 查询 aGate 可召唤列表
- aGateTasks := buildFaultRecordTasksForDev(mainTaskId, 1, sn, startTime, endTime)
- tasks = append(tasks, aGateTasks...)
- // 查询 aPower 子设备
- siteBaseInfoList, err := repositories.QuerySiteBaseInfoList(dto.SiteBaseInfoPara{
- SiteList: []string{sn},
- PageSize: 10,
- PageNum: 1,
- })
- if err != nil {
- zap.L().Error("[BatchDownload] 查询站点基本信息失败", zap.String("sn", sn), zap.String("err", err.Error()))
- return tasks
- }
- if siteBaseInfoList != nil && len(*siteBaseInfoList.Result) > 0 {
- aPowerSnList := strings.Split((*siteBaseInfoList.Result)[0].FhpSn, ",")
- for _, aPowerSn := range aPowerSnList {
- aPowerSn = strings.TrimSpace(aPowerSn)
- if aPowerSn == "" {
- continue
- }
- aPowerTasks := buildFaultRecordTasksForDev(mainTaskId, 0, aPowerSn, startTime, endTime)
- tasks = append(tasks, aPowerTasks...)
- }
- }
- return tasks
- }
- // buildFaultRecordTasksForDev 为指定设备构建故障录波下载任务
- func buildFaultRecordTasksForDev(mainTaskId string, devType int, sn string, startTime, endTime time.Time) []callFilePayload {
- callInfoList, err := repositories.QueryFaultRecordCallableList(devType, sn)
- if err != nil {
- zap.L().Error("[BatchDownload] 查询故障录波召唤选项失败", zap.String("sn", sn), zap.String("err", err.Error()))
- return nil
- }
- if len(callInfoList) == 0 {
- return nil
- }
- // 筛选日期范围内的记录
- var indexList string
- for _, info := range callInfoList {
- failureTime, err := parseFaultRecordTime(info.FailureTime)
- if err != nil {
- continue
- }
- inRange := (failureTime.Equal(startTime) || failureTime.After(startTime)) &&
- (failureTime.Equal(endTime) || failureTime.Before(endTime))
- if inRange {
- if indexList == "" {
- indexList = strconv.Itoa(info.Idx)
- } else {
- indexList = indexList + "," + strconv.Itoa(info.Idx)
- }
- }
- }
- // 没有符合日期范围的记录,不创建任务
- if indexList == "" {
- return nil
- }
- return []callFilePayload{{
- MainTaskId: mainTaskId,
- TaskInfo: domain.BatchDownloadFileTaskInfo{
- DevType: devType,
- Sn: sn,
- Date: startTime.Format(time.DateTime) + "," + endTime.Format(time.DateTime),
- FileType: jobs.FileTypeFaultRecord,
- FileSupType: indexList,
- },
- }}
- }
- // executeSyncTasks 同步执行非文件下载类任务(PowerFlow、MqttLogs、EventAlarm 等)
- func executeSyncTasks(ctx context.Context, payload domain.TaskBatchDownload, periods []timePeriod) error {
- for _, sn := range payload.Sn {
- // PowerFlow
- if payload.Options.PowerFlow {
- for _, period := range periods {
- dateStr := period.Start.Format(time.DateOnly)
- res, err := repositories.QuerySitePowerFlow(sn, period.Start)
- if err != nil {
- zap.L().Error("[BatchDownload] 查询功率流数据失败", zap.String("sn", sn), zap.String("date", dateStr), zap.String("err", err.Error()))
- return err
- }
- objName, err := jobs.SaveBatchDownloadPowerFlow(sn, dateStr, *res.Result)
- if err != nil {
- return err
- }
- if err := repositories.InsertBatchDownloadPathPowerFlow(payload.Id, sn, dateStr, objName); err != nil {
- return err
- }
- }
- }
- // MqttLogs
- for _, logType := range payload.Options.MqttLogs {
- for _, period := range periods {
- logList, err := repositories.QueryMqttLogListOnTimeRange(sn, domain.MqttCmdType(logType), period.Start, period.End)
- if err != nil {
- return err
- }
- dateStr := period.Start.Format(time.DateOnly)
- objName, err := jobs.SaveBatchDownloadMqttLog(sn, logType, dateStr, logList)
- if err != nil {
- return err
- }
- if err := repositories.InsertBatchDownloadPathMqttLogs(payload.Id, sn, logType, dateStr, objName); err != nil {
- return err
- }
- }
- }
- // EventAlarm
- if payload.Options.EventAlarm {
- for _, period := range periods {
- eventAlarmList, err := repositories.QueryEventOrAlarmListOnTimeRange(sn, period.Start, period.End, repositories.EventAndAlarm)
- if err != nil {
- return err
- }
- dateStr := period.Start.Format(time.DateOnly)
- objName, err := jobs.SaveBatchDownloadEventAlarm(sn, dateStr, eventAlarmList)
- if err != nil {
- return err
- }
- if err := repositories.InsertBatchDownloadPathEventAlarm(payload.Id, sn, dateStr, objName); err != nil {
- return err
- }
- }
- }
- // AlarmSnapshot
- if payload.Options.AlarmSnapshot {
- for _, period := range periods {
- dateStr := period.Start.Format(time.DateOnly)
- alarmSnapshotList, err := jobs.GetAlarmSnapshotData(ctx, sn, dateStr)
- if err != nil {
- return err
- }
- objName, err := jobs.SaveBatchDownloadAlarmSnapshot(sn, dateStr, alarmSnapshotList)
- if err != nil {
- return err
- }
- if err := repositories.InsertBatchDownloadPathAlarmSnapshot(payload.Id, sn, dateStr, objName); err != nil {
- return err
- }
- }
- }
- // UpdateLog
- if payload.Options.UpdateLog {
- if err := executeSyncUpdateLog(sn, payload); err != nil {
- return err
- }
- }
- }
- return nil
- }
- // waitForCallFileTasks 等待所有子任务完成,收集失败原因
- func waitForCallFileTasks(ctx context.Context, mainTaskId string) ([]string, error) {
- counterKey := callFileCounterPrefix + mainTaskId
- failuresKey := callFileFailuresPrefix + mainTaskId
- ticker := time.NewTicker(30 * time.Second)
- defer ticker.Stop()
- timeout := time.After(90 * time.Minute) // 90 分钟总超时
- for {
- select {
- case <-ctx.Done():
- return nil, ctx.Err()
- case <-timeout:
- return nil, fmt.Errorf("等待子任务完成超时(90分钟)")
- case <-ticker.C:
- val, err := cache.Get(ctx, counterKey)
- if err != nil {
- // key 不存在或读取失败
- if err.Error() == "redis: nil" {
- // key 不存在,所有子任务已完成
- val = "0"
- } else {
- zap.L().Error("[BatchDownload] 读取计数器失败", zap.String("key", counterKey), zap.String("err", err.Error()))
- continue
- }
- }
- remaining, _ := strconv.Atoi(val)
- if remaining <= 0 {
- // 收集失败原因
- failures, err := cache.LRange(ctx, failuresKey, 0, -1)
- if err != nil {
- return nil, fmt.Errorf("读取失败原因列表失败: %w", err)
- }
- // 清理
- cache.Delete(ctx, counterKey, failuresKey)
- return failures, nil
- }
- }
- }
- }
- // ==================== 辅助函数 ====================
- type timePeriod struct {
- Start time.Time
- End time.Time
- }
- func splitTimePeriods(startTime, endTime time.Time) []timePeriod {
- var periods []timePeriod
- curr := startTime
- for curr.Before(endTime) {
- dayEnd := time.Date(curr.Year(), curr.Month(), curr.Day(), 23, 59, 59, 0, curr.Location())
- if dayEnd.After(endTime) {
- dayEnd = endTime
- }
- periods = append(periods, timePeriod{Start: curr, End: dayEnd})
- curr = time.Date(curr.Year(), curr.Month(), curr.Day()+1, 0, 0, 0, 0, curr.Location())
- }
- return periods
- }
- func parseFaultRecordTime(failureTime string) (time.Time, error) {
- parts := strings.Split(failureTime, " ")
- if len(parts) < 2 {
- return time.Time{}, fmt.Errorf("invalid failureTime format: %s", failureTime)
- }
- date := parts[0]
- timeStr := parts[1]
- d := strings.Split(date, "-")
- t := strings.Split(timeStr, ":")
- if len(d) < 3 || len(t) < 3 {
- return time.Time{}, fmt.Errorf("invalid failureTime format: %s", failureTime)
- }
- standardTime := fmt.Sprintf("%04s-%02s-%02s %02s:%02s:%02s", d[0], d[1], d[2], t[0], t[1], t[2])
- return time.Parse(time.DateTime, standardTime)
- }
- // executeSyncUpdateLog 同步执行升级日志任务(从原代码提取)
- func executeSyncUpdateLog(sn string, payload domain.TaskBatchDownload) error {
- updateLogList := []domain.BatchDownloadSystemUpdateLogInfo{}
- systemUpdateLogList, err := repositories.QuerySystemUpdateLog(sn)
- if err != nil {
- return err
- }
- for _, logList := range systemUpdateLogList {
- var updateLogInfo domain.BatchDownloadSystemUpdateLogInfo
- updateLogInfo.OrderNo = logList.OrderNo
- updateLogInfo.GateSn = sn
- updateLogInfo.FileName = logList.Name
- upgradeTime, err := time.ParseInLocation(time.DateTime, logList.UpgradeTime, time.UTC)
- if err != nil {
- zap.L().Warn("[BatchDownload] 升级时间解析失败", zap.String("time", logList.UpgradeTime))
- continue
- }
- beijing, _ := time.LoadLocation("Asia/Shanghai")
- updateLogInfo.UpdateTime = upgradeTime.In(beijing).Format(time.DateTime)
- if strings.HasPrefix(logList.OrderNo, "CH") {
- updateLogInfo.TriggerType = "Auto"
- } else if strings.HasPrefix(logList.OrderNo, "DH") {
- updateLogInfo.TriggerType = "Manual"
- } else if strings.HasPrefix(logList.OrderNo, "SH") {
- updateLogInfo.TriggerType = "Span Auto"
- } else {
- updateLogInfo.TriggerType = "unknown"
- }
- switch logList.Result {
- case 1:
- updateLogInfo.Result = "Updating"
- case 2:
- updateLogInfo.Result = "Success"
- case 3:
- updateLogInfo.Result = "Fault"
- default:
- updateLogInfo.Result = fmt.Sprintf("unknown(%d)", logList.Result)
- }
- systemUpdateLogListDetail, err := repositories.QuerySystemUpdateLogDetail(logList.Id)
- if err != nil {
- return err
- }
- for _, logDetail := range systemUpdateLogListDetail {
- if logDetail.GatewayId == sn {
- for _, ibgVer := range logDetail.IbgVersionNew {
- if ibgVer.Children != nil {
- for _, childVerInfo := range ibgVer.Children {
- updateLogInfo.GateVerList = append(updateLogInfo.GateVerList, domain.BatchDownloadUpdateLogVersionInfo{
- Name: childVerInfo.Type, OldVer: childVerInfo.OldVersion, NewVer: childVerInfo.Version,
- })
- }
- } else {
- updateLogInfo.GateVerList = append(updateLogInfo.GateVerList, domain.BatchDownloadUpdateLogVersionInfo{
- Name: ibgVer.Type, OldVer: ibgVer.OldVersion, NewVer: ibgVer.Version,
- })
- }
- }
- for _, subInfo := range logDetail.SonNewDevices {
- var subDevInfo domain.BatchDownloadUpdateLogSubDevInfo
- subDevInfo.DevSn = subInfo.ApowerSn
- for _, firstItem := range subInfo.FirstList {
- for _, secondItem := range firstItem.Secondlist {
- for _, thirdItem := range secondItem.Thirdlist {
- subDevInfo.DevVerList = append(subDevInfo.DevVerList, domain.BatchDownloadUpdateLogVersionInfo{
- Name: thirdItem.SubType, OldVer: thirdItem.OldVersion, NewVer: thirdItem.Version,
- })
- }
- }
- }
- updateLogInfo.SubDevList = append(updateLogInfo.SubDevList, subDevInfo)
- }
- break
- }
- }
- updateLogList = append(updateLogList, updateLogInfo)
- }
- // aHub 升级日志
- aHubUpdateLogList, err := repositories.QueryAHubUpdateLog(sn)
- if err != nil {
- return err
- }
- var aHubUpdateLogData []domain.BatchDownloadAHubUpdateLogInfo
- for _, aHubVerInfo := range aHubUpdateLogList {
- var info domain.BatchDownloadAHubUpdateLogInfo
- info.OrderNo = aHubVerInfo.OrderNo
- info.GateSn = sn
- info.AHubSn = aHubVerInfo.AHubSn
- if strings.HasPrefix(aHubVerInfo.OrderNo, "CH") {
- info.TriggerType = "Auto"
- } else if strings.HasPrefix(aHubVerInfo.OrderNo, "DH") {
- info.TriggerType = "Manual"
- } else if strings.HasPrefix(aHubVerInfo.OrderNo, "SH") {
- info.TriggerType = "Span Auto"
- } else {
- info.TriggerType = "unknown"
- }
- switch aHubVerInfo.UpgradeResult {
- case 0:
- info.Result = "Success"
- case -1:
- info.Result = "Fault"
- default:
- info.Result = fmt.Sprintf("unknown(%d)", aHubVerInfo.UpgradeResult)
- }
- for _, detailInfo := range aHubVerInfo.DetailList {
- info.DetailList = append(info.DetailList, domain.BatchDownloadUpdateLogAHubVersionInfo{
- FileName: detailInfo.UpgradeFileName,
- FileOrder: detailInfo.UpgradeFileOrder,
- Result: detailInfo.UpgradeResult,
- ResultDesc: detailInfo.UpgradeResultDesc,
- OldVer: detailInfo.OldVersion,
- NewVer: detailInfo.NewVersion,
- })
- }
- aHubUpdateLogData = append(aHubUpdateLogData, info)
- }
- objName, err := jobs.SaveBatchDownloadUpdateLog(sn, updateLogList, aHubUpdateLogData)
- if err != nil {
- return err
- }
- return repositories.InsertBatchDownloadPathUpdateLog(payload.Id, sn, objName)
- }
|