智慧停车 — 进出场逻辑流程
更新日期: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 自动识别 → 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.1 总体流程
车辆到达入口
│
├── 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 总体流程
车辆到达出口
│
├── 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. 计费算法多场景推演
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
│
├── 3. 免费时长内
│ stayMinutes <= StartTime → return 0
│
├── 4. 超时计费
│ extraTime = stayMinutes - StartTime
│ units = ceil(extraTime / UnitTime)
│ fee = StartFee + units × UnitFee
│
├── 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) |
6.3 数字票状态转换
Created → PendingPayment → Paid → Exited
↘ ↘
Expired Expired
创建时直接到 PendingPayment,出场支付后到 Paid,可再转为 Exited。
7. 边界情况 & 注意事项
7.1 RFID 防抖设计
passageDebounceSec = 3 // 3 秒内同一 RFID/车牌忽略重复请求
同一设备读头短时间内可能多次读取同一标签,防抖避免重复入场/出场。
7.2 出场先开闸后扣费
// 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 通道 |
手动通道 |
| 小区/园区(封闭) |
主要出入口,预注册车辆自动放行 |
访客登记入场 |
| 公共停车场 |
未实现 |
临时车收费出场 |
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: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: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 |