chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
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) {
|
||||
limit := 50
|
||||
if l, err := strconv.Atoi(c.Query("limit")); err == nil && l > 0 {
|
||||
limit = l
|
||||
if limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
}
|
||||
|
||||
q := db.Model(&model.VideoClip{}).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 {
|
||||
url := fmt.Sprintf("/api/v1/video/clips/%d/stream", clips[i].ID)
|
||||
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) {
|
||||
clipId := c.Param("clipId")
|
||||
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"
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"clipId": clip.ID,
|
||||
"url": fmt.Sprintf("/api/v1/video/clips/%d/stream", clip.ID),
|
||||
"format": format,
|
||||
"expiresAt": time.Now().Add(60 * time.Minute).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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user