# 多岗亭停车场改造方案(Smart Parking) > 状态:待实施 > 适用范围:`lc_garage/` 后端 `internal/`(旧分层 + `modules/` 新模块)与前端 `frontend/` ## 1. 改造目标与原则 **目标**:让系统从「单岗亭/单区域记账」升级为「多岗亭、多区域、分岗亭核算」的架构,贯通「岗亭」这一维度到支付、交接班、报表、权限四条主线。 **原则**: - 不推翻现有 `internal/` 旧分层 + `internal/modules/` 新模块的混合架构,以**新增字段/新表 + 渐进改造**为主。 - 所有新增维度一律以「快照 + 冗余外键」方式落库,保证历史数据可读、报表稳定。 - 改造按 P0→P3 分阶段交付,每阶段可独立上线、可独立验证。 --- ## 2. 数据模型改造(核心) ### 2.1 `payment_record` 增加岗亭/通道/区域维度 ```go // 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(快照) } ``` **写入时机**:`ProcessPaymentTxAtEntry`、`ensurePrepaidPaymentTx`、月卡/续费的 `PaymentRecord` 创建处,统一由调用方传入岗亭上下文(见 §4)。 ### 2.2 `shift_record` 增加岗亭维度 ```go 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` 增加区域维度(区域差异化定价) ```go 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 加字段,支持一张卡绑定多区域): ```go // 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"` } ``` - `buildEntryAccessProfileTx` 与 `CalculateFee` 中月卡免费判断,改为「月卡有效 且 当前区域在卡绑定范围内」。 ### 2.5 新增「操作员-岗亭」绑定模型 ```go // 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 通道事件持久化 ```go // 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. `ListGateDevices`(`internal/service/parking/gate.go`)按当前登录用户过滤:查询 `BoothOperator`,只返回授权岗亭/区域下的设备。 2. `report`、`shift`、`payment` 查询接口统一注入岗亭作用域(数据权限),操作员只能看自己岗亭的数据,管理员可看全部。 3. 交接班 `start/end` 校验操作员是否有该岗亭的绑定授权。 --- ## 4. 服务层改造(关键调用链) ### 4.1 岗亭上下文贯穿支付链 定义统一上下文: ```go // 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`)。 - `ProcessPaymentTxAtEntry`、`ensurePrepaidPaymentTx`、月卡/续费支付流水写入时,把 `BoothContext` 写入 `payment_record`。 - `handleEntry`/`handleExit`(`internal/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 计费改造 ```go func CalculateFee(vehicle *dao.Vehicle, stayTime int64, parkingLotID uint) (float64, error) ``` - 跨区域出场规则:**默认按入场区域费率计费**(与「释放入场区域车位」的现有语义一致);若后续需要,再引入「出场区域费率」配置开关。 ### 4.4 报表改造 `RevenueReport` 增加维度,SQL 支持 `GROUP BY` 组合: ```go 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.ID` → `reader.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_lot`、`booth_operator`、`passage_event`、`shift_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.go` 与 `device_bus.go` 代码草案、配置与装配)见:**`doc/设备接入层设计.md`**。