fwh_mqttLog.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. package fwh_pro
  2. import (
  3. "errors"
  4. "fmt"
  5. "fwh-toolbox-server/domain"
  6. "net/url"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "go.uber.org/zap"
  11. )
  12. type QueryMqttLogParam struct {
  13. SN *string
  14. CmdTypes []domain.MqttCmdType
  15. StartTime *time.Time
  16. EndTime *time.Time
  17. }
  18. // 查询站点信息
  19. func queryMqttLog(param QueryMqttLogParam) (*domain.FwhPageResponse[[]domain.MqttLogItem], error) {
  20. // 构造 query 参数
  21. params := url.Values{}
  22. if param.SN != nil {
  23. params.Add("searchName", *param.SN)
  24. }
  25. if len(param.CmdTypes) > 0 {
  26. cmdTypesStr := make([]string, len(param.CmdTypes))
  27. for i, v := range param.CmdTypes {
  28. cmdTypesStr[i] = string(v) // 转成 string
  29. }
  30. params.Add("cmdTypes", strings.Join(cmdTypesStr, ","))
  31. }
  32. if param.StartTime != nil {
  33. params.Add("startTime", param.StartTime.Format(time.DateTime))
  34. }
  35. if param.EndTime != nil {
  36. params.Add("endTime", param.EndTime.Format(time.DateTime))
  37. }
  38. if param.StartTime != nil || param.EndTime != nil {
  39. params.Add("timeType", "1") // 0-通讯时间 1-设备时间时
  40. }
  41. params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
  42. params.Add("pageNum", strconv.Itoa(1))
  43. params.Add("lang", "ZH_CN")
  44. response, err := FwhHttpGetUnmarshalPage[[]domain.MqttLogItem](getFullEnergyUrl("/hes-gateway/manage/iotMqttCommlogs"), params)
  45. if err != nil {
  46. return nil, err
  47. }
  48. return response, nil
  49. }
  50. func queryMqttLogListOnePage(sn string, cmdType domain.MqttCmdType, startTime, endTime time.Time) ([]domain.MqttLogItem, error) {
  51. param := QueryMqttLogParam{
  52. SN: &sn,
  53. CmdTypes: []domain.MqttCmdType{cmdType},
  54. StartTime: &startTime,
  55. EndTime: &endTime,
  56. }
  57. mqttLogRes, err := queryMqttLog(param)
  58. if err != nil {
  59. return nil, err
  60. }
  61. if mqttLogRes.Code != 200 {
  62. return nil, fmt.Errorf("[queryMqttLogListOnePage] 查询失败 code: %d, message: %s", mqttLogRes.Code, mqttLogRes.Message)
  63. }
  64. // 判断是否超出可处理数据上限 10万条数据
  65. if mqttLogRes.Total > 10*10000 {
  66. return nil, fmt.Errorf("超出数据请求上限 - Total: %d (>10万)", mqttLogRes.Total)
  67. }
  68. return *mqttLogRes.Result, nil
  69. }
  70. // 从FWH获取MQTT log数据
  71. //
  72. // 限制条件(重要):startTime/endTime 必须为「不含时区」的字面时间(用 UTC 表示)。
  73. // 本函数按天拆分时间段时用 time.Truncate(24h) 推进游标,Truncate 按 UTC 零点对齐;
  74. // 若传入带非 UTC 时区(如 America/Chicago)的时间,游标会回退到前一天,
  75. // currTime.Before(endTime) 恒成立,periods 切片无限 append,导致内存 OOM。
  76. // 调用方(DataDownload)需先用 stripTimeLocation 剥离时区再传入。
  77. func QueryMqttLogListOnTimeRange(sn string, cmdType domain.MqttCmdType, startTime, endTime time.Time) ([]domain.MqttLogItem, error) {
  78. // 保证 startTime <= endTime
  79. if startTime.After(endTime) {
  80. return nil, errors.New("startTime > endTime")
  81. }
  82. // 按天拆分时间段
  83. var periods []struct{ Start, End time.Time }
  84. currTime := startTime
  85. for currTime.Before(endTime) {
  86. // 当天结束时间
  87. dayEnd := time.Date(currTime.Year(), currTime.Month(), currTime.Day(), 23, 59, 59, 0, currTime.Location())
  88. // 如果当天结束大于 endTime,使用 endTime
  89. if dayEnd.After(endTime) {
  90. dayEnd = endTime
  91. }
  92. periods = append(periods, struct{ Start, End time.Time }{Start: currTime, End: dayEnd})
  93. // 下一天开始
  94. currTime = dayEnd.Add(time.Second).Truncate(24 * time.Hour)
  95. currTime = time.Date(currTime.Year(), currTime.Month(), currTime.Day(), 0, 0, 0, 0, currTime.Location())
  96. }
  97. zap.L().Debug("[fwh_pro] MQTT periods 构建完成",
  98. zap.String("sn", sn), zap.Any("cmdType", cmdType),
  99. zap.Time("start", startTime), zap.Time("end", endTime),
  100. zap.Int("periods", len(periods)),
  101. )
  102. // 按天查询
  103. var mqttLogList []domain.MqttLogItem
  104. for _, period := range periods {
  105. var mqttLogRes []domain.MqttLogItem = nil
  106. periodEndTime := period.End
  107. pages := 0
  108. // 检查数据是否超过1000(单次查询上限)
  109. for {
  110. var err error
  111. pages++
  112. zap.L().Debug("[fwh_pro] MQTT 分页请求发起",
  113. zap.String("sn", sn), zap.Any("cmdType", cmdType),
  114. zap.Time("periodStart", period.Start), zap.Time("periodEnd", periodEndTime),
  115. zap.Int("page", pages), zap.Int("accumulated", len(mqttLogList)),
  116. )
  117. mqttLogRes, err = queryMqttLogListOnePage(sn, cmdType, period.Start, periodEndTime)
  118. if err != nil {
  119. return nil, err
  120. }
  121. time.Sleep(time.Second) // 延时1s
  122. mqttLogList = append(mqttLogList, mqttLogRes...)
  123. zap.L().Debug("[fwh_pro] MQTT 分页请求返回",
  124. zap.String("sn", sn), zap.Any("cmdType", cmdType),
  125. zap.Int("page", pages), zap.Int("pageRows", len(mqttLogRes)),
  126. zap.Int("accumulated", len(mqttLogList)),
  127. )
  128. // 判断是否未查完
  129. if len(mqttLogRes) < int(REQ_PAGE_SIZE) {
  130. // 请求完成,退出
  131. break
  132. }
  133. // 跳转时间段,再次请求
  134. // 提取最后一条数据的设备时间
  135. next, err := time.Parse(time.DateTime, mqttLogRes[len(mqttLogRes)-1].LogTime)
  136. if err != nil {
  137. return nil, err
  138. }
  139. // 游标未能前进:说明同一秒内 >= REQ_PAGE_SIZE(1000) 条数据,
  140. // 单页游标(按最后一条 LogTime)无法翻过这一秒,继续循环会死循环并 OOM。
  141. // 现实中基本不会发生,此处仅作防护:记录错误并返回,不再无限拉取。
  142. if !next.Before(periodEndTime) {
  143. zap.L().Error("[fwh_pro] MQTT 日志分页游标停滞,疑似同一秒内数据超过单页上限",
  144. zap.String("sn", sn),
  145. zap.Any("cmdType", cmdType),
  146. zap.Time("stuckAt", next),
  147. zap.Int("pageSize", int(REQ_PAGE_SIZE)),
  148. zap.Int("accumulated", len(mqttLogList)),
  149. )
  150. return nil, fmt.Errorf("mqtt log pagination stalled at %s: more than %d rows within one second", next.Format(time.DateTime), REQ_PAGE_SIZE)
  151. }
  152. periodEndTime = next
  153. }
  154. }
  155. return mqttLogList, nil
  156. }