fwhTaskBatchDownload.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  1. package fwhQueue
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "fwh-toolbox-server/domain"
  8. "fwh-toolbox-server/dto"
  9. "fwh-toolbox-server/jobs"
  10. "fwh-toolbox-server/repositories"
  11. "strconv"
  12. "strings"
  13. "time"
  14. "github.com/hibiken/asynq"
  15. "go.mongodb.org/mongo-driver/bson/primitive"
  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. // 注意:任务终态(complete/failed)由 item 聚合(maybeAggregate)异步派生写入,
  32. // 这里只处理"任务根本无法开始"的致命错误 → ProgressError。
  33. if err := executeBatchDownload(ctx, payload); 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. return nil // 返回 nil 让 Asynq 标记任务为 completed,不触发重试
  39. }
  40. // executeBatchDownload 执行批量下载逻辑。
  41. // 返回 error 仅表示"任务根本无法开始"的致命错误(→ ProgressError);
  42. // 进入执行阶段后的所有成败都以 item 形式记录,终态由聚合派生。
  43. func executeBatchDownload(ctx context.Context, payload domain.TaskBatchDownload) error {
  44. mainTaskId := payload.Id.Hex()
  45. // 解析时间范围(致命错误:早退)
  46. startTime, err := time.Parse(time.DateOnly, payload.TimePeriod[0])
  47. if err != nil {
  48. return fmt.Errorf("startTime 格式错误: %w", err)
  49. }
  50. endTime, err := time.Parse(time.DateOnly, payload.TimePeriod[1])
  51. if err != nil {
  52. return fmt.Errorf("endTime 格式错误: %w", err)
  53. }
  54. endTime = endTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
  55. if startTime.After(endTime) {
  56. return errors.New("startTime > endTime")
  57. }
  58. // 按天拆分时间段
  59. periods := splitTimePeriods(startTime, endTime)
  60. // ==================== 阶段0:规划所有 item ====================
  61. // planItems 枚举所有同步 + 异步工作单元为 pending;构建期查询失败/无数据直接写 failed/skipped。
  62. items, asyncTasks := planItems(payload, periods, startTime, endTime)
  63. if err := repositories.InitBatchDownloadItems(payload.Id, items); err != nil {
  64. return fmt.Errorf("初始化 items 失败: %w", err)
  65. }
  66. // 边界:若所有 item 在规划期就已是终态(无异步子任务、无同步任务),主动触发一次聚合。
  67. doneInit := 0
  68. for _, it := range items {
  69. if it.Status != domain.ItemPending {
  70. doneInit++
  71. }
  72. }
  73. if doneInit == len(items) {
  74. maybeAggregate(payload.Id, doneInit, len(items))
  75. return nil
  76. }
  77. // ==================== 阶段1:入队异步文件下载子任务 ====================
  78. for i, cfTask := range asyncTasks {
  79. taskPayload, _ := json.Marshal(cfTask)
  80. subTaskID := fmt.Sprintf("callfile-%s-%d", mainTaskId, i)
  81. t := asynq.NewTask(TaskTypeCallFile, taskPayload,
  82. asynq.MaxRetry(3),
  83. asynq.Timeout(35*time.Minute),
  84. asynq.Queue(QueueDownload),
  85. asynq.TaskID(subTaskID),
  86. )
  87. if _, err := TaskClient.Enqueue(t); err != nil {
  88. // 入队失败:直接把该 item 记为 failed(build),doneCount 照常推进,避免永久 pending 挂起。
  89. zap.L().Error("[BatchDownload] 子任务入队失败",
  90. zap.String("id", mainTaskId),
  91. zap.Int("index", i),
  92. zap.String("key", cfTask.ItemKey),
  93. zap.String("err", err.Error()),
  94. )
  95. finishItem(payload.Id, cfTask.ItemKey, domain.ItemFailed, domain.StageBuild, "子任务入队失败: "+err.Error())
  96. }
  97. }
  98. // ==================== 阶段2:同步处理其他数据任务 ====================
  99. // 同步任务在主任务内联执行,失败记 item failed 而非中止;每个 item 终态都会驱动 maybeAggregate。
  100. executeSyncTasks(ctx, payload, periods)
  101. return nil
  102. }
  103. // callFilePayload 是传递给子任务的 payload 结构
  104. type callFilePayload struct {
  105. MainTaskId string `json:"mainTaskId"`
  106. ItemKey string `json:"itemKey"` // 对应 BatchDownloadItem.Key,子任务据此上报结果
  107. TaskInfo domain.BatchDownloadFileTaskInfo `json:"taskInfo"`
  108. }
  109. // 同步任务的 item fileType 标识(与 executeSyncTasks 内的 key 生成保持一致)
  110. const (
  111. fileTypePowerFlow = "PowerFlow"
  112. fileTypeMqttLog = "MqttLog"
  113. fileTypeEventAlarm = "EventAlarm"
  114. fileTypeAlarmSnapshot = "AlarmSnapshot"
  115. fileTypeUpdateLog = "UpdateLog"
  116. )
  117. // planItems 枚举本次批量下载的所有工作单元:
  118. // - 返回全部 items(pending / 构建期 failed / skipped)
  119. // - 返回需要异步入队的文件下载子任务列表(EmsLog / FaultRecord,payload 带 ItemKey)
  120. //
  121. // 同步类任务(PowerFlow/MqttLog/EventAlarm/AlarmSnapshot/UpdateLog)只产出 pending item,
  122. // 由 executeSyncTasks 内联执行并上报。
  123. func planItems(payload domain.TaskBatchDownload, periods []timePeriod, startTime, endTime time.Time) ([]domain.BatchDownloadItem, []callFilePayload) {
  124. mainTaskId := payload.Id.Hex()
  125. var items []domain.BatchDownloadItem
  126. var asyncTasks []callFilePayload
  127. newItem := func(fileType, sn string, devType int, date, subType string, status domain.ItemStatus, stage, reason string) domain.BatchDownloadItem {
  128. return domain.BatchDownloadItem{
  129. Key: domain.BuildItemKey(fileType, sn, devType, date, subType),
  130. Sn: sn,
  131. DevType: devType,
  132. FileType: fileType,
  133. SubType: subType,
  134. Date: date,
  135. Status: status,
  136. Stage: stage,
  137. Reason: reason,
  138. }
  139. }
  140. for _, sn := range payload.Sn {
  141. // ---------- EmsLog(异步) ----------
  142. for _, period := range periods {
  143. dateStr := period.Start.Format(time.DateOnly)
  144. for _, logType := range payload.Options.EmsLogs {
  145. key := domain.BuildItemKey(jobs.FileTypeEmsLog, sn, -1, dateStr, logType)
  146. items = append(items, newItem(jobs.FileTypeEmsLog, sn, -1, dateStr, logType, domain.ItemPending, "", ""))
  147. asyncTasks = append(asyncTasks, callFilePayload{
  148. MainTaskId: mainTaskId,
  149. ItemKey: key,
  150. TaskInfo: domain.BatchDownloadFileTaskInfo{
  151. DevType: -1,
  152. Sn: sn,
  153. Date: dateStr,
  154. FileType: jobs.FileTypeEmsLog,
  155. FileSupType: logType,
  156. },
  157. })
  158. }
  159. }
  160. // ---------- FaultRecord(异步) ----------
  161. if payload.Options.FaultRecord {
  162. frItems, frTasks := planFaultRecordItems(mainTaskId, sn, startTime, endTime, newItem)
  163. items = append(items, frItems...)
  164. asyncTasks = append(asyncTasks, frTasks...)
  165. }
  166. // ---------- 同步类 ----------
  167. if payload.Options.PowerFlow {
  168. for _, period := range periods {
  169. dateStr := period.Start.Format(time.DateOnly)
  170. items = append(items, newItem(fileTypePowerFlow, sn, -1, dateStr, "", domain.ItemPending, "", ""))
  171. }
  172. }
  173. for _, logType := range payload.Options.MqttLogs {
  174. for _, period := range periods {
  175. dateStr := period.Start.Format(time.DateOnly)
  176. items = append(items, newItem(fileTypeMqttLog, sn, -1, dateStr, logType, domain.ItemPending, "", ""))
  177. }
  178. }
  179. if payload.Options.EventAlarm {
  180. for _, period := range periods {
  181. dateStr := period.Start.Format(time.DateOnly)
  182. items = append(items, newItem(fileTypeEventAlarm, sn, -1, dateStr, "", domain.ItemPending, "", ""))
  183. }
  184. }
  185. if payload.Options.AlarmSnapshot {
  186. for _, period := range periods {
  187. dateStr := period.Start.Format(time.DateOnly)
  188. items = append(items, newItem(fileTypeAlarmSnapshot, sn, -1, dateStr, "", domain.ItemPending, "", ""))
  189. }
  190. }
  191. if payload.Options.UpdateLog {
  192. items = append(items, newItem(fileTypeUpdateLog, sn, -1, "", "", domain.ItemPending, "", ""))
  193. }
  194. }
  195. return items, asyncTasks
  196. }
  197. // planFaultRecordItems 为单个 gateway SN 规划故障录波 item(aGate + 其下 aPower)。
  198. // 构建期查询失败 → failed(build);无满足时间范围的数据 → skipped;有数据 → pending + 异步 payload。
  199. func planFaultRecordItems(mainTaskId string, sn string, startTime, endTime time.Time,
  200. newItem func(fileType, sn string, devType int, date, subType string, status domain.ItemStatus, stage, reason string) domain.BatchDownloadItem,
  201. ) ([]domain.BatchDownloadItem, []callFilePayload) {
  202. var items []domain.BatchDownloadItem
  203. var tasks []callFilePayload
  204. // aGate 可召唤列表
  205. frItems, frTasks := planFaultRecordItemsForDev(mainTaskId, 1, sn, startTime, endTime, newItem)
  206. items = append(items, frItems...)
  207. tasks = append(tasks, frTasks...)
  208. // aPower 子设备
  209. dateRange := startTime.Format(time.DateTime) + "," + endTime.Format(time.DateTime)
  210. siteBaseInfoList, err := repositories.QuerySiteBaseInfoList(dto.SiteBaseInfoPara{
  211. SiteList: []string{sn},
  212. PageSize: 10,
  213. PageNum: 1,
  214. })
  215. if err != nil {
  216. zap.L().Error("[BatchDownload] 查询站点基本信息失败", zap.String("sn", sn), zap.String("err", err.Error()))
  217. // 站点信息查询失败 → 无法枚举 aPower,记一条 failed(build) 归属该 SN
  218. items = append(items, newItem(jobs.FileTypeFaultRecord, sn, 0, dateRange, "", domain.ItemFailed, domain.StageBuild, "查询站点基本信息失败: "+err.Error()))
  219. return items, tasks
  220. }
  221. if siteBaseInfoList != nil && siteBaseInfoList.Result != nil && len(*siteBaseInfoList.Result) > 0 {
  222. aPowerSnList := strings.Split((*siteBaseInfoList.Result)[0].FhpSn, ",")
  223. for _, aPowerSn := range aPowerSnList {
  224. aPowerSn = strings.TrimSpace(aPowerSn)
  225. if aPowerSn == "" {
  226. continue
  227. }
  228. apItems, apTasks := planFaultRecordItemsForDev(mainTaskId, 0, aPowerSn, startTime, endTime, newItem)
  229. items = append(items, apItems...)
  230. tasks = append(tasks, apTasks...)
  231. }
  232. }
  233. return items, tasks
  234. }
  235. // planFaultRecordItemsForDev 为指定设备规划故障录波 item。
  236. func planFaultRecordItemsForDev(mainTaskId string, devType int, sn string, startTime, endTime time.Time,
  237. newItem func(fileType, sn string, devType int, date, subType string, status domain.ItemStatus, stage, reason string) domain.BatchDownloadItem,
  238. ) ([]domain.BatchDownloadItem, []callFilePayload) {
  239. dateRange := startTime.Format(time.DateTime) + "," + endTime.Format(time.DateTime)
  240. callInfoList, err := repositories.QueryFaultRecordCallableList(devType, sn)
  241. if err != nil {
  242. zap.L().Error("[BatchDownload] 查询故障录波召唤选项失败", zap.Int("devType", devType), zap.String("sn", sn), zap.String("err", err.Error()))
  243. // 构建期查询失败 → failed(build),错误得以上报
  244. item := newItem(jobs.FileTypeFaultRecord, sn, devType, dateRange, "", domain.ItemFailed, domain.StageBuild, "查询故障录波召唤选项失败: "+err.Error())
  245. return []domain.BatchDownloadItem{item}, nil
  246. }
  247. // 筛选日期范围内的记录
  248. var indexList string
  249. for _, info := range callInfoList {
  250. failureTime, err := parseFaultRecordTime(info.FailureTime)
  251. if err != nil {
  252. continue
  253. }
  254. inRange := (failureTime.Equal(startTime) || failureTime.After(startTime)) &&
  255. (failureTime.Equal(endTime) || failureTime.Before(endTime))
  256. if inRange {
  257. if indexList == "" {
  258. indexList = strconv.Itoa(info.Idx)
  259. } else {
  260. indexList = indexList + "," + strconv.Itoa(info.Idx)
  261. }
  262. }
  263. }
  264. // 无符合日期范围的记录 → skipped(非错误)
  265. if indexList == "" {
  266. item := newItem(jobs.FileTypeFaultRecord, sn, devType, dateRange, "", domain.ItemSkipped, "", "无满足时间范围的录波数据")
  267. return []domain.BatchDownloadItem{item}, nil
  268. }
  269. // 有数据 → pending + 异步 payload
  270. key := domain.BuildItemKey(jobs.FileTypeFaultRecord, sn, devType, dateRange, indexList)
  271. item := newItem(jobs.FileTypeFaultRecord, sn, devType, dateRange, indexList, domain.ItemPending, "", "")
  272. task := callFilePayload{
  273. MainTaskId: mainTaskId,
  274. ItemKey: key,
  275. TaskInfo: domain.BatchDownloadFileTaskInfo{
  276. DevType: devType,
  277. Sn: sn,
  278. Date: dateRange,
  279. FileType: jobs.FileTypeFaultRecord,
  280. FileSupType: indexList,
  281. },
  282. }
  283. return []domain.BatchDownloadItem{item}, []callFilePayload{task}
  284. }
  285. // executeSyncTasks 同步执行非文件下载类任务(PowerFlow、MqttLogs、EventAlarm 等)。
  286. // 每个工作单元成功/失败都通过 finishItem 上报,失败不中止整个任务。
  287. func executeSyncTasks(ctx context.Context, payload domain.TaskBatchDownload, periods []timePeriod) {
  288. for _, sn := range payload.Sn {
  289. // PowerFlow
  290. if payload.Options.PowerFlow {
  291. for _, period := range periods {
  292. dateStr := period.Start.Format(time.DateOnly)
  293. key := domain.BuildItemKey(fileTypePowerFlow, sn, -1, dateStr, "")
  294. if err := syncPowerFlow(payload.Id, sn, dateStr, period); err != nil {
  295. finishItem(payload.Id, key, domain.ItemFailed, err.stage, err.reason)
  296. } else {
  297. finishItem(payload.Id, key, domain.ItemSuccess, "", "")
  298. }
  299. }
  300. }
  301. // MqttLogs
  302. for _, logType := range payload.Options.MqttLogs {
  303. for _, period := range periods {
  304. dateStr := period.Start.Format(time.DateOnly)
  305. key := domain.BuildItemKey(fileTypeMqttLog, sn, -1, dateStr, logType)
  306. if err := syncMqttLog(payload.Id, sn, logType, dateStr, period); err != nil {
  307. finishItem(payload.Id, key, domain.ItemFailed, err.stage, err.reason)
  308. } else {
  309. finishItem(payload.Id, key, domain.ItemSuccess, "", "")
  310. }
  311. }
  312. }
  313. // EventAlarm
  314. if payload.Options.EventAlarm {
  315. for _, period := range periods {
  316. dateStr := period.Start.Format(time.DateOnly)
  317. key := domain.BuildItemKey(fileTypeEventAlarm, sn, -1, dateStr, "")
  318. if err := syncEventAlarm(payload.Id, sn, dateStr, period); err != nil {
  319. finishItem(payload.Id, key, domain.ItemFailed, err.stage, err.reason)
  320. } else {
  321. finishItem(payload.Id, key, domain.ItemSuccess, "", "")
  322. }
  323. }
  324. }
  325. // AlarmSnapshot
  326. if payload.Options.AlarmSnapshot {
  327. for _, period := range periods {
  328. dateStr := period.Start.Format(time.DateOnly)
  329. key := domain.BuildItemKey(fileTypeAlarmSnapshot, sn, -1, dateStr, "")
  330. if err := syncAlarmSnapshot(ctx, payload.Id, sn, dateStr); err != nil {
  331. finishItem(payload.Id, key, domain.ItemFailed, err.stage, err.reason)
  332. } else {
  333. finishItem(payload.Id, key, domain.ItemSuccess, "", "")
  334. }
  335. }
  336. }
  337. // UpdateLog
  338. if payload.Options.UpdateLog {
  339. key := domain.BuildItemKey(fileTypeUpdateLog, sn, -1, "", "")
  340. if err := executeSyncUpdateLog(sn, payload); err != nil {
  341. finishItem(payload.Id, key, domain.ItemFailed, domain.StageQuery, err.Error())
  342. } else {
  343. finishItem(payload.Id, key, domain.ItemSuccess, "", "")
  344. }
  345. }
  346. }
  347. }
  348. // syncItemError 携带失败阶段与原因,便于 finishItem 记录 stage
  349. type syncItemError struct {
  350. stage string
  351. reason string
  352. }
  353. func (e *syncItemError) Error() string { return e.reason }
  354. func newSyncErr(stage string, err error) *syncItemError {
  355. return &syncItemError{stage: stage, reason: err.Error()}
  356. }
  357. // syncPowerFlow 处理单个 PowerFlow 工作单元
  358. func syncPowerFlow(id primitive.ObjectID, sn, dateStr string, period timePeriod) *syncItemError {
  359. res, err := repositories.QuerySitePowerFlow(sn, period.Start)
  360. if err != nil {
  361. return newSyncErr(domain.StageQuery, err)
  362. }
  363. objName, err := jobs.SaveBatchDownloadPowerFlow(sn, dateStr, *res.Result)
  364. if err != nil {
  365. return newSyncErr(domain.StageSave, err)
  366. }
  367. if err := repositories.InsertBatchDownloadPathPowerFlow(id, sn, dateStr, objName); err != nil {
  368. return newSyncErr(domain.StageSave, err)
  369. }
  370. return nil
  371. }
  372. // syncMqttLog 处理单个 MqttLog 工作单元
  373. func syncMqttLog(id primitive.ObjectID, sn, logType, dateStr string, period timePeriod) *syncItemError {
  374. logList, err := repositories.QueryMqttLogListOnTimeRange(sn, domain.MqttCmdType(logType), period.Start, period.End)
  375. if err != nil {
  376. return newSyncErr(domain.StageQuery, err)
  377. }
  378. objName, err := jobs.SaveBatchDownloadMqttLog(sn, logType, dateStr, logList)
  379. if err != nil {
  380. return newSyncErr(domain.StageSave, err)
  381. }
  382. if err := repositories.InsertBatchDownloadPathMqttLogs(id, sn, logType, dateStr, objName); err != nil {
  383. return newSyncErr(domain.StageSave, err)
  384. }
  385. return nil
  386. }
  387. // syncEventAlarm 处理单个 EventAlarm 工作单元
  388. func syncEventAlarm(id primitive.ObjectID, sn, dateStr string, period timePeriod) *syncItemError {
  389. eventAlarmList, err := repositories.QueryEventOrAlarmListOnTimeRange(sn, period.Start, period.End, repositories.EventAndAlarm)
  390. if err != nil {
  391. return newSyncErr(domain.StageQuery, err)
  392. }
  393. objName, err := jobs.SaveBatchDownloadEventAlarm(sn, dateStr, eventAlarmList)
  394. if err != nil {
  395. return newSyncErr(domain.StageSave, err)
  396. }
  397. if err := repositories.InsertBatchDownloadPathEventAlarm(id, sn, dateStr, objName); err != nil {
  398. return newSyncErr(domain.StageSave, err)
  399. }
  400. return nil
  401. }
  402. // syncAlarmSnapshot 处理单个 AlarmSnapshot 工作单元
  403. func syncAlarmSnapshot(ctx context.Context, id primitive.ObjectID, sn, dateStr string) *syncItemError {
  404. alarmSnapshotList, err := jobs.GetAlarmSnapshotData(ctx, sn, dateStr)
  405. if err != nil {
  406. return newSyncErr(domain.StageQuery, err)
  407. }
  408. objName, err := jobs.SaveBatchDownloadAlarmSnapshot(sn, dateStr, alarmSnapshotList)
  409. if err != nil {
  410. return newSyncErr(domain.StageSave, err)
  411. }
  412. if err := repositories.InsertBatchDownloadPathAlarmSnapshot(id, sn, dateStr, objName); err != nil {
  413. return newSyncErr(domain.StageSave, err)
  414. }
  415. return nil
  416. }
  417. // ==================== 辅助函数 ====================
  418. type timePeriod struct {
  419. Start time.Time
  420. End time.Time
  421. }
  422. func splitTimePeriods(startTime, endTime time.Time) []timePeriod {
  423. var periods []timePeriod
  424. curr := startTime
  425. for curr.Before(endTime) {
  426. dayEnd := time.Date(curr.Year(), curr.Month(), curr.Day(), 23, 59, 59, 0, curr.Location())
  427. if dayEnd.After(endTime) {
  428. dayEnd = endTime
  429. }
  430. periods = append(periods, timePeriod{Start: curr, End: dayEnd})
  431. curr = time.Date(curr.Year(), curr.Month(), curr.Day()+1, 0, 0, 0, 0, curr.Location())
  432. }
  433. return periods
  434. }
  435. func parseFaultRecordTime(failureTime string) (time.Time, error) {
  436. parts := strings.Split(failureTime, " ")
  437. if len(parts) < 2 {
  438. return time.Time{}, fmt.Errorf("invalid failureTime format: %s", failureTime)
  439. }
  440. date := parts[0]
  441. timeStr := parts[1]
  442. d := strings.Split(date, "-")
  443. t := strings.Split(timeStr, ":")
  444. if len(d) < 3 || len(t) < 3 {
  445. return time.Time{}, fmt.Errorf("invalid failureTime format: %s", failureTime)
  446. }
  447. standardTime := fmt.Sprintf("%04s-%02s-%02s %02s:%02s:%02s", d[0], d[1], d[2], t[0], t[1], t[2])
  448. return time.Parse(time.DateTime, standardTime)
  449. }
  450. // executeSyncUpdateLog 同步执行升级日志任务(从原代码提取)
  451. func executeSyncUpdateLog(sn string, payload domain.TaskBatchDownload) error {
  452. updateLogList := []domain.BatchDownloadSystemUpdateLogInfo{}
  453. systemUpdateLogList, err := repositories.QuerySystemUpdateLog(sn)
  454. if err != nil {
  455. return err
  456. }
  457. for _, logList := range systemUpdateLogList {
  458. var updateLogInfo domain.BatchDownloadSystemUpdateLogInfo
  459. updateLogInfo.OrderNo = logList.OrderNo
  460. updateLogInfo.GateSn = sn
  461. updateLogInfo.FileName = logList.Name
  462. upgradeTime, err := time.ParseInLocation(time.DateTime, logList.UpgradeTime, time.UTC)
  463. if err != nil {
  464. zap.L().Warn("[BatchDownload] 升级时间解析失败", zap.String("time", logList.UpgradeTime))
  465. continue
  466. }
  467. beijing, _ := time.LoadLocation("Asia/Shanghai")
  468. updateLogInfo.UpdateTime = upgradeTime.In(beijing).Format(time.DateTime)
  469. if strings.HasPrefix(logList.OrderNo, "CH") {
  470. updateLogInfo.TriggerType = "Auto"
  471. } else if strings.HasPrefix(logList.OrderNo, "DH") {
  472. updateLogInfo.TriggerType = "Manual"
  473. } else if strings.HasPrefix(logList.OrderNo, "SH") {
  474. updateLogInfo.TriggerType = "Span Auto"
  475. } else {
  476. updateLogInfo.TriggerType = "unknown"
  477. }
  478. switch logList.Result {
  479. case 1:
  480. updateLogInfo.Result = "Updating"
  481. case 2:
  482. updateLogInfo.Result = "Success"
  483. case 3:
  484. updateLogInfo.Result = "Fault"
  485. default:
  486. updateLogInfo.Result = fmt.Sprintf("unknown(%d)", logList.Result)
  487. }
  488. systemUpdateLogListDetail, err := repositories.QuerySystemUpdateLogDetail(logList.Id, sn)
  489. if err != nil {
  490. return err
  491. }
  492. for _, logDetail := range systemUpdateLogListDetail {
  493. if logDetail.GatewayId == sn {
  494. for _, ibgVer := range logDetail.IbgVersionNew {
  495. if ibgVer.Children != nil {
  496. for _, childVerInfo := range ibgVer.Children {
  497. updateLogInfo.GateVerList = append(updateLogInfo.GateVerList, domain.BatchDownloadUpdateLogVersionInfo{
  498. Name: childVerInfo.Type, OldVer: childVerInfo.OldVersion, NewVer: childVerInfo.Version,
  499. })
  500. }
  501. } else {
  502. updateLogInfo.GateVerList = append(updateLogInfo.GateVerList, domain.BatchDownloadUpdateLogVersionInfo{
  503. Name: ibgVer.Type, OldVer: ibgVer.OldVersion, NewVer: ibgVer.Version,
  504. })
  505. }
  506. }
  507. for _, subInfo := range logDetail.SonNewDevices {
  508. var subDevInfo domain.BatchDownloadUpdateLogSubDevInfo
  509. subDevInfo.DevSn = subInfo.ApowerSn
  510. for _, firstItem := range subInfo.FirstList {
  511. for _, secondItem := range firstItem.Secondlist {
  512. for _, thirdItem := range secondItem.Thirdlist {
  513. subDevInfo.DevVerList = append(subDevInfo.DevVerList, domain.BatchDownloadUpdateLogVersionInfo{
  514. Name: thirdItem.SubType, OldVer: thirdItem.OldVersion, NewVer: thirdItem.Version,
  515. })
  516. }
  517. }
  518. }
  519. updateLogInfo.SubDevList = append(updateLogInfo.SubDevList, subDevInfo)
  520. }
  521. break
  522. }
  523. }
  524. updateLogList = append(updateLogList, updateLogInfo)
  525. }
  526. // aHub 升级日志
  527. aHubUpdateLogList, err := repositories.QueryAHubUpdateLog(sn)
  528. if err != nil {
  529. return err
  530. }
  531. var aHubUpdateLogData []domain.BatchDownloadAHubUpdateLogInfo
  532. for _, aHubVerInfo := range aHubUpdateLogList {
  533. var info domain.BatchDownloadAHubUpdateLogInfo
  534. info.OrderNo = aHubVerInfo.OrderNo
  535. info.GateSn = sn
  536. info.AHubSn = aHubVerInfo.AHubSn
  537. if strings.HasPrefix(aHubVerInfo.OrderNo, "CH") {
  538. info.TriggerType = "Auto"
  539. } else if strings.HasPrefix(aHubVerInfo.OrderNo, "DH") {
  540. info.TriggerType = "Manual"
  541. } else if strings.HasPrefix(aHubVerInfo.OrderNo, "SH") {
  542. info.TriggerType = "Span Auto"
  543. } else {
  544. info.TriggerType = "unknown"
  545. }
  546. switch aHubVerInfo.UpgradeResult {
  547. case 0:
  548. info.Result = "Success"
  549. case -1:
  550. info.Result = "Fault"
  551. default:
  552. info.Result = fmt.Sprintf("unknown(%d)", aHubVerInfo.UpgradeResult)
  553. }
  554. for _, detailInfo := range aHubVerInfo.DetailList {
  555. info.DetailList = append(info.DetailList, domain.BatchDownloadUpdateLogAHubVersionInfo{
  556. FileName: detailInfo.UpgradeFileName,
  557. FileOrder: detailInfo.UpgradeFileOrder,
  558. Result: detailInfo.UpgradeResult,
  559. ResultDesc: detailInfo.UpgradeResultDesc,
  560. OldVer: detailInfo.OldVersion,
  561. NewVer: detailInfo.NewVersion,
  562. })
  563. }
  564. aHubUpdateLogData = append(aHubUpdateLogData, info)
  565. }
  566. objName, err := jobs.SaveBatchDownloadUpdateLog(sn, updateLogList, aHubUpdateLogData)
  567. if err != nil {
  568. return err
  569. }
  570. return repositories.InsertBatchDownloadPathUpdateLog(payload.Id, sn, objName)
  571. }
  572. // ==================== 聚合与完成判定(B2 + 策略C) ====================
  573. // deriveProgressAndSummary 从 items 聚合派生任务终态与摘要 failReasons(策略C:摘要,明细在 items)
  574. func deriveProgressAndSummary(items []domain.BatchDownloadItem) (domain.Progress, []string) {
  575. failed := 0
  576. var samples []string
  577. for _, it := range items {
  578. if it.Status == domain.ItemFailed {
  579. failed++
  580. if len(samples) < 5 { // 摘要仅保留前若干条样本
  581. samples = append(samples, fmt.Sprintf("[%s] sn=%s date=%s stage=%s: %s",
  582. it.FileType, it.Sn, it.Date, it.Stage, it.Reason))
  583. }
  584. }
  585. }
  586. if failed == 0 {
  587. return domain.ProgressComplete, []string{}
  588. }
  589. summary := append([]string{fmt.Sprintf("共 %d 项失败(详见明细 items)", failed)}, samples...)
  590. return domain.ProgressFailed, summary
  591. }
  592. // maybeAggregate 在 done==total(本任务全部 item 已终态)时聚合派生终态并写库。
  593. // 由 FinishBatchDownloadItem 的原子性保证只有一个调用会命中 done==total,聚合仅触发一次。
  594. // done/total 为 -1 表示幂等跳过(重复回调),不处理。
  595. func maybeAggregate(id primitive.ObjectID, done, total int) {
  596. if done < 0 || total < 0 {
  597. return
  598. }
  599. if done != total {
  600. return
  601. }
  602. items, err := repositories.GetBatchDownloadItems(id)
  603. if err != nil {
  604. zap.L().Error("[BatchDownload] 聚合读取 items 失败", zap.String("id", id.Hex()), zap.String("err", err.Error()))
  605. return
  606. }
  607. progress, summary := deriveProgressAndSummary(items)
  608. if err := repositories.UpdateBatchDownloadResult(id, progress, summary); err != nil {
  609. zap.L().Error("[BatchDownload] 聚合写入结果失败", zap.String("id", id.Hex()), zap.String("err", err.Error()))
  610. return
  611. }
  612. zap.L().Info("[BatchDownload] 任务聚合完成", zap.String("id", id.Hex()), zap.String("progress", string(progress)))
  613. }
  614. // finishItem 是对 repositories.FinishBatchDownloadItem + maybeAggregate 的封装,
  615. // 主任务同步任务与子任务共用,保证每个 item 转终态后驱动一次完成检查。
  616. func finishItem(id primitive.ObjectID, key string, status domain.ItemStatus, stage, reason string) {
  617. done, total, err := repositories.FinishBatchDownloadItem(id, key, status, stage, reason)
  618. if err != nil {
  619. zap.L().Error("[BatchDownload] 更新 item 结果失败",
  620. zap.String("id", id.Hex()), zap.String("key", key), zap.String("err", err.Error()))
  621. return
  622. }
  623. maybeAggregate(id, done, total)
  624. }