mqtt_gate_controller.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. package parking
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "sync"
  7. "time"
  8. mqtt "github.com/eclipse/paho.mqtt.golang"
  9. "github.com/gofrs/uuid/v5"
  10. "go.uber.org/zap"
  11. "gorm.io/gorm"
  12. "wails-app/internal/dao"
  13. "wails-app/internal/global"
  14. incidentService "wails-app/internal/modules/incident/service"
  15. )
  16. const (
  17. mqttQoS = 1
  18. mqttCmdTimeout = 5 * time.Second
  19. )
  20. // gateTopicRoute 设备在 MQTT 主题树中的路由坐标。
  21. type gateTopicRoute struct {
  22. DeviceCode string
  23. LotID uint
  24. BoothID uint
  25. ChannelID uint
  26. }
  27. func (r gateTopicRoute) cmdTopic() string {
  28. return fmt.Sprintf("parking/lot/%d/booth/%d/channel/%d/gate/cmd", r.LotID, r.BoothID, r.ChannelID)
  29. }
  30. // MQTTGateController 通过 MQTT 下发道闸指令并等待设备 ack,实现 GateController 接口。
  31. // 用于「独立网络道闸控制器」;挂在 UHF 读写器继电器上的道闸仍走 DeviceManager。
  32. type MQTTGateController struct {
  33. client mqtt.Client
  34. mu sync.RWMutex
  35. routes map[string]gateTopicRoute // device_code -> route
  36. connectionState map[string]bool // device_code -> MQTT 连接状态
  37. gateState map[string]string // device_code -> open/closed/moving/fault
  38. pending map[string]pendingGateCommand
  39. }
  40. type pendingGateCommand struct {
  41. deviceCode string
  42. ackCh chan gateAck
  43. }
  44. type gateCmd struct {
  45. Schema string `json:"schema"`
  46. CmdID string `json:"cmd_id"`
  47. DeviceCode string `json:"device_code"`
  48. Action string `json:"action"`
  49. ValidTime byte `json:"valid_time"`
  50. }
  51. type gateAck struct {
  52. Schema string `json:"schema"`
  53. CmdID string `json:"cmd_id"`
  54. DeviceCode string `json:"device_code"`
  55. Result string `json:"result"` // success / failed
  56. Error string `json:"error"`
  57. }
  58. func NewMQTTGateController(client mqtt.Client) *MQTTGateController {
  59. return &MQTTGateController{
  60. client: client,
  61. routes: make(map[string]gateTopicRoute),
  62. connectionState: make(map[string]bool),
  63. gateState: make(map[string]string),
  64. pending: make(map[string]pendingGateCommand),
  65. }
  66. }
  67. // SetClient 在 MQTT 客户端完成选项配置并创建后注入控制器。
  68. // 初始化阶段尚未开始订阅或下发指令,因此这里不需要额外同步。
  69. func (c *MQTTGateController) SetClient(client mqtt.Client) {
  70. c.mu.Lock()
  71. c.client = client
  72. c.mu.Unlock()
  73. }
  74. // LoadRoutes 从 DB 反查 device_code -> lot/booth/channel,用于构造主题。
  75. func (c *MQTTGateController) LoadRoutes() error {
  76. if global.GVA_DB == nil {
  77. return errors.New("数据库未初始化")
  78. }
  79. type row struct {
  80. DeviceCode string
  81. ChannelID uint
  82. BoothID uint
  83. ParkingLotID uint
  84. }
  85. var rows []row
  86. err := global.GVA_DB.Model(&dao.UHFReader{}).
  87. Select("uhf_reader.device_code, uhf_reader.channel_id, channel.booth_id, channel.parking_lot_id").
  88. Joins("LEFT JOIN channel ON channel.id = uhf_reader.channel_id").
  89. Where("uhf_reader.channel_id > 0 AND channel.booth_id > 0 AND channel.parking_lot_id > 0 AND uhf_reader.connect_type = ?", dao.ConnectTypeMQTT).
  90. Scan(&rows).Error
  91. if err != nil {
  92. return err
  93. }
  94. c.mu.Lock()
  95. // 配置变更后重新加载,不能保留已删除设备的旧路由。
  96. c.routes = make(map[string]gateTopicRoute)
  97. defer c.mu.Unlock()
  98. for _, r := range rows {
  99. if r.DeviceCode == "" || r.ChannelID == 0 || r.BoothID == 0 || r.ParkingLotID == 0 {
  100. continue
  101. }
  102. c.routes[r.DeviceCode] = gateTopicRoute{
  103. DeviceCode: r.DeviceCode, ChannelID: r.ChannelID,
  104. BoothID: r.BoothID, LotID: r.ParkingLotID,
  105. }
  106. }
  107. return nil
  108. }
  109. // HasRoute 判断设备是否配置了 MQTT 路由(供路由控制器决定走哪个后端)。
  110. func (c *MQTTGateController) HasRoute(deviceCode string) bool {
  111. c.mu.RLock()
  112. defer c.mu.RUnlock()
  113. _, ok := c.routes[deviceCode]
  114. return ok
  115. }
  116. func (c *MQTTGateController) routeOf(deviceCode string) (gateTopicRoute, bool) {
  117. c.mu.RLock()
  118. defer c.mu.RUnlock()
  119. r, ok := c.routes[deviceCode]
  120. return r, ok
  121. }
  122. // OpenGate 实现 GateController 接口。
  123. func (c *MQTTGateController) OpenGate(deviceCode string, validTime byte) error {
  124. return c.run(deviceCode, "open", validTime)
  125. }
  126. // CloseGate 实现 GateController 接口。
  127. func (c *MQTTGateController) CloseGate(deviceCode string, validTime byte) error {
  128. return c.run(deviceCode, "close", validTime)
  129. }
  130. func (c *MQTTGateController) run(deviceCode, action string, validTime byte) error {
  131. route, ok := c.routeOf(deviceCode)
  132. if !ok {
  133. return fmt.Errorf("设备未配置 MQTT 路由: %s", deviceCode)
  134. }
  135. cmdID := uuid.Must(uuid.NewV4()).String()
  136. ackCh := make(chan gateAck, 1)
  137. c.mu.Lock()
  138. c.pending[cmdID] = pendingGateCommand{deviceCode: deviceCode, ackCh: ackCh}
  139. c.mu.Unlock()
  140. defer func() {
  141. c.mu.Lock()
  142. delete(c.pending, cmdID)
  143. c.mu.Unlock()
  144. }()
  145. payload, _ := json.Marshal(gateCmd{
  146. Schema: "gate.cmd.v1", CmdID: cmdID, DeviceCode: deviceCode,
  147. Action: action, ValidTime: validTime,
  148. })
  149. token := c.client.Publish(route.cmdTopic(), mqttQoS, false, payload)
  150. if token.Wait() && token.Error() != nil {
  151. return fmt.Errorf("MQTT 指令下发失败: %w", token.Error())
  152. }
  153. select {
  154. case ack := <-ackCh:
  155. if ack.Result != "success" {
  156. return fmt.Errorf("道闸执行失败: %s", ack.Error)
  157. }
  158. return nil
  159. case <-time.After(mqttCmdTimeout):
  160. return errors.New("道闸指令超时未收到 ack")
  161. }
  162. }
  163. // IsGateConnected 实现 GateController 接口(以最近 state/lwt 为准)。
  164. func (c *MQTTGateController) IsGateConnected(deviceCode string) bool {
  165. c.mu.RLock()
  166. defer c.mu.RUnlock()
  167. return c.connectionState[deviceCode]
  168. }
  169. // Subscribe 订阅 state / ack / lwt 主题。
  170. func (c *MQTTGateController) Subscribe() error {
  171. for _, topic := range []string{
  172. "parking/lot/+/booth/+/channel/+/gate/state",
  173. "parking/lot/+/booth/+/channel/+/gate/cmd/ack",
  174. "parking/lot/+/booth/+/channel/+/gate/lwt",
  175. } {
  176. token := c.client.Subscribe(topic, mqttQoS, c.onMessage)
  177. if token.Wait() && token.Error() != nil {
  178. return token.Error()
  179. }
  180. }
  181. return nil
  182. }
  183. // MarkConfiguredDevicesOffline 在服务启动或 MQTT 总线重连期间清除旧进程留下的在线状态。
  184. // 后续收到设备的 retained state 或实时状态上报后,会立即恢复为在线。
  185. // 仅处理启用的 MQTT 设备,不能影响 TCP、串口设备自身的连接状态。
  186. func (c *MQTTGateController) MarkConfiguredDevicesOffline() error {
  187. if global.GVA_DB == nil {
  188. return errors.New("数据库未初始化")
  189. }
  190. return global.GVA_DB.Model(&dao.UHFReader{}).
  191. Where("connect_type = ? AND is_active = ?", dao.ConnectTypeMQTT, true).
  192. Where("status <> ?", "offline").
  193. Update("status", "offline").Error
  194. }
  195. // onMessage 分派 state / ack / lwt。
  196. func (c *MQTTGateController) onMessage(_ mqtt.Client, msg mqtt.Message) {
  197. payload := msg.Payload()
  198. var probe struct {
  199. Schema string `json:"schema"`
  200. CmdID string `json:"cmd_id"`
  201. DeviceCode string `json:"device_code"`
  202. State string `json:"state"`
  203. }
  204. if err := json.Unmarshal(payload, &probe); err != nil {
  205. return
  206. }
  207. if global.GVA_LOG != nil {
  208. global.GVA_LOG.Info("收到道闸 MQTT 消息",
  209. zap.String("topic", msg.Topic()),
  210. zap.String("schema", probe.Schema),
  211. zap.String("device_code", probe.DeviceCode),
  212. zap.String("state", probe.State))
  213. }
  214. switch probe.Schema {
  215. case "gate.ack.v1":
  216. var ack gateAck
  217. if json.Unmarshal(payload, &ack) == nil && ack.CmdID != "" && ack.DeviceCode != "" {
  218. c.mu.RLock()
  219. pending, ok := c.pending[ack.CmdID]
  220. c.mu.RUnlock()
  221. if ok && pending.deviceCode == ack.DeviceCode && ack.Schema == "gate.ack.v1" {
  222. select {
  223. case pending.ackCh <- ack:
  224. default:
  225. }
  226. }
  227. }
  228. case "gate.state.v1":
  229. if probe.DeviceCode == "" || probe.State == "" {
  230. return
  231. }
  232. c.mu.Lock()
  233. c.connectionState[probe.DeviceCode] = true
  234. c.gateState[probe.DeviceCode] = probe.State
  235. c.mu.Unlock()
  236. // 回写设备在线状态并关闭历史离线异常。
  237. // 设备编码必须与设备管理中的 device_code 一致,否则只能更新内存状态。
  238. c.persistDeviceStatus(probe.DeviceCode, "online")
  239. if global.GVA_DB != nil {
  240. incidentSvc.ResolveDeviceOffline(probe.DeviceCode)
  241. }
  242. case "gate.lwt.v1", "gate.lwt":
  243. c.mu.Lock()
  244. c.connectionState[probe.DeviceCode] = false
  245. c.mu.Unlock()
  246. // 回写设备离线状态并埋点离线异常(复用既有去重)
  247. c.persistDeviceStatus(probe.DeviceCode, "offline")
  248. if global.GVA_DB != nil {
  249. incidentSvc.RecordIncident(incidentService.RecordIncidentRequest{
  250. Category: incidentService.CategoryDeviceOffline,
  251. Source: incidentService.SourceDevice,
  252. DeviceCode: probe.DeviceCode,
  253. Description: "道闸 MQTT 遗嘱触发,设备离线",
  254. })
  255. }
  256. }
  257. }
  258. // persistDeviceStatus 将 MQTT 设备状态写回设备表,并显式暴露编码不匹配问题。
  259. // MQTT 订阅本身是按主题成功的,即使设备尚未在设备管理中配置,消息仍会进入这里;
  260. // 此时 GORM 不会报错,只会返回 RowsAffected=0,不能继续静默处理。
  261. func (c *MQTTGateController) persistDeviceStatus(deviceCode, status string) {
  262. if global.GVA_DB == nil || deviceCode == "" {
  263. return
  264. }
  265. updates := map[string]interface{}{"status": status}
  266. if status == "online" {
  267. updates["provision_status"] = gorm.Expr("CASE WHEN connect_type = ? AND provision_status IN (?, ?, ?, ?) THEN ? ELSE provision_status END", dao.ConnectTypeMQTT, "provisioned", "online", "offline", "failed", "online")
  268. } else if status == "offline" {
  269. updates["provision_status"] = gorm.Expr("CASE WHEN connect_type = ? AND provision_status IN (?, ?) THEN ? ELSE provision_status END", dao.ConnectTypeMQTT, "provisioned", "online", "offline")
  270. }
  271. if status == "online" {
  272. updates["last_online_time"] = time.Now()
  273. }
  274. result := global.GVA_DB.Model(&dao.UHFReader{}).
  275. Where("device_code = ?", deviceCode).
  276. Updates(updates)
  277. if result.Error != nil {
  278. if global.GVA_LOG != nil {
  279. global.GVA_LOG.Error("回写 MQTT 设备状态失败",
  280. zap.String("device_code", deviceCode), zap.String("status", status), zap.Error(result.Error))
  281. }
  282. return
  283. }
  284. if result.RowsAffected == 0 && global.GVA_LOG != nil {
  285. global.GVA_LOG.Warn("收到 MQTT 设备状态,但设备编码未配置或不匹配",
  286. zap.String("device_code", deviceCode), zap.String("status", status))
  287. }
  288. }