# 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: 联调测试