| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- package parking
- import (
- "encoding/json"
- "errors"
- "fmt"
- "sync"
- "time"
- mqtt "github.com/eclipse/paho.mqtt.golang"
- "github.com/gofrs/uuid/v5"
- "go.uber.org/zap"
- "gorm.io/gorm"
- "wails-app/internal/dao"
- "wails-app/internal/global"
- incidentService "wails-app/internal/modules/incident/service"
- )
- const (
- mqttQoS = 1
- mqttCmdTimeout = 5 * time.Second
- )
- // gateTopicRoute 设备在 MQTT 主题树中的路由坐标。
- type gateTopicRoute struct {
- DeviceCode string
- LotID uint
- BoothID uint
- ChannelID uint
- }
- func (r gateTopicRoute) cmdTopic() string {
- return fmt.Sprintf("parking/lot/%d/booth/%d/channel/%d/gate/cmd", r.LotID, r.BoothID, r.ChannelID)
- }
- // MQTTGateController 通过 MQTT 下发道闸指令并等待设备 ack,实现 GateController 接口。
- // 用于「独立网络道闸控制器」;挂在 UHF 读写器继电器上的道闸仍走 DeviceManager。
- type MQTTGateController struct {
- client mqtt.Client
- mu sync.RWMutex
- routes map[string]gateTopicRoute // device_code -> route
- connectionState map[string]bool // device_code -> MQTT 连接状态
- gateState map[string]string // device_code -> open/closed/moving/fault
- pending map[string]pendingGateCommand
- }
- type pendingGateCommand struct {
- deviceCode string
- ackCh chan gateAck
- }
- type gateCmd struct {
- Schema string `json:"schema"`
- CmdID string `json:"cmd_id"`
- DeviceCode string `json:"device_code"`
- Action string `json:"action"`
- ValidTime byte `json:"valid_time"`
- }
- type gateAck struct {
- Schema string `json:"schema"`
- CmdID string `json:"cmd_id"`
- DeviceCode string `json:"device_code"`
- Result string `json:"result"` // success / failed
- Error string `json:"error"`
- }
- func NewMQTTGateController(client mqtt.Client) *MQTTGateController {
- return &MQTTGateController{
- client: client,
- routes: make(map[string]gateTopicRoute),
- connectionState: make(map[string]bool),
- gateState: make(map[string]string),
- pending: make(map[string]pendingGateCommand),
- }
- }
- // SetClient 在 MQTT 客户端完成选项配置并创建后注入控制器。
- // 初始化阶段尚未开始订阅或下发指令,因此这里不需要额外同步。
- func (c *MQTTGateController) SetClient(client mqtt.Client) {
- c.mu.Lock()
- c.client = client
- c.mu.Unlock()
- }
- // LoadRoutes 从 DB 反查 device_code -> lot/booth/channel,用于构造主题。
- func (c *MQTTGateController) LoadRoutes() error {
- if global.GVA_DB == nil {
- return errors.New("数据库未初始化")
- }
- type row struct {
- DeviceCode string
- ChannelID uint
- BoothID uint
- ParkingLotID uint
- }
- var rows []row
- err := global.GVA_DB.Model(&dao.UHFReader{}).
- Select("uhf_reader.device_code, uhf_reader.channel_id, channel.booth_id, channel.parking_lot_id").
- Joins("LEFT JOIN channel ON channel.id = uhf_reader.channel_id").
- Where("uhf_reader.channel_id > 0 AND channel.booth_id > 0 AND channel.parking_lot_id > 0 AND uhf_reader.connect_type = ?", dao.ConnectTypeMQTT).
- Scan(&rows).Error
- if err != nil {
- return err
- }
- c.mu.Lock()
- // 配置变更后重新加载,不能保留已删除设备的旧路由。
- c.routes = make(map[string]gateTopicRoute)
- defer c.mu.Unlock()
- for _, r := range rows {
- if r.DeviceCode == "" || r.ChannelID == 0 || r.BoothID == 0 || r.ParkingLotID == 0 {
- continue
- }
- c.routes[r.DeviceCode] = gateTopicRoute{
- DeviceCode: r.DeviceCode, ChannelID: r.ChannelID,
- BoothID: r.BoothID, LotID: r.ParkingLotID,
- }
- }
- return nil
- }
- // HasRoute 判断设备是否配置了 MQTT 路由(供路由控制器决定走哪个后端)。
- func (c *MQTTGateController) HasRoute(deviceCode string) bool {
- c.mu.RLock()
- defer c.mu.RUnlock()
- _, ok := c.routes[deviceCode]
- return ok
- }
- func (c *MQTTGateController) routeOf(deviceCode string) (gateTopicRoute, bool) {
- c.mu.RLock()
- defer c.mu.RUnlock()
- r, ok := c.routes[deviceCode]
- return r, ok
- }
- // OpenGate 实现 GateController 接口。
- func (c *MQTTGateController) OpenGate(deviceCode string, validTime byte) error {
- return c.run(deviceCode, "open", validTime)
- }
- // CloseGate 实现 GateController 接口。
- func (c *MQTTGateController) CloseGate(deviceCode string, validTime byte) error {
- return c.run(deviceCode, "close", validTime)
- }
- func (c *MQTTGateController) run(deviceCode, action string, validTime byte) error {
- route, ok := c.routeOf(deviceCode)
- if !ok {
- return fmt.Errorf("设备未配置 MQTT 路由: %s", deviceCode)
- }
- cmdID := uuid.Must(uuid.NewV4()).String()
- ackCh := make(chan gateAck, 1)
- c.mu.Lock()
- c.pending[cmdID] = pendingGateCommand{deviceCode: deviceCode, ackCh: ackCh}
- c.mu.Unlock()
- defer func() {
- c.mu.Lock()
- delete(c.pending, cmdID)
- c.mu.Unlock()
- }()
- payload, _ := json.Marshal(gateCmd{
- Schema: "gate.cmd.v1", CmdID: cmdID, DeviceCode: deviceCode,
- Action: action, ValidTime: validTime,
- })
- token := c.client.Publish(route.cmdTopic(), mqttQoS, false, payload)
- if token.Wait() && token.Error() != nil {
- return fmt.Errorf("MQTT 指令下发失败: %w", token.Error())
- }
- select {
- case ack := <-ackCh:
- if ack.Result != "success" {
- return fmt.Errorf("道闸执行失败: %s", ack.Error)
- }
- return nil
- case <-time.After(mqttCmdTimeout):
- return errors.New("道闸指令超时未收到 ack")
- }
- }
- // IsGateConnected 实现 GateController 接口(以最近 state/lwt 为准)。
- func (c *MQTTGateController) IsGateConnected(deviceCode string) bool {
- c.mu.RLock()
- defer c.mu.RUnlock()
- return c.connectionState[deviceCode]
- }
- // Subscribe 订阅 state / ack / lwt 主题。
- func (c *MQTTGateController) Subscribe() error {
- for _, topic := range []string{
- "parking/lot/+/booth/+/channel/+/gate/state",
- "parking/lot/+/booth/+/channel/+/gate/cmd/ack",
- "parking/lot/+/booth/+/channel/+/gate/lwt",
- } {
- token := c.client.Subscribe(topic, mqttQoS, c.onMessage)
- if token.Wait() && token.Error() != nil {
- return token.Error()
- }
- }
- return nil
- }
- // MarkConfiguredDevicesOffline 在服务启动或 MQTT 总线重连期间清除旧进程留下的在线状态。
- // 后续收到设备的 retained state 或实时状态上报后,会立即恢复为在线。
- // 仅处理启用的 MQTT 设备,不能影响 TCP、串口设备自身的连接状态。
- func (c *MQTTGateController) MarkConfiguredDevicesOffline() error {
- if global.GVA_DB == nil {
- return errors.New("数据库未初始化")
- }
- return global.GVA_DB.Model(&dao.UHFReader{}).
- Where("connect_type = ? AND is_active = ?", dao.ConnectTypeMQTT, true).
- Where("status <> ?", "offline").
- Update("status", "offline").Error
- }
- // onMessage 分派 state / ack / lwt。
- func (c *MQTTGateController) onMessage(_ mqtt.Client, msg mqtt.Message) {
- payload := msg.Payload()
- var probe struct {
- Schema string `json:"schema"`
- CmdID string `json:"cmd_id"`
- DeviceCode string `json:"device_code"`
- State string `json:"state"`
- }
- if err := json.Unmarshal(payload, &probe); err != nil {
- return
- }
- if global.GVA_LOG != nil {
- global.GVA_LOG.Info("收到道闸 MQTT 消息",
- zap.String("topic", msg.Topic()),
- zap.String("schema", probe.Schema),
- zap.String("device_code", probe.DeviceCode),
- zap.String("state", probe.State))
- }
- switch probe.Schema {
- case "gate.ack.v1":
- var ack gateAck
- if json.Unmarshal(payload, &ack) == nil && ack.CmdID != "" && ack.DeviceCode != "" {
- c.mu.RLock()
- pending, ok := c.pending[ack.CmdID]
- c.mu.RUnlock()
- if ok && pending.deviceCode == ack.DeviceCode && ack.Schema == "gate.ack.v1" {
- select {
- case pending.ackCh <- ack:
- default:
- }
- }
- }
- case "gate.state.v1":
- if probe.DeviceCode == "" || probe.State == "" {
- return
- }
- c.mu.Lock()
- c.connectionState[probe.DeviceCode] = true
- c.gateState[probe.DeviceCode] = probe.State
- c.mu.Unlock()
- // 回写设备在线状态并关闭历史离线异常。
- // 设备编码必须与设备管理中的 device_code 一致,否则只能更新内存状态。
- c.persistDeviceStatus(probe.DeviceCode, "online")
- if global.GVA_DB != nil {
- incidentSvc.ResolveDeviceOffline(probe.DeviceCode)
- }
- case "gate.lwt.v1", "gate.lwt":
- c.mu.Lock()
- c.connectionState[probe.DeviceCode] = false
- c.mu.Unlock()
- // 回写设备离线状态并埋点离线异常(复用既有去重)
- c.persistDeviceStatus(probe.DeviceCode, "offline")
- if global.GVA_DB != nil {
- incidentSvc.RecordIncident(incidentService.RecordIncidentRequest{
- Category: incidentService.CategoryDeviceOffline,
- Source: incidentService.SourceDevice,
- DeviceCode: probe.DeviceCode,
- Description: "道闸 MQTT 遗嘱触发,设备离线",
- })
- }
- }
- }
- // persistDeviceStatus 将 MQTT 设备状态写回设备表,并显式暴露编码不匹配问题。
- // MQTT 订阅本身是按主题成功的,即使设备尚未在设备管理中配置,消息仍会进入这里;
- // 此时 GORM 不会报错,只会返回 RowsAffected=0,不能继续静默处理。
- func (c *MQTTGateController) persistDeviceStatus(deviceCode, status string) {
- if global.GVA_DB == nil || deviceCode == "" {
- return
- }
- updates := map[string]interface{}{"status": status}
- if status == "online" {
- 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")
- } else if status == "offline" {
- updates["provision_status"] = gorm.Expr("CASE WHEN connect_type = ? AND provision_status IN (?, ?) THEN ? ELSE provision_status END", dao.ConnectTypeMQTT, "provisioned", "online", "offline")
- }
- if status == "online" {
- updates["last_online_time"] = time.Now()
- }
- result := global.GVA_DB.Model(&dao.UHFReader{}).
- Where("device_code = ?", deviceCode).
- Updates(updates)
- if result.Error != nil {
- if global.GVA_LOG != nil {
- global.GVA_LOG.Error("回写 MQTT 设备状态失败",
- zap.String("device_code", deviceCode), zap.String("status", status), zap.Error(result.Error))
- }
- return
- }
- if result.RowsAffected == 0 && global.GVA_LOG != nil {
- global.GVA_LOG.Warn("收到 MQTT 设备状态,但设备编码未配置或不匹配",
- zap.String("device_code", deviceCode), zap.String("status", status))
- }
- }
|