# 数字票（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;"
```
