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

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