|
|
@@ -0,0 +1,180 @@
|
|
|
+# 数字票(Digital Ticket)说明文档
|
|
|
+
|
|
|
+> 版本:1.0 | 日期:2026-07-23
|
|
|
+
|
|
|
+## 1. 概述
|
|
|
+
|
|
|
+数字票(Digital Ticket)是停车业务中的标准化抽象模型,以电子化的"票"作为核心对象,遵循 **创建 → 支付 → 验证 → 放行** 的线性状态机模式。不绑定特定硬件或支付方式,具有高度的通用性和可扩展性。
|
|
|
+
|
|
|
+## 2. 核心概念
|
|
|
+
|
|
|
+### 设计原则
|
|
|
+
|
|
|
+- **统一抽象**:所有进场方式(RFID、车牌识别、手动录入)最终都创建数字票
|
|
|
+- **状态驱动**:票的状态决定下一步操作,不允许跳跃式状态变更
|
|
|
+- **事件溯源**:关键节点记录事件日志,便于审计和异常追踪
|
|
|
+- **二维码凭证**:以 ticket_no(UUID)作为唯一标识和验证依据
|
|
|
+
|
|
|
+### 与现有流程的关系
|
|
|
+
|
|
|
+```
|
|
|
+任何进场方式(手动/RFID/车牌)
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ VehicleEntry() ──→ 创建 vehicle_record
|
|
|
+ │ │
|
|
|
+ │ ▼
|
|
|
+ │ 创建 digital_ticket
|
|
|
+ │ (状态: pending_payment)
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ ExitPreview / ExitConfirm
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ digital_ticket 状态 → paid → exited
|
|
|
+```
|
|
|
+
|
|
|
+数字票是 `vehicle_record` 的业务状态层,不替代它。报表和监控继续用 `vehicle_record`,业务流转看 `digital_ticket` 状态。
|
|
|
+
|
|
|
+## 3. 状态机
|
|
|
+
|
|
|
+```
|
|
|
+ ┌──────────┐
|
|
|
+ ┌──→ │ expired │ ←──┐
|
|
|
+ │ │ 已过期 │ │
|
|
|
+ │ └──────────┘ │
|
|
|
+ │ 超时未支付 │ 超时未离场
|
|
|
+ │ │
|
|
|
+ ┌─────────┐ │ ┌───────────────┐ │ ┌──────┐ ┌────────┐
|
|
|
+ │ created │─→│ │pending_payment│─→│ paid │─→│ exited │
|
|
|
+ │ 已创建 │ │ 待支付 │ │ 已支付│ │ 已离场 │
|
|
|
+ └─────────┘ └───────────────┘ └──────┘ └────────┘
|
|
|
+ 车辆入场 自动转换 支付成功 验证放行
|
|
|
+```
|
|
|
+
|
|
|
+| 状态 | 说明 | 触发条件 |
|
|
|
+|------|------|---------|
|
|
|
+| `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 接口
|
|
|
+
|
|
|
+| 方法 | 路径 | 说明 | 鉴权 |
|
|
|
+|------|------|------|:--:|
|
|
|
+| 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
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 列表查询参数
|
|
|
+
|
|
|
+| 参数 | 说明 |
|
|
|
+|------|------|
|
|
|
+| plate_number | 车牌号(模糊匹配) |
|
|
|
+| state | 状态筛选 |
|
|
|
+| page | 页码 |
|
|
|
+| page_size | 每页条数 |
|
|
|
+
|
|
|
+## 6. 代码结构
|
|
|
+
|
|
|
+```
|
|
|
+internal/modules/digital-ticket/
|
|
|
+├── api.go # HTTP Handler + 路由注册
|
|
|
+├── service/
|
|
|
+│ └── service.go # 状态机 + 事件日志 + Create/Transition
|
|
|
+└── repository/
|
|
|
+ └── repo.go # CRUD
|
|
|
+```
|
|
|
+
|
|
|
+```
|
|
|
+internal/dao/digital_ticket.go # GORM 实体
|
|
|
+```
|
|
|
+
|
|
|
+### 关键函数
|
|
|
+
|
|
|
+| 函数 | 说明 |
|
|
|
+|------|------|
|
|
|
+| `TicketService.Create()` | 创建数字票,生成 UUID ticket_no,写入 event_log |
|
|
|
+| `TicketService.Transition()` | 状态转换,校验合法性,追加事件日志 |
|
|
|
+| `TicketService.GetByTicketNo()` | 扫码查询(二维码凭证验证入口) |
|
|
|
+
|
|
|
+## 7. 扩展性
|
|
|
+
|
|
|
+数字票作为统一抽象,后续可扩展:
|
|
|
+
|
|
|
+- **线上支付**:扫码 → 跳转支付页 → 回调 `PayTicket`
|
|
|
+- **自助缴费机**:票机扫码 → 调 `/ticketNo` 查询费用 → POS 支付 → 回调 `/ticketNo/pay`
|
|
|
+- **无牌车处理**:进场出纸质二维码小票(ticket_no 打印在票上)
|
|
|
+- **定时过期**:后台任务扫描 `pending_payment` 超时票 → Transition 到 `expired`
|
|
|
+
|
|
|
+## 8. 数据库位置
|
|
|
+
|
|
|
+```
|
|
|
+Windows: %APPDATA%\smart-parking\lc_garage.db
|
|
|
+ → C:\Users\<用户名>\AppData\Roaming\smart-parking\lc_garage.db
|
|
|
+```
|
|
|
+
|
|
|
+查询数字票数据:
|
|
|
+
|
|
|
+```sh
|
|
|
+sqlite3 "%APPDATA%/smart-parking/lc_garage.db" "SELECT id, ticket_no, plate_number, state FROM digital_ticket;"
|
|
|
+```
|