You've already forked VptPassiveAdapter
feat(core): 添加视频连续性检查功能
- 实现了连续性检查循环,每5分钟执行一次检查 - 添加了跨天跨小时的文件列表获取功能 - 实现了单个设备和所有设备的连续性检查逻辑 - 添加了连续性检查结果上报API接口 - 实现了检查结果的数据结构定义和转换功能 - 配置了9点到18点的工作时间检查范围 - 添加了详细的日志记录和OpenTelemetry追踪支持
This commit is contained in:
188
api/continuity_report.go
Normal file
188
api/continuity_report.go
Normal file
@@ -0,0 +1,188 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user