# 智慧停车 — 进出场逻辑流程 > 更新日期: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 防抖设计 ```go passageDebounceSec = 3 // 3 秒内同一 RFID/车牌忽略重复请求 ``` 同一设备读头短时间内可能多次读取同一标签,防抖避免重复入场/出场。 ### 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 通道 | 手动通道 | |------|----------|---------| | 小区/园区(封闭) | 主要出入口,预注册车辆自动放行 | 访客登记入场 | | 公共停车场 | 未实现 | 临时车收费出场 | --- ## 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` |