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
+396
View File
@@ -0,0 +1,396 @@
package handler
import (
"fmt"
"log/slog"
"net/http"
"strconv"
"time"
"silk-server-go/internal/middleware"
"silk-server-go/internal/model"
"silk-server-go/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// RegisterVideoCameraRoutes 注册摄像头管理路由
func RegisterVideoCameraRoutes(rg *gin.RouterGroup, db *gorm.DB, media *service.MediaService) {
readPerm := middleware.RequirePermission(db, "video:read")
writePerm := middleware.RequirePermission(db, "video:record")
rg.GET("/video/cameras", readPerm, listCameras(db, media))
rg.GET("/video/cameras/:id", readPerm, getCamera(db))
rg.POST("/video/cameras", writePerm, createCamera(db, media))
rg.PATCH("/video/cameras/:id", writePerm, updateCamera(db, media))
rg.DELETE("/video/cameras/:id", writePerm, deleteCamera(db, media))
rg.POST("/video/cameras/:id/play", readPerm, playCamera(db, media))
rg.POST("/video/cameras/:id/live", readPerm, playCamera(db, media))
rg.GET("/video/cameras/:id/playback", readPerm, playbackCamera(db))
rg.GET("/video/wvp-config", readPerm, getWvpConfig(media))
rg.GET("/video/wvp/devices", readPerm, listWvpDevices(media))
rg.GET("/video/wvp/devices/:deviceId/channels", readPerm, listWvpChannels(media))
rg.POST("/video/wvp/devices/:deviceId/sync", writePerm, syncWvpDevice(media))
}
// listCameras 摄像头列表(按 createdAt DESC,同步 WVP 设备信息:在线状态 + 共有参数)
func listCameras(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
var cameras []model.Camera
db.Order("created_at DESC").Find(&cameras)
// 先用 DB 中的 is_online 初始化 Online 字段(gorm:"-" 不会自动填充)
for i := range cameras {
cameras[i].Online = cameras[i].IsOnline
}
// 同步 WVP 设备信息(WVP → silk,失败时保留 DB 状态)
if devMap, err := media.SyncWvpDevices(); err == nil {
for i := range cameras {
if cameras[i].GbDeviceID == nil || *cameras[i].GbDeviceID == "" {
continue
}
info, ok := devMap[*cameras[i].GbDeviceID]
if !ok {
continue
}
// 同步在线状态
cameras[i].Online = info.OnLine
// 同步共有参数(WVP → silk),仅当 WVP 侧有值时才覆盖
updates := map[string]interface{}{}
if info.OnLine != cameras[i].IsOnline {
updates["is_online"] = info.OnLine
cameras[i].IsOnline = info.OnLine
}
if info.Name != "" && cameras[i].Name != info.Name {
updates["name"] = info.Name
cameras[i].Name = info.Name
}
if info.Manufacturer != "" {
if cameras[i].GbManufacturer == nil || *cameras[i].GbManufacturer != info.Manufacturer {
updates["gb_manufacturer"] = info.Manufacturer
cameras[i].GbManufacturer = &info.Manufacturer
}
}
if info.Password != "" {
if cameras[i].GbAuthPassword == nil || *cameras[i].GbAuthPassword != info.Password {
updates["gb_auth_password"] = info.Password
cameras[i].GbAuthPassword = &info.Password
}
}
if len(updates) > 0 {
db.Model(&model.Camera{}).Where("id = ?", cameras[i].ID).Updates(updates)
}
}
} else {
slog.Warn("同步 WVP 设备信息失败,保留 DB 状态", "error", err)
}
c.JSON(http.StatusOK, cameras)
}
}
// getCamera 摄像头详情
func getCamera(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
var camera model.Camera
if db.Where("id = ?", id).First(&camera).Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "camera not found"})
return
}
c.JSON(http.StatusOK, camera)
}
}
// createCamera 新建摄像头
func createCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
var camera model.Camera
if err := c.ShouldBindJSON(&camera); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
camera.ID = 0 // 让数据库自动生成
if camera.RoomID == nil {
defaultRoom := "1"
camera.RoomID = &defaultRoom
}
if err := db.Create(&camera).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"})
return
}
// 预添加设备到 WVP(设置独立密码,摄像头注册前 WVP 已有设备记录)
if camera.GbDeviceID != nil && *camera.GbDeviceID != "" {
manufacturer := ""
if camera.GbManufacturer != nil {
manufacturer = *camera.GbManufacturer
}
password := ""
if camera.GbAuthPassword != nil {
password = *camera.GbAuthPassword
}
if err := media.AddWvpDevice(*camera.GbDeviceID, camera.Name, manufacturer, password); err != nil {
// 设备可能已存在(之前添加过),尝试更新
if err2 := media.UpdateWvpDevice(*camera.GbDeviceID, camera.Name, manufacturer, password); err2 != nil {
slog.Warn("预添加/同步设备到 WVP 均失败", "deviceId", *camera.GbDeviceID, "addError", err, "updateError", err2)
}
}
}
c.JSON(http.StatusCreated, camera)
}
}
// updateCamera 更新摄像头
func updateCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
var camera model.Camera
if db.Where("id = ?", id).First(&camera).Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "camera not found"})
return
}
updates, err := bindUpdates(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(updates) > 0 {
if err := db.Model(&model.Camera{}).Where("id = ?", id).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败: " + err.Error()})
return
}
}
db.Where("id = ?", id).First(&camera)
// 同步共有参数到 WVPsilk → WVP,更新了 name/manufacturer/password 任一字段就触发)
if camera.GbDeviceID != nil && *camera.GbDeviceID != "" {
needSync := false
for _, key := range []string{"name", "gb_manufacturer", "gb_auth_password"} {
if _, ok := updates[key]; ok {
needSync = true
break
}
}
if needSync {
manufacturer := ""
if camera.GbManufacturer != nil {
manufacturer = *camera.GbManufacturer
}
password := ""
if camera.GbAuthPassword != nil {
password = *camera.GbAuthPassword
}
if err := media.UpdateWvpDevice(*camera.GbDeviceID, camera.Name, manufacturer, password); err != nil {
slog.Warn("同步摄像头信息到 WVP 失败", "deviceId", *camera.GbDeviceID, "error", err)
}
}
}
c.JSON(http.StatusOK, camera)
}
}
// deleteCamera 删除摄像头
func deleteCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
var camera model.Camera
if db.Where("id = ?", id).First(&camera).Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "camera not found"})
return
}
// 同步删除 WVP 中的设备
if camera.GbDeviceID != nil && *camera.GbDeviceID != "" {
if err := media.DeleteWvpDevice(*camera.GbDeviceID); err != nil {
slog.Warn("从 WVP 删除设备失败", "deviceId", *camera.GbDeviceID, "error", err)
}
}
db.Where("id = ?", id).Delete(&model.Camera{})
c.JSON(http.StatusOK, gin.H{"id": id})
}
}
// playCamera 播放摄像头实时流(body: {format},调用 media.StartPlay
func playCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
var camera model.Camera
if db.Where("id = ?", id).First(&camera).Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "camera not found"})
return
}
if !camera.IsOnline {
c.JSON(http.StatusBadRequest, gin.H{"error": "摄像头离线,无法播放"})
return
}
var body struct {
Format string `json:"format"`
}
c.ShouldBindJSON(&body)
format := body.Format
if format == "" {
format = "hls"
}
expiresAt := time.Now().Add(30 * time.Minute).UTC().Format(time.RFC3339)
// GB28181 摄像头:通过 WVP 媒体服务器播放
if camera.GbDeviceID != nil && camera.GbChannelID != nil &&
*camera.GbDeviceID != "" && *camera.GbChannelID != "" {
result, err := media.StartPlay(*camera.GbDeviceID, *camera.GbChannelID)
if err != nil {
// StartPlay 失败不等于摄像头离线,可能是 WVP/ZLM 瞬时问题,不修改 is_online
slog.Warn("StartPlay 失败", "deviceId", *camera.GbDeviceID, "channelId", *camera.GbChannelID, "error", err)
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "播放失败: " + err.Error()})
return
}
url := result.HLS
if format == "flv" && result.FLV != "" {
url = result.FLV
} else if format == "webrtc" && result.WebRtc != "" {
url = result.WebRtc
}
c.JSON(http.StatusOK, gin.H{
"cameraId": camera.ID,
"gbDeviceId": camera.GbDeviceID,
"gbChannelId": camera.GbChannelID,
"format": format,
"url": url,
"expiresAt": expiresAt,
})
return
}
// Fallback:使用摄像头自身的流地址
var url string
switch format {
case "flv":
if camera.FlvURL != nil {
url = *camera.FlvURL
}
case "webrtc":
if camera.WebrtcURL != nil {
url = *camera.WebrtcURL
}
default:
if camera.HlsURL != nil {
url = *camera.HlsURL
}
}
c.JSON(http.StatusOK, gin.H{
"cameraId": camera.ID,
"gbDeviceId": nil,
"gbChannelId": nil,
"format": format,
"url": url,
"expiresAt": expiresAt,
"mock": url == "",
})
}
}
// playbackCamera 查询摄像头的历史录像片段(query: from/to/limit
func playbackCamera(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
limit := 50
if l, err := strconv.Atoi(c.Query("limit")); err == nil && l > 0 {
limit = l
if limit > 200 {
limit = 200
}
}
q := db.Where("camera_id = ?", id).Order("start_at DESC").Limit(limit)
if from := c.Query("from"); from != "" {
q = q.Where("start_at >= ?", from)
}
if to := c.Query("to"); to != "" {
q = q.Where("start_at <= ?", to)
}
var clips []model.VideoClip
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, clips)
}
}
// getWvpConfig 查询 WVP SIP 配置(供前端展示,便于配置摄像头硬件)
func getWvpConfig(media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
config, err := media.GetServerConfig()
if err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "获取 WVP 配置失败: " + err.Error()})
return
}
sip, ok := config["sip"].(map[string]interface{})
if !ok {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "WVP 配置中未找到 SIP 部分"})
return
}
c.JSON(http.StatusOK, gin.H{
"sipId": sip["id"],
"sipDomain": sip["domain"],
"sipPassword": sip["password"],
"sipPort": sip["port"],
"sipShowIp": sip["showIp"],
})
}
}
// listWvpDevices 查询 WVP 已注册设备列表
func listWvpDevices(media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
query := c.Query("query")
devices, err := media.ListWvpDevices(query)
if err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "获取 WVP 设备列表失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, devices)
}
}
// listWvpChannels 查询 WVP 设备的通道列表
func listWvpChannels(media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
deviceId := c.Param("deviceId")
if deviceId == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "deviceId is required"})
return
}
channels, err := media.ListWvpChannels(deviceId)
if err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "获取通道列表失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, channels)
}
}
// syncWvpDevice 触发 WVP 设备通道同步
func syncWvpDevice(media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
deviceId := c.Param("deviceId")
if deviceId == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "deviceId is required"})
return
}
if err := media.SyncWvpDevice(deviceId); err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "同步失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
}