545 lines
15 KiB
Markdown
545 lines
15 KiB
Markdown
|
|
# 核销系统设计
|
|||
|
|
|
|||
|
|
> 调研时间: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
|
|||
|
|
<template>
|
|||
|
|
<view class="page padding-main">
|
|||
|
|
<!-- 标题栏 -->
|
|||
|
|
<view class="verify-header bg-white padding radius">
|
|||
|
|
<view class="fw-b text-size-lg">VR演唱会票务核销</view>
|
|||
|
|
<view class="cr-grey text-size-sm margin-top-xs">
|
|||
|
|
当前活动:{{ current_event?.name || '全部活动' }}
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 扫码/输入区 -->
|
|||
|
|
<view class="verify-input bg-white padding radius margin-top">
|
|||
|
|
<view class="flex-row align-c">
|
|||
|
|
<!-- #ifndef H5 -->
|
|||
|
|
<view class="scan-btn" @tap="scan_event">
|
|||
|
|
<uni-icons type="scan" size="56rpx" color="#2196F3"></uni-icons>
|
|||
|
|
</view>
|
|||
|
|
<!-- #endif -->
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
class="flex-1 margin-left"
|
|||
|
|
placeholder="扫描二维码或输入核销码"
|
|||
|
|
v-model="check_value"
|
|||
|
|
@confirm="verify_submit"
|
|||
|
|
/>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 提交按钮 -->
|
|||
|
|
<view class="padding margin-top">
|
|||
|
|
<button
|
|||
|
|
type="primary"
|
|||
|
|
:loading="verify_loading"
|
|||
|
|
:disabled="!check_value || verify_loading"
|
|||
|
|
@tap="verify_submit"
|
|||
|
|
>{{ verify_loading ? '核销中...' : '确认核销' }}</button>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 结果展示 -->
|
|||
|
|
<view class="result-area padding margin-top">
|
|||
|
|
<!-- 成功 -->
|
|||
|
|
<view v-if="result_type === 'success'" class="success-box">
|
|||
|
|
<uni-icons type="checkmark-filled" size="60" color="#4CAF50"></uni-icons>
|
|||
|
|
<view class="fw-b text-size-xl margin-top">核销成功</view>
|
|||
|
|
<view class="margin-top">
|
|||
|
|
<view>活动:{{ result.event_name }}</view>
|
|||
|
|
<view>场次:{{ result.session_time }}</view>
|
|||
|
|
<view>座位:{{ result.seat_info }}</view>
|
|||
|
|
<view>观演人:{{ result.real_name }}</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 失败 -->
|
|||
|
|
<view v-else-if="result_type === 'error'" class="error-box">
|
|||
|
|
<uni-icons type="close-filled" size="60" color="#F44336"></uni-icons>
|
|||
|
|
<view class="fw-b text-size-xl margin-top">核销失败</view>
|
|||
|
|
<view class="cr-red margin-top">{{ error_msg }}</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 统计栏 -->
|
|||
|
|
<view class="stats-bar fixed-bottom padding">
|
|||
|
|
<view class="flex-row jc-sb">
|
|||
|
|
<view class="stat-item">
|
|||
|
|
<view class="cr-grey text-size-xs">今日核销</view>
|
|||
|
|
<view class="fw-b text-size-xl cr-green">{{ stats.today }}</view>
|
|||
|
|
</view>
|
|||
|
|
<view class="stat-item">
|
|||
|
|
<view class="cr-grey text-size-xs">待核销</view>
|
|||
|
|
<view class="fw-b text-size-xl cr-yellow">{{ stats.pending }}</view>
|
|||
|
|
</view>
|
|||
|
|
<view class="stat-item">
|
|||
|
|
<view class="cr-grey text-size-xs">已核销</view>
|
|||
|
|
<view class="fw-b text-size-xl">{{ stats.verified }}</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 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)
|