fwhTaskCallFile.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. package fwhQueue
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "fwh-toolbox-server/domain"
  7. "fwh-toolbox-server/jobs"
  8. "fwh-toolbox-server/repositories"
  9. "path"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "github.com/hibiken/asynq"
  14. "go.mongodb.org/mongo-driver/bson/primitive"
  15. "go.uber.org/zap"
  16. )
  17. const maxCallRetry = 3
  18. // handleCallFile 是文件下载子任务的 Asynq handler
  19. // Asynq 本身会做重试(MaxRetry=3),所以 handler 内部不再做外层重试循环
  20. // 返回 error 会触发 Asynq 重试,返回 nil 表示成功(结果已通过 finishItem 上报)
  21. func handleCallFile(ctx context.Context, task *asynq.Task) error {
  22. var payload callFilePayload
  23. if err := json.Unmarshal(task.Payload(), &payload); err != nil {
  24. // payload 解析失败无法定位 item,只能记日志,不重试
  25. zap.L().Error("[CallFile] unmarshal payload 失败", zap.String("err", err.Error()))
  26. return nil
  27. }
  28. info := payload.TaskInfo
  29. mainTaskId := payload.MainTaskId
  30. mainObjId, _ := primitive.ObjectIDFromHex(mainTaskId)
  31. var stageErr *syncItemError
  32. switch info.FileType {
  33. case jobs.FileTypeEmsLog:
  34. stageErr = handleEmsLogDownload(ctx, mainTaskId, info)
  35. case jobs.FileTypeFaultRecord:
  36. stageErr = handleFaultRecordDownload(ctx, mainTaskId, info)
  37. default:
  38. stageErr = &syncItemError{stage: domain.StageBuild, reason: fmt.Sprintf("unknown file type: %s", info.FileType)}
  39. }
  40. if stageErr != nil {
  41. // 判断是否还有重试机会
  42. retryCount, _ := asynq.GetRetryCount(ctx)
  43. maxRetry, _ := asynq.GetMaxRetry(ctx)
  44. if retryCount >= maxRetry-1 {
  45. // 最后一次重试仍失败:记 failed item(带 stage),推进完成计数
  46. finishItem(mainObjId, payload.ItemKey, domain.ItemFailed, stageErr.stage, stageErr.reason)
  47. return nil // 不再重试
  48. }
  49. return stageErr // 触发 Asynq 重试
  50. }
  51. // 成功
  52. finishItem(mainObjId, payload.ItemKey, domain.ItemSuccess, "", "")
  53. return nil
  54. }
  55. // ==================== EmsLog 下载 ====================
  56. func handleEmsLogDownload(ctx context.Context, mainTaskId string, info domain.BatchDownloadFileTaskInfo) *syncItemError {
  57. // 1. 召唤 EmsLog
  58. if err := callEmsLog(info.Sn, info.Date, info.FileSupType); err != nil {
  59. return &syncItemError{stage: domain.StageCall, reason: fmt.Sprintf("[EmsLog] 召唤失败(sn=%s, date=%s, type=%s): %s", info.Sn, info.Date, info.FileSupType, err.Error())}
  60. }
  61. // 2. 轮询等待召唤结果
  62. recordInfo, sErr := pollEmsLogResult(ctx, info.Sn, info.Date, info.FileSupType)
  63. if sErr != nil {
  64. return sErr
  65. }
  66. // 3. 下载并保存文件
  67. mainObjId, _ := primitive.ObjectIDFromHex(mainTaskId)
  68. return downloadAndSaveEmsLog(ctx, mainObjId, info, recordInfo)
  69. }
  70. func pollEmsLogResult(ctx context.Context, sn, date, fileSupType string) (*domain.EmsLogRecordInfo, *syncItemError) {
  71. ticker := time.NewTicker(time.Minute)
  72. defer ticker.Stop()
  73. timeout := time.After(30 * time.Minute)
  74. for {
  75. select {
  76. case <-ctx.Done():
  77. return nil, &syncItemError{stage: domain.StagePoll, reason: ctx.Err().Error()}
  78. case <-timeout:
  79. return nil, &syncItemError{stage: domain.StagePoll, reason: fmt.Sprintf("[EmsLog] 轮询超时(sn=%s, date=%s)", sn, date)}
  80. case <-ticker.C:
  81. for attempt := 0; attempt < maxCallRetry; attempt++ {
  82. recordInfo, err := getEmsLogUrl(sn, date, fileSupType)
  83. if err != nil {
  84. continue
  85. }
  86. if recordInfo.Status == -1 {
  87. return nil, &syncItemError{stage: domain.StagePoll, reason: fmt.Sprintf("[EmsLog] 召唤失败(sn=%s)", sn)}
  88. }
  89. if recordInfo.Status == 1 {
  90. return recordInfo, nil
  91. }
  92. // status == 0: 还在处理中
  93. break
  94. }
  95. }
  96. }
  97. }
  98. func downloadAndSaveEmsLog(ctx context.Context, mainTaskId primitive.ObjectID, info domain.BatchDownloadFileTaskInfo, recordInfo *domain.EmsLogRecordInfo) *syncItemError {
  99. var dataBytes []byte
  100. var err error
  101. for attempt := 0; attempt < maxCallRetry; attempt++ {
  102. dataBytes, err = repositories.QueryDownloadEmsLogFile(recordInfo.FileUrl)
  103. if err == nil {
  104. break
  105. }
  106. zap.L().Warn("[EmsLog] 文件下载重试", zap.String("sn", info.Sn), zap.Int("attempt", attempt))
  107. }
  108. if err != nil {
  109. return &syncItemError{stage: domain.StageDownload, reason: fmt.Sprintf("[EmsLog] 文件下载失败(sn=%s): %s", info.Sn, err.Error())}
  110. }
  111. // 提取文件名
  112. baseStr := path.Base(recordInfo.FileUrl)
  113. if baseStr == "." || baseStr == "/" {
  114. baseStr = ""
  115. }
  116. // 解析召唤时间
  117. loc, err := time.LoadLocation("America/Los_Angeles")
  118. if err != nil {
  119. return &syncItemError{stage: domain.StageSave, reason: fmt.Sprintf("[EmsLog] 时区加载失败: %s", err.Error())}
  120. }
  121. callTime, err := time.ParseInLocation(time.DateTime, recordInfo.CallTime, loc)
  122. if err != nil {
  123. return &syncItemError{stage: domain.StageSave, reason: fmt.Sprintf("[EmsLog] CallTime解析失败: %s", err.Error())}
  124. }
  125. // 保存到 MinIO
  126. objName, err := jobs.SaveBatchDownloadEmsLog(info.Sn, dataBytes, baseStr, info.FileSupType, info.Date, callTime)
  127. if err != nil {
  128. return &syncItemError{stage: domain.StageSave, reason: fmt.Sprintf("[EmsLog] 文件保存失败(sn=%s): %s", info.Sn, err.Error())}
  129. }
  130. // 写入路径记录
  131. if err := repositories.InsertBatchDownloadPathEmsLogs(mainTaskId, info.Sn, info.Date, info.FileSupType, objName); err != nil {
  132. return &syncItemError{stage: domain.StageSave, reason: fmt.Sprintf("[EmsLog] 路径写入失败(sn=%s): %s", info.Sn, err.Error())}
  133. }
  134. return nil
  135. }
  136. // ==================== FaultRecord 下载 ====================
  137. func handleFaultRecordDownload(ctx context.Context, mainTaskId string, info domain.BatchDownloadFileTaskInfo) *syncItemError {
  138. // 1. 解析参数
  139. timeRange := strings.Split(info.Date, ",")
  140. if len(timeRange) != 2 {
  141. return &syncItemError{stage: domain.StageBuild, reason: fmt.Sprintf("[FaultRecord] timeRange 格式错误: %s", info.Date)}
  142. }
  143. // 2. 查询故障录波列表,判断是否存在满足时间范围的数据
  144. existingRecords, err := repositories.QueryFaultRecordList(info.Sn, timeRange[0], timeRange[1])
  145. if err != nil {
  146. zap.L().Error("[FaultRecord] 查询已有录波列表失败", zap.String("sn", info.Sn), zap.String("err", err.Error()))
  147. return &syncItemError{stage: domain.StageQuery, reason: fmt.Sprintf("[FaultRecord] 查询已有录波列表失败(sn=%s): %s", info.Sn, err.Error())}
  148. } else {
  149. // 判断列表中是否存在 earliestFaultTime 在时间范围内的记录
  150. faultID, err := checkFaultRecordInTimeRange(existingRecords, info.DevType, info.Sn, timeRange[0], timeRange[1])
  151. if err != nil {
  152. zap.L().Error("[FaultRecord] 判断时间范围失败", zap.String("sn", info.Sn), zap.String("err", err.Error()))
  153. return &syncItemError{stage: domain.StageQuery, reason: fmt.Sprintf("[FaultRecord] 判断时间范围失败(sn=%s): %s", info.Sn, err.Error())}
  154. }
  155. if faultID >= 0 {
  156. // 故障录波文件存在,直接下载所有子记录文件
  157. mainObjId, _ := primitive.ObjectIDFromHex(mainTaskId)
  158. return downloadAndSaveFaultRecords(ctx, mainObjId, faultID)
  159. }
  160. zap.L().Info("[FaultRecord] 无满足时间范围的录波数据,需要召唤",
  161. zap.String("sn", info.Sn),
  162. zap.String("startTime", timeRange[0]),
  163. zap.String("endTime", timeRange[1]),
  164. )
  165. }
  166. indexList, err := parseIndexList(info.FileSupType)
  167. if err != nil {
  168. return &syncItemError{stage: domain.StageBuild, reason: fmt.Sprintf("[FaultRecord] indexList 解析错误: %s", err.Error())}
  169. }
  170. // 3. 召唤故障录波
  171. if err := callFaultRecord(info.DevType, info.Sn, timeRange[0], timeRange[1], indexList); err != nil {
  172. return &syncItemError{stage: domain.StageCall, reason: fmt.Sprintf("[FaultRecord] 召唤失败(sn=%s): %s", info.Sn, err.Error())}
  173. }
  174. // 4. 轮询等待召唤结果
  175. faultId, sErr := pollFaultRecordResult(ctx, info.DevType, info.Sn, timeRange[0], timeRange[1])
  176. if sErr != nil {
  177. return sErr
  178. }
  179. // 5. 下载所有子记录文件
  180. mainObjId, _ := primitive.ObjectIDFromHex(mainTaskId)
  181. return downloadAndSaveFaultRecords(ctx, mainObjId, faultId)
  182. }
  183. // checkFaultRecordInTimeRange 检查故障录波列表中是否存在 earliestFaultTime 满足时间范围的记录,返回FaultID和错误信息,不在范围内FaultID = -1
  184. func checkFaultRecordInTimeRange(records []domain.FaultRecordInfo, devType int, sn string, startTime string, endTime string) (int64, error) {
  185. start, err := time.Parse(time.DateTime, startTime)
  186. if err != nil {
  187. zap.L().Error("[checkFaultRecordInTimeRange] 解析startTime失败",
  188. zap.String("startTime", startTime),
  189. zap.String("err", err.Error()),
  190. )
  191. return -1, err // 解析失败,保守处理:继续召唤
  192. }
  193. end, err := time.Parse(time.DateTime, endTime)
  194. if err != nil {
  195. zap.L().Error("[checkFaultRecordInTimeRange] 解析endTime失败",
  196. zap.String("endTime", endTime),
  197. zap.String("err", err.Error()),
  198. )
  199. return -1, err
  200. }
  201. for _, record := range records {
  202. // 匹配设备类型和 SN
  203. if record.DeviceType != devType {
  204. continue
  205. }
  206. var recordSn string
  207. switch devType {
  208. case 0: // aPower
  209. if record.ApowerId != nil {
  210. recordSn = *record.ApowerId
  211. }
  212. case 1: // aGate
  213. recordSn = record.GatewayId
  214. case 2: // MPPT
  215. if record.ApowerId != nil {
  216. recordSn = *record.ApowerId
  217. }
  218. case 3: // aHub
  219. if record.PowerlinkId != nil {
  220. recordSn = *record.PowerlinkId
  221. }
  222. }
  223. if recordSn != sn {
  224. continue
  225. }
  226. // 解析 earliestFaultTime 并判断是否在时间范围内
  227. faultTime, err := time.Parse(time.DateTime, record.EarliestFaultTime)
  228. if err != nil {
  229. // 尝试时间戳格式
  230. continue
  231. }
  232. inRange := (faultTime.Equal(start) || faultTime.After(start)) &&
  233. (faultTime.Equal(end) || faultTime.Before(end))
  234. if inRange {
  235. return record.Id, nil
  236. }
  237. }
  238. return -1, nil
  239. }
  240. func pollFaultRecordResult(ctx context.Context, devType int, sn, startTime, endTime string) (int64, *syncItemError) {
  241. ticker := time.NewTicker(time.Minute)
  242. defer ticker.Stop()
  243. timeout := time.After(30 * time.Minute)
  244. for {
  245. select {
  246. case <-ctx.Done():
  247. return 0, &syncItemError{stage: domain.StagePoll, reason: ctx.Err().Error()}
  248. case <-timeout:
  249. return 0, &syncItemError{stage: domain.StagePoll, reason: fmt.Sprintf("[FaultRecord] 轮询超时(sn=%s)", sn)}
  250. case <-ticker.C:
  251. for attempt := 0; attempt < maxCallRetry; attempt++ {
  252. status, faultId, reason, err := getFaultRecordUrl(devType, sn, startTime, endTime)
  253. if err != nil {
  254. continue
  255. }
  256. if status == -1 {
  257. return 0, &syncItemError{stage: domain.StagePoll, reason: fmt.Sprintf("[FaultRecord] 召唤失败(sn=%s): %s", sn, reason)}
  258. }
  259. if status == 2 {
  260. return faultId, nil
  261. }
  262. // status == 0: 还在处理中
  263. break
  264. }
  265. }
  266. }
  267. }
  268. func downloadAndSaveFaultRecords(ctx context.Context, mainTaskId primitive.ObjectID, faultId int64) *syncItemError {
  269. subRecordList, err := repositories.QueryFaultRecordSubList(faultId)
  270. if err != nil {
  271. return &syncItemError{stage: domain.StageDownload, reason: fmt.Sprintf("[FaultRecord] 子记录查询失败(faultId=%d): %s", faultId, err.Error())}
  272. }
  273. for _, subRecord := range subRecordList {
  274. var dataBytes []byte
  275. var downloadErr error
  276. for attempt := 0; attempt < maxCallRetry; attempt++ {
  277. _, dataBytes, downloadErr = repositories.QueryDownloadFaultRecordData(faultId, subRecord.Id)
  278. if downloadErr == nil {
  279. break
  280. }
  281. zap.L().Warn("[FaultRecord] 文件下载重试",
  282. zap.String("sn", subRecord.GatewayId),
  283. zap.Int64("subId", subRecord.Id),
  284. zap.Int("attempt", attempt),
  285. )
  286. }
  287. if downloadErr != nil {
  288. return &syncItemError{stage: domain.StageDownload, reason: fmt.Sprintf("[FaultRecord] 文件下载失败(sn=%s, id=%d): %s", subRecord.GatewayId, subRecord.Id, downloadErr.Error())}
  289. }
  290. // 保存到 MinIO
  291. objName, err := jobs.SaveBatchDownloadFaultRecord(subRecord.GatewayId, dataBytes, subRecord.FileName+".xls")
  292. if err != nil {
  293. return &syncItemError{stage: domain.StageSave, reason: fmt.Sprintf("[FaultRecord] 文件保存失败(sn=%s): %s", subRecord.GatewayId, err.Error())}
  294. }
  295. // 写入路径记录
  296. if err := repositories.InsertBatchDownloadPathFaultRecord(mainTaskId, subRecord.GatewayId, subRecord.FileName, objName); err != nil {
  297. return &syncItemError{stage: domain.StageSave, reason: fmt.Sprintf("[FaultRecord] 路径写入失败(sn=%s): %s", subRecord.GatewayId, err.Error())}
  298. }
  299. }
  300. return nil
  301. }
  302. // ==================== 辅助函数(复用原有逻辑) ====================
  303. // EMS日志类型map
  304. var mapEmsLogType = map[string]int{
  305. "EmsRunLog": 4,
  306. "EmsBattLog": 7,
  307. "EmsMonitorLog": 9,
  308. "Ems4gLog": 10,
  309. "EmsAwsLog": 11,
  310. "EmsMainLog": 12,
  311. }
  312. func callEmsLog(sn string, date string, logType string) error {
  313. logTypeNum, ok := mapEmsLogType[logType]
  314. if !ok {
  315. return fmt.Errorf("logType不存在: %s", logType)
  316. }
  317. return repositories.QueryCallEmsLog(sn, date, logTypeNum)
  318. }
  319. func getEmsLogUrl(sn string, date string, logType string) (*domain.EmsLogRecordInfo, error) {
  320. logTypeNum, ok := mapEmsLogType[logType]
  321. if !ok {
  322. return nil, fmt.Errorf("logType不存在: %s", logType)
  323. }
  324. return repositories.QueryEmsLogCallRecord(sn, date, logTypeNum)
  325. }
  326. func callFaultRecord(devType int, sn string, startTime string, endTime string, indexList []int) error {
  327. return repositories.QueryCellFaultRecord(devType, sn, indexList)
  328. }
  329. func getFaultRecordUrl(devType int, sn string, startTime string, endTime string) (int, int64, string, error) {
  330. recordList, err := repositories.QueryFaultRecordCallRecordList()
  331. if err != nil {
  332. return 0, 0, "", err
  333. }
  334. for _, record := range recordList {
  335. if record.DeviceType == devType {
  336. var devSn string
  337. switch devType {
  338. case 0:
  339. devSn = *record.ApowerId
  340. case 1:
  341. devSn = record.GatewayId
  342. case 2:
  343. devSn = *record.ApowerId
  344. case 3:
  345. devSn = *record.PowerlinkId
  346. default:
  347. continue
  348. }
  349. if devSn == sn {
  350. if *record.Status == -1 {
  351. return *record.Status, record.Id, *record.FaultReason, nil
  352. }
  353. return *record.Status, record.Id, "", nil
  354. }
  355. }
  356. }
  357. return 0, 0, "", fmt.Errorf("未找到召唤匹配记录(devType=%d, sn=%s)", devType, sn)
  358. }
  359. func parseIndexList(fileSupType string) ([]int, error) {
  360. indexStrList := strings.Split(fileSupType, ",")
  361. indexList := make([]int, 0, len(indexStrList))
  362. for _, s := range indexStrList {
  363. idx, err := strconv.Atoi(s)
  364. if err != nil {
  365. return nil, fmt.Errorf("无法解析 index '%s': %w", s, err)
  366. }
  367. indexList = append(indexList, idx)
  368. }
  369. return indexList, nil
  370. }