package parking import ( "errors" "fmt" "sync" "time" "go.uber.org/zap" "wails-app/internal/dao" "wails-app/internal/global" incidentService "wails-app/internal/modules/incident/service" ) const defaultGateValidTime byte = 2 var incidentSvc = incidentService.NewIncidentService() // GateCommandContext 道闸指令上下文(用于指令流水审计)。 type GateCommandContext struct { Source string // passage / manual / test OperatorID uint SessionID uint } // GateController 统一道闸设备控制接口,具体协议由设备模块实现。 type GateController interface { OpenGate(deviceCode string, validTime byte) error CloseGate(deviceCode string, validTime byte) error IsGateConnected(deviceCode string) bool } // GateDeviceOption 是工作台可见的安全设备信息,不包含串口、IP 等连接参数。 type GateDeviceOption 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"` Direction string `json:"direction"` ParkingLotID uint `json:"parking_lot_id"` Connected bool `json:"connected"` Simulated bool `json:"simulated"` } // GateRuntimeStatus 表示统一控制器中的设备实时状态。 type GateRuntimeStatus struct { Connected bool Simulated bool } // SimulatedGateController 用于无硬件环境联调,只记录指令,不访问串口或网络设备。 type SimulatedGateController struct { mu sync.RWMutex lastAction map[string]string } // NewSimulatedGateController 创建模拟道闸控制器。 func NewSimulatedGateController() *SimulatedGateController { return &SimulatedGateController{lastAction: make(map[string]string)} } func (c *SimulatedGateController) record(deviceCode, action string) error { if deviceCode == "" { return errors.New("道闸设备编码不能为空") } c.mu.Lock() c.lastAction[deviceCode] = action c.mu.Unlock() if global.GVA_LOG != nil { global.GVA_LOG.Warn("模拟道闸指令", zap.String("device_code", deviceCode), zap.String("action", action), zap.Time("time", time.Now())) } return nil } // OpenGate 模拟开闸。 func (c *SimulatedGateController) OpenGate(deviceCode string, _ byte) error { return c.record(deviceCode, "open") } // CloseGate 模拟关闸。 func (c *SimulatedGateController) CloseGate(deviceCode string, _ byte) error { return c.record(deviceCode, "close") } // IsGateConnected 在模拟模式下将所有有效设备编码视为在线。 func (c *SimulatedGateController) IsGateConnected(deviceCode string) bool { return deviceCode != "" } func (c *SimulatedGateController) isSimulator() bool { return true } var ( gateControllerMu sync.RWMutex gateController GateController ) // SetGateController 注入设备控制实现,避免停车业务反向依赖具体设备协议。 func SetGateController(controller GateController) { gateControllerMu.Lock() defer gateControllerMu.Unlock() gateController = controller } func currentGateController() GateController { gateControllerMu.RLock() defer gateControllerMu.RUnlock() return gateController } // GetGateRuntimeStatus 返回设备在当前控制器中的实时连接状态。 func GetGateRuntimeStatus(deviceCode string) GateRuntimeStatus { controller := currentGateController() if controller == nil || deviceCode == "" { return GateRuntimeStatus{} } status := GateRuntimeStatus{Connected: controller.IsGateConnected(deviceCode)} if simulator, ok := controller.(interface{ isSimulator() bool }); ok { status.Simulated = simulator.isSimulator() } return status } // OpenGateByDeviceCode 通过统一控制器开闸。 func (s *PassageService) OpenGateByDeviceCode(deviceCode string, validTime byte) error { if deviceCode == "" { return errors.New("道闸设备编码不能为空") } controller := currentGateController() if controller == nil { return errors.New("道闸控制器未初始化") } if validTime == 0 { validTime = defaultGateValidTime } return controller.OpenGate(deviceCode, validTime) } // CloseGateByDeviceCode 通过统一控制器关闸。 func (s *PassageService) CloseGateByDeviceCode(deviceCode string, validTime byte) error { if deviceCode == "" { return errors.New("道闸设备编码不能为空") } controller := currentGateController() if controller == nil { return errors.New("道闸控制器未初始化") } if validTime == 0 { validTime = defaultGateValidTime } return controller.CloseGate(deviceCode, validTime) } // OpenGateWithContext 开闸并记录指令流水(所有开闸路径的审计入口),返回流水 ID。 func (s *PassageService) OpenGateWithContext(ctx GateCommandContext, deviceCode string, validTime byte) (uint, error) { return s.runGateCommand(ctx, "open", deviceCode, validTime) } // CloseGateWithContext 关闸并记录指令流水,返回流水 ID。 func (s *PassageService) CloseGateWithContext(ctx GateCommandContext, deviceCode string, validTime byte) (uint, error) { return s.runGateCommand(ctx, "close", deviceCode, validTime) } // runGateCommand 执行道闸指令并写指令流水(best-effort,流水失败不影响指令结果)。 func (s *PassageService) runGateCommand(ctx GateCommandContext, action, deviceCode string, validTime byte) (uint, error) { if ctx.Source == "" { ctx.Source = "passage" } start := time.Now() var err error if action == "open" { err = s.OpenGateByDeviceCode(deviceCode, validTime) } else { err = s.CloseGateByDeviceCode(deviceCode, validTime) } rec := incidentService.GateCommandRecord{ DeviceCode: deviceCode, Action: action, Source: ctx.Source, OperatorID: ctx.OperatorID, SessionID: ctx.SessionID, DurationMs: time.Since(start).Milliseconds(), } if err != nil { rec.Result = incidentService.CommandFailed rec.ErrorMessage = err.Error() } else { rec.Result = incidentService.CommandSuccess } // 设备名称快照(查询失败不影响指令结果) var device dao.UHFReader if global.GVA_DB != nil { if qerr := global.GVA_DB.Where("device_code = ?", deviceCode).First(&device).Error; qerr == nil { rec.DeviceName = device.DeviceName } } return incidentSvc.RecordGateCommand(rec), err } // ListGateDevices 返回已启用且已绑定通道的道闸设备。 func (s *PassageService) ListGateDevices() ([]GateDeviceOption, error) { if global.GVA_DB == nil { return nil, errors.New("数据库未初始化") } var readers []dao.UHFReader if err := global.GVA_DB.Preload("Channel"). Where("is_active = ? AND channel_id > 0", true). Order("device_name ASC").Find(&readers).Error; err != nil { return nil, err } options := make([]GateDeviceOption, 0, len(readers)) for _, reader := range readers { if reader.Channel == nil { continue } parkingLotID := reader.Channel.ParkingLotID if parkingLotID == 0 { parkingLotID = reader.ParkingLotID } runtimeStatus := GetGateRuntimeStatus(reader.DeviceCode) options = append(options, GateDeviceOption{ DeviceCode: reader.DeviceCode, DeviceName: reader.DeviceName, ChannelID: reader.ChannelID, ChannelCode: reader.Channel.ChannelCode, ChannelName: reader.Channel.ChannelName, Direction: reader.Channel.Direction, ParkingLotID: parkingLotID, Connected: runtimeStatus.Connected, Simulated: runtimeStatus.Simulated, }) } return options, nil } func gateOperationError(action, deviceCode string, err error) error { return fmt.Errorf("道闸%s失败(设备 %s): %w", action, deviceCode, err) }