package handler import ( "fmt" "net/http" "strconv" "time" "silk-server-go/internal/config" "silk-server-go/internal/middleware" "silk-server-go/internal/model" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // RegisterVideoClipRoutes 注册录像片段管理路由 func RegisterVideoClipRoutes(rg *gin.RouterGroup, db *gorm.DB, cfg *config.Config) { readPerm := middleware.RequirePermission(db, "video:read") rg.GET("/video/clips", readPerm, listClips(db)) rg.GET("/video/clips/:clipId/play", readPerm, playClip(db)) rg.POST("/video/clips/internal", createClipInternal(db, cfg)) // 白名单接口,无需权限 } // listClips 录像片段列表(query: cameraId/from/to/limit,按 startAt DESC) func listClips(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { userID := currentUserID(c) if userID == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供用户信息"}) return } limit := 50 if l, err := strconv.Atoi(c.Query("limit")); err == nil && l > 0 { limit = l if limit > 200 { limit = 200 } } q := applyRoomScope(db.Model(&model.VideoClip{}), c, "room_id").Order("start_at DESC").Limit(limit) if cameraId := c.Query("cameraId"); cameraId != "" { q = q.Where("camera_id = ?", cameraId) } if from := c.Query("from"); from != "" { q = q.Where("start_at >= ?", from) } if to := c.Query("to"); to != "" { q = q.Where("start_at <= ?", to) } clips := make([]model.VideoClip, 0) q.Find(&clips) // 为每个片段设置 playbackUrl for i := range clips { token, err := IssueVideoToken(*userID, "clip", strconv.FormatUint(uint64(clips[i].ID), 10), videoTokenTTL) if err != nil { continue } url := fmt.Sprintf("/api/v1/video/clips/%d/stream?videoToken=%s", clips[i].ID, token) clips[i].PlaybackURL = &url } c.JSON(http.StatusOK, gin.H{"items": clips, "total": len(clips)}) } } // playClip 获取片段播放地址 func playClip(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { userID := currentUserID(c) if userID == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供用户信息"}) return } clipId := c.Param("clipId") if !requireObjectAccess(c, db, "video_clip", clipId) { return } var clip model.VideoClip if db.Where("id = ?", clipId).First(&clip).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "video clip not found"}) return } format := clip.Format if format == "" { format = "mp4" } token, err := IssueVideoToken(*userID, "clip", strconv.FormatUint(uint64(clip.ID), 10), videoTokenTTL) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "生成播放令牌失败"}) return } c.JSON(http.StatusOK, gin.H{ "clipId": clip.ID, "url": fmt.Sprintf("/api/v1/video/clips/%d/stream?videoToken=%s", clip.ID, token), "format": format, "expiresAt": time.Now().Add(videoTokenTTL).UTC().Format(time.RFC3339), }) } } // createClipInternal 内部接口创建录像片段记录(校验 x-api-key header) func createClipInternal(db *gorm.DB, cfg *config.Config) gin.HandlerFunc { return func(c *gin.Context) { // 校验 x-api-key apiKey := c.GetHeader("x-api-key") if apiKey != cfg.InternalAPIKey { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid api key"}) return } var body struct { CameraID string `json:"cameraId"` Trigger string `json:"trigger"` Format string `json:"format"` StartAt string `json:"startAt"` DurationSec float64 `json:"durationSec"` SizeBytes string `json:"sizeBytes"` S3Bucket string `json:"s3Bucket"` S3Key string `json:"s3Key"` Resolution string `json:"resolution"` Notes string `json:"notes"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 去重:同一 s3Key 不重复创建 var existing model.VideoClip if db.Where("s3_key = ?", body.S3Key).First(&existing).Error == nil { c.JSON(http.StatusOK, existing) return } // 解析开始时间 startAt, err := time.Parse(time.RFC3339, body.StartAt) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid startAt, expected RFC3339 format"}) return } // 设置默认值 trigger := body.Trigger if trigger == "" { trigger = "schedule" } format := body.Format if format == "" { format = "mp4" } notes := body.Notes if notes == "" { notes = "自动录制归档" } clip := model.VideoClip{ CameraID: body.CameraID, Trigger: trigger, Format: format, StartAt: startAt, DurationSec: body.DurationSec, S3Bucket: &body.S3Bucket, S3Key: &body.S3Key, Notes: ¬es, } // 从 camera 查询 room_id var camera model.Camera if err := db.Where("id = ?", body.CameraID).First(&camera).Error; err == nil { clip.RoomID = camera.RoomID } if body.SizeBytes != "" { if n, err := strconv.ParseInt(body.SizeBytes, 10, 64); err == nil { clip.SizeBytes = &n } } if body.Resolution != "" { clip.Resolution = &body.Resolution } if err := db.Create(&clip).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, clip) } }