package api import ( "ZhenTuLocalPassiveAdapter/dto" "ZhenTuLocalPassiveAdapter/logger" "bytes" "context" "encoding/json" "fmt" "io" "net/http" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.uber.org/zap" ) const ( // 连续性上报接口地址 continuityReportURL = "https://zhentuai.com/api/device/video-continuity/report" // 间隔阈值(毫秒) maxAllowedGapMs = 2000 ) // ReportContinuityCheck 上报连续性检查结果 func ReportContinuityCheck(ctx context.Context, results []dto.ContinuityCheckResult) error { _, span := tracer.Start(ctx, "ReportContinuityCheck") defer span.End() span.SetAttributes(attribute.Int("results.count", len(results))) // 统计不连续的设备数量 discontinuousCount := 0 for _, r := range results { if !r.IsContinuous { discontinuousCount++ } } span.SetAttributes(attribute.Int("discontinuous.count", discontinuousCount)) // 逐个设备上报 var lastErr error for _, result := range results { if err := reportSingleDevice(ctx, result); err != nil { logger.Error("上报设备连续性检查结果失败", zap.String("deviceNo", result.DeviceNo), zap.Error(err)) lastErr = err } } return lastErr } // reportSingleDevice 上报单个设备的连续性检查结果 func reportSingleDevice(ctx context.Context, result dto.ContinuityCheckResult) error { _, span := tracer.Start(ctx, "reportSingleDevice") defer span.End() span.SetAttributes( attribute.String("device.no", result.DeviceNo), attribute.Bool("continuous", result.IsContinuous), ) // 转换为接口请求格式 request := convertToReportRequest(result) // 序列化请求体 jsonData, err := json.Marshal(request) if err != nil { span.SetAttributes(attribute.String("error", err.Error())) span.SetStatus(codes.Error, "序列化JSON失败") logger.Error("序列化连续性检查请求失败", zap.Error(err)) return err } span.SetAttributes(attribute.String("request.body", string(jsonData))) // 创建请求 req, err := http.NewRequestWithContext(ctx, "POST", continuityReportURL, bytes.NewBuffer(jsonData)) if err != nil { span.SetAttributes(attribute.String("error", err.Error())) span.SetStatus(codes.Error, "创建请求失败") logger.Error("创建连续性检查上报请求失败", zap.Error(err)) return err } req.Header.Set("Content-Type", "application/json") span.SetAttributes( attribute.String("http.url", continuityReportURL), attribute.String("http.method", "POST"), ) // 发送请求 resp, err := GetAPIClient().Do(req) if err != nil { span.SetAttributes(attribute.String("error", err.Error())) span.SetStatus(codes.Error, "发送请求失败") logger.Error("发送连续性检查上报请求失败", zap.Error(err)) return err } defer resp.Body.Close() span.SetAttributes( attribute.String("http.status", resp.Status), attribute.Int("http.status_code", resp.StatusCode), ) // 读取响应体 body, err := io.ReadAll(resp.Body) if err != nil { span.SetAttributes(attribute.String("error", err.Error())) span.SetStatus(codes.Error, "读取响应体失败") logger.Error("读取连续性检查上报响应失败", zap.Error(err)) return err } span.SetAttributes(attribute.String("response.body", string(body))) // 解析响应 var response dto.ContinuityReportResponse if err := json.Unmarshal(body, &response); err != nil { span.SetAttributes(attribute.String("error", err.Error())) span.SetStatus(codes.Error, "解析响应失败") logger.Error("解析连续性检查上报响应失败", zap.Error(err), zap.String("body", string(body))) return err } // 检查业务响应码 if response.Code != 0 && response.Code != 200 { errMsg := fmt.Sprintf("上报失败: code=%d, msg=%s", response.Code, response.Msg) span.SetAttributes(attribute.String("error", errMsg)) span.SetStatus(codes.Error, "上报失败") logger.Warn("连续性检查上报业务失败", zap.String("deviceNo", result.DeviceNo), zap.Int("code", response.Code), zap.String("msg", response.Msg)) return fmt.Errorf(errMsg) } span.SetStatus(codes.Ok, "上报成功") if result.IsContinuous { logger.Debug("设备连续性检查上报成功", zap.String("deviceNo", result.DeviceNo), zap.Int("fileCount", result.FileCount)) } else { logger.Info("设备连续性检查上报成功(不连续)", zap.String("deviceNo", result.DeviceNo), zap.Int("gapCount", len(result.Gaps))) } return nil } // convertToReportRequest 将内部检查结果转换为接口请求格式 func convertToReportRequest(result dto.ContinuityCheckResult) dto.ContinuityReportRequest { // 时间格式 const timeFormat = "2006-01-02 15:04:05" request := dto.ContinuityReportRequest{ DeviceNo: result.DeviceNo, StartTime: result.RangeStart.Format(timeFormat), EndTime: result.RangeEnd.Format(timeFormat), Support: true, // 始终支持 Continuous: result.IsContinuous, TotalVideos: result.FileCount, TotalDurationMs: result.TotalDurationMs, MaxAllowedGapMs: maxAllowedGapMs, } // 转换间隙列表 if len(result.Gaps) > 0 { request.Gaps = make([]dto.ContinuityReportGap, 0, len(result.Gaps)) for _, gap := range result.Gaps { request.Gaps = append(request.Gaps, dto.ContinuityReportGap{ BeforeFileName: gap.PreviousFileName, AfterFileName: gap.NextFileName, GapMs: int64(gap.GapSeconds * 1000), GapStartTime: gap.PreviousEndTime.Format(timeFormat), GapEndTime: gap.NextStartTime.Format(timeFormat), }) } } else { request.Gaps = []dto.ContinuityReportGap{} } return request }