状态:待实施 适用范围:
lc_garage/后端internal/(旧分层 +modules/新模块)与前端frontend/
目标:让系统从「单岗亭/单区域记账」升级为「多岗亭、多区域、分岗亭核算」的架构,贯通「岗亭」这一维度到支付、交接班、报表、权限四条主线。
原则:
internal/ 旧分层 + internal/modules/ 新模块的混合架构,以新增字段/新表 + 渐进改造为主。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(快照)
}
写入时机:ProcessPaymentTxAtEntry、ensurePrepaidPaymentTx、月卡/续费的 PaymentRecord 创建处,统一由调用方传入岗亭上下文(见 §4)。
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) 约束,禁止同一岗亭同时两个当班、同一操作员跨岗亭同时当班。WHERE booth_id=? AND operator_id=?。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)。新增关联表(而非直接在 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"`
}
buildEntryAccessProfileTx 与 CalculateFee 中月卡免费判断,改为「月卡有效 且 当前区域在卡绑定范围内」。// 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 表示区域级授权。// 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 过滤,重启后可回放。| 角色 | 现有 | 改造后 |
|---|---|---|
| 管理员(888) | 全量 | 全量 + 可配置岗亭绑定管理 |
| 操作员(618) | 停车管理+报表+统计 | 限定可见/可操作的岗亭与通道 |
改造点:
ListGateDevices(internal/service/parking/gate.go)按当前登录用户过滤:查询 BoothOperator,只返回授权岗亭/区域下的设备。report、shift、payment 查询接口统一注入岗亭作用域(数据权限),操作员只能看自己岗亭的数据,管理员可看全部。start/end 校验操作员是否有该岗亭的绑定授权。定义统一上下文:
// 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。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
func CalculateFee(vehicle *dao.Vehicle, stayTime int64, parkingLotID uint) (float64, error)
RevenueReport 增加维度,SQL 支持 GROUP BY 组合:
type RevenueQuery struct {
StartDate string
EndDate string
ParkingLotID uint
BoothID uint // 新增
OperatorID uint // 新增
GroupBy string // lot | booth | operator | day
}
新增报表接口:
| 页面 | 改动 |
|---|---|
entryExit.vue 工作台 |
① 顶部增加「当前岗亭」标识与切换(受权限过滤);② getGateDevices 只返回本岗亭/授权设备;③ 通道事件面板按岗亭过滤 |
shiftRecord.vue |
交接班增加「岗亭」选择,列表按岗亭筛选 |
paymentRecord.vue / revenue.vue |
增加「岗亭」「操作员」筛选与聚合维度 |
parkingInfo/booth.vue |
岗亭下挂「授权操作员」配置页签 |
pricing.vue |
费率配置增加「停车场区域」维度 |
monthlyCard.vue |
月卡办理增加「适用区域」多选 |
新增 incident.vue |
独立异常处置工作台(当前仅复用 sheet.vue) |
| # | 位置 | 修复内容 |
|---|---|---|
| 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 仅作防抖 |
采用 GORM AutoMigrate + 显式迁移步骤:
payment_record.booth_id/channel_id/parking_lot_id 等,先允许为空,历史数据留空(报表对空值归入「未归类」)。monthly_card_lot、booth_operator、passage_event、shift_record 新字段。payment_record,通过 record_id → vehicle_record → entry_channel_id → channel.booth_id 回填 BoothID/ParkingLotID;对历史 shift_record,若无岗亭信息则标 BoothID=0 并提示人工补录。internal/initialize/ 下新增 migrate_booth.go,在 seed/启动流程中执行,保证幂等。结论:MQTT 做消息总线(识别事件 / 状态 / 在线心跳 / 独立道闸指令),HTTP 传图片与兜底只支持 HTTP 回调的设备,继电器/串口直连做挂在读写器上的道闸开闸。
parking/lot/{lot}/booth/{booth}/channel/{ch}/... 分区,直接呼应多岗亭维度。MQTTGateController 实现现有 GateController 接口(OpenGate/CloseGate/IsGateConnected),经 SetGateController 注入,与 SimulatedGateController 并存。DeviceBus 订阅相机识别事件,统一桥接 PassageService.HandlePassage + uhf.PushChannelEvent + incident.device_offline + plugin/ws。incident 离线异常去重逻辑。完整设计(Topic 表、JSON schema、mqtt_gate_controller.go 与 device_bus.go 代码草案、配置与装配)见:doc/设备接入层设计.md。