feat(数据库): 添加MySQL和SQL Server数据库支持并实现摄像头管理功能
实现数据库集成功能,支持MySQL和SQL Server作为后端存储。主要变更包括: 1. 新增数据库配置结构和初始化逻辑 2. 添加摄像头管理API接口和Web页面 3. 实现流配置与数据库的同步机制 4. 新增数据库初始化SQL脚本和文档 5. 优化日志系统和启动流程 同时更新了前端界面,完成中文本地化适配,并添加了相关API文档。ziyun-rtsp-web
parent
fe342c3ed2
commit
2a37fbf7be
Binary file not shown.
@ -0,0 +1,415 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CameraRequest 摄像头请求结构
|
||||||
|
type CameraRequest struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
IP string `json:"ip" binding:"required"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
RTSPURL string `json:"rtsp_url" binding:"required"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CameraResponse 摄像头响应结构
|
||||||
|
type CameraResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password,omitempty"` // 密码在响应中可选
|
||||||
|
RTSPURL string `json:"rtsp_url"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPAPIServerCameras 获取所有摄像头列表
|
||||||
|
func HTTPAPIServerCameras(c *gin.Context) {
|
||||||
|
if !Storage.Server.DatabaseEnabled {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||||
|
"error": "Database is not enabled",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if Storage.dbManager == nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": "Database manager not initialized",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cameras, err := Storage.dbManager.GetAllCameras()
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "api",
|
||||||
|
"func": "HTTPAPIServerCameras",
|
||||||
|
"call": "GetAllCameras",
|
||||||
|
}).Errorln(err.Error())
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为响应格式
|
||||||
|
var response []CameraResponse
|
||||||
|
for _, camera := range cameras {
|
||||||
|
status := "offline"
|
||||||
|
if stream, exists := Storage.Streams[camera.ID]; exists {
|
||||||
|
if len(stream.Channels) > 0 {
|
||||||
|
for _, channel := range stream.Channels {
|
||||||
|
if channel.runLock {
|
||||||
|
status = "online"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = append(response, CameraResponse{
|
||||||
|
ID: camera.ID,
|
||||||
|
Name: camera.Name,
|
||||||
|
IP: camera.IP,
|
||||||
|
Username: camera.Username,
|
||||||
|
// Password: camera.Password, // 不返回密码
|
||||||
|
RTSPURL: camera.RTSPURL,
|
||||||
|
Enabled: camera.Enabled,
|
||||||
|
Status: status,
|
||||||
|
CreatedAt: camera.CreatedAt,
|
||||||
|
UpdatedAt: camera.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"cameras": response,
|
||||||
|
"count": len(response),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPAPIServerCameraAdd 添加新摄像头
|
||||||
|
func HTTPAPIServerCameraAdd(c *gin.Context) {
|
||||||
|
if !Storage.Server.DatabaseEnabled {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||||
|
"error": "Database is not enabled",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req CameraRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成新的UUID
|
||||||
|
cameraID := uuid.New().String()
|
||||||
|
|
||||||
|
// 创建摄像头记录
|
||||||
|
camera := Camera{
|
||||||
|
ID: cameraID,
|
||||||
|
Name: req.Name,
|
||||||
|
IP: req.IP,
|
||||||
|
Username: req.Username,
|
||||||
|
Password: req.Password,
|
||||||
|
RTSPURL: req.RTSPURL,
|
||||||
|
Enabled: req.Enabled,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
err := Storage.dbManager.CreateCamera(&camera)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "api",
|
||||||
|
"func": "HTTPAPIServerCameraAdd",
|
||||||
|
"call": "CreateCamera",
|
||||||
|
}).Errorln(err.Error())
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为流配置并添加到内存
|
||||||
|
stream := CameraToStream(camera)
|
||||||
|
err = Storage.StreamAdd(cameraID, stream)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "api",
|
||||||
|
"func": "HTTPAPIServerCameraAdd",
|
||||||
|
"call": "StreamAdd",
|
||||||
|
}).Errorln(err.Error())
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, gin.H{
|
||||||
|
"message": "Camera added successfully",
|
||||||
|
"camera_id": cameraID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPAPIServerCameraUpdate 更新摄像头
|
||||||
|
func HTTPAPIServerCameraUpdate(c *gin.Context) {
|
||||||
|
if !Storage.Server.DatabaseEnabled {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||||
|
"error": "Database is not enabled",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraID := c.Param("uuid")
|
||||||
|
if cameraID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "Camera ID is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req CameraRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查摄像头是否存在
|
||||||
|
existingCamera, err := Storage.dbManager.GetCameraByID(cameraID)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "api",
|
||||||
|
"func": "HTTPAPIServerCameraUpdate",
|
||||||
|
"call": "GetCameraByID",
|
||||||
|
}).Errorln(err.Error())
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if existingCamera == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{
|
||||||
|
"error": "Camera not found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新摄像头信息
|
||||||
|
updatedCamera := Camera{
|
||||||
|
ID: cameraID,
|
||||||
|
Name: req.Name,
|
||||||
|
IP: req.IP,
|
||||||
|
Username: req.Username,
|
||||||
|
Password: req.Password,
|
||||||
|
RTSPURL: req.RTSPURL,
|
||||||
|
Enabled: req.Enabled,
|
||||||
|
CreatedAt: existingCamera.CreatedAt,
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新数据库
|
||||||
|
err = Storage.dbManager.UpdateCamera(&updatedCamera)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "api",
|
||||||
|
"func": "HTTPAPIServerCameraUpdate",
|
||||||
|
"call": "UpdateCamera",
|
||||||
|
}).Errorln(err.Error())
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新内存中的流配置
|
||||||
|
stream := CameraToStream(updatedCamera)
|
||||||
|
err = Storage.StreamEdit(cameraID, stream)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "api",
|
||||||
|
"func": "HTTPAPIServerCameraUpdate",
|
||||||
|
"call": "StreamEdit",
|
||||||
|
}).Errorln(err.Error())
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "Camera updated successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPAPIServerCameraDelete 删除摄像头
|
||||||
|
func HTTPAPIServerCameraDelete(c *gin.Context) {
|
||||||
|
if !Storage.Server.DatabaseEnabled {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||||
|
"error": "Database is not enabled",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraID := c.Param("uuid")
|
||||||
|
if cameraID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "Camera ID is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查摄像头是否存在
|
||||||
|
existingCamera, err := Storage.dbManager.GetCameraByID(cameraID)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "api",
|
||||||
|
"func": "HTTPAPIServerCameraDelete",
|
||||||
|
"call": "GetCameraByID",
|
||||||
|
}).Errorln(err.Error())
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if existingCamera == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{
|
||||||
|
"error": "Camera not found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从内存中删除流
|
||||||
|
err = Storage.StreamDelete(cameraID)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "api",
|
||||||
|
"func": "HTTPAPIServerCameraDelete",
|
||||||
|
"call": "StreamDelete",
|
||||||
|
}).Errorln(err.Error())
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "Camera deleted successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPAPIServerCameraGet 获取单个摄像头信息
|
||||||
|
func HTTPAPIServerCameraGet(c *gin.Context) {
|
||||||
|
if !Storage.Server.DatabaseEnabled {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||||
|
"error": "Database is not enabled",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraID := c.Param("uuid")
|
||||||
|
if cameraID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "Camera ID is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
camera, err := Storage.dbManager.GetCameraByID(cameraID)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "api",
|
||||||
|
"func": "HTTPAPIServerCameraGet",
|
||||||
|
"call": "GetCameraByID",
|
||||||
|
}).Errorln(err.Error())
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if camera == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{
|
||||||
|
"error": "Camera not found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查流状态
|
||||||
|
status := "offline"
|
||||||
|
if stream, exists := Storage.Streams[camera.ID]; exists {
|
||||||
|
if len(stream.Channels) > 0 {
|
||||||
|
for _, channel := range stream.Channels {
|
||||||
|
if channel.runLock {
|
||||||
|
status = "online"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response := CameraResponse{
|
||||||
|
ID: camera.ID,
|
||||||
|
Name: camera.Name,
|
||||||
|
IP: camera.IP,
|
||||||
|
Username: camera.Username,
|
||||||
|
// Password: camera.Password, // 不返回密码
|
||||||
|
RTSPURL: camera.RTSPURL,
|
||||||
|
Enabled: camera.Enabled,
|
||||||
|
Status: status,
|
||||||
|
CreatedAt: camera.CreatedAt,
|
||||||
|
UpdatedAt: camera.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPAPIServerCameraRefresh 刷新摄像头列表(从数据库重新加载)
|
||||||
|
func HTTPAPIServerCameraRefresh(c *gin.Context) {
|
||||||
|
if !Storage.Server.DatabaseEnabled {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||||
|
"error": "Database is not enabled",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Storage.RefreshStreamsFromDatabase()
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "api",
|
||||||
|
"func": "HTTPAPIServerCameraRefresh",
|
||||||
|
"call": "RefreshStreamsFromDatabase",
|
||||||
|
}).Errorln(err.Error())
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "Cameras refreshed successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPAPIServerDatabaseStatus 获取数据库状态
|
||||||
|
func HTTPAPIServerDatabaseStatus(c *gin.Context) {
|
||||||
|
status := Storage.GetDatabaseStatus()
|
||||||
|
c.JSON(http.StatusOK, status)
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPAPICameraManagement 摄像头管理页面
|
||||||
|
func HTTPAPICameraManagement(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "cameras.tmpl", gin.H{
|
||||||
|
"title": "摄像头管理",
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -0,0 +1,271 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/denisenkom/go-mssqldb"
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DatabaseConfig 数据库配置结构
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Type string `json:"type"` // mysql 或 sqlserver
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Database string `json:"database"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
SSLMode string `json:"ssl_mode,omitempty"` // for mysql
|
||||||
|
}
|
||||||
|
|
||||||
|
// Camera 摄像头数据库模型
|
||||||
|
type Camera struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
Name string `json:"name" db:"name"`
|
||||||
|
IP string `json:"ip" db:"ip"`
|
||||||
|
Username string `json:"username" db:"username"`
|
||||||
|
Password string `json:"password" db:"password"`
|
||||||
|
RTSPURL string `json:"rtsp_url" db:"rtsp_url"`
|
||||||
|
Enabled bool `json:"enabled" db:"enabled"`
|
||||||
|
OnDemand bool `json:"on_demand" db:"on_demand"`
|
||||||
|
Audio bool `json:"audio" db:"audio"`
|
||||||
|
Debug bool `json:"debug" db:"debug"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DatabaseManager 数据库管理器
|
||||||
|
type DatabaseManager struct {
|
||||||
|
db *sql.DB
|
||||||
|
config DatabaseConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDatabaseManager 创建数据库管理器
|
||||||
|
func NewDatabaseManager(config DatabaseConfig) (*DatabaseManager, error) {
|
||||||
|
if !config.Enabled {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var dsn string
|
||||||
|
switch config.Type {
|
||||||
|
case "mysql":
|
||||||
|
dsn = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||||
|
config.Username, config.Password, config.Host, config.Port, config.Database)
|
||||||
|
if config.SSLMode != "" {
|
||||||
|
dsn += "&tls=" + config.SSLMode
|
||||||
|
}
|
||||||
|
case "sqlserver":
|
||||||
|
dsn = fmt.Sprintf("server=%s;port=%d;database=%s;user id=%s;password=%s",
|
||||||
|
config.Host, config.Port, config.Database, config.Username, config.Password)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported database type: %s", config.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := sql.Open(config.Type, dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试连接
|
||||||
|
if err := db.Ping(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to ping database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置连接池参数
|
||||||
|
db.SetMaxOpenConns(25)
|
||||||
|
db.SetMaxIdleConns(5)
|
||||||
|
db.SetConnMaxLifetime(5 * time.Minute)
|
||||||
|
|
||||||
|
dm := &DatabaseManager{
|
||||||
|
db: db,
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化数据库表
|
||||||
|
if err := dm.initTables(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize tables: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// initTables 初始化数据库表
|
||||||
|
func (dm *DatabaseManager) initTables() error {
|
||||||
|
var createTableSQL string
|
||||||
|
|
||||||
|
switch dm.config.Type {
|
||||||
|
case "mysql":
|
||||||
|
createTableSQL = `
|
||||||
|
CREATE TABLE IF NOT EXISTS cameras (
|
||||||
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
ip VARCHAR(45) NOT NULL,
|
||||||
|
username VARCHAR(100),
|
||||||
|
password VARCHAR(100),
|
||||||
|
rtsp_url TEXT NOT NULL,
|
||||||
|
enabled BOOLEAN DEFAULT TRUE,
|
||||||
|
on_demand BOOLEAN DEFAULT FALSE,
|
||||||
|
audio BOOLEAN DEFAULT TRUE,
|
||||||
|
debug BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_enabled (enabled),
|
||||||
|
INDEX idx_ip (ip)
|
||||||
|
)`
|
||||||
|
case "sqlserver":
|
||||||
|
createTableSQL = `
|
||||||
|
IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='cameras' AND xtype='U')
|
||||||
|
CREATE TABLE cameras (
|
||||||
|
id NVARCHAR(36) PRIMARY KEY,
|
||||||
|
name NVARCHAR(255) NOT NULL,
|
||||||
|
ip NVARCHAR(45) NOT NULL,
|
||||||
|
username NVARCHAR(100),
|
||||||
|
password NVARCHAR(100),
|
||||||
|
rtsp_url NTEXT NOT NULL,
|
||||||
|
enabled BIT DEFAULT 1,
|
||||||
|
on_demand BIT DEFAULT 0,
|
||||||
|
audio BIT DEFAULT 1,
|
||||||
|
debug BIT DEFAULT 0,
|
||||||
|
created_at DATETIME2 DEFAULT GETDATE(),
|
||||||
|
updated_at DATETIME2 DEFAULT GETDATE()
|
||||||
|
)`
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := dm.db.Exec(createTableSQL)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllCameras 获取所有摄像头
|
||||||
|
func (dm *DatabaseManager) GetAllCameras() ([]Camera, error) {
|
||||||
|
query := `SELECT id, name, ip, username, password, rtsp_url, enabled, on_demand, audio, debug, created_at, updated_at FROM cameras WHERE enabled = ?`
|
||||||
|
|
||||||
|
rows, err := dm.db.Query(query, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var cameras []Camera
|
||||||
|
for rows.Next() {
|
||||||
|
var camera Camera
|
||||||
|
err := rows.Scan(&camera.ID, &camera.Name, &camera.IP, &camera.Username,
|
||||||
|
&camera.Password, &camera.RTSPURL, &camera.Enabled, &camera.OnDemand,
|
||||||
|
&camera.Audio, &camera.Debug, &camera.CreatedAt, &camera.UpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cameras = append(cameras, camera)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cameras, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCameraByID 根据ID获取摄像头
|
||||||
|
func (dm *DatabaseManager) GetCameraByID(id string) (*Camera, error) {
|
||||||
|
query := `SELECT id, name, ip, username, password, rtsp_url, enabled, on_demand, audio, debug, created_at, updated_at FROM cameras WHERE id = ?`
|
||||||
|
|
||||||
|
var camera Camera
|
||||||
|
err := dm.db.QueryRow(query, id).Scan(&camera.ID, &camera.Name, &camera.IP,
|
||||||
|
&camera.Username, &camera.Password, &camera.RTSPURL, &camera.Enabled,
|
||||||
|
&camera.OnDemand, &camera.Audio, &camera.Debug, &camera.CreatedAt, &camera.UpdatedAt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &camera, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCamera 创建摄像头
|
||||||
|
func (dm *DatabaseManager) CreateCamera(camera *Camera) error {
|
||||||
|
if camera.ID == "" {
|
||||||
|
camera.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
camera.CreatedAt = time.Now()
|
||||||
|
camera.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
query := `INSERT INTO cameras (id, name, ip, username, password, rtsp_url, enabled, on_demand, audio, debug, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
|
||||||
|
_, err := dm.db.Exec(query, camera.ID, camera.Name, camera.IP, camera.Username,
|
||||||
|
camera.Password, camera.RTSPURL, camera.Enabled, camera.OnDemand,
|
||||||
|
camera.Audio, camera.Debug, camera.CreatedAt, camera.UpdatedAt)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCamera 更新摄像头
|
||||||
|
func (dm *DatabaseManager) UpdateCamera(camera *Camera) error {
|
||||||
|
camera.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
query := `UPDATE cameras SET name=?, ip=?, username=?, password=?, rtsp_url=?,
|
||||||
|
enabled=?, on_demand=?, audio=?, debug=?, updated_at=? WHERE id=?`
|
||||||
|
|
||||||
|
_, err := dm.db.Exec(query, camera.Name, camera.IP, camera.Username,
|
||||||
|
camera.Password, camera.RTSPURL, camera.Enabled, camera.OnDemand,
|
||||||
|
camera.Audio, camera.Debug, camera.UpdatedAt, camera.ID)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCamera 删除摄像头
|
||||||
|
func (dm *DatabaseManager) DeleteCamera(id string) error {
|
||||||
|
query := `DELETE FROM cameras WHERE id = ?`
|
||||||
|
_, err := dm.db.Exec(query, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭数据库连接
|
||||||
|
func (dm *DatabaseManager) Close() error {
|
||||||
|
if dm.db != nil {
|
||||||
|
return dm.db.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CameraToStream 将摄像头转换为流配置
|
||||||
|
func CameraToStream(camera Camera) StreamST {
|
||||||
|
return StreamST{
|
||||||
|
Name: camera.Name,
|
||||||
|
Channels: map[string]ChannelST{
|
||||||
|
"0": {
|
||||||
|
Name: camera.Name,
|
||||||
|
URL: camera.RTSPURL,
|
||||||
|
OnDemand: camera.OnDemand,
|
||||||
|
Debug: camera.Debug,
|
||||||
|
Audio: camera.Audio,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamToCamera 将流配置转换为摄像头
|
||||||
|
func StreamToCamera(id string, stream StreamST) Camera {
|
||||||
|
camera := Camera{
|
||||||
|
ID: id,
|
||||||
|
Name: stream.Name,
|
||||||
|
Enabled: true,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取第一个通道的配置
|
||||||
|
if len(stream.Channels) > 0 {
|
||||||
|
for _, channel := range stream.Channels {
|
||||||
|
camera.RTSPURL = channel.URL
|
||||||
|
camera.OnDemand = channel.OnDemand
|
||||||
|
camera.Debug = channel.Debug
|
||||||
|
camera.Audio = channel.Audio
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return camera
|
||||||
|
}
|
||||||
@ -0,0 +1,237 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitDatabase 初始化数据库连接
|
||||||
|
func (obj *StorageST) InitDatabase() error {
|
||||||
|
obj.mutex.Lock()
|
||||||
|
defer obj.mutex.Unlock()
|
||||||
|
|
||||||
|
if !obj.Database.Enabled {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "database",
|
||||||
|
"func": "InitDatabase",
|
||||||
|
}).Infoln("Database is disabled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dbManager, err := NewDatabaseManager(obj.Database)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "database",
|
||||||
|
"func": "InitDatabase",
|
||||||
|
"call": "NewDatabaseManager",
|
||||||
|
}).Errorln(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.dbManager = dbManager
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "database",
|
||||||
|
"func": "InitDatabase",
|
||||||
|
}).Infoln("Database initialized successfully")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadStreamsFromDatabase 从数据库加载流配置
|
||||||
|
func (obj *StorageST) LoadStreamsFromDatabase() error {
|
||||||
|
obj.mutex.Lock()
|
||||||
|
defer obj.mutex.Unlock()
|
||||||
|
|
||||||
|
if obj.dbManager == nil {
|
||||||
|
return fmt.Errorf("database manager not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
cameras, err := obj.dbManager.GetAllCameras()
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "database",
|
||||||
|
"func": "LoadStreamsFromDatabase",
|
||||||
|
"call": "GetAllCameras",
|
||||||
|
}).Errorln(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空现有流配置(如果启用数据库模式)
|
||||||
|
if obj.Server.DatabaseEnabled {
|
||||||
|
obj.Streams = make(map[string]StreamST)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将摄像头转换为流配置
|
||||||
|
for _, camera := range cameras {
|
||||||
|
stream := CameraToStream(camera)
|
||||||
|
obj.Streams[camera.ID] = stream
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "database",
|
||||||
|
"func": "LoadStreamsFromDatabase",
|
||||||
|
"camera": camera.Name,
|
||||||
|
"id": camera.ID,
|
||||||
|
}).Infoln("Loaded camera from database")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "database",
|
||||||
|
"func": "LoadStreamsFromDatabase",
|
||||||
|
"count": len(cameras),
|
||||||
|
}).Infoln("Loaded cameras from database")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncStreamToDatabase 同步流配置到数据库
|
||||||
|
func (obj *StorageST) SyncStreamToDatabase(streamID string, stream StreamST) error {
|
||||||
|
if obj.dbManager == nil || !obj.Server.DatabaseEnabled {
|
||||||
|
return nil // 数据库未启用,跳过同步
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查摄像头是否已存在
|
||||||
|
existingCamera, err := obj.dbManager.GetCameraByID(streamID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
camera := StreamToCamera(streamID, stream)
|
||||||
|
|
||||||
|
if existingCamera == nil {
|
||||||
|
// 创建新摄像头
|
||||||
|
err = obj.dbManager.CreateCamera(&camera)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "database",
|
||||||
|
"func": "SyncStreamToDatabase",
|
||||||
|
"call": "CreateCamera",
|
||||||
|
"id": streamID,
|
||||||
|
}).Errorln(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "database",
|
||||||
|
"func": "SyncStreamToDatabase",
|
||||||
|
"id": streamID,
|
||||||
|
"name": camera.Name,
|
||||||
|
}).Infoln("Created camera in database")
|
||||||
|
} else {
|
||||||
|
// 更新现有摄像头
|
||||||
|
camera.CreatedAt = existingCamera.CreatedAt // 保持创建时间
|
||||||
|
err = obj.dbManager.UpdateCamera(&camera)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "database",
|
||||||
|
"func": "SyncStreamToDatabase",
|
||||||
|
"call": "UpdateCamera",
|
||||||
|
"id": streamID,
|
||||||
|
}).Errorln(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "database",
|
||||||
|
"func": "SyncStreamToDatabase",
|
||||||
|
"id": streamID,
|
||||||
|
"name": camera.Name,
|
||||||
|
}).Infoln("Updated camera in database")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteStreamFromDatabase 从数据库删除流配置
|
||||||
|
func (obj *StorageST) DeleteStreamFromDatabase(streamID string) error {
|
||||||
|
if obj.dbManager == nil || !obj.Server.DatabaseEnabled {
|
||||||
|
return nil // 数据库未启用,跳过删除
|
||||||
|
}
|
||||||
|
|
||||||
|
err := obj.dbManager.DeleteCamera(streamID)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "database",
|
||||||
|
"func": "DeleteStreamFromDatabase",
|
||||||
|
"call": "DeleteCamera",
|
||||||
|
"id": streamID,
|
||||||
|
}).Errorln(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "database",
|
||||||
|
"func": "DeleteStreamFromDatabase",
|
||||||
|
"id": streamID,
|
||||||
|
}).Infoln("Deleted camera from database")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshStreamsFromDatabase 刷新数据库中的流配置
|
||||||
|
func (obj *StorageST) RefreshStreamsFromDatabase() error {
|
||||||
|
if obj.dbManager == nil || !obj.Server.DatabaseEnabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止所有现有流
|
||||||
|
obj.StopAll()
|
||||||
|
|
||||||
|
// 等待流停止
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
// 重新加载流配置
|
||||||
|
err := obj.LoadStreamsFromDatabase()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动所有流
|
||||||
|
obj.StreamChannelRunAll()
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "database",
|
||||||
|
"func": "RefreshStreamsFromDatabase",
|
||||||
|
}).Infoln("Refreshed streams from database")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDatabaseStatus 获取数据库状态
|
||||||
|
func (obj *StorageST) GetDatabaseStatus() map[string]interface{} {
|
||||||
|
status := map[string]interface{}{
|
||||||
|
"enabled": obj.Database.Enabled,
|
||||||
|
"type": obj.Database.Type,
|
||||||
|
"host": obj.Database.Host,
|
||||||
|
"port": obj.Database.Port,
|
||||||
|
"database": obj.Database.Database,
|
||||||
|
"connected": false,
|
||||||
|
"error": nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.dbManager != nil && obj.dbManager.db != nil {
|
||||||
|
if err := obj.dbManager.db.Ping(); err == nil {
|
||||||
|
status["connected"] = true
|
||||||
|
} else {
|
||||||
|
status["error"] = err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseDatabase 关闭数据库连接
|
||||||
|
func (obj *StorageST) CloseDatabase() error {
|
||||||
|
obj.mutex.Lock()
|
||||||
|
defer obj.mutex.Unlock()
|
||||||
|
|
||||||
|
if obj.dbManager != nil {
|
||||||
|
err := obj.dbManager.Close()
|
||||||
|
obj.dbManager = nil
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"module": "database",
|
||||||
|
"func": "CloseDatabase",
|
||||||
|
}).Infoln("Database connection closed")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -0,0 +1,408 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>摄像头管理 - RTSPtoWeb</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
.camera-card {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.camera-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.status-online {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
.status-offline {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
.btn-group-actions {
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.modal-header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.form-floating {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="/">
|
||||||
|
<i class="fas fa-video"></i> RTSPtoWeb
|
||||||
|
</a>
|
||||||
|
<div class="navbar-nav ms-auto">
|
||||||
|
<a class="nav-link" href="/">首页</a>
|
||||||
|
<a class="nav-link active" href="/pages/cameras">摄像头管理</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2><i class="fas fa-camera"></i> 摄像头管理</h2>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addCameraModal">
|
||||||
|
<i class="fas fa-plus"></i> 添加摄像头
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-info" onclick="refreshCameras()">
|
||||||
|
<i class="fas fa-sync-alt"></i> 刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据库状态 -->
|
||||||
|
<div class="alert alert-info" id="dbStatus">
|
||||||
|
<i class="fas fa-database"></i> 数据库状态: <span id="dbStatusText">检查中...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 摄像头列表 -->
|
||||||
|
<div class="row" id="cameraList">
|
||||||
|
<!-- 摄像头卡片将在这里动态生成 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加摄像头模态框 -->
|
||||||
|
<div class="modal fade" id="addCameraModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><i class="fas fa-plus"></i> 添加摄像头</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="addCameraForm">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="text" class="form-control" id="addName" placeholder="摄像头名称" required>
|
||||||
|
<label for="addName">摄像头名称</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="text" class="form-control" id="addIP" placeholder="IP地址" required>
|
||||||
|
<label for="addIP">IP地址</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="text" class="form-control" id="addUsername" placeholder="用户名">
|
||||||
|
<label for="addUsername">用户名</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="password" class="form-control" id="addPassword" placeholder="密码">
|
||||||
|
<label for="addPassword">密码</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="text" class="form-control" id="addRTSPURL" placeholder="RTSP URL" required>
|
||||||
|
<label for="addRTSPURL">RTSP URL</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="addEnabled" checked>
|
||||||
|
<label class="form-check-label" for="addEnabled">
|
||||||
|
启用摄像头
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||||
|
<button type="button" class="btn btn-success" onclick="addCamera()">添加</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑摄像头模态框 -->
|
||||||
|
<div class="modal fade" id="editCameraModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><i class="fas fa-edit"></i> 编辑摄像头</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="editCameraForm">
|
||||||
|
<input type="hidden" id="editCameraId">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="text" class="form-control" id="editName" placeholder="摄像头名称" required>
|
||||||
|
<label for="editName">摄像头名称</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="text" class="form-control" id="editIP" placeholder="IP地址" required>
|
||||||
|
<label for="editIP">IP地址</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="text" class="form-control" id="editUsername" placeholder="用户名">
|
||||||
|
<label for="editUsername">用户名</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="password" class="form-control" id="editPassword" placeholder="密码">
|
||||||
|
<label for="editPassword">密码</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="text" class="form-control" id="editRTSPURL" placeholder="RTSP URL" required>
|
||||||
|
<label for="editRTSPURL">RTSP URL</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="editEnabled">
|
||||||
|
<label class="form-check-label" for="editEnabled">
|
||||||
|
启用摄像头
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="updateCamera()">更新</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// 页面加载时初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
checkDatabaseStatus();
|
||||||
|
loadCameras();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查数据库状态
|
||||||
|
function checkDatabaseStatus() {
|
||||||
|
fetch('/database/status')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
const statusElement = document.getElementById('dbStatusText');
|
||||||
|
const alertElement = document.getElementById('dbStatus');
|
||||||
|
|
||||||
|
if (data.enabled && data.connected) {
|
||||||
|
statusElement.textContent = `已连接 (${data.type}://${data.host}:${data.port}/${data.database})`;
|
||||||
|
alertElement.className = 'alert alert-success';
|
||||||
|
} else if (data.enabled && !data.connected) {
|
||||||
|
statusElement.textContent = `连接失败: ${data.error || '未知错误'}`;
|
||||||
|
alertElement.className = 'alert alert-danger';
|
||||||
|
} else {
|
||||||
|
statusElement.textContent = '未启用';
|
||||||
|
alertElement.className = 'alert alert-warning';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('检查数据库状态失败:', error);
|
||||||
|
document.getElementById('dbStatusText').textContent = '检查失败';
|
||||||
|
document.getElementById('dbStatus').className = 'alert alert-danger';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载摄像头列表
|
||||||
|
function loadCameras() {
|
||||||
|
fetch('/cameras')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.cameras) {
|
||||||
|
displayCameras(data.cameras);
|
||||||
|
} else {
|
||||||
|
document.getElementById('cameraList').innerHTML =
|
||||||
|
'<div class="col-12"><div class="alert alert-info">暂无摄像头数据</div></div>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('加载摄像头失败:', error);
|
||||||
|
document.getElementById('cameraList').innerHTML =
|
||||||
|
'<div class="col-12"><div class="alert alert-danger">加载摄像头失败</div></div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示摄像头列表
|
||||||
|
function displayCameras(cameras) {
|
||||||
|
const container = document.getElementById('cameraList');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
cameras.forEach(camera => {
|
||||||
|
const statusClass = camera.status === 'online' ? 'status-online' : 'status-offline';
|
||||||
|
const statusIcon = camera.status === 'online' ? 'fas fa-circle' : 'far fa-circle';
|
||||||
|
const enabledBadge = camera.enabled ?
|
||||||
|
'<span class="badge bg-success">已启用</span>' :
|
||||||
|
'<span class="badge bg-secondary">已禁用</span>';
|
||||||
|
|
||||||
|
const card = `
|
||||||
|
<div class="col-md-6 col-lg-4 mb-3">
|
||||||
|
<div class="card camera-card h-100">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0">${camera.name}</h6>
|
||||||
|
${enabledBadge}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-text">
|
||||||
|
<strong>IP:</strong> ${camera.ip}<br>
|
||||||
|
<strong>用户名:</strong> ${camera.username || '无'}<br>
|
||||||
|
<strong>状态:</strong>
|
||||||
|
<i class="${statusIcon} ${statusClass}"></i>
|
||||||
|
<span class="${statusClass}">${camera.status === 'online' ? '在线' : '离线'}</span>
|
||||||
|
</p>
|
||||||
|
<small class="text-muted">
|
||||||
|
创建时间: ${new Date(camera.created_at).toLocaleString()}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="d-flex btn-group-actions">
|
||||||
|
<button class="btn btn-sm btn-primary" onclick="editCamera('${camera.id}')">
|
||||||
|
<i class="fas fa-edit"></i> 编辑
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-success" onclick="previewCamera('${camera.id}')">
|
||||||
|
<i class="fas fa-play"></i> 预览
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="deleteCamera('${camera.id}', '${camera.name}')">
|
||||||
|
<i class="fas fa-trash"></i> 删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
container.innerHTML += card;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加摄像头
|
||||||
|
function addCamera() {
|
||||||
|
const formData = {
|
||||||
|
name: document.getElementById('addName').value,
|
||||||
|
ip: document.getElementById('addIP').value,
|
||||||
|
username: document.getElementById('addUsername').value,
|
||||||
|
password: document.getElementById('addPassword').value,
|
||||||
|
rtsp_url: document.getElementById('addRTSPURL').value,
|
||||||
|
enabled: document.getElementById('addEnabled').checked
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('/camera/add', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.message) {
|
||||||
|
alert('摄像头添加成功!');
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('addCameraModal')).hide();
|
||||||
|
document.getElementById('addCameraForm').reset();
|
||||||
|
loadCameras();
|
||||||
|
} else {
|
||||||
|
alert('添加失败: ' + (data.error || '未知错误'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('添加摄像头失败:', error);
|
||||||
|
alert('添加失败: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑摄像头
|
||||||
|
function editCamera(cameraId) {
|
||||||
|
fetch(`/camera/${cameraId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(camera => {
|
||||||
|
document.getElementById('editCameraId').value = camera.id;
|
||||||
|
document.getElementById('editName').value = camera.name;
|
||||||
|
document.getElementById('editIP').value = camera.ip;
|
||||||
|
document.getElementById('editUsername').value = camera.username || '';
|
||||||
|
document.getElementById('editPassword').value = '';
|
||||||
|
document.getElementById('editRTSPURL').value = camera.rtsp_url;
|
||||||
|
document.getElementById('editEnabled').checked = camera.enabled;
|
||||||
|
|
||||||
|
new bootstrap.Modal(document.getElementById('editCameraModal')).show();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('获取摄像头信息失败:', error);
|
||||||
|
alert('获取摄像头信息失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新摄像头
|
||||||
|
function updateCamera() {
|
||||||
|
const cameraId = document.getElementById('editCameraId').value;
|
||||||
|
const formData = {
|
||||||
|
name: document.getElementById('editName').value,
|
||||||
|
ip: document.getElementById('editIP').value,
|
||||||
|
username: document.getElementById('editUsername').value,
|
||||||
|
password: document.getElementById('editPassword').value,
|
||||||
|
rtsp_url: document.getElementById('editRTSPURL').value,
|
||||||
|
enabled: document.getElementById('editEnabled').checked
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch(`/camera/${cameraId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.message) {
|
||||||
|
alert('摄像头更新成功!');
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('editCameraModal')).hide();
|
||||||
|
loadCameras();
|
||||||
|
} else {
|
||||||
|
alert('更新失败: ' + (data.error || '未知错误'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('更新摄像头失败:', error);
|
||||||
|
alert('更新失败: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除摄像头
|
||||||
|
function deleteCamera(cameraId, cameraName) {
|
||||||
|
if (confirm(`确定要删除摄像头 "${cameraName}" 吗?`)) {
|
||||||
|
fetch(`/camera/${cameraId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.message) {
|
||||||
|
alert('摄像头删除成功!');
|
||||||
|
loadCameras();
|
||||||
|
} else {
|
||||||
|
alert('删除失败: ' + (data.error || '未知错误'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('删除摄像头失败:', error);
|
||||||
|
alert('删除失败: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览摄像头
|
||||||
|
function previewCamera(cameraId) {
|
||||||
|
window.open(`/pages/player/all/${cameraId}/0`, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新摄像头列表
|
||||||
|
function refreshCameras() {
|
||||||
|
fetch('/cameras/refresh', {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.message) {
|
||||||
|
alert('刷新成功!');
|
||||||
|
loadCameras();
|
||||||
|
} else {
|
||||||
|
alert('刷新失败: ' + (data.error || '未知错误'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('刷新失败:', error);
|
||||||
|
alert('刷新失败: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue