274 lines
6.0 KiB
Markdown
274 lines
6.0 KiB
Markdown
|
|
# Phase 4.3 实现计划:C端票夹
|
|||
|
|
|
|||
|
|
> 创建日期:2026-04-23
|
|||
|
|
> 状态:**待实现**
|
|||
|
|
> 依赖:Phase 4.1 (HMAC-XOR) + Phase 4.2 (issueTicket) 已完成
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、文件清单
|
|||
|
|
|
|||
|
|
| 步骤 | 文件 | 类型 | 说明 |
|
|||
|
|
|------|------|------|------|
|
|||
|
|
| 1 | `api/Ticket.php` | 新建 | C端 API 控制器 |
|
|||
|
|
| 2 | `service/WalletService.php` | 新建 | 票夹核心服务 |
|
|||
|
|
| 3 | `view/goods/ticket_card.html` | 新建 | 票卡片共享组件 |
|
|||
|
|
| 4 | `view/goods/ticket_wallet.html` | 新建 | 票夹列表页 |
|
|||
|
|
| 5 | `Hook.php` | 修改 | 注册 C 端挂载点钩子 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、API 设计
|
|||
|
|
|
|||
|
|
### 2.1 获取用户票列表
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
GET ?s=api/plugins/index&pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction=list
|
|||
|
|
|
|||
|
|
Headers:
|
|||
|
|
X-Token: {user_token}
|
|||
|
|
|
|||
|
|
Response:
|
|||
|
|
{
|
|||
|
|
"code": 0,
|
|||
|
|
"msg": "success",
|
|||
|
|
"data": {
|
|||
|
|
"tickets": [
|
|||
|
|
{
|
|||
|
|
"id": 1,
|
|||
|
|
"goods_id": 118,
|
|||
|
|
"goods_title": "周杰伦演唱会",
|
|||
|
|
"seat_info": "A区-3排-15座",
|
|||
|
|
"session_time": "2026-06-01 20:00",
|
|||
|
|
"venue_name": "国家体育馆",
|
|||
|
|
"real_name": "张三",
|
|||
|
|
"verify_status": 0,
|
|||
|
|
"issued_at": 1745286000,
|
|||
|
|
"short_code": "003a2hgmgety"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"count": 1
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.2 获取票详情(含 QR payload)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
GET ?s=api/plugins/index&pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction=detail&id={ticket_id}
|
|||
|
|
|
|||
|
|
Headers:
|
|||
|
|
X-Token: {user_token}
|
|||
|
|
|
|||
|
|
Response:
|
|||
|
|
{
|
|||
|
|
"code": 0,
|
|||
|
|
"msg": "success",
|
|||
|
|
"data": {
|
|||
|
|
"ticket": {
|
|||
|
|
"id": 1,
|
|||
|
|
"goods_id": 118,
|
|||
|
|
"goods_title": "周杰伦演唱会",
|
|||
|
|
"seat_info": "A区-3排-15座",
|
|||
|
|
"session_time": "2026-06-01 20:00",
|
|||
|
|
"venue_name": "国家体育馆",
|
|||
|
|
"real_name": "张三",
|
|||
|
|
"phone": "138****1234",
|
|||
|
|
"verify_status": 0,
|
|||
|
|
"short_code": "003a2hgmgety",
|
|||
|
|
"qr_payload": "eyJpZCI6MSwiZyI6MTE4LCJpYXQiOjE3NDUyODY2MDAsImV4cCI6MTc0NTI4NzIwMCwic2lnIjoiQTNGOUIyQzEifQ==",
|
|||
|
|
"qr_expires_at": 1745287200,
|
|||
|
|
"qr_expires_in": 1800
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.3 刷新 QR payload
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
GET ?s=api/plugins/index&pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction=refreshQr&id={ticket_id}
|
|||
|
|
|
|||
|
|
Headers:
|
|||
|
|
X-Token: {user_token}
|
|||
|
|
|
|||
|
|
Response: 同 2.2
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、数据流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
用户访问票夹页
|
|||
|
|
↓
|
|||
|
|
Hook 注入票夹入口(或直接在商品详情页显示)
|
|||
|
|
↓
|
|||
|
|
ticket_wallet.html 加载
|
|||
|
|
↓
|
|||
|
|
JS 调用 /ticket/list API 获取票列表
|
|||
|
|
↓
|
|||
|
|
渲染 ticket_card 列表
|
|||
|
|
↓
|
|||
|
|
点击单个票 → 调用 /ticket/detail API → 展示 QR + 短码
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、实现步骤
|
|||
|
|
|
|||
|
|
### Step 1: WalletService.php
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// service/WalletService.php
|
|||
|
|
namespace app\plugins\vr_ticket\service;
|
|||
|
|
|
|||
|
|
class WalletService extends BaseService
|
|||
|
|
{
|
|||
|
|
/**
|
|||
|
|
* 获取用户所有票
|
|||
|
|
*/
|
|||
|
|
public static function getUserTickets(int $userId): array
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取单个票详情
|
|||
|
|
*/
|
|||
|
|
public static function getTicketDetail(int $ticketId, int $userId): ?array
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 生成 QR payload(含缓存逻辑)
|
|||
|
|
*
|
|||
|
|
* 缓存策略:
|
|||
|
|
* - QR 有效期 30 分钟
|
|||
|
|
* - 剩余有效期 > 15 分钟:返回缓存
|
|||
|
|
* - 剩余有效期 ≤ 15 分钟:刷新
|
|||
|
|
*/
|
|||
|
|
public static function getQrPayload(int $ticketId): array
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Step 2: api/Ticket.php
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// api/Ticket.php
|
|||
|
|
namespace app\plugins\vr_ticket\api;
|
|||
|
|
|
|||
|
|
class Ticket
|
|||
|
|
{
|
|||
|
|
/**
|
|||
|
|
* 获取用户票列表
|
|||
|
|
* GET ?s=api/plugins/...&pluginsaction=list
|
|||
|
|
*/
|
|||
|
|
public function list(): Json
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取票详情
|
|||
|
|
* GET ?s=api/plugins/...&pluginsaction=detail&id=X
|
|||
|
|
*/
|
|||
|
|
public function detail(): Json
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 刷新 QR payload
|
|||
|
|
* GET ?s=api/plugins/...&pluginsaction=refreshQr&id=X
|
|||
|
|
*/
|
|||
|
|
public function refreshQr(): Json
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**路由验证**:
|
|||
|
|
- `pluginsname=vr_ticket` → `vr_ticket` 目录
|
|||
|
|
- `pluginscontrol=ticket` → `api/Ticket.php` (ucfirst('ticket') = 'Ticket')
|
|||
|
|
- `pluginsaction=list` → `Ticket::list()`
|
|||
|
|
|
|||
|
|
### Step 3: ticket_card.html
|
|||
|
|
|
|||
|
|
票卡片组件,包含:
|
|||
|
|
- 商品信息(标题、场次、座位)
|
|||
|
|
- 观演人信息
|
|||
|
|
- QR 码展示区(懒加载)
|
|||
|
|
- 短码展示
|
|||
|
|
- 核销状态标识
|
|||
|
|
|
|||
|
|
### Step 4: ticket_wallet.html
|
|||
|
|
|
|||
|
|
票夹页面:
|
|||
|
|
- 加载 JS/CSS(qrcode.js、jQuery)
|
|||
|
|
- 调用 `/ticket/list` 获取票列表
|
|||
|
|
- 渲染票卡片列表
|
|||
|
|
- 空状态提示
|
|||
|
|
|
|||
|
|
### Step 5: Hook.php 修改
|
|||
|
|
|
|||
|
|
注册 C 端挂载点钩子:
|
|||
|
|
```php
|
|||
|
|
// 在 handle() 方法中添加
|
|||
|
|
case 'plugins_service_order_detail_page_info':
|
|||
|
|
$this->InjectTicketCard($params);
|
|||
|
|
break;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、QR 码展示逻辑
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// ticket_card.html 伪代码
|
|||
|
|
function loadQrCode(ticketId) {
|
|||
|
|
// 1. 检查 localStorage 缓存
|
|||
|
|
const cached = localStorage.getItem('vr_qr_' + ticketId);
|
|||
|
|
if (cached) {
|
|||
|
|
const data = JSON.parse(cached);
|
|||
|
|
if (data.expires_at > Date.now() / 1000) {
|
|||
|
|
// 缓存有效,剩余 > 15 分钟则直接展示
|
|||
|
|
const remaining = data.expires_at - Date.now() / 1000;
|
|||
|
|
if (remaining > 900) {
|
|||
|
|
renderQr(data.payload);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 调用 API 获取新 QR
|
|||
|
|
$.get('/api/plugins/...&pluginsaction=detail&id=' + ticketId, function(res) {
|
|||
|
|
if (res.code === 0) {
|
|||
|
|
// 3. 缓存到 localStorage
|
|||
|
|
localStorage.setItem('vr_qr_' + ticketId, JSON.stringify({
|
|||
|
|
payload: res.data.ticket.qr_payload,
|
|||
|
|
expires_at: res.data.ticket.qr_expires_at
|
|||
|
|
}));
|
|||
|
|
// 4. 渲染 QR
|
|||
|
|
renderQr(res.data.ticket.qr_payload);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderQr(base64Payload) {
|
|||
|
|
const payload = atob(base64Payload);
|
|||
|
|
$('#qrcode').qrcode({ text: payload });
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、测试用例
|
|||
|
|
|
|||
|
|
| 用例 | 预期结果 |
|
|||
|
|
|------|----------|
|
|||
|
|
| 未登录访问 | 返回 401 |
|
|||
|
|
| 无票用户 | 返回空列表 |
|
|||
|
|
| 有票用户 | 返回票列表 |
|
|||
|
|
| 点击票卡片 | 展示 QR + 短码 |
|
|||
|
|
| QR 过期前刷新 | 获取新 QR |
|
|||
|
|
| 核销后展示 | 显示已核销状态 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七、进度记录
|
|||
|
|
|
|||
|
|
- [ ] Step 1: WalletService.php
|
|||
|
|
- [ ] Step 2: api/Ticket.php
|
|||
|
|
- [ ] Step 3: ticket_card.html
|
|||
|
|
- [ ] Step 4: ticket_wallet.html
|
|||
|
|
- [ ] Step 5: Hook.php
|
|||
|
|
- [ ] Step 6: 联调测试
|