|
|
@@ -1,245 +1,541 @@
|
|
|
# 智慧停车 — 进出场逻辑流程
|
|
|
|
|
|
-> 日期:2026-07-24
|
|
|
+> 更新日期:2026-07-29
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 0. 车辆类型体系
|
|
|
+
|
|
|
+系统通过 `vehicle_type` 表管理车辆类型,每种类型关联独立的计费配置 `fee_config`。
|
|
|
+
|
|
|
+### 0.1 车辆类型分类
|
|
|
+
|
|
|
+| 维度 | 分类 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| **按身份** | 临时车(is_system=true) | 新进场自动创建,无车主关联 |
|
|
|
+| | 固定车(is_system=false) | 预先注册,关联 `Owner`,可配置 VIP |
|
|
|
+| **按名单** | 白名单(包月车) | 在 `shortlist` 表中有记录,ListType=白名单 |
|
|
|
+| | 黑名单 | 在 `shortlist` 表中有记录,ListType=黑名单 |
|
|
|
+| | 普通车辆 | 不在任何名单中 |
|
|
|
+| **按VIP** | VIP 车辆 | Owner.IsVip=true 且 VipExpireTime 未过期 |
|
|
|
+| | 非 VIP 车辆 | 无 VIP 或已过期 |
|
|
|
+
|
|
|
+### 0.2 默认类型
|
|
|
+
|
|
|
+系统初始化时自动创建唯一系统内置类型:**临时车**(Name="临时车", IsSystem=true)。
|
|
|
+管理员可在管理后台创建更多车辆类型(如"月租车"、"内部车"、"VIP 车辆"等),每种类型可独立配置 `fee_config`。
|
|
|
+
|
|
|
+### 0.3 计费配置(fee_config)
|
|
|
+
|
|
|
+每个 `vehicle_type` 对应一条计费规则:
|
|
|
+
|
|
|
+| 字段 | 说明 |
|
|
|
+|------|------|
|
|
|
+| StartTime | 免费时长(分钟) |
|
|
|
+| StartFee | 起步价 |
|
|
|
+| UnitTime | 计费单位时间(分钟) |
|
|
|
+| UnitFee | 单位费用 |
|
|
|
+| DailyMaxFee | 每日封顶(0=不封顶) |
|
|
|
+| IsVIPFree | VIP 是否免费 |
|
|
|
+| VIPDiscount | VIP 折扣率(0.8=打8折) |
|
|
|
+
|
|
|
+---
|
|
|
|
|
|
## 1. 触发方式总览
|
|
|
|
|
|
```
|
|
|
车辆到达/离开
|
|
|
│
|
|
|
- ├─ RFID 自动识别 → PassageService.HandlePassage() → 自动进出场
|
|
|
- ├─ 手动操作 → entryExit.vue → 手动进出场
|
|
|
- └─ 摄像头车牌识别 → (待实现 P0-5)
|
|
|
+ ├─ ① RFID 自动识别 → reader.go → HandlePassage() → 自动进出场(开闸)
|
|
|
+ ├─ ② 手动操作前端 → entryExit.vue → 手动进场 / 出场
|
|
|
+ └─ ③ 摄像头车牌识别 → (待实现 P0-5)
|
|
|
```
|
|
|
|
|
|
+### 触发方式对比
|
|
|
+
|
|
|
+| 特性 | RFID 自动 | 手动录入 |
|
|
|
+|------|-----------|---------|
|
|
|
+| 用户体验 | 无需停车,自动抬杆 | 需操作员输入车牌/RFID |
|
|
|
+| 入场流程 | HandlePassage → handleEntry → VehicleEntry → 自动开闸 | 前端 API → VehicleEntry → 手动抬杆(仅记录) |
|
|
|
+| 出场流程 | HandlePassage → handleExit → ExitConfirm(free) → 自动开闸 | 前端 ExitPreview → 选择支付方式 → ExitConfirm → 手动抬杆(仅记录) |
|
|
|
+| 支付方式 | 统一走 `free`(无现金交易,账户结算) | 临时车:现金收款;固定车:免密 free |
|
|
|
+| 通道限制 | 绑定 UHF 设备 → 查 Channel → 校验方向 + 临时车权限 | 无通道级校验 |
|
|
|
+
|
|
|
---
|
|
|
|
|
|
-## 2. 入场流程
|
|
|
+## 2. 进场流程
|
|
|
|
|
|
-### 2.1 流程图
|
|
|
+### 2.1 总体流程
|
|
|
|
|
|
```
|
|
|
车辆到达入口
|
|
|
│
|
|
|
- ├── 手动录入 ──────────────────────────────┐
|
|
|
- │ 前端 entryExit.vue │
|
|
|
- │ 输入车牌/RFID → POST /vehicle/entry │
|
|
|
- │ │
|
|
|
- ├── RFID 自动识别 ───────────────────────── │
|
|
|
- │ reader.go → ParseBuffer() │
|
|
|
- │ → PassageService.HandlePassage() │
|
|
|
- │ → 防抖检查(3秒去重) │
|
|
|
- │ → handleEntry() │
|
|
|
- │ │
|
|
|
- ▼ ▼
|
|
|
-┌─────────────────────────────────────────────────┐
|
|
|
-│ VehicleEntry() │
|
|
|
-│ internal/service/vehicle/vehicle.go │
|
|
|
-│ │
|
|
|
-│ ① 黑名单检查 │
|
|
|
-│ CheckVehicleShortlist(plate, rfid) │
|
|
|
-│ ├─ 在黑名单 → 拦截,返回错误 │
|
|
|
-│ └─ 不在 → 继续 │
|
|
|
-│ │
|
|
|
-│ ② 车辆查询 │
|
|
|
-│ GetVehicleByPlateNumber(plate, rfid) │
|
|
|
-│ ├─ 找到 → 使用现有车辆 │
|
|
|
-│ └─ 未找到 → 新建车辆(默认 is_system类型=临时车) │
|
|
|
-│ │
|
|
|
-│ ③ 重复入场检查 │
|
|
|
-│ ┌── 已有未出场记录 → 拦截 │
|
|
|
-│ └── 无 → 继续 │
|
|
|
-│ │
|
|
|
-│ ④ 创建入场记录 │
|
|
|
-│ INSERT vehicle_record │
|
|
|
-│ (plate_number, rfid_tag, entry_time, │
|
|
|
-│ parking_lot_id, payment_status=unpaid) │
|
|
|
-│ │
|
|
|
-│ ⑤ 创建数字票 │
|
|
|
-│ digital_ticket.Create() │
|
|
|
-│ 状态: pending_payment │
|
|
|
-│ 事件日志: [{"event":"created", ...}] │
|
|
|
-│ │
|
|
|
-│ ⑥ 更新车辆统计 │
|
|
|
-│ vehicle.last_entry_time = now │
|
|
|
-│ │
|
|
|
-│ ⑦ RFID → 开闸(GateOpener) │
|
|
|
-│ 手动 → 仅记录,需手动命令抬杆 │
|
|
|
-└─────────────────────────────────────────────────┘
|
|
|
-```
|
|
|
-
|
|
|
-### 2.2 RFID 入场特有检查
|
|
|
-
|
|
|
-```
|
|
|
-RFID 扫描 → PassageService.HandlePassage()
|
|
|
- │
|
|
|
- ├── 防抖(3秒内重复扫描忽略)
|
|
|
- ├── 查询设备 → 获取通道 → 确定方向/direction
|
|
|
- ├── 查询通道 → allowTemporary(是否允许临停)
|
|
|
- ├── 查询车辆 → 判断是否为临时车(VehicleType.IsSystem)
|
|
|
- ├── 黑名单检查
|
|
|
- ├── 临时车 && 通道不允许临停 → 拦截
|
|
|
- └── 入场状态检查(epcStatus 防重入)
|
|
|
+ ├── RFID 自动识别 ─────────────────────────────────────────────
|
|
|
+ │ reader.go 协程读取 UHF 数据
|
|
|
+ │ → ParseBuffer() 解析报文 + CRC校验
|
|
|
+ │ → HandlePassage(PassageRequest{RFIDTag: epc, DeviceCode})
|
|
|
+ │
|
|
|
+ ├── 手动录入 ──────────────────────────────────────────────────
|
|
|
+ │ entryExit.vue 进场Tab
|
|
|
+ │ 输入 车牌/RFID → 查询车辆信息(显示类型/车主)
|
|
|
+ │ 选择停车区域 → 点击"确认入场"
|
|
|
+ │ → POST /vehicle/entry → VehicleEntry()
|
|
|
+ │
|
|
|
+ ├── (摄像头计划) ──────────────────────────────────────────────
|
|
|
+ │ (待实现)
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+┌─────────────────────────────────────────────────────────────────┐
|
|
|
+│ VehicleEntry() 核心入口 │
|
|
|
+│ internal/service/vehicle/vehicle.go:177 │
|
|
|
+│ │
|
|
|
+│ ① 黑名单检查 │
|
|
|
+│ CheckVehicleShortlist(plate, rfid) │
|
|
|
+│ ├─ 在黑名单 → 拦截,返回 "车辆在黑名单中,禁止入场" │
|
|
|
+│ └─ 不在 → 继续 │
|
|
|
+│ │
|
|
|
+│ ② 车辆查询/自动创建 │
|
|
|
+│ GetVehicleByPlateNumber(plate, rfid) │
|
|
|
+│ ├─ 找到 → 使用现有车辆信息(类型/RFID/Owner) │
|
|
|
+│ └─ 未找到 → 自动创建临时车 │
|
|
|
+│ 创建 vehicle(IsSystem=true 的类型, 无Owner) │
|
|
|
+│ │
|
|
|
+│ ③ 重复入场检查 │
|
|
|
+│ WHERE (plate=? OR rfid=?) AND exit_time IS NULL │
|
|
|
+│ ├─ 已有未出场记录 → "车辆已入场,未出场" │
|
|
|
+│ └─ 无 → 继续 │
|
|
|
+│ │
|
|
|
+│ ④ 创建入场记录 │
|
|
|
+│ INSERT vehicle_record │
|
|
|
+│ (plate_number, rfid_tag, entry_time, │
|
|
|
+│ parking_lot_id, parking_space_id, payment_status='unpaid') │
|
|
|
+│ │
|
|
|
+│ ⑤ 创建数字票 │
|
|
|
+│ digital_ticket.Create() │
|
|
|
+│ 状态: pending_payment │
|
|
|
+│ event_log: [{"event":"created","trigger":"manual|rfid"}] │
|
|
|
+│ │
|
|
|
+│ ⑥ 更新车辆统计 │
|
|
|
+│ vehicle.last_entry_time = now │
|
|
|
+│ │
|
|
|
+│ ⑦ 响应返回 │
|
|
|
+│ RFID(HandlePassage内)→ GateOpener 开闸 │
|
|
|
+│ 手动 → 仅返回成功,操作员另行手动抬杆 │
|
|
|
+└─────────────────────────────────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### 2.2 RFID 入场特有流程(HandlePassage)
|
|
|
+
|
|
|
+```
|
|
|
+RFID 上报 → reader.go handleReportData()
|
|
|
+ │
|
|
|
+ ├── 解析 EPC(hex编码 12字节)
|
|
|
+ ├── 构造 PassageRequest{RFIDTag: epc, DeviceCode: 设备编码}
|
|
|
+ └── HandlePassage()
|
|
|
+ │
|
|
|
+ ├── 1. 防抖(3秒内同一 identifier 的重复请求忽略)
|
|
|
+ │ identifier = RFID优先,其次车牌
|
|
|
+ │ passageLastTime[identifier] 记录时间戳
|
|
|
+ │
|
|
|
+ ├── 2. 查 UHF 设备 → 获取绑定通道
|
|
|
+ │ device = first(uhf_reader WHERE device_code = ?)
|
|
|
+ │ channel = device.Channel
|
|
|
+ │
|
|
|
+ ├── 3. 获取通道方向
|
|
|
+ │ channel.Direction → "in" / "out" / "inout"
|
|
|
+ │
|
|
|
+ ├── 4. 查询车辆
|
|
|
+ │ v = GetVehicleByPlateNumber(PlateNumber, RFIDTag)
|
|
|
+ │ isTempVehicle = v.VehicleType.IsSystem 或 未找到车辆
|
|
|
+ │
|
|
|
+ ├── 5. 黑名单检查
|
|
|
+ │ CheckVehicleShortlist(plate, rfid)
|
|
|
+ │
|
|
|
+ ├── 6. 临时车规则检查
|
|
|
+ │ if isTempVehicle && !channel.AllowTemporary
|
|
|
+ │ → "临时车不允许此通道" 拦截
|
|
|
+ │
|
|
|
+ ├── 7. 方向判断
|
|
|
+ │ direction == "in" → handleEntry()
|
|
|
+ │
|
|
|
+ ├── 8. handleEntry() 入场状态检查
|
|
|
+ │ passageStatus[identifier] → true? "车辆已在场内" 拦截
|
|
|
+ │
|
|
|
+ ├── 9. GateOpener(deviceCode, 2) → 开闸
|
|
|
+ │ 通过 UHF 设备继电器控制道闸(Relay1 抬起)
|
|
|
+ │
|
|
|
+ ├── 10. VehicleEntry() → 创建入场记录
|
|
|
+ │
|
|
|
+ └── 11. passageStatus[identifier] = true
|
|
|
+```
|
|
|
+
|
|
|
+### 2.3 手动入场特有流程(entryExit.vue)
|
|
|
+
|
|
|
+```
|
|
|
+操作员打开「进出场操作」页面
|
|
|
+ │
|
|
|
+ ├── 选择「进场」Tab
|
|
|
+ │
|
|
|
+ ├── 输入车牌 或 RFID → 点击查询
|
|
|
+ │ ├─ 查到车辆 → 显示车辆类型、车主、VIP状态
|
|
|
+ │ └─ 未查到 → 显示 "未找到车辆" 提示,仍可入场(自动创建临时车)
|
|
|
+ │
|
|
|
+ ├── 选择停车区域(下拉框选择 parking_lot)
|
|
|
+ │
|
|
|
+ └── 点击「确认入场」
|
|
|
+ → POST /vehicle/entry → VehicleEntry()
|
|
|
+ → 成功:弹窗提示 "入场成功"
|
|
|
+ → 失败:弹窗提示错误原因
|
|
|
+
|
|
|
+注意:手动入场不涉及道闸控制,操作员需另行手动抬杆。
|
|
|
```
|
|
|
|
|
|
---
|
|
|
|
|
|
## 3. 出场流程
|
|
|
|
|
|
-### 3.1 流程图
|
|
|
+### 3.1 总体流程
|
|
|
|
|
|
```
|
|
|
车辆到达出口
|
|
|
│
|
|
|
- ├── 手动录入 ─────────────────────────────────┐
|
|
|
- │ entryExit.vue 出场Tab │
|
|
|
- │ ┌─ 输入车牌/RFID → 查询车辆 │
|
|
|
- │ ├─ POST /vehicle/exit/preview → 费用预览 │
|
|
|
- │ ├─ 显示费用 + 支付方式选择 │
|
|
|
- │ ├─ 临时车: 现金收款 → 输入实收 → 计算找零 │
|
|
|
- │ ├─ 非临时车: 免密(payment_method=free) │
|
|
|
- │ └─ POST /vehicle/exit/confirm → 确认 │
|
|
|
- │ │
|
|
|
- ├── RFID 自动识别 ──────────────────────────── │
|
|
|
- │ PassageService.handleExit() │
|
|
|
- │ → 直接调 ExitConfirm(payment_method=free) │
|
|
|
- │ │
|
|
|
- ▼ ▼
|
|
|
-┌─────────────────────────────────────────────────────┐
|
|
|
-│ ExitConfirm() │
|
|
|
-│ internal/service/vehicle/vehicle.go │
|
|
|
-│ │
|
|
|
-│ ① 黑名单检查 │
|
|
|
-│ CheckVehicleShortlist(plate, rfid) │
|
|
|
-│ └─ 在黑名单 → 拦截 │
|
|
|
-│ │
|
|
|
-│ ② 查询未出场记录 │
|
|
|
-│ WHERE (plate=? OR rfid=?) AND exit_time IS NULL │
|
|
|
-│ └─ 未找到 → "车辆未入场或已出场" │
|
|
|
-│ │
|
|
|
-│ ③ 计算费用 │
|
|
|
-│ calculateFee(vehicle, stayTime) │
|
|
|
-│ ├─ 查 fee_config(按车型) │
|
|
|
-│ ├─ VIP检查(IsVip + 过期检查 + 非零值) │
|
|
|
-│ ├─ 免费时长内 → 0 元 │
|
|
|
-│ ├─ 超时计费(StartFee + units × UnitFee) │
|
|
|
-│ └─ 每日封顶 / VIP折扣 │
|
|
|
-│ │
|
|
|
-│ ④ 更新出场记录 │
|
|
|
-│ vehicle_record.exit_time = now │
|
|
|
-│ vehicle_record.fee = calculated │
|
|
|
-│ vehicle_record.payment_method = req.method │
|
|
|
-│ │
|
|
|
-│ ⑤ 更新车辆统计 │
|
|
|
-│ vehicle.last_exit_time / total_stay / total_fee │
|
|
|
-│ │
|
|
|
-│ ⑥ 创建支付记录 │
|
|
|
-│ payment_record (amount, method, operator, change) │
|
|
|
-│ vehicle_record.payment_status = "paid" │
|
|
|
-│ │
|
|
|
-│ ⑦ RFID → 开闸 │
|
|
|
-│ 手动 → 仅记录 │
|
|
|
-└─────────────────────────────────────────────────────┘
|
|
|
-```
|
|
|
-
|
|
|
-### 3.2 计费算法(calculateFee)
|
|
|
-
|
|
|
-```
|
|
|
-calculateFee(vehicle, stayTime)
|
|
|
- │
|
|
|
- ├─ 1. 查 fee_config WHERE vehicle_type_id = ?
|
|
|
- │ ├─ 未找到 → 默认费率 0.1元/分钟
|
|
|
- │ └─ 全零配置 → 同样回退默认费率
|
|
|
- │
|
|
|
- ├─ 2. VIP 判断
|
|
|
- │ owner.IsVip && VipExpireTime.After(now) && !VipExpireTime.IsZero()
|
|
|
- │ └─ 过期 / 零值 → 非 VIP
|
|
|
- │
|
|
|
- ├─ 3. VIP 免费(isVIP && feeConfig.IsVIPFree)
|
|
|
- │ └─ return 0
|
|
|
- │
|
|
|
- ├─ 4. 免费时长内
|
|
|
- │ stayTime ≤ StartTime → return 0
|
|
|
- │
|
|
|
- ├─ 5. 超时计费
|
|
|
- │ extraTime = stayTime - StartTime
|
|
|
- │ units = ceil(extraTime / UnitTime)
|
|
|
- │ fee = StartFee + units × UnitFee
|
|
|
- │
|
|
|
- ├─ 6. 每日封顶
|
|
|
- │ fee > DailyMaxFee → fee = DailyMaxFee
|
|
|
- │
|
|
|
- └─ 7. VIP 折扣
|
|
|
- fee ×= VIPDiscount
|
|
|
+ ├── RFID 自动识别 ────────────────────────────────────────────
|
|
|
+ │ HandlePassage() → direction == "out" → handleExit()
|
|
|
+ │ → 直接调用 ExitConfirm(free, 免密)
|
|
|
+ │ → 自动开闸
|
|
|
+ │
|
|
|
+ ├── 手动操作 ─────────────────────────────────────────────────
|
|
|
+ │ entryExit.vue 出场Tab
|
|
|
+ │ ① 输入车牌/RFID → 查询车辆
|
|
|
+ │ ② 调用 ExitPreview → 显示费用/停留时间/车型/是否临时车
|
|
|
+ │ ③ 选择支付方式(临时车+现金: 输入实收金额)
|
|
|
+ │ ④ 点击"确认出场并支付"
|
|
|
+ │ → POST /vehicle/exit/confirm → ExitConfirm()
|
|
|
+ │ → 创建支付记录
|
|
|
+ │
|
|
|
+ ├── (摄像头计划) ─────────────────────────────────────────────
|
|
|
+ │ (待实现)
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+┌─────────────────────────────────────────────────────────────────────┐
|
|
|
+│ ExitConfirm() 核心入口 │
|
|
|
+│ internal/service/vehicle/vehicle.go:339 │
|
|
|
+│ │
|
|
|
+│ ① 黑名单检查(出场同样拦截) │
|
|
|
+│ CheckVehicleShortlist(plate, rfid) │
|
|
|
+│ └─ 在黑名单 → "车辆在黑名单中,禁止出场" │
|
|
|
+│ │
|
|
|
+│ ② 查询未出场记录 │
|
|
|
+│ WHERE (plate=? OR rfid=?) AND exit_time IS NULL │
|
|
|
+│ └─ 未找到 → "车辆未入场或已出场" │
|
|
|
+│ │
|
|
|
+│ ③ 计算费用 │
|
|
|
+│ calculateFee(vehicle, stayMinutes) │
|
|
|
+│ ├─ 查 fee_config(按 vehicle_type_id) │
|
|
|
+│ ├─ VIP检查(IsVip + 过期时间未到 + 非零值) │
|
|
|
+│ ├─ 免费时长内 → 0 元 │
|
|
|
+│ ├─ 超时计费(StartFee + units × UnitFee) │
|
|
|
+│ ├─ 每日封顶 │
|
|
|
+│ └─ VIP折扣 │
|
|
|
+│ │
|
|
|
+│ ④ 更新出场记录 │
|
|
|
+│ vehicle_record.exit_time = now │
|
|
|
+│ vehicle_record.stay_time = duration │
|
|
|
+│ vehicle_record.fee = calculated │
|
|
|
+│ vehicle_record.payment_method = req.method │
|
|
|
+│ │
|
|
|
+│ ⑤ 更新车辆统计 │
|
|
|
+│ vehicle.last_exit_time = now │
|
|
|
+│ vehicle.total_stay_time += duration │
|
|
|
+│ vehicle.total_fee += fee │
|
|
|
+│ │
|
|
|
+│ ⑥ 创建支付记录 │
|
|
|
+│ payment_record (record_id, method, amount, paid_amount, │
|
|
|
+│ change_amount, operator_id, paid_at) │
|
|
|
+│ vehicle_record.payment_status = "paid" │
|
|
|
+│ │
|
|
|
+│ ⑦ 响应返回 │
|
|
|
+│ RFID(handleExit内)→ GateOpener 开闸 │
|
|
|
+│ 手动 → 仅返回成功 │
|
|
|
+└─────────────────────────────────────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 RFID 出场特有流程(handleExit)
|
|
|
+
|
|
|
+```
|
|
|
+RFID 上报 → HandlePassage() → direction == "out"
|
|
|
+ │
|
|
|
+ ├── 1. 防抖检查(与入场共用逻辑)
|
|
|
+ ├── 2. 查设备/通道(同上)
|
|
|
+ ├── 3. 判断方向为 out
|
|
|
+ │
|
|
|
+ └── handleExit()
|
|
|
+ │
|
|
|
+ ├── 打印日志 + GateOpener 开闸(先开闸,后处理出场)
|
|
|
+ │
|
|
|
+ ├── 构造 ExitConfirmRequest
|
|
|
+ │ PaymentMethod: "free" ← 重要!RFID 出场免密
|
|
|
+ │ PaidAmount: 0
|
|
|
+ │ OperatorID: 0(系统自动)
|
|
|
+ │
|
|
|
+ ├── ExitConfirm() → 计算费用 + 记录出场 + 创建支付记录
|
|
|
+ │
|
|
|
+ └── passageStatus[identifier] = false(标记离场)
|
|
|
+```
|
|
|
+
|
|
|
+### 3.3 手动出场特有流程(entryExit.vue)
|
|
|
+
|
|
|
+```
|
|
|
+操作员选择「出场」Tab
|
|
|
+ │
|
|
|
+ ├── ① 输入车牌/RFID → 查询车辆信息
|
|
|
+ │
|
|
|
+ ├── ② 自动调用 ExitPreview API
|
|
|
+ │ 返回:plate_number, entry_time, stay_time, fee,
|
|
|
+ │ vehicle_type, is_temp(是否临时车)
|
|
|
+ │
|
|
|
+ ├── ③ 显示费用预览面板
|
|
|
+ │ ├─ 入场时间、停留时长、应收费用
|
|
|
+ │ ├─ 车辆类型(临时车/固定车)
|
|
|
+ │ │
|
|
|
+ │ └─ 临时车 → 选支付方式
|
|
|
+ │ ├─ 现金(已实现)
|
|
|
+ │ ├─ 微信(disabled,待对接)
|
|
|
+ │ └─ POS机(disabled,待对接)
|
|
|
+ │
|
|
|
+ ├── ④ 临时车 + 现金
|
|
|
+ │ └─ 输入实收金额 → 自动计算找零
|
|
|
+ │
|
|
|
+ ├── ⑤ 确认出场
|
|
|
+ │ ├─ 临时车:校验 paid_amount >= fee
|
|
|
+ │ ├─ 固定车:payment_method = "free", paid_amount = 0
|
|
|
+ │ └─ 弹出确认弹窗
|
|
|
+ │
|
|
|
+ └── ⑥ POST /vehicle/exit/confirm → ExitConfirm()
|
|
|
+ → 创建支付记录 + 更新出场
|
|
|
+ → 弹窗显示:入场时间 / 停留时长 / 缴费金额
|
|
|
```
|
|
|
|
|
|
---
|
|
|
|
|
|
-## 4. 名单检查逻辑
|
|
|
+## 4. 计费算法多场景推演
|
|
|
|
|
|
```
|
|
|
-CheckVehicleShortlist(plateNumber, rfidTag)
|
|
|
+calculateFee(vehicle, stayMinutes)
|
|
|
+ │
|
|
|
+ ├── 0. 查 fee_config WHERE vehicle_type_id = ?
|
|
|
+ │ ├─ 未找到 → 默认费率 0.1元/分钟(回退)
|
|
|
+ │ └─ 找到且全零字段 → 同样回退默认费率
|
|
|
+ │
|
|
|
+ ├── 1. VIP 判断
|
|
|
+ │ vehicle.OwnerId != nil
|
|
|
+ │ → 查 Owner.IsVip && VipExpireTime.After(now) && !VipExpireTime.IsZero()
|
|
|
+ │
|
|
|
+ ├── 2. VIP 免费
|
|
|
+ │ isVIP && feeConfig.IsVIPFree → return 0
|
|
|
│
|
|
|
- ├─ 按车牌或 RFID 查 vehicle 表
|
|
|
- │ └─ 未找到 → 返回 false, false
|
|
|
+ ├── 3. 免费时长内
|
|
|
+ │ stayMinutes <= StartTime → return 0
|
|
|
│
|
|
|
- ├─ 按 vehicle_id 查 shortlist 表
|
|
|
- │ └─ 未找到 → 返回 false, false
|
|
|
+ ├── 4. 超时计费
|
|
|
+ │ extraTime = stayMinutes - StartTime
|
|
|
+ │ units = ceil(extraTime / UnitTime)
|
|
|
+ │ fee = StartFee + units × UnitFee
|
|
|
│
|
|
|
- └─ shortlist.ListType == "黑名单" → isBlack=true
|
|
|
- shortlist.ListType == "白名单" → isWhite=true
|
|
|
+ ├── 5. 每日封顶
|
|
|
+ │ fee > DailyMaxFee && DailyMaxFee > 0 → fee = DailyMaxFee
|
|
|
+ │
|
|
|
+ └── 6. VIP折扣
|
|
|
+ isVIP && !IsVIPFree → fee *= VIPDiscount
|
|
|
+```
|
|
|
+
|
|
|
+### 场景示例
|
|
|
+
|
|
|
+| 场景 | 时长(分钟) | StartTime | StartFee | UnitTime | UnitFee | DailyMax | VIP | 结果 |
|
|
|
+|------|-----------|-----------|----------|----------|---------|----------|-----|------|
|
|
|
+| 临时车30分钟内免费 | 25 | 30 | 5 | 15 | 2 | 30 | 否 | 0 |
|
|
|
+| 临时车超1h | 90 | 30 | 5 | 15 | 2 | 30 | 否 | 5+ceil(60/15)×2=13 |
|
|
|
+| 临时车超1h封顶 | 480 | 30 | 5 | 15 | 2 | 30 | 否 | min(5+ceil(450/15)×2=65, 30)=30 |
|
|
|
+| VIP免费 | 120 | 30 | 5 | 15 | 2 | 30 | 是(IsVIPFree) | 0 |
|
|
|
+| VIP折扣 | 120 | 30 | 5 | 15 | 2 | 30 | 是(折扣0.8) | ceil(5+ceil(90/15)×2)×0.8=10.4 |
|
|
|
+| 无配置回退 | 60 | - | - | - | - | - | - | 60×0.1=6 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 各车辆类型的完整进出场行为
|
|
|
+
|
|
|
+### 5.1 临时车(无固定车主)
|
|
|
+
|
|
|
+| 阶段 | 行为 |
|
|
|
+|------|------|
|
|
|
+| **入场** | • 手动:前端查询不到车辆 → 仍可入场,自动创建临时车记录 |
|
|
|
+| | • RFID:通道 AllowTemporary=true 才放行,否则拦截 |
|
|
|
+| **出场** | • 手动:费用预览 → 收款(现金)→ 确认出场 |
|
|
|
+| | • RFID:自动出场,免密 |
|
|
|
+| **计费** | 按车型 fee_config 计算,VIP 相关规则不适用(无 Owner) |
|
|
|
+
|
|
|
+### 5.2 固定车(有车主,非 VIP)
|
|
|
+
|
|
|
+| 阶段 | 行为 |
|
|
|
+|------|------|
|
|
|
+| **入场** | • 手动:查询到车辆信息(显示类型/车主)→ 确认入场 |
|
|
|
+| | • RFID:白名单校验 → 黑名单校验 → 入场 |
|
|
|
+| **出场** | • 手动:ExitPreview 后按 free(免密)出场 |
|
|
|
+| | • RFID:自动出场,免密 |
|
|
|
+| **计费** | 按 fee_config 正常计费(如有 StartFee/UnitFee 则收费) |
|
|
|
+
|
|
|
+### 5.3 VIP 车辆(车主 IsVip=true 且未过期)
|
|
|
+
|
|
|
+| 阶段 | 行为 |
|
|
|
+|------|------|
|
|
|
+| **入场** | 同固定车 |
|
|
|
+| **出场** | 同固定车 |
|
|
|
+| **计费** | • feeConfig.IsVIPFree → 免费 0 元 |
|
|
|
+| | • 否则 fee × VIPDiscount(如 0.8=8折) |
|
|
|
+
|
|
|
+### 5.4 白名单车辆(包月车)
|
|
|
+
|
|
|
+| 阶段 | 行为 |
|
|
|
+|------|------|
|
|
|
+| **定义** | 车辆加入 `shortlist` 表,ListType="白名单",记录 ExpirationTime |
|
|
|
+| **入场** | • CheckVehicleShortlist → isWhite=true → 放行(无拦截) |
|
|
|
+| | • 即使 ExpirationTime 过期,白名单不拦截入场 |
|
|
|
+| **出场** | • 不拦截出场 |
|
|
|
+| | • 包月车结费规则在 Shortlist 过期处理中决定:若 ExpirationTime < now 则按临时车计费;若未过期则依赖 fee_config 中的费用判断 |
|
|
|
+
|
|
|
+**说明**:目前代码中白名单本身**不修改计费逻辑**,只是放行/不拦截。
|
|
|
+包月车真正的费用减免靠 `fee_config` 的 StartTime/IsVIPFree 等配置控制,
|
|
|
+或通过 `MonthlyCard` 模块进行包月办理管理(只记录办理记录)。
|
|
|
+
|
|
|
+### 5.5 黑名单车辆
|
|
|
+
|
|
|
+| 阶段 | 行为 |
|
|
|
+|------|------|
|
|
|
+| **入场** | CheckVehicleShortlist → isBlack=true → **拦截**,禁止入场 |
|
|
|
+| **出场** | ExitConfirm → CheckVehicleShortlist → isBlack=true → **拦截** |
|
|
|
+| | 注意:黑名单在出场也拦截,需要管理员手动解除黑名单才能出场 |
|
|
|
+| **设置** | 通过后台「黑白名单」管理页面将车辆加入黑名单 |
|
|
|
+
|
|
|
+### 5.6 进出场状态检查矩阵
|
|
|
+
|
|
|
+| 状态 | 手动入场 | RFID入场 | 手动出场 | RFID出场 |
|
|
|
+|------|---------|---------|---------|---------|
|
|
|
+| 黑名单 | ❌ 拦截 | ❌ 拦截 | ❌ 拦截 | ❌ 拦截 |
|
|
|
+| 未入场 | ✅ 正常 | ✅ 正常 | ❌ "未入场或已出场" | ❌(先开闸再出错?待商榷) |
|
|
|
+| 已在场内 | ❌ "已入场,未出场" | ❌ "已在场内" | ✅ 正常 | ✅ 正常 |
|
|
|
+| 临时车+不允许临时通道 | N/A(手动无通道校验) | ❌ 拦截 | N/A | N/A |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 数据表变更汇总
|
|
|
+
|
|
|
+### 6.1 入场写入
|
|
|
+
|
|
|
+| 表 | 操作 | 关键字段 |
|
|
|
+|----|------|---------|
|
|
|
+| vehicle | 查/创建 | plate_number, rfid_tag, vehicle_type_id, last_entry_time |
|
|
|
+| vehicle_record | INSERT | plate_number, rfid_tag, entry_time=now, parking_lot_id, payment_status=unpaid |
|
|
|
+| digital_ticket | INSERT | ticket_no, plate_number, trigger_mode, state=pending_payment, entry_time=now |
|
|
|
+
|
|
|
+### 6.2 出场写入
|
|
|
+
|
|
|
+| 表 | 操作 | 关键字段 |
|
|
|
+|----|------|---------|
|
|
|
+| vehicle_record | UPDATE | exit_time=now, stay_time=fee, payment_method, payment_status=paid |
|
|
|
+| vehicle | UPDATE | last_exit_time, total_stay_time+=, total_fee+= |
|
|
|
+| payment_record | INSERT | record_id, payment_method, amount, paid_amount, change_amount, operator_id, paid_at=now |
|
|
|
+| digital_ticket | UPDATE | state→paid/exit(手动调用Transition) |
|
|
|
|
|
|
-名单检查位置:
|
|
|
- ├─ VehicleEntry (手动+RIFD进场) ✅
|
|
|
- ├─ ExitConfirm (手动+RIFD出场) ✅
|
|
|
- └─ PassageService.HandlePassage ✅
|
|
|
+### 6.3 数字票状态转换
|
|
|
+
|
|
|
+```
|
|
|
+Created → PendingPayment → Paid → Exited
|
|
|
+ ↘ ↘
|
|
|
+ Expired Expired
|
|
|
```
|
|
|
|
|
|
+创建时直接到 PendingPayment,出场支付后到 Paid,可再转为 Exited。
|
|
|
+
|
|
|
---
|
|
|
|
|
|
-## 5. 数字票状态跟踪
|
|
|
+## 7. 边界情况 & 注意事项
|
|
|
+
|
|
|
+### 7.1 RFID 防抖设计
|
|
|
|
|
|
+```go
|
|
|
+passageDebounceSec = 3 // 3 秒内同一 RFID/车牌忽略重复请求
|
|
|
```
|
|
|
-VehicleEntry()
|
|
|
- │
|
|
|
- └─ digital_ticket.Create()
|
|
|
- │
|
|
|
- 状态: pending_payment
|
|
|
- event_log: [{"event":"created","trigger":"manual|rfid"}]
|
|
|
- │
|
|
|
- ▼ ExitConfirm()
|
|
|
- │ └─ 创建 payment_record
|
|
|
- │
|
|
|
- 状态: paid (手动调用)
|
|
|
- event_log: [...,{"event":"paid","payment_method":"cash"}]
|
|
|
+同一设备读头短时间内可能多次读取同一标签,防抖避免重复入场/出场。
|
|
|
+
|
|
|
+### 7.2 出场先开闸后扣费
|
|
|
+
|
|
|
+```go
|
|
|
+// passage.go handleExit() — 先开闸
|
|
|
+GateOpener(req.DeviceCode, 2)
|
|
|
+// 再处理出场扣费
|
|
|
+vehicleSvc.ExitConfirm(confirmReq, 0)
|
|
|
```
|
|
|
+**风险**:如果 ExitConfirm 失败(如数据库错误),车辆已出场但未记录。
|
|
|
+设计意图是保证通行效率 > 计费准确性,损失可通过后台对账发现。
|
|
|
+
|
|
|
+### 7.3 RFID 出场免密
|
|
|
+
|
|
|
+RFID 自动出场统一走 `PaymentMethod: "free"`,即设备触发不开单、不收费、仅记录。
|
|
|
+临时车通过 RFID 出场时一样免密——这意味着 RFID 出场不产生实收费用。
|
|
|
+
|
|
|
+**设计决策**:如果需要临时车 RFID 出场时收费,需要在出口部署收费岗亭或对接线上支付。目前设计为:
|
|
|
+- **RFID 通道**:封闭场景(如小区/园区),车辆都经过授权(白名单或固定车),计费走账户/月租
|
|
|
+- **临时车**:走人工通道,现金/扫码支付后出场
|
|
|
+
|
|
|
+### 7.4 手动出场支付方法
|
|
|
+
|
|
|
+| 场景 | payment_method | paid_amount | operator_id |
|
|
|
+|------|---------------|-------------|-------------|
|
|
|
+| 临时车现金收款 | "cash" | 实收金额 | 当前登录用户ID |
|
|
|
+| 固定车免密 | "free" | 0 | 当前登录用户ID |
|
|
|
+| RFID自动出场 | "free" | 0 | 0(系统) |
|
|
|
+
|
|
|
+### 7.5 黑名单出场拦截
|
|
|
+
|
|
|
+黑名单在 `ExitConfirm()` 中同样检查。如果车辆在出场时被加入黑名单,会拦截出场:
|
|
|
+需要先通过「黑白名单管理」移除该车辆的黑名单状态,才能正常出场。
|
|
|
+
|
|
|
+### 7.6 小区/园区封闭场景 vs 公共停车场
|
|
|
+
|
|
|
+| 场景 | RFID 通道 | 手动通道 |
|
|
|
+|------|----------|---------|
|
|
|
+| 小区/园区(封闭) | 主要出入口,预注册车辆自动放行 | 访客登记入场 |
|
|
|
+| 公共停车场 | 未实现 | 临时车收费出场 |
|
|
|
|
|
|
---
|
|
|
|
|
|
-## 6. 代码入口汇总
|
|
|
+## 8. 相关 API 汇总
|
|
|
+
|
|
|
+| 方法 | 路由 | 功能 | 文件 |
|
|
|
+|------|------|------|------|
|
|
|
+| POST | /vehicle/entry | 手动入场 | `internal/api/v1/vehicle/vehicle.go:131` |
|
|
|
+| POST | /vehicle/exit | 出场(旧接口) | `internal/api/v1/vehicle/vehicle.go:148` |
|
|
|
+| POST | /vehicle/exit/preview | 出场费用预览 | `internal/modules/payment/api.go:17` |
|
|
|
+| POST | /vehicle/exit/confirm | 确认出场+支付 | `internal/modules/payment/api.go:32` |
|
|
|
+| GET | /vehicle/get-by-plate | 查询车辆信息 | `internal/api/v1/vehicle/vehicle.go:66` |
|
|
|
+| POST | /vehicle/entry/rfid | RFID 自动进出场 | `internal/service/uhf/reader.go` 内部调用 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 9. 代码入口汇总
|
|
|
|
|
|
| 触发方式 | 入口函数 | 文件 |
|
|
|
|---------|---------|------|
|
|
|
| 手动进场 | `VehicleEntry()` | `internal/service/vehicle/vehicle.go:177` |
|
|
|
| RFID 进场 | `PassageService.handleEntry()` | `internal/service/parking/passage.go:103` |
|
|
|
-| 手动出场(预览) | `ExitPreview()` | `internal/service/vehicle/vehicle.go:296` |
|
|
|
-| 手动出场(确认) | `ExitConfirm()` | `internal/service/vehicle/vehicle.go:334` |
|
|
|
-| RFID 出场 | `PassageService.handleExit()` | `internal/service/parking/passage.go:150` |
|
|
|
-| 计费引擎 | `calculateFee()` | `internal/service/vehicle/vehicle.go:384` |
|
|
|
+| 手动出场(预览) | `ExitPreview()` | `internal/service/vehicle/vehicle.go:302` |
|
|
|
+| 手动出场(确认) | `ExitConfirm()` | `internal/service/vehicle/vehicle.go:339` |
|
|
|
+| RFID 出场 | `PassageService.handleExit()` | `internal/service/parking/passage.go:148` |
|
|
|
+| RFID 统一入口 | `HandlePassage()` | `internal/service/parking/passage.go:40` |
|
|
|
+| 计费引擎 | `calculateFee()` | `internal/service/vehicle/vehicle.go:390` |
|
|
|
| 名单检查 | `CheckVehicleShortlist()` | `internal/service/vehicle/shortlist.go:30` |
|
|
|
-| 数字票创建 | `TicketService.Create()` | `internal/modules/digital-ticket/service/service.go:52` |
|
|
|
-| 道闸控制 | `GateOpener()` | `internal/service/uhf/reader.go:88` |
|
|
|
-
|
|
|
-## 7. 包月车进出场识别
|
|
|
-
|
|
|
-```
|
|
|
-车辆进场 / 出场
|
|
|
- │
|
|
|
- ├─ CheckVehicleShortlist(plate, rfid)
|
|
|
- │
|
|
|
- └─ shortlist.ListType == "白名单" && ExpirationTime > now
|
|
|
- ├─ 未过期 → 白名单放行(后续按免密处理)
|
|
|
- └─ 已过期 → 按临时车计费
|
|
|
-```
|
|
|
-
|
|
|
-包月车办理时自动写入 `shortlist(白名单, ExpirationTime=end_date)`,进出场复用现有白名单检查,零改动。
|
|
|
+| 数字票创建 | `TicketService.Create()` | `internal/modules/digital-ticket/service/service.go:46` |
|
|
|
+| 道闸控制 | `GateOpener()` / `OpenGateByDeviceCode()` | `internal/service/uhf/reader.go:96` |
|
|
|
+| UHF 数据解析 | `parseReportData()` | `internal/service/uhf/reader.go:221` |
|
|
|
+| UHF 上报处理 | `handleReportData()` | `internal/service/uhf/reader.go:250` |
|
|
|
+| 支付处理 | `ProcessPayment()` | `internal/modules/payment/service/service.go:29` |
|