getAlarmSnapshot.go 8.5 KB

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