getAlarmSnapshot.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. package jobs
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "fwh-toolbox-server/domain"
  8. "fwh-toolbox-server/dto"
  9. function "fwh-toolbox-server/function/SnapFile"
  10. "fwh-toolbox-server/logger"
  11. "fwh-toolbox-server/repositories"
  12. "fwh-toolbox-server/utils/files"
  13. "os"
  14. "path/filepath"
  15. "regexp"
  16. "strconv"
  17. "strings"
  18. "time"
  19. "go.uber.org/zap"
  20. "gorm.io/gorm"
  21. )
  22. type AlarmSnapshotListResponse = domain.FwhPageResponse[[]domain.AlarmSnapshotInfo]
  23. // 获取告警快照数据
  24. // 从FHW后端请求快照文件后,处理并放入数据库
  25. // logTime 格式 YYYY-MM-DD
  26. func GetAlarmSnapshotData(ctx context.Context, sn, logTime string) ([]*domain.SiteAlarmSnapshotData, error) {
  27. // 请求获取和解析快照数据到数据库
  28. if err := GetAlarmSnapshotToDB(ctx, sn, logTime); err != nil {
  29. return nil, err
  30. }
  31. // 从数据库读取数据
  32. return repositories.QuerySnapshotData(ctx, sn, logTime)
  33. }
  34. // 获取告警快照
  35. // 从FHW后端请求快照文件后,处理并放入数据库
  36. // logTime 格式 YYYY-MM-DD
  37. func GetAlarmSnapshotToDB(ctx context.Context, sn, logTime string) error {
  38. // 请求快照信息列表
  39. snapshotList, err := repositories.QueryAlarmSnapshotInfo(sn, logTime)
  40. if err != nil {
  41. return fmt.Errorf("[GetAlarmSnapshot] %s", err.Error())
  42. }
  43. // 创建临时目录存储下载和解压数据
  44. tempDir, err := os.MkdirTemp("", "fwhtb-alarmSnapshot-*")
  45. if err != nil {
  46. logger.Std.Printf("创建临时目录失败: %v\n", err)
  47. return err
  48. }
  49. defer os.RemoveAll(tempDir) // 程序结束时清理临时目录
  50. for _, info := range snapshotList {
  51. // 判断文件是否已经导入
  52. _, err := repositories.QuerySnapshotFileRecord(ctx, info.FileName)
  53. if errors.Is(err, gorm.ErrRecordNotFound) {
  54. // 不存在 - 继续运行
  55. } else if err != nil {
  56. // 查询出错
  57. logger.Std.Printf("数据库快照文件记录查询失败: %v\n", err)
  58. return err
  59. } else {
  60. // 存在 record
  61. continue
  62. }
  63. // 获取站点信息,获取设备时区信息
  64. res, err := repositories.QuerySiteInfoList(dto.SiteInfoPara{
  65. SN: &info.LinkId,
  66. PageSize: 1,
  67. PageNum: 1,
  68. })
  69. if err != nil {
  70. zap.L().Error("站点信息查询失败。", zap.String("err", err.Error()))
  71. return err
  72. }
  73. // 提取ZoneInfo
  74. if len(res.Result) != 1 {
  75. zap.L().Error("站点信息查询失败,未查询待站点数据。", zap.Any("res", res))
  76. return fmt.Errorf("站点信息查询失败,未查询待站点数据。")
  77. }
  78. zoneInfo := res.Result[0].ZoneInfo
  79. snapFileName := filepath.Join(tempDir, info.FileName+".tgz")
  80. // 下载快照文件
  81. fileUrl := "https://energy.franklinwh.com/hes-upload/v1/file/download/" + info.FileUrl
  82. if err = files.DownloadFile(fileUrl, snapFileName); err != nil {
  83. zap.L().Error("快照文件下载失败。", zap.String("err", err.Error()))
  84. return err
  85. }
  86. // 创建解压目标文件夹
  87. snapDir := filepath.Join(tempDir, fmt.Sprintf("AlarmSnapshot_%d", info.Id))
  88. if err = files.CheckAndCreateDir(snapDir); err != nil {
  89. zap.L().Error("文件夹创建失败", zap.String("err", err.Error()))
  90. return err
  91. }
  92. // 解压包文件
  93. if err = files.AutoExtract(snapFileName, snapDir); err != nil {
  94. zap.L().Error("解压快照包失败", zap.String("err", err.Error()))
  95. return err
  96. }
  97. startTime, err := time.Parse(time.DateTime, info.StartTime)
  98. if err != nil {
  99. zap.L().Error("StartTime格式错误", zap.String("err", err.Error()))
  100. return err
  101. }
  102. endTime, err := time.Parse(time.DateTime, info.EndTime)
  103. if err != nil {
  104. zap.L().Error("EndTime格式错误", zap.String("err", err.Error()))
  105. return err
  106. }
  107. completeTime, err := time.Parse(time.DateTime, info.CompleteTime)
  108. if err != nil {
  109. zap.L().Error("CompleteTime格式错误", zap.String("err", err.Error()))
  110. return err
  111. }
  112. logTime, err := time.Parse(time.DateOnly, info.LogTime)
  113. if err != nil {
  114. zap.L().Error("LogTime格式错误", zap.String("err", err.Error()))
  115. return err
  116. }
  117. fileRecord := domain.SiteAlarmSnapshotFileRecord{
  118. CodeType: info.CodeType,
  119. FileName: info.FileName,
  120. FileURL: info.FileUrl,
  121. Sn: info.LinkId,
  122. LogTime: logTime,
  123. StartTime: startTime,
  124. EndTime: endTime,
  125. CompleteTime: completeTime,
  126. State: 0,
  127. }
  128. // 创建文件解析记录(冲突时覆盖)
  129. err = repositories.InsertSnapshotFileRecord(ctx, &fileRecord)
  130. if err != nil {
  131. return err
  132. }
  133. // 遍历文件
  134. var fileList []string
  135. if fileList, err = files.ListFiles(snapDir, true); err != nil {
  136. zap.L().Error("遍历文件失败", zap.String("err", err.Error()))
  137. return err
  138. }
  139. // 包文件有两个版本:
  140. // 1 - 包中文件内容为字符串,直接读取即可
  141. // 2 - 包中数据为16进制数据,须配合VID/PID表解析
  142. // 解析文件并写入数据库
  143. if err = handleAlarmSnapshotFiles(ctx, fileRecord, fileList, zoneInfo); err != nil {
  144. zap.L().Error("解析文件或写入数据库失败", zap.String("err", err.Error()))
  145. return err
  146. }
  147. }
  148. return nil
  149. }
  150. // handleAlarmSnapshotFiles 遍历文件列表并处理符合格式的文件
  151. func handleAlarmSnapshotFiles(ctx context.Context, record domain.SiteAlarmSnapshotFileRecord, fileList []string, zoneInfo string) error {
  152. // 正则规则
  153. reDigits10 := regexp.MustCompile(`^\d{10}$`)
  154. reZip := regexp.MustCompile(`^\d{8}_\d{6}_\d{3}\.zip$`)
  155. for _, file := range fileList {
  156. // 获取文件名(去掉路径)
  157. name := filepath.Base(file)
  158. // 处理纯数字长度为10
  159. if reDigits10.MatchString(name) {
  160. if err := parserOldAlarmSnapshotFile(ctx, record, file, zoneInfo); err != nil {
  161. zap.L().Error("解析OLD告警快照文件失败", zap.String("err", err.Error()))
  162. }
  163. continue
  164. }
  165. // 处理 zip 包 (数字+下划线组合)
  166. if strings.HasSuffix(name, ".zip") && reZip.MatchString(name) {
  167. parserNewAlarmSnapshotFile(ctx, record, file, zoneInfo)
  168. continue
  169. }
  170. // 其他文件忽略
  171. zap.L().Warn("忽略文件", zap.String("fileName", name), zap.String("filePath", file))
  172. }
  173. return nil
  174. }
  175. func parserOldAlarmSnapshotFile(ctx context.Context, record domain.SiteAlarmSnapshotFileRecord, filePath string, zoneInfo string) error {
  176. // 获取文件名(去掉路径)
  177. name := filepath.Base(filePath)
  178. // 读取文件内容
  179. data, err := os.ReadFile(filePath)
  180. if err != nil {
  181. return fmt.Errorf("读取文件失败 %s: %s", filePath, err.Error())
  182. }
  183. if len(data) == 0 {
  184. return fmt.Errorf("读取文件内容为空")
  185. }
  186. // 校验 JSON 格式
  187. var js json.RawMessage
  188. if err := json.Unmarshal(data, &js); err != nil {
  189. return fmt.Errorf("解析 JSON 失败 %s: %s", filePath, err.Error())
  190. }
  191. // 获取设备时区,计算日志记录时间
  192. tsInt, err := strconv.ParseInt(name, 10, 64)
  193. if err != nil {
  194. return fmt.Errorf("解析 LogTime 失败 %s: %s", filePath, err.Error())
  195. }
  196. t := time.Unix(tsInt, 0) // 秒级
  197. // 存入数据库
  198. siteAlarmSnapshotData := domain.SiteAlarmSnapshotData{
  199. Sn: record.Sn,
  200. FileRecordID: record.ID,
  201. LogTime: t,
  202. JSONData: string(js),
  203. ZoneInfo: zoneInfo,
  204. }
  205. err = repositories.InsertSnapshotData(ctx, &siteAlarmSnapshotData)
  206. if err != nil {
  207. return err
  208. }
  209. return nil
  210. }
  211. func parserNewAlarmSnapshotFile(ctx context.Context, record domain.SiteAlarmSnapshotFileRecord, filePath string, zoneInfo string) error {
  212. // 获取文件名(去掉路径)
  213. name := filepath.Base(filePath)
  214. // 生成文件夹路径
  215. destPath := strings.TrimSuffix(filePath, filepath.Ext(name))
  216. if err := files.Unzip(filePath, destPath); err != nil {
  217. zap.L().Error("zip文件解压失败", zap.String("err", err.Error()))
  218. return err
  219. }
  220. // 遍历文件
  221. fileList, err := files.ListFiles(destPath, true)
  222. if err != nil {
  223. zap.L().Error("遍历文件失败", zap.String("err", err.Error()))
  224. return err
  225. }
  226. if len(fileList) != 1 {
  227. zap.L().Error("zip文件内容不正确", zap.String("err", err.Error()))
  228. return err
  229. }
  230. snapFilePath := fileList[0]
  231. parser := function.NewParserAlarmSnapshot(snapFilePath)
  232. if err := parser.ParserSnapFile(); err != nil {
  233. zap.L().Error("告警快照文件解析失败", zap.String("err", err.Error()), zap.String("filePath", snapFilePath))
  234. return err
  235. }
  236. for _, d := range parser.SnapJson {
  237. // 转 JSON 字符串
  238. dataStr, err := json.Marshal(d)
  239. if err != nil {
  240. return fmt.Errorf("告警快照数据转 JSON 失败 %s: %s", filePath, err.Error())
  241. }
  242. // 存入数据库
  243. siteAlarmSnapshotData := domain.SiteAlarmSnapshotData{
  244. Sn: record.Sn,
  245. FileRecordID: record.ID,
  246. LogTime: time.Unix(int64(d.TimeStamp), 0),
  247. JSONData: string(dataStr),
  248. ZoneInfo: zoneInfo,
  249. }
  250. err = repositories.InsertSnapshotData(ctx, &siteAlarmSnapshotData)
  251. if err != nil {
  252. return err
  253. }
  254. }
  255. return nil
  256. }