gate.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. package parking
  2. import (
  3. "errors"
  4. "fmt"
  5. "sync"
  6. "time"
  7. "go.uber.org/zap"
  8. "wails-app/internal/dao"
  9. "wails-app/internal/global"
  10. incidentService "wails-app/internal/modules/incident/service"
  11. )
  12. const defaultGateValidTime byte = 2
  13. var incidentSvc = incidentService.NewIncidentService()
  14. // GateCommandContext 道闸指令上下文(用于指令流水审计)。
  15. type GateCommandContext struct {
  16. Source string // passage / manual / test
  17. OperatorID uint
  18. SessionID uint
  19. }
  20. // GateController 统一道闸设备控制接口,具体协议由设备模块实现。
  21. type GateController interface {
  22. OpenGate(deviceCode string, validTime byte) error
  23. CloseGate(deviceCode string, validTime byte) error
  24. IsGateConnected(deviceCode string) bool
  25. }
  26. // GateDeviceOption 是工作台可见的安全设备信息,不包含串口、IP 等连接参数。
  27. type GateDeviceOption struct {
  28. DeviceCode string `json:"device_code"`
  29. DeviceName string `json:"device_name"`
  30. ChannelID uint `json:"channel_id"`
  31. ChannelCode string `json:"channel_code"`
  32. ChannelName string `json:"channel_name"`
  33. Direction string `json:"direction"`
  34. ParkingLotID uint `json:"parking_lot_id"`
  35. Connected bool `json:"connected"`
  36. Simulated bool `json:"simulated"`
  37. // ConnectType 设备接入方式(serial/tcp/mqtt)。通道上同时有读卡器(继电器
  38. // 接线开闸)和独立 MQTT 道闸时,前端道闸控制优先选 mqtt 设备。
  39. ConnectType string `json:"connect_type"`
  40. }
  41. // GateRuntimeStatus 表示统一控制器中的设备实时状态。
  42. type GateRuntimeStatus struct {
  43. Connected bool
  44. Simulated bool
  45. }
  46. // SimulatedGateController 用于无硬件环境联调,只记录指令,不访问串口或网络设备。
  47. type SimulatedGateController struct {
  48. mu sync.RWMutex
  49. lastAction map[string]string
  50. }
  51. // NewSimulatedGateController 创建模拟道闸控制器。
  52. func NewSimulatedGateController() *SimulatedGateController {
  53. return &SimulatedGateController{lastAction: make(map[string]string)}
  54. }
  55. func (c *SimulatedGateController) record(deviceCode, action string) error {
  56. if deviceCode == "" {
  57. return errors.New("道闸设备编码不能为空")
  58. }
  59. c.mu.Lock()
  60. c.lastAction[deviceCode] = action
  61. c.mu.Unlock()
  62. if global.GVA_LOG != nil {
  63. global.GVA_LOG.Warn("模拟道闸指令", zap.String("device_code", deviceCode), zap.String("action", action), zap.Time("time", time.Now()))
  64. }
  65. return nil
  66. }
  67. // OpenGate 模拟开闸。
  68. func (c *SimulatedGateController) OpenGate(deviceCode string, _ byte) error {
  69. return c.record(deviceCode, "open")
  70. }
  71. // CloseGate 模拟关闸。
  72. func (c *SimulatedGateController) CloseGate(deviceCode string, _ byte) error {
  73. return c.record(deviceCode, "close")
  74. }
  75. // IsGateConnected 在模拟模式下将所有有效设备编码视为在线。
  76. func (c *SimulatedGateController) IsGateConnected(deviceCode string) bool {
  77. return deviceCode != ""
  78. }
  79. func (c *SimulatedGateController) isSimulator() bool {
  80. return true
  81. }
  82. var (
  83. gateControllerMu sync.RWMutex
  84. gateController GateController
  85. )
  86. // SetGateController 注入设备控制实现,避免停车业务反向依赖具体设备协议。
  87. func SetGateController(controller GateController) {
  88. gateControllerMu.Lock()
  89. defer gateControllerMu.Unlock()
  90. gateController = controller
  91. }
  92. func currentGateController() GateController {
  93. gateControllerMu.RLock()
  94. defer gateControllerMu.RUnlock()
  95. return gateController
  96. }
  97. // GetGateRuntimeStatus 返回设备在当前控制器中的实时连接状态。
  98. func GetGateRuntimeStatus(deviceCode string) GateRuntimeStatus {
  99. controller := currentGateController()
  100. if controller == nil || deviceCode == "" {
  101. return GateRuntimeStatus{}
  102. }
  103. status := GateRuntimeStatus{Connected: controller.IsGateConnected(deviceCode)}
  104. if simulator, ok := controller.(interface{ isSimulator() bool }); ok {
  105. status.Simulated = simulator.isSimulator()
  106. }
  107. return status
  108. }
  109. // OpenGateByDeviceCode 通过统一控制器开闸。
  110. func (s *PassageService) OpenGateByDeviceCode(deviceCode string, validTime byte) error {
  111. if deviceCode == "" {
  112. return errors.New("道闸设备编码不能为空")
  113. }
  114. controller := currentGateController()
  115. if controller == nil {
  116. return errors.New("道闸控制器未初始化")
  117. }
  118. if validTime == 0 {
  119. validTime = defaultGateValidTime
  120. }
  121. return controller.OpenGate(deviceCode, validTime)
  122. }
  123. // CloseGateByDeviceCode 通过统一控制器关闸。
  124. func (s *PassageService) CloseGateByDeviceCode(deviceCode string, validTime byte) error {
  125. if deviceCode == "" {
  126. return errors.New("道闸设备编码不能为空")
  127. }
  128. controller := currentGateController()
  129. if controller == nil {
  130. return errors.New("道闸控制器未初始化")
  131. }
  132. if validTime == 0 {
  133. validTime = defaultGateValidTime
  134. }
  135. return controller.CloseGate(deviceCode, validTime)
  136. }
  137. // OpenGateWithContext 开闸并记录指令流水(所有开闸路径的审计入口),返回流水 ID。
  138. func (s *PassageService) OpenGateWithContext(ctx GateCommandContext, deviceCode string, validTime byte) (uint, error) {
  139. return s.runGateCommand(ctx, "open", deviceCode, validTime)
  140. }
  141. // CloseGateWithContext 关闸并记录指令流水,返回流水 ID。
  142. func (s *PassageService) CloseGateWithContext(ctx GateCommandContext, deviceCode string, validTime byte) (uint, error) {
  143. return s.runGateCommand(ctx, "close", deviceCode, validTime)
  144. }
  145. // runGateCommand 执行道闸指令并写指令流水(best-effort,流水失败不影响指令结果)。
  146. func (s *PassageService) runGateCommand(ctx GateCommandContext, action, deviceCode string, validTime byte) (uint, error) {
  147. if ctx.Source == "" {
  148. ctx.Source = "passage"
  149. }
  150. start := time.Now()
  151. if global.GVA_LOG != nil {
  152. global.GVA_LOG.Info("道闸指令开始执行",
  153. zap.String("action", action),
  154. zap.String("device_code", deviceCode),
  155. zap.String("source", ctx.Source),
  156. zap.Uint("session_id", ctx.SessionID),
  157. zap.Uint8("valid_time_seconds", validTime),
  158. )
  159. }
  160. var err error
  161. if action == "open" {
  162. err = s.OpenGateByDeviceCode(deviceCode, validTime)
  163. } else {
  164. err = s.CloseGateByDeviceCode(deviceCode, validTime)
  165. }
  166. rec := incidentService.GateCommandRecord{
  167. DeviceCode: deviceCode,
  168. Action: action,
  169. Source: ctx.Source,
  170. OperatorID: ctx.OperatorID,
  171. SessionID: ctx.SessionID,
  172. DurationMs: time.Since(start).Milliseconds(),
  173. }
  174. if err != nil {
  175. rec.Result = incidentService.CommandFailed
  176. rec.ErrorMessage = err.Error()
  177. if global.GVA_LOG != nil {
  178. global.GVA_LOG.Warn("道闸指令执行失败",
  179. zap.String("action", action),
  180. zap.String("device_code", deviceCode),
  181. zap.Int64("duration_ms", rec.DurationMs),
  182. zap.Error(err),
  183. )
  184. }
  185. } else {
  186. rec.Result = incidentService.CommandSuccess
  187. if global.GVA_LOG != nil {
  188. global.GVA_LOG.Info("道闸指令执行成功",
  189. zap.String("action", action),
  190. zap.String("device_code", deviceCode),
  191. zap.Int64("duration_ms", rec.DurationMs),
  192. )
  193. }
  194. }
  195. // 设备名称快照(查询失败不影响指令结果)
  196. var device dao.UHFReader
  197. if global.GVA_DB != nil {
  198. if qerr := global.GVA_DB.Where("device_code = ?", deviceCode).First(&device).Error; qerr == nil {
  199. rec.DeviceName = device.DeviceName
  200. }
  201. }
  202. return incidentSvc.RecordGateCommand(rec), err
  203. }
  204. // ListGateDevices 返回已启用且已绑定通道的道闸设备。
  205. func (s *PassageService) ListGateDevices() ([]GateDeviceOption, error) {
  206. if global.GVA_DB == nil {
  207. return nil, errors.New("数据库未初始化")
  208. }
  209. var readers []dao.UHFReader
  210. if err := global.GVA_DB.Preload("Channel").
  211. Where("is_active = ? AND channel_id > 0", true).
  212. Order("device_name ASC").Find(&readers).Error; err != nil {
  213. return nil, err
  214. }
  215. options := make([]GateDeviceOption, 0, len(readers))
  216. for _, reader := range readers {
  217. if reader.Channel == nil {
  218. continue
  219. }
  220. parkingLotID := reader.Channel.ParkingLotID
  221. if parkingLotID == 0 {
  222. parkingLotID = reader.ParkingLotID
  223. }
  224. runtimeStatus := GetGateRuntimeStatus(reader.DeviceCode)
  225. options = append(options, GateDeviceOption{
  226. DeviceCode: reader.DeviceCode, DeviceName: reader.DeviceName,
  227. ChannelID: reader.ChannelID, ChannelCode: reader.Channel.ChannelCode,
  228. ChannelName: reader.Channel.ChannelName, Direction: reader.Channel.Direction,
  229. ParkingLotID: parkingLotID,
  230. Connected: runtimeStatus.Connected,
  231. Simulated: runtimeStatus.Simulated,
  232. ConnectType: string(reader.ConnectType),
  233. })
  234. }
  235. return options, nil
  236. }
  237. func gateOperationError(action, deviceCode string, err error) error {
  238. return fmt.Errorf("道闸%s失败(设备 %s): %w", action, deviceCode, err)
  239. }