chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)

This commit is contained in:
weijuesen
2026-08-10 22:30:53 +08:00
commit 84abf4454c
358 changed files with 75993 additions and 0 deletions
+170
View File
@@ -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: &notes,
}
// 从 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)
}
}