fwhTaskBatchDownload.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. package fwhQueue
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "fwh-toolbox-server/cache"
  8. "fwh-toolbox-server/domain"
  9. "fwh-toolbox-server/dto"
  10. "fwh-toolbox-server/jobs"
  11. "fwh-toolbox-server/repositories"
  12. "strconv"
  13. "strings"
  14. "time"
  15. "github.com/hibiken/asynq"
  16. "go.uber.org/zap"
  17. )
  18. // handleBatchDownload 是 BatchDownload 主任务的 Asynq handler
  19. func handleBatchDownload(ctx context.Context, task *asynq.Task) error {
  20. var payload domain.TaskBatchDownload
  21. if err := json.Unmarshal(task.Payload(), &payload); err != nil {
  22. return fmt.Errorf("unmarshal BatchDownload payload: %w", err)
  23. }
  24. mainTaskId := payload.Id.Hex()
  25. // 更新运行状态
  26. if err := repositories.UpdateBatchDownloadResult(payload.Id, domain.ProgressRunning, []string{}); err != nil {
  27. zap.L().Error("[BatchDownload] 更新状态失败(Running)", zap.String("id", mainTaskId))
  28. return err
  29. }
  30. // 执行主逻辑
  31. reasons, err := executeBatchDownload(ctx, payload)
  32. // 记录结果
  33. if err != nil {
  34. zap.L().Error("[BatchDownload] 任务执行失败", zap.String("id", mainTaskId), zap.String("err", err.Error()))
  35. repositories.UpdateBatchDownloadResult(payload.Id, domain.ProgressError, []string{err.Error()})
  36. return err
  37. }
  38. if len(reasons) == 0 {
  39. zap.L().Info("[BatchDownload] 任务执行成功", zap.String("id", mainTaskId))
  40. repositories.UpdateBatchDownloadResult(payload.Id, domain.ProgressComplete, []string{})
  41. } else {
  42. zap.L().Warn("[BatchDownload] 任务部分失败", zap.String("id", mainTaskId), zap.Strings("reasons", reasons))
  43. repositories.UpdateBatchDownloadResult(payload.Id, domain.ProgressFailed, reasons)
  44. }
  45. return nil // 返回 nil 让 Asynq 标记任务为 completed,不触发重试
  46. }
  47. // executeBatchDownload 执行批量下载逻辑
  48. func executeBatchDownload(ctx context.Context, payload domain.TaskBatchDownload) ([]string, error) {
  49. mainTaskId := payload.Id.Hex()
  50. // 解析时间范围
  51. startTime, err := time.Parse(time.DateOnly, payload.TimePeriod[0])
  52. if err != nil {
  53. return nil, fmt.Errorf("startTime 格式错误: %w", err)
  54. }
  55. endTime, err := time.Parse(time.DateOnly, payload.TimePeriod[1])
  56. if err != nil {
  57. return nil, fmt.Errorf("endTime 格式错误: %w", err)
  58. }
  59. endTime = endTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
  60. if startTime.After(endTime) {
  61. return nil, errors.New("startTime > endTime")
  62. }
  63. // 按天拆分时间段
  64. periods := splitTimePeriods(startTime, endTime)
  65. // ==================== 阶段1:创建文件下载子任务 ====================
  66. callFileTasks := buildCallFileTasks(payload, periods, startTime, endTime)
  67. totalSubTasks := len(callFileTasks)
  68. if totalSubTasks > 0 {
  69. // 初始化 Redis 计数器(待完成数)
  70. counterKey := callFileCounterPrefix + mainTaskId
  71. failuresKey := callFileFailuresPrefix + mainTaskId
  72. cache.Set(ctx, counterKey, totalSubTasks, 2*time.Hour)
  73. cache.Delete(ctx, failuresKey)
  74. // 入队所有子任务
  75. for i, cfTask := range callFileTasks {
  76. taskPayload, _ := json.Marshal(cfTask)
  77. subTaskID := fmt.Sprintf("callfile-%s-%d", mainTaskId, i)
  78. t := asynq.NewTask(TaskTypeCallFile, taskPayload,
  79. asynq.MaxRetry(3),
  80. asynq.Timeout(35*time.Minute),
  81. asynq.Queue(QueueDownload),
  82. asynq.TaskID(subTaskID),
  83. )
  84. if _, err := TaskClient.Enqueue(t); err != nil {
  85. zap.L().Error("[BatchDownload] 子任务入队失败",
  86. zap.String("id", mainTaskId),
  87. zap.Int("index", i),
  88. zap.String("err", err.Error()),
  89. )
  90. }
  91. }
  92. }
  93. // ==================== 阶段2:同步处理其他数据任务 ====================
  94. if err := executeSyncTasks(ctx, payload, periods); err != nil {
  95. return nil, err
  96. }
  97. // ==================== 阶段3:等待子任务完成 ====================
  98. if totalSubTasks > 0 {
  99. failReasons, err := waitForCallFileTasks(ctx, mainTaskId)
  100. if err != nil {
  101. return nil, err
  102. }
  103. return failReasons, nil
  104. }
  105. return nil, nil
  106. }
  107. // callFilePayload 是传递给子任务的 payload 结构
  108. type callFilePayload struct {
  109. MainTaskId string `json:"mainTaskId"`
  110. TaskInfo domain.BatchDownloadFileTaskInfo `json:"taskInfo"`
  111. }
  112. // buildCallFileTasks 构建所有需要异步下载的文件任务列表
  113. func buildCallFileTasks(payload domain.TaskBatchDownload, periods []timePeriod, startTime, endTime time.Time) []callFilePayload {
  114. mainTaskId := payload.Id.Hex()
  115. var tasks []callFilePayload
  116. for _, sn := range payload.Sn {
  117. // EmsLog 任务
  118. for _, period := range periods {
  119. dateStr := period.Start.Format(time.DateOnly)
  120. for _, logType := range payload.Options.EmsLogs {
  121. tasks = append(tasks, callFilePayload{
  122. MainTaskId: mainTaskId,
  123. TaskInfo: domain.BatchDownloadFileTaskInfo{
  124. DevType: -1,
  125. Sn: sn,
  126. Date: dateStr,
  127. FileType: jobs.FileTypeEmsLog,
  128. FileSupType: logType,
  129. },
  130. })
  131. }
  132. }
  133. // FaultRecord 任务
  134. if payload.Options.FaultRecord {
  135. frTasks := buildFaultRecordTasks(mainTaskId, sn, startTime, endTime)
  136. tasks = append(tasks, frTasks...)
  137. }
  138. }
  139. return tasks
  140. }
  141. // buildFaultRecordTasks 为单个 SN 构建故障录波下载任务
  142. func buildFaultRecordTasks(mainTaskId string, sn string, startTime, endTime time.Time) []callFilePayload {
  143. var tasks []callFilePayload
  144. // 查询 aGate 可召唤列表
  145. aGateTasks := buildFaultRecordTasksForDev(mainTaskId, 1, sn, startTime, endTime)
  146. tasks = append(tasks, aGateTasks...)
  147. // 查询 aPower 子设备
  148. siteBaseInfoList, err := repositories.QuerySiteBaseInfoList(dto.SiteBaseInfoPara{
  149. SiteList: []string{sn},
  150. PageSize: 10,
  151. PageNum: 1,
  152. })
  153. if err != nil {
  154. zap.L().Error("[BatchDownload] 查询站点基本信息失败", zap.String("sn", sn), zap.String("err", err.Error()))
  155. return tasks
  156. }
  157. if siteBaseInfoList != nil && len(*siteBaseInfoList.Result) > 0 {
  158. aPowerSnList := strings.Split((*siteBaseInfoList.Result)[0].FhpSn, ",")
  159. for _, aPowerSn := range aPowerSnList {
  160. aPowerSn = strings.TrimSpace(aPowerSn)
  161. if aPowerSn == "" {
  162. continue
  163. }
  164. aPowerTasks := buildFaultRecordTasksForDev(mainTaskId, 0, aPowerSn, startTime, endTime)
  165. tasks = append(tasks, aPowerTasks...)
  166. }
  167. }
  168. return tasks
  169. }
  170. // buildFaultRecordTasksForDev 为指定设备构建故障录波下载任务
  171. func buildFaultRecordTasksForDev(mainTaskId string, devType int, sn string, startTime, endTime time.Time) []callFilePayload {
  172. callInfoList, err := repositories.QueryFaultRecordCallableList(devType, sn)
  173. if err != nil {
  174. zap.L().Error("[BatchDownload] 查询故障录波召唤选项失败", zap.String("sn", sn), zap.String("err", err.Error()))
  175. return nil
  176. }
  177. if len(callInfoList) == 0 {
  178. return nil
  179. }
  180. // 筛选日期范围内的记录
  181. var indexList string
  182. for _, info := range callInfoList {
  183. failureTime, err := parseFaultRecordTime(info.FailureTime)
  184. if err != nil {
  185. continue
  186. }
  187. inRange := (failureTime.Equal(startTime) || failureTime.After(startTime)) &&
  188. (failureTime.Equal(endTime) || failureTime.Before(endTime))
  189. if inRange {
  190. if indexList == "" {
  191. indexList = strconv.Itoa(info.Idx)
  192. } else {
  193. indexList = indexList + "," + strconv.Itoa(info.Idx)
  194. }
  195. }
  196. }
  197. // 没有符合日期范围的记录,不创建任务
  198. if indexList == "" {
  199. return nil
  200. }
  201. return []callFilePayload{{
  202. MainTaskId: mainTaskId,
  203. TaskInfo: domain.BatchDownloadFileTaskInfo{
  204. DevType: devType,
  205. Sn: sn,
  206. Date: startTime.Format(time.DateTime) + "," + endTime.Format(time.DateTime),
  207. FileType: jobs.FileTypeFaultRecord,
  208. FileSupType: indexList,
  209. },
  210. }}
  211. }
  212. // executeSyncTasks 同步执行非文件下载类任务(PowerFlow、MqttLogs、EventAlarm 等)
  213. func executeSyncTasks(ctx context.Context, payload domain.TaskBatchDownload, periods []timePeriod) error {
  214. for _, sn := range payload.Sn {
  215. // PowerFlow
  216. if payload.Options.PowerFlow {
  217. for _, period := range periods {
  218. dateStr := period.Start.Format(time.DateOnly)
  219. res, err := repositories.QuerySitePowerFlow(sn, period.Start)
  220. if err != nil {
  221. zap.L().Error("[BatchDownload] 查询功率流数据失败", zap.String("sn", sn), zap.String("date", dateStr), zap.String("err", err.Error()))
  222. return err
  223. }
  224. objName, err := jobs.SaveBatchDownloadPowerFlow(sn, dateStr, *res.Result)
  225. if err != nil {
  226. return err
  227. }
  228. if err := repositories.InsertBatchDownloadPathPowerFlow(payload.Id, sn, dateStr, objName); err != nil {
  229. return err
  230. }
  231. }
  232. }
  233. // MqttLogs
  234. for _, logType := range payload.Options.MqttLogs {
  235. for _, period := range periods {
  236. logList, err := repositories.QueryMqttLogListOnTimeRange(sn, domain.MqttCmdType(logType), period.Start, period.End)
  237. if err != nil {
  238. return err
  239. }
  240. dateStr := period.Start.Format(time.DateOnly)
  241. objName, err := jobs.SaveBatchDownloadMqttLog(sn, logType, dateStr, logList)
  242. if err != nil {
  243. return err
  244. }
  245. if err := repositories.InsertBatchDownloadPathMqttLogs(payload.Id, sn, logType, dateStr, objName); err != nil {
  246. return err
  247. }
  248. }
  249. }
  250. // EventAlarm
  251. if payload.Options.EventAlarm {
  252. for _, period := range periods {
  253. eventAlarmList, err := repositories.QueryEventOrAlarmListOnTimeRange(sn, period.Start, period.End, repositories.EventAndAlarm)
  254. if err != nil {
  255. return err
  256. }
  257. dateStr := period.Start.Format(time.DateOnly)
  258. objName, err := jobs.SaveBatchDownloadEventAlarm(sn, dateStr, eventAlarmList)
  259. if err != nil {
  260. return err
  261. }
  262. if err := repositories.InsertBatchDownloadPathEventAlarm(payload.Id, sn, dateStr, objName); err != nil {
  263. return err
  264. }
  265. }
  266. }
  267. // AlarmSnapshot
  268. if payload.Options.AlarmSnapshot {
  269. for _, period := range periods {
  270. dateStr := period.Start.Format(time.DateOnly)
  271. alarmSnapshotList, err := jobs.GetAlarmSnapshotData(ctx, sn, dateStr)
  272. if err != nil {
  273. return err
  274. }
  275. objName, err := jobs.SaveBatchDownloadAlarmSnapshot(sn, dateStr, alarmSnapshotList)
  276. if err != nil {
  277. return err
  278. }
  279. if err := repositories.InsertBatchDownloadPathAlarmSnapshot(payload.Id, sn, dateStr, objName); err != nil {
  280. return err
  281. }
  282. }
  283. }
  284. // UpdateLog
  285. if payload.Options.UpdateLog {
  286. if err := executeSyncUpdateLog(sn, payload); err != nil {
  287. return err
  288. }
  289. }
  290. }
  291. return nil
  292. }
  293. // waitForCallFileTasks 等待所有子任务完成,收集失败原因
  294. func waitForCallFileTasks(ctx context.Context, mainTaskId string) ([]string, error) {
  295. counterKey := callFileCounterPrefix + mainTaskId
  296. failuresKey := callFileFailuresPrefix + mainTaskId
  297. ticker := time.NewTicker(30 * time.Second)
  298. defer ticker.Stop()
  299. timeout := time.After(90 * time.Minute) // 90 分钟总超时
  300. for {
  301. select {
  302. case <-ctx.Done():
  303. return nil, ctx.Err()
  304. case <-timeout:
  305. return nil, fmt.Errorf("等待子任务完成超时(90分钟)")
  306. case <-ticker.C:
  307. val, err := cache.Get(ctx, counterKey)
  308. if err != nil {
  309. // key 不存在或读取失败
  310. if err.Error() == "redis: nil" {
  311. // key 不存在,所有子任务已完成
  312. val = "0"
  313. } else {
  314. zap.L().Error("[BatchDownload] 读取计数器失败", zap.String("key", counterKey), zap.String("err", err.Error()))
  315. continue
  316. }
  317. }
  318. remaining, _ := strconv.Atoi(val)
  319. if remaining <= 0 {
  320. // 收集失败原因
  321. failures, err := cache.LRange(ctx, failuresKey, 0, -1)
  322. if err != nil {
  323. return nil, fmt.Errorf("读取失败原因列表失败: %w", err)
  324. }
  325. // 清理
  326. cache.Delete(ctx, counterKey, failuresKey)
  327. return failures, nil
  328. }
  329. }
  330. }
  331. }
  332. // ==================== 辅助函数 ====================
  333. type timePeriod struct {
  334. Start time.Time
  335. End time.Time
  336. }
  337. func splitTimePeriods(startTime, endTime time.Time) []timePeriod {
  338. var periods []timePeriod
  339. curr := startTime
  340. for curr.Before(endTime) {
  341. dayEnd := time.Date(curr.Year(), curr.Month(), curr.Day(), 23, 59, 59, 0, curr.Location())
  342. if dayEnd.After(endTime) {
  343. dayEnd = endTime
  344. }
  345. periods = append(periods, timePeriod{Start: curr, End: dayEnd})
  346. curr = time.Date(curr.Year(), curr.Month(), curr.Day()+1, 0, 0, 0, 0, curr.Location())
  347. }
  348. return periods
  349. }
  350. func parseFaultRecordTime(failureTime string) (time.Time, error) {
  351. parts := strings.Split(failureTime, " ")
  352. if len(parts) < 2 {
  353. return time.Time{}, fmt.Errorf("invalid failureTime format: %s", failureTime)
  354. }
  355. date := parts[0]
  356. timeStr := parts[1]
  357. d := strings.Split(date, "-")
  358. t := strings.Split(timeStr, ":")
  359. if len(d) < 3 || len(t) < 3 {
  360. return time.Time{}, fmt.Errorf("invalid failureTime format: %s", failureTime)
  361. }
  362. standardTime := fmt.Sprintf("%04s-%02s-%02s %02s:%02s:%02s", d[0], d[1], d[2], t[0], t[1], t[2])
  363. return time.Parse(time.DateTime, standardTime)
  364. }
  365. // executeSyncUpdateLog 同步执行升级日志任务(从原代码提取)
  366. func executeSyncUpdateLog(sn string, payload domain.TaskBatchDownload) error {
  367. updateLogList := []domain.BatchDownloadSystemUpdateLogInfo{}
  368. systemUpdateLogList, err := repositories.QuerySystemUpdateLog(sn)
  369. if err != nil {
  370. return err
  371. }
  372. for _, logList := range systemUpdateLogList {
  373. var updateLogInfo domain.BatchDownloadSystemUpdateLogInfo
  374. updateLogInfo.OrderNo = logList.OrderNo
  375. updateLogInfo.GateSn = sn
  376. updateLogInfo.FileName = logList.Name
  377. upgradeTime, err := time.ParseInLocation(time.DateTime, logList.UpgradeTime, time.UTC)
  378. if err != nil {
  379. zap.L().Warn("[BatchDownload] 升级时间解析失败", zap.String("time", logList.UpgradeTime))
  380. continue
  381. }
  382. beijing, _ := time.LoadLocation("Asia/Shanghai")
  383. updateLogInfo.UpdateTime = upgradeTime.In(beijing).Format(time.DateTime)
  384. if strings.HasPrefix(logList.OrderNo, "CH") {
  385. updateLogInfo.TriggerType = "Auto"
  386. } else if strings.HasPrefix(logList.OrderNo, "DH") {
  387. updateLogInfo.TriggerType = "Manual"
  388. } else if strings.HasPrefix(logList.OrderNo, "SH") {
  389. updateLogInfo.TriggerType = "Span Auto"
  390. } else {
  391. updateLogInfo.TriggerType = "unknown"
  392. }
  393. switch logList.Result {
  394. case 1:
  395. updateLogInfo.Result = "Updating"
  396. case 2:
  397. updateLogInfo.Result = "Success"
  398. case 3:
  399. updateLogInfo.Result = "Fault"
  400. default:
  401. updateLogInfo.Result = fmt.Sprintf("unknown(%d)", logList.Result)
  402. }
  403. systemUpdateLogListDetail, err := repositories.QuerySystemUpdateLogDetail(logList.Id)
  404. if err != nil {
  405. return err
  406. }
  407. for _, logDetail := range systemUpdateLogListDetail {
  408. if logDetail.GatewayId == sn {
  409. for _, ibgVer := range logDetail.IbgVersionNew {
  410. if ibgVer.Children != nil {
  411. for _, childVerInfo := range ibgVer.Children {
  412. updateLogInfo.GateVerList = append(updateLogInfo.GateVerList, domain.BatchDownloadUpdateLogVersionInfo{
  413. Name: childVerInfo.Type, OldVer: childVerInfo.OldVersion, NewVer: childVerInfo.Version,
  414. })
  415. }
  416. } else {
  417. updateLogInfo.GateVerList = append(updateLogInfo.GateVerList, domain.BatchDownloadUpdateLogVersionInfo{
  418. Name: ibgVer.Type, OldVer: ibgVer.OldVersion, NewVer: ibgVer.Version,
  419. })
  420. }
  421. }
  422. for _, subInfo := range logDetail.SonNewDevices {
  423. var subDevInfo domain.BatchDownloadUpdateLogSubDevInfo
  424. subDevInfo.DevSn = subInfo.ApowerSn
  425. for _, firstItem := range subInfo.FirstList {
  426. for _, secondItem := range firstItem.Secondlist {
  427. for _, thirdItem := range secondItem.Thirdlist {
  428. subDevInfo.DevVerList = append(subDevInfo.DevVerList, domain.BatchDownloadUpdateLogVersionInfo{
  429. Name: thirdItem.SubType, OldVer: thirdItem.OldVersion, NewVer: thirdItem.Version,
  430. })
  431. }
  432. }
  433. }
  434. updateLogInfo.SubDevList = append(updateLogInfo.SubDevList, subDevInfo)
  435. }
  436. break
  437. }
  438. }
  439. updateLogList = append(updateLogList, updateLogInfo)
  440. }
  441. // aHub 升级日志
  442. aHubUpdateLogList, err := repositories.QueryAHubUpdateLog(sn)
  443. if err != nil {
  444. return err
  445. }
  446. var aHubUpdateLogData []domain.BatchDownloadAHubUpdateLogInfo
  447. for _, aHubVerInfo := range aHubUpdateLogList {
  448. var info domain.BatchDownloadAHubUpdateLogInfo
  449. info.OrderNo = aHubVerInfo.OrderNo
  450. info.GateSn = sn
  451. info.AHubSn = aHubVerInfo.AHubSn
  452. if strings.HasPrefix(aHubVerInfo.OrderNo, "CH") {
  453. info.TriggerType = "Auto"
  454. } else if strings.HasPrefix(aHubVerInfo.OrderNo, "DH") {
  455. info.TriggerType = "Manual"
  456. } else if strings.HasPrefix(aHubVerInfo.OrderNo, "SH") {
  457. info.TriggerType = "Span Auto"
  458. } else {
  459. info.TriggerType = "unknown"
  460. }
  461. switch aHubVerInfo.UpgradeResult {
  462. case 0:
  463. info.Result = "Success"
  464. case -1:
  465. info.Result = "Fault"
  466. default:
  467. info.Result = fmt.Sprintf("unknown(%d)", aHubVerInfo.UpgradeResult)
  468. }
  469. for _, detailInfo := range aHubVerInfo.DetailList {
  470. info.DetailList = append(info.DetailList, domain.BatchDownloadUpdateLogAHubVersionInfo{
  471. FileName: detailInfo.UpgradeFileName,
  472. FileOrder: detailInfo.UpgradeFileOrder,
  473. Result: detailInfo.UpgradeResult,
  474. ResultDesc: detailInfo.UpgradeResultDesc,
  475. OldVer: detailInfo.OldVersion,
  476. NewVer: detailInfo.NewVersion,
  477. })
  478. }
  479. aHubUpdateLogData = append(aHubUpdateLogData, info)
  480. }
  481. objName, err := jobs.SaveBatchDownloadUpdateLog(sn, updateLogList, aHubUpdateLogData)
  482. if err != nil {
  483. return err
  484. }
  485. return repositories.InsertBatchDownloadPathUpdateLog(payload.Id, sn, objName)
  486. }