getAlarmSnapshot.go 11 KB

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