Files
VptPassiveAdapter/api/continuity_report.go
Jerry Yan 27dfda32fa feat(core): 添加视频连续性检查功能
- 实现了连续性检查循环,每5分钟执行一次检查
- 添加了跨天跨小时的文件列表获取功能
- 实现了单个设备和所有设备的连续性检查逻辑
- 添加了连续性检查结果上报API接口
- 实现了检查结果的数据结构定义和转换功能
- 配置了9点到18点的工作时间检查范围
- 添加了详细的日志记录和OpenTelemetry追踪支持
2025-12-30 10:38:30 +08:00

189 lines
5.5 KiB
Go

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 {
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 {
// 时间格式:ISO 8601
const timeFormat = "2006-01-02T15: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),
})
}
}
return request
}