fwhTaskCallFile.go 13 KB

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