# 核销系统设计 > 调研时间:2026-04-14 > 关键参考:`realstore/check/check.vue`(自提点核销页)、`sxo_order_extraction_code` 表 --- ## 一、系统概述 核销系统解决:用户持电子票 QR 码 → 工作人员验证 → 标记已入场。 ### 1.1 核销模式 | 模式 | 说明 | 适用场景 | |---|---|---| | **扫码核销** | B 端扫用户 QR 码 | 演唱会入口、活动签到 | | **手动输入** | B 端输入票码 | 网络不稳定场景 | | **双重核销** | 用户扫码 + B 端扫码 | 高安全要求 | ### 1.2 核销粒度 | 粒度 | 说明 | 实现难度 | |---|---|---| | 按订单 | 一个订单一个码 | ⭐ 最简单 | | 按座位 | 每个座位一个码 | ⭐⭐ 推荐 | **推荐**:按座位核销(每人一个 QR 码),与演唱会场景完全匹配。 --- ## 二、QR 票生成 ### 2.1 触发时机 **支付成功回调时**(`plugins_service_buy_order_insert_success` 钩子) ### 2.2 QR 码内容设计 ```json { "id": 12345, // vr_attendees.id "code": "UUID-v4", // ticket_code,唯一标识 "event_id": 8, "session_id": 15, "seat": "A区-3排-15座", "exp": 1735689600 // 过期时间(时间戳) } ``` ### 2.3 加密方式 不加密(明文 QR): - 优点:调试方便,可人工识别 - 缺点:可伪造 - 适用:低安全要求、内部活动 **加密 QR**(推荐): ```php $qr_data = json_encode([...]); $encrypted = base64_encode( openssl_encrypt($qr_data, 'AES-256-CBC', $secret_key, OPENSSL_RAW_DATA, $iv) ); ``` 核销时解密验证: ```php $decrypted = openssl_decrypt( base64_decode($encrypted), 'AES-256-CBC', $secret_key, OPENSSL_RAW_DATA, $iv ); ``` ### 2.4 QR 码生成 使用 ShopXO 内置 `\base\Qrcode` 类: ```php // 生成展示用 QR 码 URL $ticket_code = $attendee->ticket_code; $qr_url = MyUrl('index/qrcode/index', [ 'content' => urlencode(base64_encode($ticket_code)), 'size' => 8, 'level' => 'H', // 高容错率,扫码成功率高 'mr' => 2, ]); // 生成文件(用于发送邮件/消息) $qr_path = (new \base\Qrcode())->Create([ 'content' => $ticket_code, 'path' => 'static/upload/tickets/' . date('Y/md') . '/', 'filename' => $ticket_code . '.png', 'level' => 'H', 'size' => 10, 'mr' => 2, ]); ``` --- ## 三、数据存储 ### 3.1 扩展方案 vs 独立表 **方案 A:用 `sxo_order.extension_data`** ```json { "item_type": "ticket", "event_id": 8, "session_id": 15, "tickets": [ { "attendee_id": 101, "ticket_code": "uuid-xxx", "seat": "A区-3排-15座", "verify_status": 0, "verify_time": null }, { "attendee_id": 102, "ticket_code": "uuid-yyy", "seat": "A区-3排-16座", "verify_status": 0, "verify_time": null } ] } ``` 优点:无需新建表,查询方便 缺点:JSON 更新需要整体替换 **方案 B:新建 `vr_tickets` 表**(推荐) ```sql CREATE TABLE `vr_tickets` ( `id` int UNSIGNED PRIMARY KEY AUTO_INCREMENT, `order_id` int UNSIGNED NOT NULL COMMENT '订单ID', `order_no` char(60) NOT NULL COMMENT '订单号', `goods_id` int UNSIGNED NOT NULL COMMENT '商品ID', `user_id` int UNSIGNED NOT NULL COMMENT '用户ID', `event_id` int UNSIGNED NOT NULL COMMENT '活动ID', `session_id` int UNSIGNED NOT NULL COMMENT '场次ID', `ticket_code` char(36) NOT NULL COMMENT '票码(UUID)', `qr_data` text COMMENT '加密QR内容', `seat_info` varchar(255) COMMENT '座位信息', `real_name` varchar(60) COMMENT '观演人姓名', `id_card` char(20) COMMENT '身份证号', `phone` char(15) COMMENT '手机号', `verify_status` tinyint DEFAULT 0 COMMENT '核销状态(0未核销, 1已核销)', `verify_time` int UNSIGNED DEFAULT 0 COMMENT '核销时间', `verifier_id` int UNSIGNED DEFAULT 0 COMMENT '核销员ID', `issued_at` int UNSIGNED DEFAULT 0 COMMENT '发放时间', `created_at` int UNSIGNED DEFAULT 0, `updated_at` int UNSIGNED DEFAULT 0, UNIQUE KEY `ticket_code` (`ticket_code`), KEY `order_id` (`order_id`), KEY `user_id` (`user_id`), KEY `event_id` (`event_id`), KEY `session_id` (`session_id`), KEY `verify_status` (`verify_status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='VR电子票表'; ``` ### 3.2 核销记录表 ```sql CREATE TABLE `vr_verifications` ( `id` int UNSIGNED PRIMARY KEY AUTO_INCREMENT, `ticket_id` int UNSIGNED NOT NULL COMMENT '票ID', `ticket_code` char(36) NOT NULL COMMENT '票码', `verifier_id` int UNSIGNED NOT NULL COMMENT '核销员ID', `verifier_name` varchar(60) COMMENT '核销员名称', `event_id` int UNSIGNED COMMENT '活动ID', `session_id` int UNSIGNED COMMENT '场次ID', `ip_address` varchar(45) COMMENT '核销IP', `location` varchar(255) COMMENT '核销地点备注', `created_at` int UNSIGNED DEFAULT 0, KEY `ticket_id` (`ticket_id`), KEY `verifier_id` (`verifier_id`), KEY `created_at` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='核销记录表'; ``` ### 3.3 核销员表 ```sql CREATE TABLE `vr_verifiers` ( `id` int UNSIGNED PRIMARY KEY AUTO_INCREMENT, `user_id` int UNSIGNED NOT NULL COMMENT '关联用户ID(ShopXO user.id)', `name` varchar(60) NOT NULL COMMENT '核销员名称', `mobile` char(15) COMMENT '手机号', `event_ids` varchar(255) COMMENT '可核销的活动ID列表(逗号分隔)', `status` tinyint DEFAULT 1 COMMENT '状态(0禁用, 1启用)', `created_at` int UNSIGNED DEFAULT 0, KEY `user_id` (`user_id`), KEY `status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='核销员表'; ``` --- ## 四、B 端核销页面 ### 4.1 参考实现 **直接 fork** `pages/plugins/realstore/check/check.vue` ### 4.2 页面 UI 设计 ```vue ``` ### 4.3 API 调用 ```javascript verify_submit() { if (!this.check_value) return; uni.showLoading({ title: '核销中...' }); this.verify_loading = true; uni.request({ url: app.globalData.get_request_url('verify', 'ticket', 'vrticket'), method: 'POST', data: { ticket_code: this.check_value, event_id: this.current_event?.id || 0, }, success: (res) => { uni.hideLoading(); if (res.data.code == 0) { this.result_type = 'success'; this.result = res.data.data; this.stats.today++; this.stats.pending--; } else { this.result_type = 'error'; this.error_msg = res.data.msg; } // 清空输入框,支持连续扫描 this.check_value = ''; }, fail: () => { uni.hideLoading(); this.result_type = 'error'; this.error_msg = '网络错误,请重试'; }, complete: () => { this.verify_loading = false; } }); } ``` --- ## 五、后端核销 API ### 5.1 接口定义 ``` POST /?s=admin/vrticket/verify Content-Type: application/json { "ticket_code": "uuid-xxx", // 票码 "event_id": 8 // 活动ID(可选) } ``` ### 5.2 返回格式 **成功**: ```json { "code": 0, "msg": "核销成功", "data": { "ticket_id": 101, "ticket_code": "uuid-xxx", "event_name": "周杰伦2026巡回演唱会", "session_time": "2026-06-01 20:00", "seat_info": "A区-3排-15座", "real_name": "张三", "verify_time": 1745328000 } } ``` **失败**: ```json { "code": -1, "msg": "该票已核销" } ``` ### 5.3 核销逻辑实现 ```php // app/plugins/vr_ticket/service/TicketService.php public static function VerifyTicket($ticket_code, $verifier_id, $event_id = 0) { // 1. 查询票 $ticket = Db::name('vr_tickets') ->where('ticket_code', $ticket_code) ->find(); if (!$ticket) { return DataReturn('票码不存在', -1); } // 2. 检查活动匹配 if ($event_id > 0 && $ticket['event_id'] != $event_id) { return DataReturn('该票不属于当前活动', -1); } // 3. 检查是否已核销 if ($ticket['verify_status'] == 1) { return DataReturn('该票已核销(' . date('Y-m-d H:i', $ticket['verify_time']) . ')', -1); } // 4. 解密验证(如果加密了) if (!empty($ticket['qr_data'])) { $decrypted = openssl_decrypt( base64_decode($ticket['qr_data']), 'AES-256-CBC', MyC('vrticket_secret_key'), OPENSSL_RAW_DATA, substr(md5($ticket['ticket_code']), 0, 16) ); $qr_content = json_decode($decrypted, true); // 检查过期 if (!empty($qr_content['exp']) && $qr_content['exp'] < time()) { return DataReturn('票已过期', -1); } } // 5. 执行核销(事务) Db::startTrans(); try { // 更新票状态 Db::name('vr_tickets')->where('id', $ticket['id'])->update([ 'verify_status' => 1, 'verify_time' => time(), 'verifier_id' => $verifier_id, 'updated_at' => time(), ]); // 写入核销记录 Db::name('vr_verifications')->insert([ 'ticket_id' => $ticket['id'], 'ticket_code' => $ticket_code, 'verifier_id' => $verifier_id, 'verifier_name' => self::GetVerifierName($verifier_id), 'event_id' => $ticket['event_id'], 'session_id' => $ticket['session_id'], 'created_at' => time(), ]); Db::commit(); } catch (\Exception $e) { Db::rollback(); return DataReturn('核销失败:' . $e->getMessage(), -1); } // 6. 返回票信息 $ticket['verify_status'] = 1; $ticket['verify_time'] = time(); return DataReturn('核销成功', 0, self::FormatTicketInfo($ticket)); } ``` --- ## 六、C 端票夹(用户查看已购票) ### 6.1 页面入口 通过用户中心钩子注入:`plugins_view_user_various_inside_top` ### 6.2 页面内容 显示用户所有已支付订单中的票务商品,每张票一行: ``` ┌─────────────────────────────────────┐ │ 🎵 周杰伦2026巡回演唱会 │ │ 📅 2026-06-01 20:00 │ │ 📍 国家体育馆 │ │ 💺 A区-3排-15座 │ │ │ │ ┌─────────┐ 状态: │ │ │ QR CODE │ ✅ 已核销 / ⏳ 待使用 │ │ └─────────┘ │ └─────────────────────────────────────┘ ``` ### 6.3 核销状态实时更新(可选) 如果需要实时更新核销状态: - 方案 A(推荐):用户进入票夹时刷新状态 - 方案 B:WebSocket 推送(ShopXO 无内置 WebSocket) - 方案 C:页面可见时通过 `onShow` 刷新 --- ## 七、部署形态 ### 7.1 B 端核销页面部署 | 形态 | 说明 | 推荐度 | |---|---|---| | **uni-app B 端插件页** | `pages/plugins/vr-ticket-verify/` | ✅ 最佳 | | **ShopXO H5 管理后台** | `/admin/ticket-verify/` | ⚠️ 个人小程序无法内嵌 | | **独立微信小程序** | 单独注册 B 端小程序 | ⚠️ 需额外资质 | ### 7.2 个人主体小程序限制 - ❌ 禁止 web-view(无法内嵌 H5) - ✅ 可以新建另一个小程序(独立 AppID) - ✅ 可以用 `uni.scanCode` 做纯小程序核销页 ### 7.3 推荐方案 **演唱会现场**:用 ShopXO 的 uni-app 新建 `vr-ticket-verify` 插件页面。工作人员用自己的微信账号(授权为核销员)登录 ShopXO 小程序,访问核销页面。 **部署**: 1. 把 `vr-ticket-verify` 作为 ShopXO uni-app 的插件发布 2. 工作人员在小程序中访问该页面 3. 手机对着票 QR 码扫描 --- ## 八、核销统计 ### 8.1 实时统计 API ``` GET /?s=admin/vrticket/stats&event_id=8 ``` 返回: ```json { "code": 0, "data": { "total_tickets": 500, "verified": 320, "pending": 180, "today": 45 } } ``` ### 8.2 核销大屏 可在演唱会入口放置一个大屏电视,显示实时核销进度: - 总票数 / 已核销数 / 待核销数 - 每分钟核销速度 - 最近核销记录滚动列表 实现方式:轮询 stats API + 数字动画(CountUp.js)