package jobs import ( "context" "encoding/json" "errors" "fmt" "fwh-toolbox-server/domain" "fwh-toolbox-server/dto" function "fwh-toolbox-server/function/SnapFile" "fwh-toolbox-server/logger" "fwh-toolbox-server/repositories" "fwh-toolbox-server/utils/files" "os" "path/filepath" "regexp" "strconv" "strings" "time" "go.uber.org/zap" "gorm.io/gorm" ) // 获取告警快照数据 // 从FHW后端请求快照文件后,处理并放入数据库 // logTime 格式 YYYY-MM-DD func GetAlarmSnapshotData(ctx context.Context, sn, logTime string) ([]*domain.SiteAlarmSnapshotData, error) { // 请求获取和解析快照数据到数据库 if err := GetAlarmSnapshotToDB(ctx, sn, logTime); err != nil { return nil, err } // 从数据库读取数据 return repositories.QuerySnapshotData(ctx, sn, logTime) } // 获取告警快照 // 从FHW后端请求快照文件后,处理并放入数据库 // logTime 格式 YYYY-MM-DD func GetAlarmSnapshotToDB(ctx context.Context, sn, logTime string) error { // 请求快照信息列表 snapshotList, err := repositories.QueryAlarmSnapshotInfo(sn, logTime) if err != nil { return fmt.Errorf("[GetAlarmSnapshot] %s", err.Error()) } // 创建临时目录存储下载和解压数据 tempDir, err := os.MkdirTemp("", "fwhtb-alarmSnapshot-*") if err != nil { logger.Std.Printf("创建临时目录失败: %v\n", err) return err } defer os.RemoveAll(tempDir) // 程序结束时清理临时目录 for _, info := range snapshotList { // 判断文件是否已经导入 _, err := repositories.QuerySnapshotFileRecord(ctx, info.FileName) if errors.Is(err, gorm.ErrRecordNotFound) { // 不存在 - 继续运行 } else if err != nil { // 查询出错 logger.Std.Printf("数据库快照文件记录查询失败: %v\n", err) return err } else { // 存在 record continue } // 获取站点信息,获取设备时区信息 res, err := repositories.QuerySiteInfoList(dto.SiteInfoPara{ SN: &info.LinkId, PageSize: 1, PageNum: 1, }) if err != nil { zap.L().Error("站点信息查询失败。", zap.String("err", err.Error())) return err } // 提取ZoneInfo if len(*(*res).Result) != 1 { zap.L().Error("站点信息查询失败,未查询待站点数据。", zap.Any("res", res)) return fmt.Errorf("站点信息查询失败,未查询待站点数据。") } zoneInfo := (*(*res).Result)[0].ZoneInfo snapFileName := filepath.Join(tempDir, info.FileName+".tgz") // 下载快照文件 fileUrl := "https://energy.franklinwh.com/hes-upload/v1/file/download/" + info.FileUrl if err = files.DownloadFile(fileUrl, snapFileName); err != nil { zap.L().Error("快照文件下载失败。", zap.String("err", err.Error())) return err } // 创建解压目标文件夹 snapDir := filepath.Join(tempDir, fmt.Sprintf("AlarmSnapshot_%d", info.Id)) if err = files.CheckAndCreateDir(snapDir); err != nil { zap.L().Error("文件夹创建失败", zap.String("err", err.Error())) return err } // 解压包文件 if err = files.AutoExtract(snapFileName, snapDir); err != nil { zap.L().Error("解压快照包失败", zap.String("err", err.Error())) return err } startTime, err := time.Parse(time.DateTime, info.StartTime) if err != nil { zap.L().Error("StartTime格式错误", zap.String("err", err.Error())) return err } endTime, err := time.Parse(time.DateTime, info.EndTime) if err != nil { zap.L().Error("EndTime格式错误", zap.String("err", err.Error())) return err } completeTime, err := time.Parse(time.DateTime, info.CompleteTime) if err != nil { zap.L().Error("CompleteTime格式错误", zap.String("err", err.Error())) return err } logTime, err := time.Parse(time.DateOnly, info.LogTime) if err != nil { zap.L().Error("LogTime格式错误", zap.String("err", err.Error())) return err } fileRecord := domain.SiteAlarmSnapshotFileRecord{ CodeType: info.CodeType, FileName: info.FileName, FileURL: info.FileUrl, Sn: info.LinkId, LogTime: logTime, StartTime: startTime, EndTime: endTime, CompleteTime: completeTime, State: 0, } // 创建文件解析记录(冲突时覆盖) err = repositories.InsertSnapshotFileRecord(ctx, &fileRecord) if err != nil { return err } // 遍历文件 var fileList []string if fileList, err = files.ListFiles(snapDir, true); err != nil { zap.L().Error("遍历文件失败", zap.String("err", err.Error())) return err } // 包文件有两个版本: // 1 - 包中文件内容为字符串,直接读取即可 // 2 - 包中数据为16进制数据,须配合VID/PID表解析 // 解析文件并写入数据库 if err = handleAlarmSnapshotFiles(ctx, fileRecord, fileList, zoneInfo); err != nil { zap.L().Error("解析文件或写入数据库失败", zap.String("err", err.Error())) return err } } return nil } // handleAlarmSnapshotFiles 遍历文件列表并处理符合格式的文件 func handleAlarmSnapshotFiles(ctx context.Context, record domain.SiteAlarmSnapshotFileRecord, fileList []string, zoneInfo string) error { // 正则规则 reDigits10 := regexp.MustCompile(`^\d{10}$`) reZip := regexp.MustCompile(`^\d{8}_\d{6}_\d{3}\.zip$`) for _, file := range fileList { // 获取文件名(去掉路径) name := filepath.Base(file) // 处理纯数字长度为10 if reDigits10.MatchString(name) { if err := parserOldAlarmSnapshotFile(ctx, record, file, zoneInfo); err != nil { zap.L().Error("解析OLD告警快照文件失败", zap.String("err", err.Error())) } continue } // 处理 zip 包 (数字+下划线组合) if strings.HasSuffix(name, ".zip") && reZip.MatchString(name) { parserNewAlarmSnapshotFile(ctx, record, file, zoneInfo) continue } // 其他文件忽略 zap.L().Warn("忽略文件", zap.String("fileName", name), zap.String("filePath", file)) } return nil } func parserOldAlarmSnapshotFile(ctx context.Context, record domain.SiteAlarmSnapshotFileRecord, filePath string, zoneInfo string) error { // 获取文件名(去掉路径) name := filepath.Base(filePath) // 读取文件内容 data, err := os.ReadFile(filePath) if err != nil { return fmt.Errorf("读取文件失败 %s: %s", filePath, err.Error()) } if len(data) == 0 { return fmt.Errorf("读取文件内容为空") } // 校验 JSON 格式 var js json.RawMessage if err := json.Unmarshal(data, &js); err != nil { return fmt.Errorf("解析 JSON 失败 %s: %s", filePath, err.Error()) } // 获取设备时区,计算日志记录时间 tsInt, err := strconv.ParseInt(name, 10, 64) if err != nil { return fmt.Errorf("解析 LogTime 失败 %s: %s", filePath, err.Error()) } t := time.Unix(tsInt, 0) // 秒级 // 存入数据库 siteAlarmSnapshotData := domain.SiteAlarmSnapshotData{ Sn: record.Sn, FileRecordID: record.ID, LogTime: t, JSONData: string(js), ZoneInfo: zoneInfo, } err = repositories.InsertSnapshotData(ctx, &siteAlarmSnapshotData) if err != nil { return err } return nil } func parserNewAlarmSnapshotFile(ctx context.Context, record domain.SiteAlarmSnapshotFileRecord, filePath string, zoneInfo string) error { // 获取文件名(去掉路径) name := filepath.Base(filePath) // 生成文件夹路径 destPath := strings.TrimSuffix(filePath, filepath.Ext(name)) if err := files.Unzip(filePath, destPath); err != nil { zap.L().Error("zip文件解压失败", zap.String("err", err.Error())) return err } // 遍历文件 fileList, err := files.ListFiles(destPath, true) if err != nil { zap.L().Error("遍历文件失败", zap.String("err", err.Error())) return err } if len(fileList) != 1 { zap.L().Error("zip文件内容不正确", zap.String("err", err.Error())) return err } snapFilePath := fileList[0] parser := function.NewParserAlarmSnapshot(snapFilePath) if err := parser.ParserSnapFile(); err != nil { zap.L().Error("告警快照文件解析失败", zap.String("err", err.Error()), zap.String("filePath", snapFilePath)) return err } for _, d := range parser.SnapJson { // 转 JSON 字符串 dataStr, err := json.Marshal(d) if err != nil { return fmt.Errorf("告警快照数据转 JSON 失败 %s: %s", filePath, err.Error()) } // 存入数据库 siteAlarmSnapshotData := domain.SiteAlarmSnapshotData{ Sn: record.Sn, FileRecordID: record.ID, LogTime: time.Unix(int64(d.TimeStamp), 0), JSONData: string(dataStr), ZoneInfo: zoneInfo, } err = repositories.InsertSnapshotData(ctx, &siteAlarmSnapshotData) if err != nil { return err } } return nil }