进出场逻辑流程.md 25 KB

智慧停车 — 进出场逻辑流程

更新日期: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