device_bus.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. package devicebus
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "time"
  6. mqtt "github.com/eclipse/paho.mqtt.golang"
  7. "go.uber.org/zap"
  8. "wails-app/internal/dao"
  9. "wails-app/internal/global"
  10. common "wails-app/internal/model/common"
  11. parking "wails-app/internal/service/parking"
  12. "wails-app/internal/service/uhf"
  13. )
  14. // DeviceBus 订阅相机识别事件,并将事件转发到统一通行入口。
  15. // 道闸状态、ACK 和 LWT 由 MQTTGateController 统一处理,避免重叠订阅覆盖回调。
  16. type DeviceBus struct {
  17. passage *parking.PassageService
  18. }
  19. func NewDeviceBus() *DeviceBus {
  20. return &DeviceBus{passage: &parking.PassageService{}}
  21. }
  22. // CameraEvent 是相机识别事件的 MQTT 数据结构。
  23. type CameraEvent struct {
  24. Schema string `json:"schema"`
  25. DeviceCode string `json:"device_code"`
  26. PlateNumber string `json:"plate_number"`
  27. RFIDTag string `json:"rfid_tag"`
  28. Direction string `json:"direction"`
  29. TS int64 `json:"ts"`
  30. ImagePath string `json:"image_path"`
  31. ImageID string `json:"image_id"`
  32. EventID string `json:"event_id"`
  33. Confidence float64 `json:"confidence"`
  34. }
  35. // Subscribe 只订阅相机事件,设备状态类消息由道闸控制器订阅。
  36. func (b *DeviceBus) Subscribe(client mqtt.Client) error {
  37. topic := "parking/lot/+/booth/+/channel/+/camera/event"
  38. token := client.Subscribe(topic, 1, b.onMessage)
  39. if token.Wait() && token.Error() != nil {
  40. return token.Error()
  41. }
  42. if global.GVA_LOG != nil {
  43. global.GVA_LOG.Info("MQTT 设备主题订阅成功", zap.String("topic", topic))
  44. }
  45. return nil
  46. }
  47. func (b *DeviceBus) onMessage(_ mqtt.Client, msg mqtt.Message) {
  48. var probe struct {
  49. Schema string `json:"schema"`
  50. }
  51. if err := json.Unmarshal(msg.Payload(), &probe); err != nil {
  52. if global.GVA_LOG != nil {
  53. global.GVA_LOG.Warn("MQTT 设备消息 JSON 无效", zap.String("topic", msg.Topic()), zap.Error(err))
  54. }
  55. return
  56. }
  57. if global.GVA_LOG != nil {
  58. global.GVA_LOG.Info("收到设备 MQTT 消息", zap.String("topic", msg.Topic()), zap.String("schema", probe.Schema))
  59. }
  60. if probe.Schema == "camera.event.v1" {
  61. // 识别处理包含数据库事务和等待道闸 ACK,异步执行避免阻塞 MQTT 回调。
  62. // 回调返回后 MQTT 客户端可以复用消息缓冲区,因此协程必须使用副本。
  63. payload := append([]byte(nil), msg.Payload()...)
  64. if global.GVA_LOG != nil {
  65. global.GVA_LOG.Info("收到车辆识别 MQTT 事件", zap.String("topic", msg.Topic()), zap.Int("payload_bytes", len(payload)))
  66. }
  67. go b.onCameraEvent(payload)
  68. }
  69. }
  70. func (b *DeviceBus) onCameraEvent(payload []byte) {
  71. startedAt := time.Now()
  72. var ev CameraEvent
  73. if err := json.Unmarshal(payload, &ev); err != nil {
  74. if global.GVA_LOG != nil {
  75. global.GVA_LOG.Warn("车辆识别 MQTT 事件解析失败", zap.Int("payload_bytes", len(payload)), zap.Error(err))
  76. }
  77. return
  78. }
  79. if global.GVA_LOG != nil {
  80. global.GVA_LOG.Info("车辆识别事件字段已解析", zap.String("device_code", ev.DeviceCode), zap.String("event_id", ev.EventID), zap.String("image_id", ev.ImageID), zap.String("plate_number", ev.PlateNumber), zap.String("direction", ev.Direction), zap.Int64("ts", ev.TS))
  81. }
  82. imagePath, err := resolveImagePath(ev.DeviceCode, ev.ImageID, ev.ImagePath)
  83. if err != nil {
  84. if global.GVA_LOG != nil {
  85. global.GVA_LOG.Warn("车辆识别事件引用的图片不可用", zap.String("device_code", ev.DeviceCode), zap.String("image_id", ev.ImageID), zap.String("event_id", ev.EventID), zap.Error(err))
  86. }
  87. return
  88. }
  89. if global.GVA_LOG != nil {
  90. global.GVA_LOG.Info("车辆识别事件图片引用校验成功", zap.String("device_code", ev.DeviceCode), zap.String("event_id", ev.EventID), zap.String("image_id", ev.ImageID), zap.Bool("legacy_image_path", ev.ImageID == ""))
  91. }
  92. req := common.PassageRequest{
  93. DeviceCode: ev.DeviceCode, PlateNumber: ev.PlateNumber, RFIDTag: ev.RFIDTag,
  94. Direction: ev.Direction, Image: imagePath, TriggerSource: "camera",
  95. }
  96. result, err := b.passage.HandlePassage(req)
  97. status := "completed"
  98. message := "识别通过"
  99. fee := 0.0
  100. stayTime := int64(0)
  101. if err != nil {
  102. status = "failed"
  103. message = err.Error()
  104. } else if result != nil {
  105. fee = result.Fee
  106. stayTime = result.StayTime
  107. if result.Message != "" {
  108. message = result.Message
  109. }
  110. }
  111. if global.GVA_LOG != nil {
  112. global.GVA_LOG.Info("处理车辆识别事件",
  113. zap.String("device_code", ev.DeviceCode), zap.String("plate_number", ev.PlateNumber),
  114. zap.String("direction", ev.Direction), zap.String("event_id", ev.EventID), zap.String("image_id", ev.ImageID), zap.String("status", status), zap.String("message", message), zap.Duration("duration", time.Since(startedAt)))
  115. }
  116. uhf.PushChannelEvent(uhf.ChannelEvent{
  117. DeviceCode: ev.DeviceCode, RFIDTag: ev.RFIDTag, PlateNumber: ev.PlateNumber,
  118. Direction: ev.Direction, Timestamp: ev.TS, Status: status, Message: message,
  119. Fee: fee, StayTime: stayTime,
  120. })
  121. }
  122. // resolveImagePath accepts the new uploaded-image reference. image_path stays
  123. // supported for deployed legacy cameras until their firmware is upgraded.
  124. func resolveImagePath(deviceCode, imageID, legacyPath string) (string, error) {
  125. if imageID == "" {
  126. return legacyPath, nil
  127. }
  128. if global.GVA_DB == nil {
  129. return "", fmt.Errorf("数据库未初始化")
  130. }
  131. var image dao.DeviceImage
  132. if err := global.GVA_DB.Where("device_code = ? AND image_id = ?", deviceCode, imageID).First(&image).Error; err != nil {
  133. return "", fmt.Errorf("图片未上传或不属于该设备")
  134. }
  135. return image.URL, nil
  136. }