Ver Fonte

docs: 更新项目文档(进度/流程/规划)

Co-Authored-By: Claude <noreply@anthropic.com>
lq há 3 semanas atrás
pai
commit
f53794392b
7 ficheiros alterados com 566 adições e 1605 exclusões
  1. 86 64
      doc/PROJECT.md
  2. 97 148
      doc/digital-ticket.md
  3. 82 178
      doc/代码规划.md
  4. 72 117
      doc/收费流程.md
  5. 90 298
      doc/进出场流程详解.md
  6. 41 541
      doc/进出场逻辑流程.md
  7. 98 259
      doc/项目进度.md

+ 86 - 64
doc/PROJECT.md

@@ -1,88 +1,110 @@
-# 智慧停车管理系统 (Smart Parking) — Wails 桌面应用
+# 智慧停车管理系统项目说明
 
-> **项目名称**:lc_garage
-> **框架**:Wails v2.13 + GIN-VUE-ADMIN v2.6.3
-> **最后更新**:2026-07-10
+> 项目:`lc_garage`
+> 更新日期:2026-08-07
+> 形态:Wails 单进程 Windows 桌面应用
 
----
+## 1. 项目定位
 
-## 架构概述
+智慧停车管理系统用于停车场的基础配置、车辆进出场、收费结算、数字票、交接班、报表和本地设备联动。
 
-单进程 Wails 桌面应用:Go 后端(Gin HTTP)在 goroutine 运行,Vue 3 前端通过 WebView2 加载,`/api/*` 请求经内置反向代理转发到 Gin
+系统以**停车会话**作为一辆车一次停车过程的事实载体,以**数字票**作为支付和离场状态载体,以**支付流水**作为资金事实载体。三者职责分离,但在进出场事务中保持一致
 
-```
-Wails .exe
-  ├─ goroutine: Gin HTTP Server (:8888)
-  └─ WebView2: Vue 3 SPA
-       └─ /api/* → reverse proxy → Gin (strip /api prefix)
-```
-
----
-
-## 技术栈
+## 2. 技术架构
 
 | 层级 | 技术 |
-|------|------|
-| 桌面框架 | Wails v2.13 |
-| 后端 | Go 1.25 + Gin + GORM + Casbin + JWT |
-| 前端 | Vue 3 + Vite 4 + Element Plus + Pinia + ECharts |
-| 数据库 | SQLite(`%APPDATA%/smart-parking/lc_garage.db`) |
-
----
+| --- | --- |
+| 桌面容器 | Wails v2.13 + WebView2 |
+| 后端 | Go 1.25、Gin、GORM、Casbin、JWT |
+| 前端 | Vue 3、Vite 4、Element Plus、Pinia、ECharts |
+| 数据库 | SQLite,默认 `%APPDATA%/smart-parking/lc_garage.db` |
+| 设备 | UHF 读卡器、模拟/真实闸机、串口或 USB 小票打印机 |
+
+```text
+Wails 桌面程序
+├─ Gin API(默认 :8888)
+├─ Vue 3 WebView2 前端
+│  └─ /api/* 由 Wails 反向代理至 Gin
+└─ SQLite 本地数据库
+```
 
-## 项目结构
+## 3. 当前代码结构
 
-```
+```text
 lc_garage/
-├── main.go                   # Wails 入口
-├── wails.json                # Wails 配置
-├── go.mod                    # 根模块 (wails-app)
-├── config.yaml               # 后端配置
-├── server/                   # Go 后端(独立模块 "server")
-├── frontend/                 # Vue 3 前端
-├── build/bin/                # 构建输出
-├── deploy/                   # Docker/K8s
-├── lc_garage.sql             # 数据库初始化
-└── doc/                      # 文档
+├─ main.go                         # Wails 入口与 API 反向代理
+├─ internal/
+│  ├─ api/v1/                      # 既有 API 控制器
+│  ├─ service/                     # 既有业务与设备服务
+│  ├─ router/                       # 既有路由组
+│  ├─ dao/                          # GORM 实体
+│  ├─ model/                        # 通用、请求与响应模型
+│  ├─ initialize/                   # 数据库、种子数据、路由、设备初始化
+│  └─ modules/                      # 新领域模块
+│     ├─ parking-session/           # 停车会话
+│     ├─ digital-ticket/            # 数字票
+│     ├─ payment/                   # 支付入口、方式和支付流水
+│     ├─ monthly/                   # 包月车
+│     ├─ shift/                     # 交接班
+│     ├─ report/                    # 收入报表
+│     └─ printer/                   # 串口/USB 小票打印
+├─ frontend/src/
+│  ├─ view/parking/                 # 进出场、数字票、支付配置、包月车、交接班
+│  ├─ view/report/                  # 进出记录、支付记录、收入报表
+│  └─ view/dashboard/               # 仪表盘与车辆监控
+└─ doc/                             # 项目、流程、进度和测试文档
 ```
 
----
+## 4. 核心领域模型
 
-## 后端改动(相对于原始 GVA)
+| 对象 | 表/模块 | 职责 |
+| --- | --- | --- |
+| 停车会话 | `vehicle_record` / `parking-session` | 一次停车的入场、出场、停车场、通道设备快照和结算摘要 |
+| 数字票 | `digital_ticket` / `digital-ticket` | 会话的支付和离场状态、票号、事件日志 |
+| 支付流水 | `payment_record` / `payment` | 实际收款、实收、找零、支付入口、支付方式、操作员 |
+| 通道设备 | `channel`、`uhf_reader` | 通道方向、停车场归属、设备绑定与在线状态 |
 
-| 文件 | 改动 | 说明 |
-|------|------|------|
-| `server/main.go` | `package main` → `package server`,导出 `InitBackend()` | 供 Wails 入口调用 |
-| `server/core/viper.go` | 新增 exe 目录 fallback | 双击 exe 时能找到 config.yaml |
+### 停车会话的通道设备快照
 
-其余 `api/` `service/` `dao/` `router/` `middleware/` 等全部不变
+入场和出场分别保存通道 ID、通道编码、通道名称、设备编码、设备名称。名称用于业务查看,编码和 ID 用于追踪;通道或设备后续改名不会影响新记录的历史可读性。
 
----
+对于旧记录,查询时会根据已保存的通道 ID、设备编码补全当前名称;若关联配置已删除,则回退显示编码。
 
-## 前端改动(相对于 lc_garage/web)
+## 5. 启动与构建
 
-| 文件 | 改动 |
-|------|------|
-| `view/parking/parkingInfo/` | 新增:停车场左列表+右Tab联动布局 |
-| `pinia/parkArea.js` | 新增:停车场联动状态 |
-| `view/layout/aside/index.vue` | 新增:角色菜单筛选(operator 隐藏 admin 菜单) |
-| `.env.production` | `VITE_BASE_API = /api`(Wails 内置代理处理) |
-| `vite.config.js` | Dev server 端口 5173 |
+```powershell
+# 首次安装前端依赖
+cd frontend
+npm install
+cd ..
 
----
+# 开发模式
+wails dev
 
-## 启动
+# 前端生产构建校验
+cd frontend
+npm run build
 
-```bash
-# 开发
-cd lc_garage && wails dev
+# 构建 Windows 应用
+cd ..
+wails build
+```
 
-# 构建
-cd lc_garage && wails build
-# 产物: build/bin/smart-parking.exe
+构建产物为 `build/bin/smart-parking.exe`。首次启动会执行 SQLite 自动迁移和系统种子数据初始化。
 
-# 运行
-双击 build/bin/smart-parking.exe
-```
+## 6. 默认账户
+
+首次初始化后可使用:
+
+- 管理员:`admin` / `123456`
+- 操作员权限标识:`618`
+- 管理员权限标识:`888`
+
+## 7. 相关文档
 
-管理员:`admin` / `123456`
+- [项目进度](项目进度.md)
+- [进出场流程详解](进出场流程详解.md)
+- [数字票说明](digital-ticket.md)
+- [收费流程](收费流程.md)
+- [测试用例编写规范](测试用例编写规范.md)
+- [目标代码规划](代码规划.md)

+ 97 - 148
doc/digital-ticket.md

@@ -1,180 +1,129 @@
-# 数字票(Digital Ticket)说明文档
+# 数字票(Digital Ticket)说明
 
-> 版本:1.0 | 日期:2026-07-23
+> 版本:2.0
+> 更新日期:2026-08-07
 
-## 1. 概述
+## 1. 定位
 
-数字票(Digital Ticket)是停车业务中的标准化抽象模型,以电子化的"票"作为核心对象,遵循 **创建 → 支付 → 验证 → 放行** 的线性状态机模式。不绑定特定硬件或支付方式,具有高度的通用性和可扩展性
+数字票是一次停车会话的**支付与离场状态凭证**,不是纸质小票的替代品,也不绑定某一种支付入口
 
-## 2. 核心概念
+- 每次车辆入场都会创建一张数字票,无论来源是 RFID、人工录入、票机还是后续接入的车牌识别。
+- 数字票关联一条停车会话(`vehicle_record`),但不复制会话的通道、设备、车辆图片等事实数据。
+- 支付流水(`payment_record`)是资金事实;数字票和停车会话保存用于业务查询的结算摘要和状态。
+- 纸质小票可打印数字票票号或二维码,但只是数字票的物理凭证之一。
 
-### 设计原则
-
-- **统一抽象**:所有进场方式(RFID、车牌识别、手动录入)最终都创建数字票
-- **状态驱动**:票的状态决定下一步操作,不允许跳跃式状态变更
-- **事件溯源**:关键节点记录事件日志,便于审计和异常追踪
-- **二维码凭证**:以 ticket_no(UUID)作为唯一标识和验证依据
+```text
+停车会话 vehicle_record
+  ├─ 入场/出场时间、停车场、通道设备快照、结算摘要
+  └─ 1 : 1 数字票 digital_ticket
+       ├─ ticket_no、状态、支付摘要、事件日志
+       └─ 关联结算支付流水 payment_record
+            └─ 实际金额、实收、找零、入口、方式、操作员
+```
 
-### 与现有流程的关系
+## 2. 生命周期
 
-```
-任何进场方式(手动/RFID/车牌)
-        │
-        ▼
-  VehicleEntry() ──→ 创建 vehicle_record
-        │                │
-        │                ▼
-        │         创建 digital_ticket
-        │         (状态: pending_payment)
-        │
-        ▼
-  ExitPreview / ExitConfirm
-        │
-        ▼
-  digital_ticket 状态 → paid → exited
+```text
+车辆入场
+  -> 创建停车会话
+  -> 创建数字票(pending_payment)
+  -> 支付成功(paid)
+  -> 确认离场(exited)
 ```
 
-数字票是 `vehicle_record` 的业务状态层,不替代它。报表和监控继续用 `vehicle_record`,业务流转看 `digital_ticket` 状态。
+| 状态 | 含义 | 可执行操作 |
+| --- | --- | --- |
+| `pending_payment` | 已入场、等待支付或免费放行判断 | 支付、出场确认(仅零费用/免费) |
+| `paid` | 已完成支付、尚未出场 | 出场确认 |
+| `exited` | 会话已关闭、车辆已离场 | 只读 |
+| `expired` | 已过期或业务终止 | 只读,需按异常流程处理 |
 
-## 3. 状态机
+`created` 仅作为创建过程中的内部初始状态,创建完成后会进入 `pending_payment`。
 
-```
-                    ┌──────────┐
-               ┌──→ │  expired │ ←──┐
-               │    │  已过期   │    │
-               │    └──────────┘    │
-               │     超时未支付      │ 超时未离场
-               │                    │
-  ┌─────────┐ │  ┌───────────────┐ │  ┌──────┐  ┌────────┐
-  │ created │─→│  │pending_payment│─→│ paid │─→│ exited │
-  │  已创建  │   │    待支付      │  │ 已支付│  │ 已离场  │
-  └─────────┘   └───────────────┘  └──────┘  └────────┘
-   车辆入场        自动转换         支付成功    验证放行
-```
+## 3. 与支付入口、支付方式的关系
 
-| 状态 | 说明 | 触发条件 |
-|------|------|---------|
-| `created` | 车辆入场,数字票生成 | 入场事件触发 |
-| `pending_payment` | 待支付 | 创建完成后自动进入 |
-| `paid` | 已支付,费用结清 | 支付成功回调 |
-| `exited` | 已离场,流程结束 | 验证通过后开闸 |
-| `expired` | 超时未支付或未离场 | 定时任务或超时逻辑 |
-
-### 状态转换规则
-
-| 当前状态 | 允许转换到 |
-|---------|-----------|
-| `created` | `pending_payment` |
-| `pending_payment` | `paid`, `expired` |
-| `paid` | `exited`, `expired` |
-| `exited` | 无(终态) |
-| `expired` | 无(终态) |
-
-状态转换是原子操作,代码层做了合法性校验,不允许跳跃式变更。
-
-## 4. 数据模型
-
-### digital_ticket 表
-
-| 字段 | 类型 | 约束 | 说明 |
-|------|------|------|------|
-| id | INTEGER | PK | 主键 |
-| ticket_no | TEXT(64) | UNIQUE | 票号(32位十六进制UUID) |
-| plate_number | TEXT(20) | | 车牌号 |
-| trigger_mode | TEXT(20) | | 触发方式:rfid / plate_recognition / manual |
-| state | TEXT(20) | default:created | 状态 |
-| vehicle_record_id | INTEGER | FK | 关联 vehicle_record.id |
-| entry_time | DATETIME | | 入场时间 |
-| exit_time | DATETIME | | 出场时间 |
-| fee | REAL | | 计费金额 |
-| payment_method | TEXT(20) | | 支付方式:cash / free / wechat / alipay |
-| payment_order_no | TEXT(64) | | 支付单号 |
-| paid_at | DATETIME | | 支付时间 |
-| expire_at | DATETIME | | 过期时间 |
-| event_log | TEXT | | 事件日志(JSON数组) |
-| created_at | DATETIME | | 创建时间 |
-| updated_at | DATETIME | | 更新时间 |
-
-### event_log 格式
+### 支付入口
 
-```json
-[
-  {"at": "2026-07-23T15:27:08+08:00", "event": "created", "trigger": "manual"},
-  {"at": "2026-07-23T15:35:12+08:00", "event": "pending_payment"},
-  {"at": "2026-07-23T15:36:01+08:00", "event": "paid", "payment_method": "cash", "paid_amount": 20, "payment_order_no": "PO20260723001"},
-  {"at": "2026-07-23T15:36:05+08:00", "event": "exited"}
-]
-```
+支付入口回答“在哪里完成支付”,例如:
 
-## 5. API 接口
+- `counter`:收费岗亭
+- `ticket`:停车小票/扫码入口
+- `central_payment_machine`:中央缴费机
 
-| 方法 | 路径 | 说明 | 鉴权 |
-|------|------|------|:--:|
-| GET | `/digital-ticket/list` | 分页列表(按车牌/状态筛选) | JWT |
-| GET | `/digital-ticket/:ticketNo` | 根据票号查询详情 | JWT |
-| POST | `/digital-ticket/:ticketNo/pay` | 支付(→ paid) | JWT |
-| POST | `/digital-ticket/:ticketNo/exit` | 离场验证(→ exited) | JWT |
+### 支付方式
 
-### 支付请求体
+支付方式回答“如何完成支付”,例如:
 
-```json
-{
-  "payment_method": "cash",
-  "payment_order_no": "PO20260723001",
-  "paid_amount": 20.0
-}
-```
+- `cash`:人工现金
+- `pos`:POS 机
+- `ticket_qr`:小票扫码支付
+- `free`:免费放行
 
-### 列表查询参数
+入口和方式通过 `payment_entry_method` 配置关联。关闭入口会阻止从该入口发起新支付;关闭方式会从该入口可选方式中移除。二者都不改变历史数字票或历史支付流水。
 
-| 参数 | 说明 |
-|------|------|
-| plate_number | 车牌号(模糊匹配) |
-| state | 状态筛选 |
-| page | 页码 |
-| page_size | 每页条数 |
+## 4. 一致性和幂等性
 
-## 6. 代码结构
+支付和离场均在事务内完成:
 
-```
-internal/modules/digital-ticket/
-├── api.go                 # HTTP Handler + 路由注册
-├── service/
-│   └── service.go         # 状态机 + 事件日志 + Create/Transition
-└── repository/
-    └── repo.go            # CRUD
-```
+1. 校验当前数字票和停车会话状态。
+2. 校验支付金额、支付入口和支付方式配置。
+3. 写入 `payment_record`。
+4. 更新停车会话支付摘要及数字票状态。
+5. 出场时保存出口通道/设备快照,关闭停车会话并将数字票更新为 `exited`。
 
-```
-internal/dao/digital_ticket.go  # GORM 实体
-```
+系统会拒绝负金额、金额不足、重复支付和已关闭会话再次离场。并发结算使用会话状态条件更新保证只有一个请求成功;事务失败不会遗留部分支付流水。
 
-### 关键函数
+## 5. 核心字段
 
-| 函数 | 说明 |
-|------|------|
-| `TicketService.Create()` | 创建数字票,生成 UUID ticket_no,写入 event_log |
-| `TicketService.Transition()` | 状态转换,校验合法性,追加事件日志 |
-| `TicketService.GetByTicketNo()` | 扫码查询(二维码凭证验证入口) |
+| 字段 | 说明 |
+| --- | --- |
+| `ticket_no` | 数字票唯一票号,用于二维码、查询和外部支付回调关联 |
+| `vehicle_record_id` | 关联的停车会话 ID |
+| `trigger_mode` | 入场来源,如 `rfid`、`manual`、`camera` |
+| `state` | 当前数字票状态 |
+| `fee` / `paid_amount` | 应收与实收摘要 |
+| `payment_entry` / `payment_method` | 实际完成支付的入口与方式 |
+| `payment_order_no` | 外部支付订单号,可选 |
+| `event_log` | 票据状态变化事件日志 |
 
-## 7. 扩展性
+票号是面向业务和外部凭证的随机唯一标识;停车会话 ID 是内部数据库流水编号,两者不应混用。
 
-数字票作为统一抽象,后续可扩展:
+## 6. API
 
-- **线上支付**:扫码 → 跳转支付页 → 回调 `PayTicket`
-- **自助缴费机**:票机扫码 → 调 `/ticketNo` 查询费用 → POS 支付 → 回调 `/ticketNo/pay`
-- **无牌车处理**:进场出纸质二维码小票(ticket_no 打印在票上)
-- **定时过期**:后台任务扫描 `pending_payment` 超时票 → Transition 到 `expired`
+以下路径会叠加系统 API 前缀(开发环境通常为 `/api`):
 
-## 8. 数据库位置
+| 方法 | 路径 | 说明 |
+| --- | --- | --- |
+| GET | `/digital-ticket/summary` | 按筛选条件获取待支付、已支付、已离场等汇总 |
+| GET | `/digital-ticket/list` | 数字票分页列表 |
+| GET | `/digital-ticket/:ticketNo` | 数字票详情 |
+| POST | `/digital-ticket/:ticketNo/pay` | 支付数字票 |
+| POST | `/digital-ticket/:ticketNo/exit` | 确认数字票对应车辆离场 |
 
-```
-Windows: %APPDATA%\smart-parking\lc_garage.db
-        → C:\Users\<用户名>\AppData\Roaming\smart-parking\lc_garage.db
+支付请求示例:
+
+```json
+{
+  "payment_entry": "counter",
+  "payment_method": "cash",
+  "payment_order_no": "OPTIONAL-ORDER-NO",
+  "paid_amount": 20.0
+}
 ```
 
-查询数字票数据:
+## 7. 前端页面
 
-```sh
-sqlite3 "%APPDATA%/smart-parking/lc_garage.db" "SELECT id, ticket_no, plate_number, state FROM digital_ticket;"
-```
+`frontend/src/view/parking/digitalTicket.vue` 提供:
+
+- 票据状态汇总和分页筛选
+- 票号复制、票据详情、状态事件展示
+- 基于可用支付入口和支付方式的结算操作
+- 已支付票据的离场确认
+
+进出场工作台 `entryExit.vue` 使用同一会话和数字票逻辑,不会创建另一套支付状态。
+
+## 8. 扩展方式
+
+新增中央缴费机、POS 或线上支付时,应新增或启用一个支付入口、配置可用支付方式,并在支付成功回调中调用已有支付服务。不得绕开数字票、停车会话和支付流水分别修改状态。
+
+建议外部支付回调以 `ticket_no + payment_order_no` 作为幂等键,并保留网关原始流水号以便对账。

+ 82 - 178
doc/代码规划.md

@@ -1,184 +1,88 @@
-针对 **Wails v2 (Go) + Vue3** 的技术栈,建议采用**“按业务功能切片”**的架构设计。
-相较于传统的“把所有 handler 放一个目录,所有 service 放一个目录”,按功能切片(每个模块内部自带 handler/service/repo)能更好地隔离业务逻辑,非常适合这种模块边界清晰的停车系统。
-以下是完整的项目代码模块结构规划:
-### 顶层目录结构
-```text
-parking-system/
-├── build/                      # Wails 构建配置 (图标、安装包脚本)
-├── frontend/                   # Vue3 前端源码 (Vite)
-├── internal/                   # 核心业务代码 (不可被外部引用)
-├── pkg/                        # 通用工具包
-├── app.go                      # Wails 应用主结构体 (统一绑定前端方法)
-├── main.go                     # Wails 程序入口
-├── go.mod
-└── wails.json
-```
-### `internal/` 详细拆解 (核心)
-这是整个后端的灵魂。我们将基础架构、硬件代理、边缘端通信和业务模块分开管理。
+# 代码组织与演进规划
+
+> 更新日期:2026-08-07
+> 本文档描述当前工程边界和后续演进原则,不代表已创建的目录均已实现。
+
+## 1. 当前组织方式
+
+项目保留既有 GVA 风格目录用于稳定功能维护,同时将新增领域能力放到 `internal/modules/`。
+
 ```text
 internal/
-├── core/                       # 基础设施与共享内核
-│   ├── config/                 # 全局配置加载 (解析 config.yaml)
-│   ├── database/               # SQLite/GORM 初始化、连接池、自动迁移
-│   ├── logger/                 # Zap 日志封装
-│   └── response/               # 统一的 API 返回结构封装
-│
-├── hardware/                   # [P0] 本地硬件交互层 (Go直接控制物理设备)
-│   ├── pos/
-│   │   ├── pos_driver.go       # 串口底层管理 (打开/关闭/重连)
-│   │   └── ingenico.go         # POS 机协议解析与指令收发
-│   └── printer/
-│       ├── escpos.go           # ESC/POS 指令生成
-│       └── ticket_template.go  # 小票排版模板渲染
-│
-├── edge/                       # [P0] 边缘端(RK3568)通信层
-│   ├── tcp_server.go           # 监听 RK3568 的 TCP 连接与心跳
-│   ├── protocol.go             # 定义与 RK3568 的上下行报文协议
-│   └── event_bus.go            # 将底层硬件事件分发到业务模块
-│
-├── modules/                    # ★ 业务功能模块 (按功能切片划分) ★
-│   ├── system/                 # [P1] 系统与权限管理模块
-│   ├── parking/                # [P0] 车场与设备管理模块
-│   ├── vehicle/                # [P0] 车辆与车流管理模块
-│   ├── billing/                # [P0] 计费规则引擎模块
-│   ├── order/                  # [P0] 收费与订单管理模块
-│   ├── monitor/                # [P2] 监控与大屏展示模块
-│   └── report/                 # [P2] 数据统计与报表模块
-│
-└── wails_bindings/             # Wails 前端绑定注册中心
-    └── register.go             # 集中实例化所有模块的 Service,并返回给 app.go 用于 Bind
+├─ api/v1/               # 既有 HTTP 控制器
+├─ service/              # 既有业务、设备与统一通行服务
+├─ router/               # 既有路由组
+├─ dao/                  # GORM 实体
+├─ model/                # 请求、响应和公共模型
+├─ initialize/           # GORM、路由、种子数据、设备初始化
+├─ global/ core/         # 配置、数据库、日志、服务启动
+└─ modules/              # 新领域模块
+   ├─ parking-session/   # 停车会话
+   ├─ digital-ticket/    # 数字票
+   ├─ payment/           # 支付配置和支付流水
+   ├─ monthly/           # 包月车
+   ├─ shift/             # 交接班
+   ├─ report/            # 收入报表
+   └─ printer/           # 小票打印
 ```
-### `internal/modules/` 业务模块内部结构
-每一个业务模块内部保持一致的“微型三层架构”。以最核心的 `order` (订单收费) 和 `billing` (计费) 模块为例:
+
+## 2. 新模块约定
+
+新增业务模块采用下列结构,按实际复杂度选择是否需要全部层次:
+
 ```text
-modules/
-├── billing/                    # [P0] 计费规则引擎
-│   ├── model/
-│   │   └── billing_rule.go     # 计费规则表结构体 (GORM Model)
-│   ├── repository/
-│   │   └── rule_repo.go        # 规则的 CRUD 操作
-│   ├── service/
-│   │   ├── calculator.go       # ★ 核心计费算法 (按时长、阶梯、封顶计算)
-│   │   └── rule_service.go     # 规则的增删改查逻辑
-│   └── handler.go              # (可选) 如果有 REST API 路由则放这里
-│
-├── order/                      # [P0] 收费与订单管理 (前端交互最频繁)
-│   ├── model/
-│   │   ├── order.go            # 订单主表 (入场时间、金额、状态)
-│   │   └── shift_record.go     # 交接班记录表
-│   ├── repository/
-│   │   ├── order_repo.go       # 订单流水查询
-│   │   └── shift_repo.go       # 交接班统计查询
-│   ├── service/
-│   │   ├── checkout_service.go # ★ 出场结算逻辑 (调用 billing 计算费用)
-│   │   ├── payment_service.go  # ★ 支付执行 (联动 hardware/pos 和 printer)
-│   │   └── shift_service.go    # 当班对账逻辑
-│   └── api.go                  # ★ 暴露给 Wails 前端的方法 (如 ProcessCheckout, PayByCash)
-│
-├── vehicle/                    # [P0] 车辆管理
-│   ├── model/
-│   │   ├── whitelist.go        # 白名单/内部车
-│   │   └── monthly_card.go     # 月卡车及续费记录
-│   ├── repository/
-│   │   └── vehicle_repo.go
-│   ├── service/
-│   │   ├── whitelist_service.go# 白名单校验逻辑
-│   │   └── monthly_service.go  # 月卡办理与有效期校验
-│   └── api.go                  # 暴露给前端的车辆管理方法
-│
-├── parking/                    # [P0] 车场与设备配置
-│   ├── model/
-│   │   ├── lane.go             # 车道配置
-│   │   └── device.go           # RK3568节点与本地外设参数
-│   ├── repository/
-│   │   └── config_repo.go
-│   ├── service/
-│   │   └── device_service.go   # 设备状态监控、参数下发
-│   └── api.go
-│
-├── system/                     # [P1] 基础权限
-│   ├── model/
-│   │   └── user.go
-│   ├── service/
-│   │   └── auth_service.go     # 登录验证、密码修改
-│   └── api.go
-│
-├── monitor/                    # [P2] 监控大屏
-│   ├── service/
-│   │   └── realtime_service.go # 聚合设备状态与实时过车数据
-│   └── api.go
-│
-└── report/                     # [P2] 报表
-    ├── service/
-    │   ├── finance_service.go  # 财务收入统计
-    │   └── traffic_service.go  # 车流量分析
-    └── api.go
+internal/modules/<module>/
+├─ api.go
+├─ service/
+├─ repository/
+└─ model/
+   ├─ request/
+   └─ response/
 ```
-### 如何在 Wails 中将这些模块暴露给前端?
-在 `app.go` 中,我们不需要把所有的逻辑都塞进去,而是通过 `internal/wails_bindings/register.go` 统一管理依赖注入。
-**1. `internal/wails_bindings/register.go`**
-```go
-package wails_bindings
-import (
-	"parking-system/internal/core/database"
-	"parking-system/internal/hardware/pos"
-	"parking-system/internal/modules/order"
-	"parking-system/internal/modules/billing"
-	// ... 其他模块
-)
-// Register 初始化所有依赖并返回需要绑定给前端的结构体实例
-func Register(db *gorm.DB) []interface{} {
-	// 1. 初始化底层硬件
-	posDriver := pos.NewDriver("COM3", 9600)
-	
-	// 2. 初始化各模块的 Repository
-	orderRepo := order.NewOrderRepo(db)
-	billingRepo := billing.NewRuleRepo(db)
-	
-	// 3. 初始化各模块的 Service (注入 Repo 和硬件依赖)
-	billingSvc := billing.NewService(billingRepo)
-	orderSvc := order.NewService(orderRepo, billingSvc, posDriver)
-	
-	// 4. 初始化各模块的 API (注入 Service)
-	orderAPI := order.NewAPI(orderSvc)
-	// billingAPI := billing.NewAPI(billingSvc)
-	
-	// 返回所有需要暴露给前端的 API 实例
-	return []interface{}{
-		orderAPI,
-		// billingAPI,
-	}
-}
-```
-**2. `main.go` (Wails 入口)**
-```go
-package main
-import (
-	"context"
-	"parking-system/internal/core/database"
-	"parking-system/internal/wails_bindings"
-	"github.com/wailsapp/wails/v2/pkg/options"
-)
-func main() {
-	// 1. 初始化数据库
-	db := database.Init("%APPDATA%/smart-parking/lc_garage.db")
-	
-	// 2. 获取所有前端绑定实例
-	binds := wails_bindings.Register(db)
-	// 3. 启动 Wails
-	err := wails.Run(&options.App{
-		Title:  "智慧停车收费系统",
-		Width:  1280,
-		Height: 800,
-		Bind:   binds, // ★ 将所有模块的 API 绑定给前端
-	})
-	if err != nil {
-		panic(err)
-	}
-}
+
+- `api.go`:HTTP Handler 和路由注册函数。
+- `service`:事务、状态机、领域校验和跨模块编排。
+- `repository`:GORM 查询、条件更新、分页查询。
+- `model`:模块自身请求和响应模型;共享模型仍放在 `internal/model/common`。
+
+## 3. 模块边界
+
+| 模块 | 所有权 | 不应承担 |
+| --- | --- | --- |
+| `parking-session` | 在场会话、身份定位、进出场事实快照、会话状态 | 支付网关、票据状态机 |
+| `digital-ticket` | 票号、状态、事件日志、票据查询 | 通道设备事实、资金流水 |
+| `payment` | 支付入口/方式配置、金额校验、支付流水 | 车辆入场、闸机控制 |
+| `printer` | 票据版式、串口/USB 打印 | 创建会话或决定支付状态 |
+| `service/parking` | 设备通道定位、统一通行、闸机动作 | 直接修改多个领域表绕过事务服务 |
+
+## 4. 当前关键调用关系
+
+```text
+前端/设备事件
+  -> PassageService.HandlePassage
+  -> ParkingSessionService 定位或创建会话
+  -> TicketService 创建或查询数字票
+  -> PaymentService 完成结算
+  -> ParkingSessionService.CloseSessionTx 关闭会话
+  -> TicketService 标记已离场
+  -> GateController 执行开闸/关闸
 ```
-### 这种架构的优势
-1. **按需开发与裁剪**:如果要先开发 P0,你只需要关注 `billing`、`order`、`parking` 和 `hardware` 目录,其他的 `monitor` 和 `report` 目录甚至可以先不建,互不干扰。
-2. **清晰的调用链路**:
-   - 前端按钮点击 -> `order/api.go` -> `order/service/checkout_service.go` (计算费) -> `billing/service/calculator.go` -> 返回金额 -> `order/service/payment_service.go` (调硬件) -> `hardware/pos/pos_driver.go` -> 返回结果给前端。
-3. **Wails 绑定解耦**:通过 `wails_bindings` 统一做依赖注入,避免了业务代码与 Wails 框架强耦合,方便未来做单元测试。
+
+跨模块协作优先通过 `internal/service/enter.go` 的服务注册中心或稳定接口进行,避免模块间随意相互导入形成循环依赖。
+
+## 5. 后续演进
+
+| 优先级 | 演进项 | 目标 |
+| --- | --- | --- |
+| P0 | 摄像头识别适配层 | 将 OCR 识别结果转换为统一通行请求,不复制进出场逻辑 |
+| P0 | 支付回调适配层 | POS、中央缴费机、线上支付通过统一幂等回调进入支付服务 |
+| P1 | 异常处置模块 | 无票、人工抬杆、设备离线、支付不确定状态形成可审计工单 |
+| P1 | 设备事件和告警 | 统一记录设备在线、开闸失败、打印失败等事件 |
+| P2 | 报表查询仓储化 | 报表复杂查询逐步从既有 service 收敛到模块 repository |
+
+## 6. 禁止事项
+
+- 不新增第二套车辆出场或支付状态机。
+- 不把支付入口当作支付方式,或把纸质小票当作数字票本体。
+- 不在 API 层跨表直接更新会话、数字票和支付流水。
+- 不为了新功能修改或删除旧数据字段;新增字段应兼容历史数据,并说明查询回退策略。

+ 72 - 117
doc/收费流程.md

@@ -1,118 +1,73 @@
-根据您补充的业务场景,这已经涉及到了**硬件交互逻辑**和**岗亭工作流**的设计。这比单纯的计费算法更贴近实际落地。
-我将针对您描述的两种主要模式(月租/VIP、临时车),梳理成详细的**进出场业务流程图**,供开发对接使用。
----
-### 一、 月租车 / VIP车 流程梳理
-**核心逻辑:** 身份识别 -> 权限校验(有效期) -> 自动放行。
-#### 1. 进场流程
-*   **触发方式 A:车牌识别(摄像头)**
-    1.  车辆驶入入口,触发地感/红外。
-    2.  摄像头抓拍,系统识别车牌号。
-    3.  系统查询数据库:
-        *   查询 确认是否为月租车/VIP。
-        *   检查 `Expire_Date`(过期时间)。
-    4.  **判断逻辑:**
-        *   **有效:** 语音播报“欢迎光临,月租有效”,自动开闸。
-        *   **过期:** 语音播报“月租已过期”,转为临时车流程(视策略:拒绝入场 或 按临时车入场)。
-    5.  记录入场日志(车牌、时间、快照)。
-*   **触发方式 B:刷卡/RFID(非车牌识别)**
-    1.  车主在入口票机/读卡器处刷卡/RFID卡。
-    2.  系统读取卡片ID,通过ID查询绑定的车辆信息。
-    3.  校验有效期(逻辑同上)。
-    4.  **有效:** 开闸放行。
-    5.  *注意:此类进场通常需要人工辅助确认车辆身份,或配合视频录像留存。*
-#### 2. 出场流程
-*   **流程:**
-    1.  车辆驶入出口,触发识别(车牌或刷卡)。
-    2.  系统校验状态:
-        *   是否有入场记录?
-        *   月租是否在有效期内?
-    3.  **判断逻辑:**
-        *   **有效:** 语音播报“一路顺风”,自动抬杆放行。
-        *   **过期:**
-            *   策略1:拒绝出场,需去岗亭缴费续费后放行。
-            *   策略2:自动转为临时车计费,需缴纳临时停车费。
-    4.  记录出场日志,销除在场车辆记录。
----
-### 二、 临时车 流程梳理
-**核心逻辑:** 取票进场 -> 扫码计费 -> 支付确认 -> 放行。
-#### 1. 进场流程(统一取票)
-*   **动作:**
-    1.  车辆驶入入口,驾驶员按下票机按钮(或自动感应出票)。
-    2.  **票机动作:**
-        *   打印二维码小票(包含:入场时间、流水号、二维码)。
-        *   *系统后台:* 生成一条“无车牌”或“待关联”的入场记录。
-    3.  道闸开启,车辆进场。
-    4.  *痛点提示:* 此时不识别车牌,出场时完全依赖小票。若小票丢失,需人工核对入场记录(通过入场抓拍图片)。
-#### 2. 出场流程(非人工自助缴费)
-*   **场景:** 票机自带扫码枪 + POS机连接
-*   **流程:**
-    1.  驾驶员在出口票机处,将小票二维码对准扫码窗。
-    2.  **系统计算:**
-        *   解析小票获取入场时间 ($T_{in}$)。
-        *   获取当前时间 ($T_{out}$)。
-        *   调用计费引擎,算出金额 `Amount`。
-    3.  **交互:**
-        *   屏幕/语音提示:“请缴纳费用 XX 元”。
-        *   票机连接的POS机进入待支付状态。
-    4.  **支付:**
-        *   驾驶员在POS机上刷卡(银行卡/会员卡)。
-        *   POS机返回“交易成功”信号给票机/系统。
-    5.  **结果:**
-        *   系统接收到支付成功信号 -> 下发开闸指令 -> 道闸抬杆。
-        *   票机打印/吐出收据凭证(可选)。
-#### 3. 出场流程(人工收费)
-*   **场景:** 现金支付,需岗亭保安协助
-*   **流程:**
-    1.  驾驶员到达出口,将小票交给保安,或自己在票机扫码。
-    2.  **系统计算:**
-        *   屏幕显示计费金额(保安岗亭端可见)。
-    3.  **人工交互:**
-        *   保安收取现金(纸币/硬币)。
-        *   保安在管理软件界面点击“现金收款”或“确认放行”。
-    4.  **结果:**
-        *   系统记录“现金支付”流水。
-        *   软件下发开闸指令 -> 道闸抬杆。
----
-### 三、 系统交互逻辑图解 (开发梳理)
-为了方便您开发,我将上述逻辑转化为系统的**模块交互序列**:
-#### 场景:临时车非人工出场(票机+POS联动)
-```mermaid
-sequenceDiagram
-    participant User as 驾驶员
-    participant Machine as 出口票机(硬件)
-    participant Server as 管理服务器
-    participant POS as 刷卡POS机
-    User->>Machine: 扫描小票二维码
-    Machine->>Server: 上报票号/入场ID
-    Server->>Server: 查询入场时间,计算费用
-    Server-->>Machine: 返回金额 (例如: 15元)
-    Machine->>User: 屏幕显示/语音播报金额
-    Machine->>POS: 发送扣款指令 (金额15元)
-    User->>POS: 刷卡支付
-    POS->>POS: 银行卡扣款处理
-    POS-->>Machine: 返回扣款成功信号
-    Machine->>Server: 上报支付成功
-    Server->>Server: 更新订单状态为“已支付”
-    Server-->>Machine: 下发开闸指令
-    Machine->>Machine: 触发道闸抬杆
+# 停车收费与结算流程
+
+> 更新日期:2026-08-07
+> 本文档以当前实现为准,真实 POS、中央缴费机和线上支付网关尚待接入。
+
+## 1. 结算对象与职责
+
+| 对象 | 作用 | 是否可替代 |
+| --- | --- | --- |
+| 停车会话 `vehicle_record` | 保存本次停车的费用、支付摘要和进出场事实 | 不可替代 |
+| 数字票 `digital_ticket` | 保存待支付、已支付、已离场状态和票号 | 不可替代 |
+| 支付流水 `payment_record` | 保存实际收款、实收、找零、操作员 | 财务事实来源 |
+
+不得只修改数字票状态或只插入支付流水。所有结算必须通过支付服务和停车会话事务统一更新。
+
+## 2. 支付入口与支付方式
+
+支付入口和支付方式为独立配置。
+
+| 概念 | 解决的问题 | 示例 |
+| --- | --- | --- |
+| 支付入口 | 在哪里发起结算 | 收费岗亭、停车小票、中央缴费机 |
+| 支付方式 | 用什么方式收款 | 现金、POS、小票扫码、免费放行 |
+
+入口与方式通过配置关系限制可用组合。例如,中央缴费机可配置扫码和 POS;收费岗亭可配置现金和 POS。禁用配置只影响后续新结算,不修改历史记录。
+
+## 3. 当前结算主流程
+
+```text
+定位停车会话和数字票
+  -> 计算应收金额
+  -> 校验支付入口和支付方式可用
+  -> 校验金额
+  -> 插入 payment_record
+  -> 更新 vehicle_record.payment_status / fee / payment_entry / payment_method
+  -> 更新 digital_ticket 为 paid
+  -> 确认出场时关闭会话并更新数字票为 exited
 ```
----
-### 四、 关键技术点与开发建议
-在实现这套逻辑时,请重点关注以下几个技术细节:
-**1. 票机与服务器的通信协议**
-*   票机通常通过 TCP/IP 或串口与服务端通信。
-*   **建议:** 采用心跳包机制。服务器需要知道票机是否“在线”。如果票机断网,应立即报警,避免车辆堵在出口。
-**2. 小票二维码的数据安全**
-*   小票上的二维码是进出的唯一凭证。
-*   **建议:** 二维码内容不要直接写“时间:20231010”,容易被伪造。建议生成一个**加密的UUID**或**动态Token**,服务器端通过Token反查入场记录,防止逃票。
-**3. POS机支付的异步回调**
-*   刷卡支付可能有延迟(网络波动)。
-*   **建议:** 票机端应设置“支付等待状态”(比如等待30秒)。如果POS机返回成功,但票机没收到信号,需要有“重试机制”或“人工干预按钮”,避免道闸不开导致堵车。
-**4. 无牌车/无票车的兜底逻辑**
-*   **进场:** 取票进场,但进场时摄像头没拍到车牌。
-*   **出场:** 用户票丢了。
-    *   **解决方案:** 保安端软件需提供“查询入场记录”功能,通过时间范围查找入场记录,并匹配摄像头抓拍的图片,确认后手动补交费放行。
-**5. 月租车“过期”的临界点处理**
-*   如果月租车在进场时有效,出场时过期了怎么办?
-*   **逻辑建议:** 通常允许出场(因为在场内过期不应惩罚),或者在进场时检查如果“距离过期少于1小时”,则语音提醒“月租即将到期,请及时续费”。
-这套逻辑梳理涵盖了您提到的硬件交互细节,您可以直接基于此流程图编写开发文档。
+
+### 3.1 规则
+
+- 应收金额、实收金额不能为负数。
+- 现金等手工金额方式,实收不得低于应收;超出部分记录为找零。
+- 零费用允许零金额结算。
+- 已支付会话不允许再次支付;已出场会话不允许再次结算或离场。
+- 预支付数字票在出口只做离场确认,不再创建第二笔支付流水。
+- 任意步骤失败时,事务回滚,不能遗留半完成支付数据。
+
+## 4. 已实现支付场景
+
+| 场景 | 状态 | 说明 |
+| --- | --- | --- |
+| 收费岗亭人工现金 | 已实现 | 支持实收、找零、操作员记录 |
+| 免费放行 | 已实现 | 用于零费用、白名单等合法免费场景 |
+| 数字票结算 | 已实现 | 从数字票页面选择入口和方式并完成支付 |
+| 出口消费预支付票 | 已实现 | 已支付数字票在出口确认离场,不重复收费 |
+| POS 支付方式配置 | 已实现 | 可配置、可选择,真实 POS 交易未接入 |
+| 小票扫码支付方式配置 | 已实现 | 可配置、可选择,真实支付网关未接入 |
+| 中央缴费机入口配置 | 已实现 | 可配置为支付入口,终端协议和回调未接入 |
+
+## 5. 外部支付接入要求
+
+POS、中央缴费机、线上支付接入时必须遵守:
+
+1. 外部终端先根据 `ticket_no` 查询数字票及应收金额。
+2. 终端支付成功后携带 `ticket_no`、外部订单号、支付入口和支付方式回调。
+3. 服务端以票号和外部订单号进行幂等校验,不允许因回调重试产生重复支付。
+4. 支付服务成功后,出口仍由统一出场流程关闭停车会话并开闸。
+5. 终端超时或状态不确定时进入人工核对,不得直接标记支付成功。
+
+## 6. 对账
+
+交接班和收入报表以 `payment_record` 为主进行汇总;停车会话和数字票的支付字段用于业务展示和快速筛选。出现差异时,应以支付流水、操作日志和外部支付订单号核对。

+ 90 - 298
doc/进出场流程详解.md

@@ -1,328 +1,120 @@
-# 智慧停车进出场流程详解
+# 智慧停车进出场流程详解
 
-> 日期:2026-07-30
+> 更新日期:2026-08-07
+> 本文档描述当前已实现的统一进出场主路径。
 
-本文档详细描述车辆进出场完整流程,涵盖车辆类型区分、入场方式、收费结算方式。
+## 1. 核心原则
 
----
+1. 所有触发方式先定位设备和通道,再进入统一通行服务。
+2. 一辆车一次在场过程只能有一个未关闭的停车会话。
+3. 入场原子创建停车会话和数字票;支付、出场均定位同一会话。
+4. 支付事实写入支付流水,通道和设备事实写入停车会话快照。
+5. 开闸是业务成功后的设备动作;闸机失败必须向操作员返回“业务已完成、设备动作失败”的明确状态。
 
-## 1. 车辆类型体系
+## 2. 触发入口
 
-系统通过 `vehicle_type` 表 + `shortlist` 名单 + `monthly_card` 包月卡三层结构区分车辆身份。
+| 来源 | 车辆标识 | 当前状态 | 进入方式 |
+| --- | --- | --- | --- |
+| UHF RFID | RFID,可带车牌 | 已实现 | 设备事件调用 `PassageService.HandlePassage` |
+| 工作台人工操作 | 车牌或 RFID | 已实现 | `entryExit.vue` 调用 `/vehicle/passage` |
+| 票机按钮 | 票机输入/临时车 | 已实现,待现场验收 | 创建入场会话、数字票并打印小票 |
+| 摄像头识别 | 车牌 | 未实现 | 规划接入同一 `PassageService` |
 
-### 1.1 类型分类
+## 3. 进场流程
 
-```
-车辆
-  │
-  ├─ 临时车 (vehicle_type.is_system = true)
-  │    └─ 首次进场自动创建,无车主
-  │
-  ├─ 固定车 (vehicle_type.is_system = false,已注册)
-  │    ├─ VIP 车 (owner.is_vip = true 且未过期)
-  │    │    └─ 享受 VIP 折扣 / 免费
-  │    │
-  │    └─ 普通固定车
-  │         └─ 按对应 vehicle_type 计费
-  │
-  ├─ 月租车 (monthly_card 有效记录)
-  │    └─ 自动加入白名单,有效期内免费通行
-  │
-  ├─ 白名单车 (shortlist.list_type = "白名单")
-  │    └─ 放行,不拦截
-  │
-  └─ 黑名单车 (shortlist.list_type = "黑名单")
-       └─ 拦截,禁止进出
-```
-
-### 1.2 优先级判断
-
-进出场时判断顺序:
-
-```
-1. 黑名单 → 拦截,禁止通行
-2. 白名单 → 放行(月租车在此层)
-3. 余额/支付 → 按车辆类型计费规则
+```text
+识别车辆
+  -> 通过 device_code 查询 UHF 设备和绑定通道
+  -> 校验设备启用、通道方向、停车场归属、黑名单、临停规则
+  -> 按车牌/RFID 定位是否存在未关闭停车会话
+  -> 不存在:创建会话 + 数字票
+  -> 保存入口通道和设备快照
+  -> 更新车辆最近入场时间
+  -> 请求开闸
 ```
 
----
+### 3.1 入场数据写入
 
-## 2. 入场流程
+| 对象 | 入场写入内容 |
+| --- | --- |
+| 停车会话 `vehicle_record` | 车牌、RFID、入场时间、停车场、停车位、入口图片、入口通道/设备 ID、编码、名称 |
+| 数字票 `digital_ticket` | 票号、入场来源、会话 ID、`pending_payment` 状态、事件日志 |
+| 车辆 `vehicle` | 最近入场时间 |
 
-### 2.1 入场方式
+同一车牌或 RFID 已存在未关闭会话时,入场会被拒绝,不会重复建票。
 
-| 方式 | 触发 | 场景 | 当前状态 |
-|------|------|------|:--:|
-| RFID 自动 | UHF 读卡器读取标签 | 有标签车辆 | ✅ |
-| 车牌识别 | 摄像头 OCR | 无标签车辆 | ❌ P0-5 |
-| 手动录入 | 操作员在 entryExit.vue 输入 | 无标签/系统故障兜底 | ✅ |
-| 二维码扫码 | 扫描预约/月卡二维码 | 线上预约 | 🔜 规划中 |
+## 4. 出场与结算流程
 
-### 2.2 入场判断流程
-
-```
-车辆到达入口
-  │
-  ├─ 触发识别 (RFID / 车牌 / 手动)
-  │
-  ▼
-┌─────────────────────────────────────────────────┐
-│  ① 黑名单检查                                    │
-│     CheckVehicleShortlist(plate, rfid)           │
-│     └─ 黑名单 → 🚫 拦截                          │
-│                                                  │
-│  ② 车辆查询/创建                                 │
-│     存在 → 使用现有记录                           │
-│     不存在 → 自动创建 (临时车类型)                 │
-│                                                  │
-│  ③ 重复入场检查                                  │
-│     有未出场记录 → "车辆已入场"                   │
-│                                                  │
-│  ④ 入场方式判断                                  │
-│     ┌─ 月租车 (白名单+未过期) → 🟢 直接放行       │
-│     ├─ 固定车/VIP → 🟢 直接放行                  │
-│     └─ 临时车 → 通道判断                         │
-│          ├─ 通道允许临停 → 🟡 取票/二维码放行     │
-│          └─ 通道不允许 → 🚫 拦截                  │
-│                                                  │
-│  ⑤ 创建入场记录 + 数字票                         │
-│     INSERT vehicle_record                        │
-│     INSERT digital_ticket (state=pending_payment) │
-│     ┌─ RFID → GateOpener 抬杆                    │
-│     └─ 手动 → 仅记录,人工抬杆                   │
-└─────────────────────────────────────────────────┘
+```text
+识别车辆
+  -> 定位未关闭停车会话和数字票
+  -> 计算当前费用,返回出场预览
+  -> 判断数字票是否已支付
+     -> 已支付:直接进入出场确认
+     -> 未支付且应收为零:免费放行确认
+     -> 未支付且应收大于零:选择支付入口和支付方式完成结算
+  -> 写支付流水、更新会话支付摘要、数字票 -> paid
+  -> 保存出口通道和设备快照,关闭会话,数字票 -> exited
+  -> 更新车辆最近出场时间、累计停车时长和费用
+  -> 请求开闸
 ```
 
-### 2.3 放行策略
+### 4.1 支付判断
 
-| 车辆类型 | 放行方式 | 说明 |
-|---------|---------|------|
-| 月租车(有效期内) | **直接放行** | RFID 识别→白名单验证→抬杆,无需任何操作 |
-| VIP/固定车 | **直接放行** | 已注册车辆,自动抬杆 |
-| 临时车(允许临停通道) | **取票放行** | 按票机按钮出纸质小票/或系统生成数字票,抬杆入场 |
-| 临时车(禁止临停通道) | **拦截** | 提示"临时车禁止入场" |
-| 黑名单 | **拦截** | 任何通道均不放行 |
+| 场景 | 处理 |
+| --- | --- |
+| 已支付数字票 | 不再次收费,直接消费原支付记录并确认出场 |
+| 零费用、白名单或免费放行 | 生成零金额结算记录后出场 |
+| 人工现金 | 校验实收不低于应收,记录找零 |
+| POS/扫码 | 由已配置支付入口和方式决定;真实设备或网关回调待接入时不得伪造成功 |
+| 重复请求 | 已支付或已关闭会话会被拒绝,避免重复收费和重复出场 |
 
----
+## 5. 通道与设备快照
 
-## 3. 出场流程
+停车会话保存下列字段:
 
-### 3.1 计费引擎
+| 阶段 | 字段 |
+| --- | --- |
+| 入场 | `entry_channel_id`、`entry_channel_code`、`entry_channel_name`、`entry_device_code`、`entry_device_name` |
+| 出场 | `exit_channel_id`、`exit_channel_code`、`exit_channel_name`、`exit_device_code`、`exit_device_name` |
 
-```
-calculateFee(vehicle, stayTime)
-  │
-  ├─ 1. 查 fee_config WHERE vehicle_type_id = ?
-  │     未找到/全零 → 默认费率 0.1元/分钟
-  │
-  ├─ 2. VIP 判断
-  │     isVip=true && 过期时间未到 && 非零值
-  │
-  ├─ 3. VIP 免费(isVip && IsVIPFree)→ fee=0
-  │
-  ├─ 4. 免费时长内 (stayTime ≤ StartTime) → fee=0
-  │
-  ├─ 5. 超时计费
-  │     extraUnits = ceil((stayTime - StartTime) / UnitTime)
-  │     fee = StartFee + extraUnits × UnitFee
-  │
-  ├─ 6. 每日封顶 (DailyMaxFee)
-  │
-  └─ 7. VIP 折扣 (fee ×= VIPDiscount)
-```
+进出记录详情优先显示名称;编码为名称缺失或配置已删除时的回退值。ID 仅用于关联和查询,不直接作为运营页面的主要显示内容。
 
-### 3.2 出场判断流程
+## 6. 状态关系
 
-```
-车辆到达出口
-  │
-  ├─ 触发识别 (RFID / 车牌 / 手动 / 扫码)
-  │
-  ▼
-┌─────────────────────────────────────────────────┐
-│  ① 黑名单检查                                    │
-│     └─ 黑名单 → 🚫 拦截                          │
-│                                                  │
-│  ② 查询未出场记录                                 │
-│     无记录 → "车辆未入场"                         │
-│                                                  │
-│  ③ 计费                                         │
-│     calculateFee() → 算出金额                    │
-│                                                  │
-│  ④ 结算方式判断                                  │
-│     ┌─ 月租车 (白名单+未过期) → 🟢 直接放行(免费) │
-│     ├─ VIP (免费策略) → 🟢 直接放行(免费)         │
-│     ├─ 临时车 fee=0 → 🟢 直接放行                 │
-│     └─ 临时车 fee>0 → 需结算                     │
-│          ├─ 人工现金收费 → 收费员收款 → 放行      │
-│          ├─ 出口 POS 刷卡 → 刷卡支付 → 放行       │
-│          └─ 中央缴费机 → 提前扫码支付 → 出口验证  │
-│                                                  │
-│  ⑤ 创建支付记录 + 更新数字票状态                  │
-│     INSERT payment_record                        │
-│     UPDATE digital_ticket (state=paid)           │
-│     ┌─ RFID → GateOpener 抬杆                    │
-│     └─ 手动 → 仅记录,人工抬杆                   │
-└─────────────────────────────────────────────────┘
-```
+| 停车会话 | 数字票 | 含义 |
+| --- | --- | --- |
+| `exit_time IS NULL` + `payment_status=unpaid` | `pending_payment` | 在场待支付 |
+| `exit_time IS NULL` + `payment_status=paid` | `paid` | 已支付待离场 |
+| `exit_time IS NOT NULL` | `exited` | 已离场 |
 
----
+`payment_record` 不承担状态机角色,它记录每次实际结算的金额和操作员,是财务对账依据。
 
-## 4. 交易结算方式
+## 7. 闸机处理
 
-### 4.1 结算方式对比
-
-| 结算方式 | 适用场景 | 费用归属 | 当前状态 |
-|---------|---------|---------|:--:|
-| **人工收费** | 岗亭操作员收现金 | 记录到 payment_record + shift_record | ✅ |
-| **免密放行** | 月租/VIP/免费车辆 | payment_method=free | ✅ |
-| **出口 POS 机** | 临时车出口刷卡 | 银行卡/会员卡扣款 | 🔜 P0-4 |
-| **中央缴费机** | 车主出场前在机器扫码支付 | 与中央缴费系统对接 | 🔜 规划中 |
-| **线上支付** | APP/小程序预支付 | 微信/支付宝 | 🔜 规划中 |
-
-### 4.2 人工收费流程(当前)
-
-```
-临时车到达出口
-  │
-  ├─ 操作员在 entryExit.vue 输入车牌 → 查询
-  │
-  ├─ ExitPreview → 显示费用/停留时长/车型
-  │
-  ├─ 临时车:弹出支付界面
-  │    ├─ 选择支付方式 [现金] [扫码(预留)] [POS(预留)]
-  │    └─ 选现金 → 输入实收金额 → 显示找零
-  │
-  ├─ 点击"确认收款并放行"
-  │    └─ ExitConfirm(payment_method=cash, paid_amount, operator_id)
-  │
-  └─ 创建 payment_record + 抬杆
-```
-
-### 4.3 中央缴费机流程(规划中)
-
-```
-出场前车主操作流程:
-
-  车主在自助缴费机 → 输入车牌 或 扫描数字票二维码
-    │
-    ├─ 系统查询 digital_ticket 表
-    │     └─ 找到未支付票 → 显示费用
-    │
-    ├─ 车主选择支付方式
-    │     ├─ 微信/支付宝扫码 → 调支付网关 → 支付成功回调
-    │     └─ 现金/硬币 → 机内识别 → 确认收款
-    │
-    ├─ 支付成功
-    │     └─ digital_ticket.state → paid
-    │     └─ INSERT payment_record
-    │
-    └─ 打印收据(可选)
-
-  车主开车到出口
-    │
-    ├─ RFID/车牌识别 → 查到 digital_ticket
-    │     └─ state=paid → 🟢 直接抬杆放行
-    │
-    └─ 超时未离场(如缴费后15分钟未到出口)
-          └─ 加收超时费 或 自动过期
-```
+开闸和关闸由统一闸机控制服务封装。进出场业务先完成数据库事务,再下发开闸指令:
 
-### 4.4 出口 POS 机流程(规划中)
+- 开闸成功:返回通行成功和闸机已开启。
+- 开闸失败:返回业务已完成、闸机失败,避免操作员误以为可以重复收费或重新出场。
+- 无真实设备时:可使用已启用的模拟设备完成工作台流程验证。
 
-```
-临时车到达出口 → 识别车牌
-  │
-  ├─ 计费 → 屏幕显示金额
-  │
-  ├─ POS 机进入待支付状态
-  │    └─ 车主刷卡/闪付/扫码(银行卡或会员卡)
-  │
-  ├─ POS 返回支付成功
-  │
-  └─ 系统记录 payment_record → 抬杆放行
-```
-
----
-
-## 5. 月租车完整流程
-
-### 5.1 办卡
-
-```
-操作员 → 包月车管理 → 选择车辆 → 选卡类型(月/季/年)
-  │
-  ├─ INSERT monthly_card (start_date, end_date, fee)
-  │
-  ├─ INSERT shortlist (list_type=白名单, expiration_time=end_date)
-  │
-  └─ INSERT payment_record (card fee)
-```
-
-### 5.2 进出场
-
-```
-月租车到达入口
-  │
-  ├─ RFID/车牌识别
-  │
-  ├─ CheckVehicleShortlist → 白名单 + 未过期
-  │    └─ 🟢 直接抬杆放行(零费用)
-  │
-  └─ 创建 vehicle_record + digital_ticket(记录进出时间)
-
-月租车到达出口
-  │
-  ├─ 白名单 + 未过期
-  │    └─ 🟢 直接抬杆放行
-  │
-  └─ 创建 payment_record(payment_method=free)
-```
-
-### 5.3 到期处理
-
-```
-月租到期 → shortlist.expiration_time < now
-  │
-  ├─ 进场时:白名单过期 → 按车辆原类型计费(如临时车)
-  │
-  ├─ 出场时:白名单过期 → 按普通费率收费
-  │
-  └─ 续费:操作员在包月车管理中点击续费
-       └─ end_date 延长 → shortlist 同步更新
-```
-
----
-
-## 6. 数字票状态跟踪
-
-```
-车辆入场
-  │
-  └─ digital_ticket.Create()
-       state: pending_payment
-       │
-       ├─ 月租/VIP/免费 → 直接 paid → exited
-       │
-       └─ 临时车需付费
-            │
-            ├─ 人工收费 → paid → exited
-            ├─ POS 刷卡 → paid → exited(规划)
-            └─ 中央缴费 → paid → exited(规划)
-```
+## 8. 异常与人工兜底
 
----
+| 异常 | 当前处理 | 后续完善 |
+| --- | --- | --- |
+| 黑名单车辆 | 拦截通行 | 增加异常处置记录和审批 |
+| 无未关闭会话的出场 | 拒绝出场 | 增加无票/丢票人工处理工作流 |
+| 闸机失败 | 业务状态保留,返回设备失败 | 增加重试、告警和人工确认 |
+| 设备离线 | 禁止使用该设备通行 | 增加离线事件和仪表盘告警 |
+| 摄像头未识别 | 当前未接入 | 接入 OCR 后进入同一通行入口 |
 
-## 7. 代码入口汇总
+## 9. 关键代码入口
 
-| 流程节点 | 函数 | 文件 |
-|---------|------|------|
-| RFID 入场 | `HandlePassage()` | `internal/service/parking/passage.go` |
-| 手动入场 | `VehicleEntry()` | `internal/service/vehicle/vehicle.go` |
-| 计费引擎 | `calculateFee()` | `internal/service/vehicle/vehicle.go` |
-| 出场预览 | `ExitPreview()` | `internal/service/vehicle/vehicle.go` |
-| 出场确认 | `ExitConfirm()` | `internal/service/vehicle/vehicle.go` |
-| 名单检查 | `CheckVehicleShortlist()` | `internal/service/vehicle/shortlist.go` |
-| 数字票 | `TicketService.Create()` | `internal/modules/digital-ticket/service/service.go` |
-| 支付处理 | `ProcessPayment()` | `internal/modules/payment/service/service.go` |
-| 通道事件 | `PushChannelEvent()` | `internal/service/uhf/reader.go` |
+| 节点 | 文件 | 关键方法 |
+| --- | --- | --- |
+| 统一通行 | `internal/service/parking/passage.go` | `HandlePassage` |
+| 停车会话 | `internal/modules/parking-session/service/service.go` | `OpenSessionTx`、`ResolveActiveSessionTx`、`CloseSessionTx` |
+| 车辆进出场 | `internal/service/vehicle/vehicle.go` | `VehicleEntry`、`ExitPreview`、`ExitConfirm` |
+| 支付 | `internal/modules/payment/service/service.go` | `ProcessPaymentTxAtEntry` |
+| 数字票 | `internal/modules/digital-ticket/service/service.go` | `CreateTx`、`MarkPaidAndExitedTx` |
+| 闸机 | `internal/service/parking/gate.go` | 开闸、关闸、设备状态 |

+ 41 - 541
doc/进出场逻辑流程.md

@@ -1,541 +1,41 @@
-# 智慧停车 — 进出场逻辑流程
-
-> 更新日期: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` |
+# 进出场逻辑流程图
+
+> 更新日期:2026-08-07
+> 本文档为流程总览;字段和异常细节见 [进出场流程详解](进出场流程详解.md)。
+
+## 1. 统一流程
+
+```mermaid
+flowchart TD
+    A[设备事件或人工录入] --> B[校验车辆标识]
+    B --> C[通过设备编码定位设备和通道]
+    C --> D{通道方向}
+    D -->|入场| E[定位未关闭停车会话]
+    E -->|不存在| F[创建停车会话和数字票]
+    E -->|已存在| G[拒绝重复入场]
+    F --> H[保存入口通道和设备快照]
+    H --> I[请求开闸]
+    D -->|出场| J[定位停车会话和数字票]
+    J --> K[预览停车费用]
+    K --> L{数字票是否已支付}
+    L -->|否| M[按入口和方式完成支付]
+    M --> N[写支付流水并更新会话和数字票]
+    L -->|是| O[复用既有支付结果]
+    N --> P[保存出口通道和设备快照]
+    O --> P
+    P --> Q[关闭会话并标记数字票已离场]
+    Q --> R[请求开闸]
+```
+
+## 2. 状态约束
+
+| 操作 | 前置状态 | 成功后的状态 |
+| --- | --- | --- |
+| 入场 | 不存在未关闭会话 | 会话在场,数字票 `pending_payment` |
+| 支付 | 会话在场且未支付 | 会话已支付,数字票 `paid` |
+| 出场 | 会话在场且已支付,或合法零费用放行 | 会话关闭,数字票 `exited` |
+| 再次支付/出场 | 已支付或已关闭 | 拒绝,不产生新流水 |
+
+## 3. 设备失败原则
+
+业务数据事务成功后才请求开闸。开闸失败不回滚已完成的支付或出场,而是返回设备失败状态,由操作员重试开闸或按异常流程处理。

+ 98 - 259
doc/项目进度.md

@@ -1,266 +1,105 @@
-# 智慧停车管理系统 — 项目进度
-
-> 更新日期:2026-07-24
-
-## 技术栈
-
-| 层级 | 技术 |
-|------|------|
-| 桌面框架 | Wails v2.13(WebView2) |
-| 后端 | Go 1.25 + Gin + GORM + Casbin v3 + JWT |
-| 前端 | Vue 3 + Vite 4 + Element Plus + Pinia |
-| 数据库 | SQLite(`%APPDATA%/smart-parking/`) |
-
-## 整体进度
-
-| 模块 | 后端 | 前端 | 状态 |
-|------|:--:|:--:|:----:|
-| 用户管理 | ✅ | ✅ | 完成 |
-| 角色权限 | ✅ | ✅ | Casbin RBAC,动态菜单 |
-| 菜单管理 | ✅ | ✅ | - |
-| API 管理 | ✅ | ✅ | - |
-| 数据字典 | ✅ | ✅ | - |
-| 操作日志 | ✅ | ✅ | OperationRecord中间件已全局启用 |
-| **异常日志** | ❌ | ❌ | panic recovery + error log to file |
-| 停车场管理 | ✅ | ✅ | 含 /parking 前缀对齐 |
-| 收费岗亭 | ✅ | ✅ | - |
-| 通道管理 | ✅ | ✅ | 方向/临停开关 |
-| UHF 设备 | ✅ | ✅ | TCP/串口连接/启停 |
-| 车辆信息 | ✅ | ✅ | CRUD |
-| 车辆类型 | ✅ | ✅ | 含 is_system 标记 |
-| 车主管理 | ✅ | ✅ | 含 VIP |
-| 收费配置 | ✅ | ✅ | 起步价/计费单位/每日封顶/VIP折扣 |
-| 黑白名单 | ✅ | ✅ | 进出场三入口全覆盖 |
-| 车辆进场 | ✅ | ✅ | RFID 自动 + 手动 |
-| 车辆出场 | ✅ | ✅ | 计费算法完整 |
-| 进出记录 | ✅ | ✅ | 查询 |
-| 道闸控制 | ✅ | ✅ | RFID识别→继电器开闸 |
-| 手动进出场 | ✅ | ✅ | entryExit.vue,双 Tab 页面 |
-| 收费结算 | ✅ | ✅ | 现金+找零/免密,payment_record |
-| **收费记录查询** | ✅ | ✅ | 按车牌/支付方式/日期/停车场筛选 |
-| **包月车管理** | ✅ | ✅ | 月/季/年卡,办卡=白名单 |
-| **监控仪表盘** | ✅ | ✅ | 车位/在场/收入/进出+设备状态 |
-| 摄像头 | ⚠️ | ❌ | DAO 有定义,无 API/页面 |
-| 个人中心 | ✅ | ✅ | - |
-| 登录页 | ✅ | ✅ | RSA 加密 + 验证码 |
-| **数字票** | ✅ | ❌ | 5状态机+ticket_no二维码+事件日志 |
-| **在场车辆统计** | ✅ | ✅ | 停车场/车牌筛选+详情卡片 |
-| **exe混淆** | ❌ | ❌ | garble 编译混淆,防反编译 |
-
-## 核心业务流完成度
-
-```
-1. 系统设置 ──────── 停车场→岗亭→通道→设备  ✅ 100%
-2. 收费规则配置 ──── 按车型/免费时长/VIP     ✅ 100%
-3. 名单管理 ──────── 白名单/黑名单           ✅ 100%
-4. 车辆进场 ──────── RFID识别→查单→开闸     ✅ 100%
-5. 车辆出场 ──────── 识别→计费→收费→开闸    ✅ 100%
-6. 收费结算 ──────── 现金+找零/免密          ✅ 100%
-7. 手动进出场页面 ─── 车牌/RFID输入→进出场   ✅ 100%
-8. 收费记录查询 ──── 筛选/汇总               ✅ 100%
-9. 包月车管理 ───── 月/季/年卡→白名单       ✅ 100%
-10. 监控仪表盘 ───── 车位/收入/设备状态      ✅ 100%
-11. 小票打印 ─────── ESC/POS 串口打印        ❌ 0%
-12. 摄像头识别 ───── RTSP抓拍→OCR→触发       ❌ 0%
-13. LED车位屏 ────── 余位实时显示             ❌ 0%
-14. 交接班对账 ───── 当班统计/实时收款/差异   ✅ 100%
-15. 收入报表 ─────── 按日+停车场汇总          ✅ 100%
-16. 在场车辆统计 ──── 停车场/车牌筛选+详情    ✅ 100%
-17. 数字票 ──────── 5状态机+ticket_no+日志   ✅ 100%
-18. 操作日志 ──────── OperationRecord中间件   ✅ 100%
-19. 异常日志 ──────── panic recovery+文件    ❌ 0%
-20. exe混淆 ──────── garble编译              ❌ 0%
+# 智慧停车管理系统项目进度
+
+> 更新日期:2026-08-07
+> 状态说明:`已完成` 指功能已接入前后端;`已验证` 指自动化测试或构建已通过;`待验收` 指需在真实设备或现场流程继续验证。
+
+## 1. 当前完成度
+
+| 领域 | 状态 | 当前能力 |
+| --- | --- | --- |
+| 基础权限与系统管理 | 已完成 | 用户、角色、菜单、API、Casbin、字典、操作日志、登录 |
+| 停车场基础配置 | 已完成 | 停车场、岗亭、通道、UHF 设备,通道方向与临停开关 |
+| 车辆与名单 | 已完成 | 车辆、车主、车型、黑白名单、包月车 |
+| 统一进出场 | 已完成 | 手动和 UHF 统一经过 `PassageService`,支持入场、出场、计费、开闸 |
+| 停车会话 | 已完成并验证 | 防止重复在场、统一定位、支付与离场状态更新、入口/出口通道设备快照 |
+| 数字票 | 已完成并验证 | 创建、待支付、已支付、已离场、事件日志、列表、详情、汇总、支付和离场页面 |
+| 支付 | 已完成并验证 | 支付入口与支付方式分离配置,现金、POS、扫码等可配置方式,支付流水与找零 |
+| 交接班与收入报表 | 已完成 | 交接班记录、收费记录、收入汇总与查询 |
+| 仪表盘与车辆监控 | 已完成 | 在场车辆、进出统计、收入及设备状态展示 |
+| 闸机控制 | 已完成,待现场验收 | 统一开闸/关闸封装,支持真实设备和模拟在线设备 |
+| 小票打印 | 已完成,待真实打印验收 | 串口 ESC/POS 与 USB CSN SDK,二维码与票据排版测试 |
+| 摄像头识别 | 未完成 | 已有摄像头实体,尚未接入 RTSP、OCR 和进出场事件 |
+| LED 车位屏 | 未完成 | 尚未接入硬件协议和余位推送 |
+
+## 2. 本轮已完成工作
+
+### 2.1 停车会话统一化
+
+- 以 `vehicle_record` 作为停车会话持久化载体。
+- 入场时原子创建停车会话和数字票,避免只有入场记录或只有数字票的半成品数据。
+- 出场、支付、数字票操作都通过会话编号、票号、车牌或 RFID 定位同一在场会话。
+- 旧出场路径已收敛至 `ExitConfirm` 和统一通行入口。
+
+### 2.2 支付与数字票解耦
+
+- 数字票不再绑定“停车小票”这一单一场景,所有车辆入场均创建数字票。
+- 支付入口和支付方式分别配置:入口决定在哪里完成支付,方式决定如何收款。
+- 支付成功写入 `payment_record`,同步更新停车会话和数字票;出场只消费已完成的结算结果,避免重复收费。
+- 已补充负金额、零金额、重复支付、并发结算、事务回滚等测试。
+
+### 2.3 设备、通道和会话可追溯
+
+- `HandlePassage` 根据设备编码定位 UHF 设备及绑定通道。
+- 入场保存入口通道和入口设备快照;出场保存出口通道和出口设备快照。
+- 快照包含 ID、编码和名称。进出记录详情页面优先显示名称。
+- 历史记录若没有名称快照,会根据当前通道 ID、设备编码补全;无法补全时保留编码兜底。
+
+### 2.4 小票打印
+
+- 支持串口 ESC/POS 与 USB CSN DLL 直连两种打印链路。
+- USB 票据按实际纸张出纸方向输出,并包含二维码、切纸前走纸和短写失败处理。
+- 已完成打印命令与版式自动测试;最终字号、物理偏移和二维码密度仍以现场纸张为准。
+
+## 3. 核心业务闭环
+
+```text
+设备/人工识别车辆
+  -> 定位设备和通道
+  -> 定位在场停车会话
+  -> 无会话:创建会话 + 数字票 + 入场快照 + 开闸
+  -> 有会话:预览费用
+  -> 通过支付入口选择支付方式完成结算
+  -> 支付流水 + 会话支付摘要 + 数字票同步更新
+  -> 保存出场通道设备快照 + 关闭会话 + 数字票离场 + 开闸
 ```
 
-## 车辆进出场触发方式
-
-| 触发方式 | 状态 | 说明 |
-|---------|:--:|------|
-| UHF RFID 自动识别 | ✅ | 读标签→查车辆→进出场+开闸,PassageService 统一入口 |
-| 前端手动录入 | ✅ | entryExit.vue 进出场操作页面,含收费结算 |
-| 数字票统一抽象 | ✅ | 所有进场方式统一创建 digital_ticket,5状态机驱动 |
-| 摄像头车牌识别 | ❌ | `camera` 表已建,无拍照/OCR/触发逻辑 |
-
-```
-车辆靠近
-  ├─ UHF 读卡器 → RFID 标签 → PassageService → VehicleEntry/Exit ✅
-  ├─ 人工       → 手动录入车牌 → VehicleEntry/Exit ✅
-  ├─ 摄像头     → 抓拍 → OCR 车牌 → VehicleEntry/Exit ❌
-              ↓
-         三者统一创建 digital_ticket(数字票状态机)
-```
-
-**摄像头识别待实现内容**:
-
-| 组件 | 说明 |
-|------|------|
-| 硬件对接 | RTSP 视频流抓拍或 SDK 回调 |
-| OCR 识别 | 车牌识别算法或第三方云服务 |
-| 业务触发 | 识别到车牌 → 自动调用 `VehicleEntry()`/`VehicleExit()` |
-| 图片留存 | `VehicleRecord` 已有 `EntryImage`/`ExitImage` 字段 |
-
-## 待开发模块
-
-### P0-1 ─ 道闸控制 ✅
-
-> 已完成:继电器 `CloseRelay1(2)` 已解除注释,`OpenGate()` 公开方法。进场/出场自动开闸。
-
-### P0-2 ─ 手动进出场页面 ✅
-
-> 已完成:`entryExit.vue` 支持车牌/RFID 输入、车辆查询、进场/出场双 Tab。含多语言支持。
-
-### P0-3 ─ 收费结算 ✅
-
-> 已完成:`payment_record` 表、`PaymentService`、现金收款+找零、免密放行。预留 POS/扫码扩展。
+## 4. 自动化验证记录
 
-### P0-4 ─ 小票打印
+| 范围 | 已覆盖的关键场景 |
+| --- | --- |
+| `parking-session` | 会话创建、重复入场拒绝、身份冲突、支付、关闭、通道设备快照 |
+| `payment` | 负金额、零金额、金额不足、重复结算、并发结算、事务回滚、缺失会话 |
+| `vehicle` | 车辆入场创建会话和数字票、RFID 定位、预览、免费出场、重复支付回滚 |
+| `parking` | 统一通行和闸机相关服务回归 |
+| `printer` | USB 票据顺序、文字方向、二维码、切纸走纸、短写和二维码失败 |
+| 前端 | `npm run build` 生产构建通过 |
 
-> **收完钱要出票**:配合收费结算,收费完成后自动打印小票给车主。
+最近一次相关验证命令:
 
-| 功能 | 后端 | 前端 | 说明 |
-|------|:--:|:--:|------|
-| 小票打印 | ❌ | ❌ | 串口 ESC/POS 指令,58mm 热敏小票打印机 |
+```powershell
+$env:GOCACHE='D:\lq\Smart Parking\lc_garage\.gocache'
+go test ./internal/modules/parking-session/service ./internal/service/vehicle ./internal/service/parking
 
-**待实现内容**:
-
-| 组件 | 说明 |
-|------|------|
-| 串口通信 | 通过串口/USB 连接 58mm 热敏打印机 |
-| ESC/POS 指令 | 封装打印指令集:文字、条码、二维码、切纸 |
-| 小票模板 | 停车小票格式:车牌号、进场时间、出场时间、费用、收款方式 |
-| 打印触发 | 收费完成后自动打印,支持补打 |
-
-### P0-5 ─ 摄像头车牌识别
-
-> **双重校验防作弊**:RFID 标签可能被拆换,车牌识别作为第二道校验。也是第三种进出场触发方式(无标签车辆)。
-
-| 功能 | 后端 | 前端 | 说明 |
-|------|:--:|:--:|------|
-| 摄像头车牌识别 | ❌ | ❌ | RTSP 视频流抓拍 + OCR 车牌识别,`camera` 表已建 |
-
-**待实现内容**:
-
-| 组件 | 说明 |
-|------|------|
-| 硬件对接 | RTSP 视频流拉取或 SDK 回调(海康/大华/宇视等) |
-| 抓拍触发 | 车辆进入识别区 → 抓拍一张图片 |
-| OCR 识别 | 车牌识别算法或第三方云服务(百度/阿里 OCR) |
-| 业务触发 | 识别到车牌 → 自动调用 `VehicleEntry()`/`VehicleExit()` |
-| 图片留存 | `VehicleRecord` 已有 `EntryImage`/`ExitImage` 字段 |
-
-### P1 — 运营管理
-
-| 功能 | 后端 | 前端 | 说明 |
-|------|:--:|:--:|------|
-| 交接班 | ✅ | ✅ | 接班/交班/当班收款/差异计算 |
-| 收入报表 | ✅ | ✅ | 按日+停车场汇总,底部合计行 |
-| 异常报表 | ❌ | ⚠️ | 无牌车、手动抬杆、断网异常记录,前端空占位 |
-| LED 车位显示 | ❌ | ❌ | 入口剩余车位显示屏对接 |
-
-### P2 — 结算终端
-
-| 功能 | 后端 | 前端 | 说明 |
-|------|:--:|:--:|------|
-| **出口 POS 机** | ❌ | ❌ | 临时车出口刷卡支付(银行卡/会员卡),调用支付网关 |
-| **中央缴费机** | ❌ | ❌ | 停车场内自助缴费终端:车牌查询 → 扫码/现金支付 → 出口自动放行 |
-
-### P2 — 监控与边缘端
-
-| 功能 | 后端 | 前端 | 说明 |
-|------|:--:|:--:|------|
-| 实时监控大屏 | ✅ | ✅ | 暗色控制室风格,LED读表字体 |
-| 在线车辆统计 | ✅ | ✅ | 停车场/车牌筛选,卡片网格+详情 |
-| RK3568 边缘端 | ❌ | ❌ | edge/ 通信层(TCP协议 + 事件总线) |
-
-## 前端页面清单(37 页)
-
-| 位置 | 页面 | 功能 | 状态 |
-|------|------|------|:--:|
-| `login/` | index.vue | 登录 | ✅ |
-| `dashboard/` | index.vue + charts + table | 仪表盘 | ✅ |
-| `parking/` | parkingInfo/index.vue | 停车场信息(左列表右Tab) | ✅ |
-| | parkingInfo/booth.vue | 岗亭管理 | ✅ |
-| | parkingInfo/channel.vue | 通道管理 | ✅ |
-| | parkingInfo/device.vue | UHF设备管理 | ✅ |
-| | carInfo.vue | 车辆信息 | ✅ |
-| | carOwner.vue | 车主管理 | ✅ |
-| | pricing.vue | 收费配置 | ✅ |
-| | roster.vue | 黑白名单 | ✅ |
-| `report/` | enter.vue | 进出记录 | ✅ |
-| | sheet.vue | 异常报表 | ⚠️ 空 |
-| `statistics/` | vehicle.vue | 在线车辆统计 | ✅ |
-| `superAdmin/` | user/user.vue | 用户管理 | ✅ |
-| | authority/authority.vue | 角色管理 | ✅ |
-| | menu/menu.vue | 菜单管理 | ✅ |
-| | api/api.vue | API管理 | ✅ |
-| | dictionary/*.vue | 字典管理 | ✅ |
-| | operation/*.vue | 操作日志 | ✅ |
-| `person/` | person.vue | 个人中心 | ✅ |
-| `layout/` | 9 个组件 | 布局框架 | ✅ |
-| `error/` | 2 个组件 | 错误页 | ✅ |
-
-## 后端 API(110+ 接口)
-
-| 模块 | 接口数 | 状态 |
-|------|:-----:|:----:|
-| Base (login/captcha) | 3 | ✅ |
-| User | 11 | ✅ |
-| Menu | 9 | ✅ |
-| API | 9 | ✅ |
-| Authority | 6 | ✅ |
-| Casbin | 2 | ✅ |
-| Dictionary | 10 | ✅ |
-| OperationRecord | 5 | ✅ |
-| FileUpload | 8 | ✅ |
-| Email | 2 | ✅ |
-| Parking Lot | 7 | ✅ |
-| Booth | 7 | ✅ |
-| Channel | 7 | ✅ |
-| Device (UHF) | 8 | ✅ |
-| Vehicle | 8 | ✅ |
-| Vehicle Type | 6 | ✅ |
-| Vehicle Record | 3 | ✅ |
-| Fee Config | 5 | ✅ |
-| Owner | 6 | ✅ |
-| Shortlist | 5 | ✅ |
-
-## 数据库(24 张业务表)
-
-| 表 | 说明 | 状态 |
-|------|------|:--:|
-| parking_lot | 停车场 | ✅ |
-| booth | 收费岗亭 | ✅ |
-| channel | 通道 | ✅ |
-| uhf_reader | UHF读卡器 | ✅ |
-| camera | 摄像头 | ⚠️ 表有,功能无 |
-| vehicle | 车辆 | ✅ |
-| vehicle_type | 车辆类型 | ✅ |
-| vehicle_record | 进出记录 | ✅ |
-| fee_config | 收费标准 | ✅ |
-| owner | 车主 | ✅ |
-| shortlist | 黑白名单 | ✅ |
-| payment_record | 支付记录 | ✅ |
-| monthly_card | 包月卡 | ✅ |
-| digital_ticket | 数字票 | ✅ |
-| sys_* (13张) | GVA 系统表 | ✅ |
-
-## 下一步建议
+cd frontend
+npm run build
+```
 
-| 优先级 | 功能 | 预估工时 | 状态 |
-|:--:|------|:--:|:--:|
-| ~~P0-1~~ | ~~道闸控制~~ | - | ✅ |
-| ~~P0-2~~ | ~~手动进出场页面~~ | - | ✅ |
-| ~~P0-3~~ | ~~收费结算~~ | - | ✅ |
-| ~~P0-3~~ | ~~收费结算~~ | - | ✅ |
-| ~~P1~~ | ~~包月车管理~~ | - | ✅ |
-| ~~P2~~ | ~~监控大屏~~ | - | ✅ |
-| ~~--~~ | ~~在场车辆统计~~ | - | ✅ |
-| ~~--~~ | ~~数字票~~ | - | ✅ |
-| **P0-4** | **小票打印** | 1-2天 | ESC/POS 串口指令封装 + 小票模板 |
-| **P0-5** | **摄像头车牌识别** | 3-5天 | RTSP+OCR,RFID+车牌双重校验 |
-| ~~P1~~ | ~~交接班 + 收入报表~~ | - | ✅ |
-| P1 | LED 车位显示 | 1天 | 入口剩余车位屏 |
-| P1 | 异常日志 | 半天 | panic recovery + error log to file |
-| P1 | exe混淆 | 半天 | garble 编译混淆 |
-| P2 | 出口 POS 机 | 3-5天 | 串口/TCP通信+支付网关对接 |
-| P2 | 中央缴费机 | 5-7天 | 自助缴费终端+扫码支付+出口联动放行 |
+## 5. 待办与优先级
+
+| 优先级 | 项目 | 状态 | 说明 |
+| --- | --- | --- | --- |
+| P0 | 真实闸机联调 | 待验收 | 开闸、关闸、设备离线、超时和人工兜底 |
+| P0 | 小票打印现场验收 | 待验收 | 真实 USB/串口打印机的方向、物理偏移、二维码大小和切纸 |
+| P0 | 摄像头车牌识别 | 未开始 | RTSP/SDK 抓拍、OCR、车牌置信度和统一通行入口接入 |
+| P1 | 异常处置闭环 | 待完善 | 无牌车、票据丢失、人工抬杆、设备离线、支付不确定状态 |
+| P1 | LED 车位屏 | 未开始 | 余位计算、设备协议、定时/事件推送 |
+| P1 | 现场数据与权限验收 | 待验收 | 角色菜单、操作日志、数据归属、交接班对账 |
+| P2 | POS 真机支付 | 待接入 | 以已配置的 POS 支付方式为基础接入交易请求和异步回调 |
+| P2 | 中央缴费机 | 待接入 | 作为支付入口接入,复用数字票和支付流水,不创建独立业务主线 |
+| P2 | 线上支付 | 待接入 | 小程序/App 扫码支付与回调幂等处理 |
+
+## 6. 文档维护规则
+
+- 业务行为变化时,同时更新本文件和对应流程文档。
+- “已完成”不等于真实硬件已验收;涉及闸机、打印机、POS 的功能必须保留现场验收记录。
+- 数据库字段新增依赖 `AutoMigrate` 时,文档应说明旧数据的兼容与回退策略。