chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)
This commit is contained in:
@@ -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)
|
||||
// 同步共有参数到 WVP(silk → 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})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user