多岗亭改造方案.md 12 KB

多岗亭停车场改造方案(Smart Parking)

状态:待实施 适用范围:lc_garage/ 后端 internal/(旧分层 + modules/ 新模块)与前端 frontend/

1. 改造目标与原则

目标:让系统从「单岗亭/单区域记账」升级为「多岗亭、多区域、分岗亭核算」的架构,贯通「岗亭」这一维度到支付、交接班、报表、权限四条主线。

原则

  • 不推翻现有 internal/ 旧分层 + internal/modules/ 新模块的混合架构,以新增字段/新表 + 渐进改造为主。
  • 所有新增维度一律以「快照 + 冗余外键」方式落库,保证历史数据可读、报表稳定。
  • 改造按 P0→P3 分阶段交付,每阶段可独立上线、可独立验证。

2. 数据模型改造(核心)

2.1 payment_record 增加岗亭/通道/区域维度

// internal/dao/payment_record.go 新增字段
type PaymentRecord struct {
    // ...原有字段...
    BoothID      uint   `gorm:"index" json:"booth_id"`        // 收款岗亭ID(快照)
    BoothName    string `gorm:"size:100" json:"booth_name"`   // 岗亭名称快照
    ChannelID    uint   `gorm:"index" json:"channel_id"`      // 收款通道ID(快照)
    ParkingLotID uint   `gorm:"index" json:"parking_lot_id"`  // 停车场ID(快照)
}

写入时机ProcessPaymentTxAtEntryensurePrepaidPaymentTx、月卡/续费的 PaymentRecord 创建处,统一由调用方传入岗亭上下文(见 §4)。

2.2 shift_record 增加岗亭维度

type ShiftRecord struct {
    // ...原有字段...
    BoothID      uint   `gorm:"index" json:"booth_id"`
    BoothName    string `gorm:"size:100" json:"booth_name"`
    ParkingLotID uint   `gorm:"index" json:"parking_lot_id"`
}
  • Start/End 改为按 (BoothID, OperatorID) 约束,禁止同一岗亭同时两个当班、同一操作员跨岗亭同时当班。
  • 交接班对账 SQL 改为 WHERE booth_id=? AND operator_id=?

2.3 fee_config 增加区域维度(区域差异化定价)

type FeeConfig struct {
    // ...原有字段...
    ParkingLotID uint `gorm:"index;default:0" json:"parking_lot_id"` // 0 = 全局默认费率
}
  • 查重约束从「一车型一配置」放宽为「一车型 + 一区域一配置」,ParkingLotID=0 作为缺省兜底。
  • CalculateFee(vehicle, stayTime, parkingLotID):优先命中 (vehicle_type_id, parking_lot_id),未命中回退 (vehicle_type_id, 0)

2.4 月卡区域化

新增关联表(而非直接在 monthly_card 加字段,支持一张卡绑定多区域):

// internal/dao/monthly_card_lot.go
type MonthlyCardLot struct {
    global.GVA_MODEL
    MonthlyCardID uint `gorm:"index" json:"monthly_card_id"`
    ParkingLotID  uint `gorm:"index" json:"parking_lot_id"`
}
  • buildEntryAccessProfileTxCalculateFee 中月卡免费判断,改为「月卡有效 且 当前区域在卡绑定范围内」。

2.5 新增「操作员-岗亭」绑定模型

// internal/dao/booth_operator.go
type BoothOperator struct {
    global.GVA_MODEL
    UserID       uint `gorm:"index" json:"user_id"`        // 对应 sys_users.id
    BoothID      uint `gorm:"index" json:"booth_id"`       // 0 = 允许区域下全部岗亭
    ParkingLotID uint `gorm:"index" json:"parking_lot_id"`
}
  • 支持「绑定到具体岗亭」或「绑定到区域(该区域下所有岗亭)」,BoothID=0 表示区域级授权。

2.6 通道事件持久化

// internal/dao/passage_event.go
type PassageEvent struct {
    global.GVA_MODEL
    DeviceCode   string `gorm:"index"`
    RFIDTag      string
    PlateNumber  string
    Direction    string
    ChannelID    uint `gorm:"index"`
    BoothID      uint `gorm:"index"`
    ParkingLotID uint `gorm:"index"`
    Status       string
    Message      string
    Fee          float64
    StayTime     int64
    EventAt      time.Time `gorm:"index"`
}
  • 保留内存队列做实时推送(配合 plugin/ws),但每次 Push 同时落库GetChannelEvents 支持按 booth_id/parking_lot_id 过滤,重启后可回放。

3. 权限模型改造

角色 现有 改造后
管理员(888) 全量 全量 + 可配置岗亭绑定管理
操作员(618) 停车管理+报表+统计 限定可见/可操作的岗亭与通道

改造点:

  1. ListGateDevicesinternal/service/parking/gate.go)按当前登录用户过滤:查询 BoothOperator,只返回授权岗亭/区域下的设备。
  2. reportshiftpayment 查询接口统一注入岗亭作用域(数据权限),操作员只能看自己岗亭的数据,管理员可看全部。
  3. 交接班 start/end 校验操作员是否有该岗亭的绑定授权。

4. 服务层改造(关键调用链)

4.1 岗亭上下文贯穿支付链

定义统一上下文:

// internal/model/common/passage.go 扩展
type BoothContext struct {
    BoothID      uint
    BoothName    string
    ChannelID    uint
    ParkingLotID uint
}
  • VehicleEntry / ExitConfirm 的 Request 增加 BoothContext,入场时从 Channel → Booth → ParkingLot 派生并快照到 vehicle_record(可选冗余 entry_booth_id)。
  • ProcessPaymentTxAtEntryensurePrepaidPaymentTx、月卡/续费支付流水写入时,把 BoothContext 写入 payment_record
  • handleEntry/handleExitinternal/service/parking/passage.go)在调用 vehicleSvc 前先根据 device.Channel.Booth 组装 BoothContext

4.2 交接班改造

Start(operatorID, boothID, startingCash)
  ├─ 校验操作员对 boothID 有授权
  ├─ 校验该 boothID 无 active 班次
  └─ 写入 shift_record(含 booth 快照)

End(operatorID, boothID, actualCash, remark)
  ├─ collected = SUM(amount) WHERE payment_method=cash
  │              AND booth_id=boothID AND operator_id=operatorID
  │              AND paid_at BETWEEN 当班区间
  └─ 计算 expected/diff

4.3 计费改造

func CalculateFee(vehicle *dao.Vehicle, stayTime int64, parkingLotID uint) (float64, error)
  • 跨区域出场规则:默认按入场区域费率计费(与「释放入场区域车位」的现有语义一致);若后续需要,再引入「出场区域费率」配置开关。

4.4 报表改造

RevenueReport 增加维度,SQL 支持 GROUP BY 组合:

type RevenueQuery struct {
    StartDate    string
    EndDate      string
    ParkingLotID uint
    BoothID      uint   // 新增
    OperatorID   uint   // 新增
    GroupBy      string // lot | booth | operator | day
}

新增报表接口:

  • 岗亭营收报表(现金/免密/POS/扫码/退款,按岗亭聚合)
  • 班次对账报表(关联 shift_record 与 payment_record)
  • 月卡办理/续费/退款报表

5. 前端改造点

页面 改动
entryExit.vue 工作台 ① 顶部增加「当前岗亭」标识与切换(受权限过滤);② getGateDevices 只返回本岗亭/授权设备;③ 通道事件面板按岗亭过滤
shiftRecord.vue 交接班增加「岗亭」选择,列表按岗亭筛选
paymentRecord.vue / revenue.vue 增加「岗亭」「操作员」筛选与聚合维度
parkingInfo/booth.vue 岗亭下挂「授权操作员」配置页签
pricing.vue 费率配置增加「停车场区域」维度
monthlyCard.vue 月卡办理增加「适用区域」多选
新增 incident.vue 独立异常处置工作台(当前仅复用 sheet.vue)

6. 缺陷修复清单(随改造一并落地)

# 位置 修复内容
F1 parking/booth.go:61 读写器区域更新主键 channel.IDreader.ID
F2 parking/channel.go:26 岗亭不存在时返回错误,禁止静默 ParkingLotID=0
F3 parking/channel.go:61 BoothID 时同步刷新 ParkingLotID
F4 parking/channel.go/booth.go 删除前校验:岗亭有通道、通道有读写器/在场会话时禁止删除
F5 parking/passage.go:249-251 修复 UHF 免密放行漏洞:fee>0 且非白名单时(无论临时/注册)禁止 free
F6 monthly/service/service.go:137 退卡写负向退款 payment_record(计入账务与交接班)
F7 vehicle.go GetInParkVehicles join 改为 rfid_tag 兜底,避免无牌/RFID 临时车信息丢失
F8 passage.go 内存 map 重复入场判断以 DB ResolveActiveSession 为唯一权威,内存 map 仅作防抖

7. 数据迁移方案

采用 GORM AutoMigrate + 显式迁移步骤:

  1. 新增字段 payment_record.booth_id/channel_id/parking_lot_id 等,先允许为空,历史数据留空(报表对空值归入「未归类」)。
  2. 新增表:monthly_card_lotbooth_operatorpassage_eventshift_record 新字段。
  3. 回填脚本:对历史 payment_record,通过 record_id → vehicle_record → entry_channel_id → channel.booth_id 回填 BoothID/ParkingLotID;对历史 shift_record,若无岗亭信息则标 BoothID=0 并提示人工补录。
  4. 迁移逻辑放入 internal/initialize/ 下新增 migrate_booth.go,在 seed/启动流程中执行,保证幂等。

8. 分阶段实施计划

阶段 P0(骨架,1 个迭代)

  • 2.1 / 2.2 数据模型 + 4.2 交接班岗亭化 + 4.4 报表岗亭维度
  • F1~F4 数据一致性 bug 修复
  • 验证:能按岗亭出营收报表、按岗亭交接班对账

阶段 P1(区域化,1 个迭代)

  • 2.3 区域费率 + 2.4 月卡区域化 + 4.3 计费改造
  • F5、F6 漏洞修复
  • 验证:不同区域不同费率、月卡限区生效

阶段 P2(权限与设备,1 个迭代)

  • 2.5 操作员-岗亭绑定 + 3 权限改造 + 5 工作台岗亭隔离
  • 2.6 通道事件持久化
  • 验证:操作员只见本岗亭设备与数据

阶段 P3(补齐与体验,1 个迭代)

  • 摄像头/车牌识别模块
  • 异常独立工作台 + 实时告警
  • 报表体系补全(车型/支付方式/退款/月卡)

9. 验收标准

  1. 一个操作员在岗亭 A 收款、交接班时,系统能精确统计岗亭 A 本班次的现金应收/实收/差异。
  2. 操作员登录后只能看到被授权岗亭的通道设备与数据,无法跨岗亭开闸/收费。
  3. 同一车辆类型在不同区域可按不同费率计费,跨区出场按入场区域正确计费并释放入场区域车位。
  4. 月卡只在绑定区域内免费,满位准入按区域校验。
  5. UHF 自动通道对有费车辆(含注册车辆)不再免密放行。
  6. 月卡退卡产生负向流水,可被报表与交接班捕获。
  7. 删除岗亭/通道前有引用校验,不存在悬空通道/设备。
  8. 通道事件重启后可回放,且按岗亭隔离。

10. 设备接入层(摄像头 / 道闸通信)

结论:MQTT 做消息总线(识别事件 / 状态 / 在线心跳 / 独立道闸指令),HTTP 传图片与兜底只支持 HTTP 回调的设备,继电器/串口直连做挂在读写器上的道闸开闸。

  • Topic 按 parking/lot/{lot}/booth/{booth}/channel/{ch}/... 分区,直接呼应多岗亭维度。
  • MQTTGateController 实现现有 GateController 接口(OpenGate/CloseGate/IsGateConnected),经 SetGateController 注入,与 SimulatedGateController 并存。
  • DeviceBus 订阅相机识别事件,统一桥接 PassageService.HandlePassage + uhf.PushChannelEvent + incident.device_offline + plugin/ws
  • 道闸 LWT 遗嘱消息对接既有 incident 离线异常去重逻辑。

完整设计(Topic 表、JSON schema、mqtt_gate_controller.godevice_bus.go 代码草案、配置与装配)见:doc/设备接入层设计.md