| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- package parking
- import (
- "errors"
- "fmt"
- "net/url"
- "strings"
- "wails-app/internal/dao"
- "wails-app/internal/global"
- "wails-app/internal/model/parking/request"
- )
- // CameraStreamOption 是工作台使用的安全摄像头信息,不包含用户名和密码。
- type CameraStreamOption struct {
- DeviceCode string `json:"device_code"`
- DeviceName string `json:"device_name"`
- ChannelID uint `json:"channel_id"`
- ChannelCode string `json:"channel_code"`
- ChannelName string `json:"channel_name"`
- Protocol string `json:"protocol"`
- PlayURL string `json:"play_url"`
- SnapshotURL string `json:"snapshot_url"`
- Status string `json:"status"`
- Online bool `json:"online"`
- }
- // CreateCamera 创建摄像头配置,并校验它绑定的通道。
- func (s *PassageService) CreateCamera(req request.CameraCreate) error {
- if err := validateCameraConfig(req); err != nil {
- return err
- }
- var existing dao.Camera
- if global.GVA_DB.Where("device_code = ?", req.DeviceCode).First(&existing).RowsAffected > 0 {
- return errors.New("摄像头编码已存在")
- }
- return global.GVA_DB.Create(&dao.Camera{
- DeviceCode: req.DeviceCode, DeviceName: req.DeviceName, ChannelID: req.ChannelID,
- IPAddress: req.IPAddress, Port: req.Port, Username: req.Username, Password: req.Password,
- Channel: req.Channel, IsActive: req.IsActive, StreamProtocol: req.StreamProtocol,
- SourceURL: req.SourceURL, StreamURL: req.StreamURL, SnapshotURL: req.SnapshotURL, Description: req.Description,
- }).Error
- }
- // UpdateCamera 更新摄像头配置;为了避免浏览器回显凭据,空密码不会覆盖数据库已有密码。
- func (s *PassageService) UpdateCamera(req request.CameraUpdate) error {
- if err := validateCameraConfig(req.CameraCreate); err != nil {
- return err
- }
- var camera dao.Camera
- if err := global.GVA_DB.First(&camera, req.ID).Error; err != nil {
- return errors.New("摄像头不存在")
- }
- var existing dao.Camera
- if global.GVA_DB.Where("device_code = ? AND id != ?", req.DeviceCode, req.ID).First(&existing).RowsAffected > 0 {
- return errors.New("摄像头编码已被其他设备使用")
- }
- previousDeviceCode := camera.DeviceCode
- camera.DeviceCode, camera.DeviceName, camera.ChannelID = req.DeviceCode, req.DeviceName, req.ChannelID
- camera.IPAddress, camera.Port = req.IPAddress, req.Port
- if strings.TrimSpace(req.Username) != "" {
- camera.Username = req.Username
- }
- if strings.TrimSpace(req.Password) != "" {
- camera.Password = req.Password
- }
- camera.Channel, camera.IsActive, camera.StreamProtocol = req.Channel, req.IsActive, req.StreamProtocol
- // 前端编辑时密码会被掩码;此时保留数据库中的完整 RTSP 地址。
- if !strings.Contains(req.SourceURL, "***") {
- camera.SourceURL = req.SourceURL
- }
- camera.StreamURL, camera.SnapshotURL, camera.Description = req.StreamURL, req.SnapshotURL, req.Description
- if err := global.GVA_DB.Save(&camera).Error; err != nil {
- return err
- }
- cameraMJPEGHub.stop(previousDeviceCode)
- if camera.DeviceCode != previousDeviceCode {
- cameraMJPEGHub.stop(camera.DeviceCode)
- }
- return nil
- }
- func (s *PassageService) ListCameras(req request.CameraQuery) (int64, []dao.Camera, error) {
- var total int64
- var cameras []dao.Camera
- query := global.GVA_DB.Model(&dao.Camera{}).Preload("BoundChannel")
- if req.ParkingLotID != 0 {
- query = query.Joins("JOIN channel ON channel.id = camera.channel_id AND channel.deleted_at IS NULL").Where("channel.parking_lot_id = ?", req.ParkingLotID)
- }
- if err := query.Count(&total).Error; err != nil {
- return 0, nil, err
- }
- if err := query.Order("camera.device_name ASC").Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find(&cameras).Error; err != nil {
- return 0, nil, err
- }
- // 播放地址可能含凭据,配置列表不向浏览器返回它。
- for i := range cameras {
- cameras[i].Status = cameraDisplayStatus(cameras[i])
- cameras[i].Username = ""
- cameras[i].Password = ""
- cameras[i].SourceURL = redactCameraURL(cameras[i].SourceURL)
- }
- return total, cameras, nil
- }
- func (s *PassageService) DeleteCamera(id uint) error {
- var camera dao.Camera
- if err := global.GVA_DB.First(&camera, id).Error; err != nil {
- return errors.New("摄像头不存在")
- }
- result := global.GVA_DB.Delete(&camera)
- if result.Error != nil {
- return result.Error
- }
- if result.RowsAffected == 0 {
- return errors.New("摄像头不存在")
- }
- cameraMJPEGHub.stop(camera.DeviceCode)
- return nil
- }
- func validateCameraConfig(req request.CameraCreate) error {
- if global.GVA_DB == nil {
- return errors.New("数据库未初始化")
- }
- if req.StreamProtocol == "system-mjpeg" && strings.TrimSpace(req.SourceURL) == "" {
- return errors.New("系统转换模式必须填写原始 RTSP/HTTP 地址")
- }
- if req.StreamProtocol != "system-mjpeg" && strings.TrimSpace(req.StreamURL) == "" {
- return errors.New("边缘播放模式必须填写浏览器播放地址")
- }
- var channel dao.Channel
- if err := global.GVA_DB.First(&channel, req.ChannelID).Error; err != nil {
- return errors.New("绑定通道不存在")
- }
- return nil
- }
- // ListCameraStreams 返回启用摄像头的浏览器播放配置,并按通道绑定到道闸设备。
- func (s *PassageService) ListCameraStreams() ([]CameraStreamOption, error) {
- if global.GVA_DB == nil {
- return nil, errors.New("数据库未初始化")
- }
- var cameras []dao.Camera
- if err := global.GVA_DB.Preload("BoundChannel").Where("is_active = ?", true).
- Order("device_name ASC").Find(&cameras).Error; err != nil {
- return nil, err
- }
- options := make([]CameraStreamOption, 0, len(cameras))
- for _, camera := range cameras {
- protocol := camera.StreamProtocol
- if protocol == "" {
- protocol = "webrtc"
- }
- playURL := camera.StreamURL
- systemConversionEnabled := strings.ToLower(strings.TrimSpace(global.GVA_CONFIG.Camera.ConversionMode)) != "edge"
- if systemConversionEnabled && isSystemMJPEG(protocol) {
- token := issueCameraStreamToken(camera.DeviceCode)
- prefix := global.GVA_CONFIG.System.RouterPrefix
- if prefix == "" {
- prefix = "/api"
- }
- playURL = fmt.Sprintf("%s/parking/camera/stream/%s/%s", strings.TrimRight(prefix, "/"), url.PathEscape(camera.DeviceCode), token)
- protocol = "mjpeg"
- } else if isSystemMJPEG(protocol) {
- // 切到 edge 模式后,沿用数据库中的 StreamURL(由边缘或外部媒体网关提供)。
- protocol = "webrtc"
- }
- status := cameraDisplayStatus(camera)
- options = append(options, CameraStreamOption{
- DeviceCode: camera.DeviceCode,
- DeviceName: camera.DeviceName,
- ChannelID: camera.ChannelID,
- ChannelCode: cameraChannelCode(camera.BoundChannel),
- ChannelName: cameraChannelName(camera.BoundChannel),
- Protocol: protocol,
- PlayURL: playURL,
- SnapshotURL: camera.SnapshotURL,
- Status: status,
- Online: status == "online" || status == "connected",
- })
- }
- return options, nil
- }
- // cameraDisplayStatus returns the observed stream state for system-side
- // conversion. Camera.Status is not updated by FFmpeg, so using it there made
- // an actively playing RTSP stream appear offline indefinitely.
- func cameraDisplayStatus(camera dao.Camera) string {
- if isSystemMJPEG(camera.StreamProtocol) && strings.ToLower(strings.TrimSpace(global.GVA_CONFIG.Camera.ConversionMode)) != "edge" {
- if hasRecentCameraStreamFrame(camera.DeviceCode) {
- return "online"
- }
- return "offline"
- }
- if camera.Status == "" {
- return "offline"
- }
- return camera.Status
- }
- func cameraChannelCode(channel *dao.Channel) string {
- if channel == nil {
- return ""
- }
- return channel.ChannelCode
- }
- func cameraChannelName(channel *dao.Channel) string {
- if channel == nil {
- return ""
- }
- return channel.ChannelName
- }
|