400 lines
16 KiB
Markdown
400 lines
16 KiB
Markdown
# ShopXO VR票务插件 — 架构文档
|
||
|
||
> 版本:v2.1(2026-04-14 更新,新增通用扩展方法论 + Q4 spec_value 复用粒度)
|
||
> 源码位置:council-research/shopxo-eval/.worktrees/shopxo-evaluator/shopxo-src/
|
||
|
||
## 项目概述
|
||
|
||
基于 ShopXO 生态的 VR 演唱会票务插件(Plan B)。
|
||
|
||
当 vr-ticket-mp 主线项目因维护成本或架构限制无法继续时,此插件作为 Plan B:
|
||
- **完全复用** ShopXO 已有能力(会员体系/积分/优惠券/微信支付)
|
||
- **仅扩展** 票务专属逻辑(座位/观演人/QR核销)
|
||
- **优先通过插件机制扩展**,如插件机制不够用,允许直接修改 ShopXO 源码的最小范围(MIT 协议允许)。原则:改源码比绕弯快时,直接改;以进度为先,不为「不修改」而引入额外复杂度。
|
||
|
||
---
|
||
|
||
## 核心设计决策(v2.0 重大更新)
|
||
|
||
### 1. spec = 场次 ✅ 已确认
|
||
|
||
**ShopXO 的每个 spec 选项 = 一个演出场次**
|
||
|
||
- 不需要独立的 `vr_sessions` 表
|
||
- 商品的多个 spec 值(如"2026-06-01 19:30"、"2026-06-02 14:00")= 多个场次
|
||
- 用户在前端选择场次(spec)→ 再选择座位 → 提交购买
|
||
|
||
### 2. 座位模板绑定分类 ✅ 已确认
|
||
|
||
座位模板 `vr_seat_templates` 通过分类 ID 绑定到商品:
|
||
- 后台创建座位模板 → 选择绑定分类(如"VR演唱会-A区")
|
||
- 创建商品 → 选择该分类
|
||
- 插件自动将模板 seat_map 写入商品 `venue_data`
|
||
|
||
### 3. venue_data 仅存座位配置 ✅ 已确认
|
||
|
||
`sxo_goods.venue_data` 精简为:
|
||
```json
|
||
{
|
||
"seat_map": { /* 座位图配置(来自 vr_seat_templates)*/ },
|
||
"spec_base_id_map": { /* seat_id → spec_base_id */ }
|
||
}
|
||
```
|
||
sessions 信息由 ShopXO spec 系统提供,不再存储在 venue_data 中。
|
||
|
||
### 4. 票务商品识别:venue_data 非空 ✅ 已确认
|
||
|
||
- 不新增 `item_type` 字段
|
||
- 判断逻辑:`!empty($goods['venue_data'])` → 票务商品
|
||
- 前端 Vue:判断 `goods.venue_data` 是否有值
|
||
|
||
### 5. 完全通过 Hook 实现,不改 Goods.php ✅ 已确认
|
||
|
||
- `plugins_view_goods_detail_base_sku_top` 注入票务选座 UI
|
||
- 完全覆盖原有规格选择区
|
||
- Goods.php 修改方案降级为备案(如 Hook 注入范围不够时启用)
|
||
|
||
---
|
||
|
||
## 核心技术发现(2026-04-14 调研)
|
||
|
||
### 1. CustomView Ace 编辑器 ⭐
|
||
|
||
ShopXO 内置全代码自定义页面编辑器,HTML/CSS/JS 三栏,实时预览。
|
||
文件:`app/admin/view/default/customview/saveinfo.html`
|
||
访问:后台 → 营销 → 自定义页面管理
|
||
|
||
### 2. 商品详情页 30+ 钩子 ⭐
|
||
|
||
最佳注入点:`plugins_view_goods_detail_base_sku_top`(规格选择区顶部)
|
||
- 完全注入票务选座 UI
|
||
- 不修改核心代码
|
||
|
||
### 3. 票务商品识别方案(v2.0 更新)⭐
|
||
|
||
> **已废弃 Goods.php 修改方案**:完全通过 Hook + 前端判断实现
|
||
> - `plugins_view_goods_detail_base_sku_top` 注入票务选座 UI
|
||
> - 前端 Vue 判断 `goods.venue_data` 是否有值 → 有值则加载票务专用前端
|
||
> - **Goods.php 不再需要修改**
|
||
|
||
### 4. shopxo-uniapp 支持微信小程序 ⭐
|
||
|
||
HBuilderX 导入 → 配置 AppID → 发行 → 微信开发者工具
|
||
条件编译指令已配置(`#ifdef MP-WEIXIN`)
|
||
|
||
### 5. QR 码生成内置 ⭐
|
||
|
||
使用 `\base\Qrcode` 类 + phpqrcode 库:
|
||
```php
|
||
$qr_url = MyUrl('index/qrcode/index', ['content' => base64_encode($data)]);
|
||
```
|
||
|
||
### 6. 自提点核销页面可直接参考 ⭐
|
||
|
||
`pages/plugins/realstore/check/check.vue` — 完整 B 端核销 UI
|
||
- uni.scanCode 扫码
|
||
- 手动输入兼容
|
||
- 成功/失败状态展示
|
||
|
||
### 7. 订单 extension_data 存票务信息 ⭐
|
||
|
||
`sxo_order.extension_data` — JSON 扩展字段,可存核销状态
|
||
|
||
---
|
||
|
||
## 整体架构
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────┐
|
||
│ ShopXO PHP 后端 │
|
||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
|
||
│ │ 会员/积分 │ │ 商品/订单 │ │ 微信支付 │ │
|
||
│ │ coupons │ │ items/orders│ │ payment │ │
|
||
│ └──────────────┘ └──────────────┘ └──────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────┐ │
|
||
│ │ vr_ticket 插件 │ │
|
||
│ │ ┌────────────────────────────────────────┐ │ │
|
||
│ │ │ SeatTemplateService — 座位模板管理 │ │ │
|
||
│ │ │ TicketService — QR票生成/发放 │ │ │
|
||
│ │ │ VerifyService — 核销验证 │ │ │
|
||
│ │ └────────────────────────────────────────┘ │ │
|
||
│ │ ┌────────────────────────────────────────┐ │ │
|
||
│ │ │ 钩子: plugins_view_goods_detail_* │ │ │
|
||
│ │ │ 钩子: plugins_service_buy_order_* │ │ │
|
||
│ │ └────────────────────────────────────────┘ │ │
|
||
│ └─────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────┐
|
||
│ shopxo-uniapp │
|
||
│ ┌──────────────┐ ┌──────────────────┐ │
|
||
│ │ 商品详情页 │ │ 票务选座页 │ ← Fork │
|
||
│ │goods-detail.vue│ │ ticket-buy.vue │ 自定义 │
|
||
│ └──────────────┘ └──────────────────┘ │
|
||
│ ┌──────────────┐ ┌──────────────────┐ │
|
||
│ │ 用户中心 │ │ 票夹页 │ ← 新建 │
|
||
│ │ user.vue │ │ ticket-wallet.vue│ │
|
||
│ └──────────────┘ └──────────────────┘ │
|
||
│ ┌──────────────┐ │
|
||
│ │ 核销页 │ ← Fork realstore/check.vue │
|
||
│ │ verify.vue │ │
|
||
│ └──────────────┘ │
|
||
└─────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 数据模型
|
||
|
||
### ShopXO 复用表
|
||
|
||
| 表 | 用途 |
|
||
|---|---|
|
||
| `sxo_user` | 会员 |
|
||
| `sxo_wallet` | 余额钱包 |
|
||
| `sxo_integral` | 积分 |
|
||
| `sxo_coupon` | 优惠券 |
|
||
| `sxo_order` | 订单 |
|
||
| `sxo_order_address` | 地址(含身份证) |
|
||
| `sxo_payment` | 支付配置 |
|
||
| `sxo_goods` | 商品(含票务商品,venue_data 存座位配置) |
|
||
| `sxo_goods_spec_base` | SKU(每个座位 = inventory=1 的 SKU) |
|
||
|
||
### 插件独立表(v2.0 精简版)
|
||
|
||
| 表 | 用途 |
|
||
|---|---|
|
||
| `vr_seat_templates` | 座位模板(seat_map JSON + 绑定分类ID) |
|
||
| `vr_tickets` | 电子票(含QR数据 + 观演人) |
|
||
| `vr_verifiers` | 核销员 |
|
||
| `vr_verifications` | 核销记录 |
|
||
|
||
> **v2.0 变更**:删除 `vr_events`、`vr_sessions`、`vr_venues` 表,座位配置由 `vr_seat_templates` 通过分类绑定实现。
|
||
|
||
### vr_seat_templates 表结构
|
||
|
||
```sql
|
||
CREATE TABLE vr_seat_templates (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
name VARCHAR(180) NOT NULL COMMENT '模板名称(如:鸟巢-A区)',
|
||
category_id BIGINT UNSIGNED NOT NULL COMMENT '绑定的 ShopXO 分类ID',
|
||
seat_map LONGTEXT NOT NULL COMMENT '座位地图JSON',
|
||
status TINYINT UNSIGNED DEFAULT 1,
|
||
add_time INT UNSIGNED DEFAULT 0,
|
||
upd_time INT UNSIGNED DEFAULT 0,
|
||
UNIQUE KEY category_id (category_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='VR演唱会座位模板';
|
||
```
|
||
|
||
### vr_tickets 表结构
|
||
|
||
```sql
|
||
CREATE TABLE vr_tickets (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单ID',
|
||
order_no CHAR(60) NOT NULL,
|
||
goods_id BIGINT UNSIGNED NOT NULL,
|
||
user_id BIGINT UNSIGNED NOT NULL,
|
||
ticket_code CHAR(36) NOT NULL COMMENT 'UUID票码',
|
||
qr_data TEXT COMMENT '加密QR内容',
|
||
seat_info VARCHAR(255) COMMENT '座位信息(如 A区-3排-5座)',
|
||
real_name VARCHAR(60) COMMENT '观演人姓名',
|
||
phone CHAR(15) COMMENT '手机号',
|
||
id_card CHAR(18) COMMENT '身份证号',
|
||
verify_status TINYINT DEFAULT 0 COMMENT '0未核销, 1已核销',
|
||
verify_time INT UNSIGNED DEFAULT 0,
|
||
verifier_id BIGINT UNSIGNED DEFAULT 0,
|
||
issued_at INT UNSIGNED DEFAULT 0,
|
||
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 verify_status (verify_status)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='VR演唱会电子票';
|
||
```
|
||
|
||
---
|
||
|
||
## venue_data JSON 结构(v2.0 精简版)
|
||
|
||
```json
|
||
{
|
||
"seat_map": {
|
||
"map": ["aaaaaaaaaaaa", "aaaaaaaaaaaa", "bbbbbb__bb", "bbbbbbbbbbbb"],
|
||
"row_labels": ["A", "B", "C", "D"],
|
||
"seats": {
|
||
"a": { "price": 599, "label": "VIP区", "classes": "seat-vip" },
|
||
"b": { "price": 399, "label": "普通区", "classes": "seat-normal" },
|
||
"_": null
|
||
},
|
||
"sections": [
|
||
{ "name": "VIP区", "color": "#FF6B6B", "rows": [0, 1] },
|
||
{ "name": "普通区", "color": "#4ECDC4", "rows": [2, 3] }
|
||
]
|
||
},
|
||
"spec_base_id_map": {
|
||
"1_1": { "spec_base_id": 10001, "row": "A", "col": 1, "seat_type": "a", "price": 599 },
|
||
"1_2": { "spec_base_id": 10002, "row": "A", "col": 2, "seat_type": "a", "price": 599 },
|
||
"3_5": { "spec_base_id": 10003, "row": "C", "col": 5, "seat_type": "b", "price": 399 }
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 插件目录结构
|
||
|
||
```
|
||
app/plugins/vr_ticket/
|
||
├── plugin.json # 插件声明
|
||
├── service/
|
||
│ ├── BaseService.php # 基础配置
|
||
│ ├── SeatTemplateService.php # 座位模板管理(替代 vr_sessions/vr_venues)
|
||
│ ├── TicketService.php # 票生成/发放
|
||
│ └── VerifyService.php # 核销逻辑
|
||
├── view/
|
||
│ └── Goods.php # 商品详情页钩子(备用,不修改核心)
|
||
├── Admin/
|
||
│ ├── Controller/
|
||
│ │ ├── SeatTemplateController.php # 座位模板管理
|
||
│ │ └── TicketController.php # 票务管理
|
||
│ └── View/
|
||
│ ├── seat_template_list.html
|
||
│ ├── seat_template_save.html
|
||
│ ├── ticket_list.html
|
||
│ └── verification_list.html
|
||
├── Api/
|
||
│ └── Controller/
|
||
│ ├── SeatTemplateController.php # 座位模板API
|
||
│ └── TicketController.php # 票/核销API
|
||
└── EventListener.php # ShopXO事件监听
|
||
|
||
database/migrations/ # 数据库迁移
|
||
static/vr_ticket/ # 静态资源
|
||
```
|
||
|
||
---
|
||
|
||
## 完整购票流程
|
||
|
||
```
|
||
1. 商家后台
|
||
→ VR票务插件 → 座位模板管理(创建 seat_map → 绑定分类)
|
||
→ 商品管理 → 创建商品 → 选择绑定分类(自动继承 venue_data)
|
||
→ 商品规格管理 → 添加规格(每个规格 = 一个场次,如"2026-06-01 晚场")
|
||
|
||
2. 用户前端
|
||
→ 首页浏览 → 进入商品详情页
|
||
→ 判断 goods.venue_data 是否有值
|
||
→ 跳转到票务选座页(pages/ticket-buy)
|
||
→ 选择场次(spec)→ 选择座位 → 填写观演人信息
|
||
→ 提交订单 → 微信支付
|
||
|
||
3. 支付成功
|
||
→ ShopXO 支付回调
|
||
→ TicketService::OnOrderPaid() 触发
|
||
→ 生成 QR 票 → 存入 vr_tickets 表
|
||
→ 用户可在票夹页查看
|
||
|
||
4. 核销(独立页面)
|
||
→ 工作人员打开核销页(pages/plugins/vr-ticket-verify)
|
||
→ 扫描用户 QR 码
|
||
→ POST /api/ticket/verify
|
||
→ VerifyService 更新核销状态
|
||
→ 返回核销结果
|
||
```
|
||
|
||
---
|
||
|
||
## 与 vr-ticket-mp 对比
|
||
|
||
| 维度 | vr-ticket-mp(主线) | vr-shopxo-plugin(Plan B) |
|
||
|---|---|---|
|
||
| **后端** | Go + Gin(自建) | PHP + ThinkPHP(ShopXO) |
|
||
| **数据库** | Supabase Postgres | ShopXO MySQL |
|
||
| **前端** | uni-app(自建) | shopxo-uniapp(已有) |
|
||
| **会员体系** | Supabase Auth | ShopXO 内置 |
|
||
| **积分/优惠券** | 自建 | ShopXO 内置 |
|
||
| **微信支付** | 自建 | ShopXO 内置 |
|
||
| **QR 核销** | 自建 | 基于 ShopXO 已有机制 |
|
||
| **场次管理** | vr_sessions 独立表 | ShopXO spec(已确认) |
|
||
| **座位配置** | venue_data 完整配置 | vr_seat_templates 绑定分类(已确认) |
|
||
| **部署** | Docker | 虚拟主机即可 |
|
||
| **AI 参与度** | 80% | 85-90% |
|
||
| **维护成本** | 高 | 低 |
|
||
|
||
---
|
||
|
||
## 通用扩展方法论(v2.1 新增)
|
||
|
||
> 已确认的通用插件扩展模式,可套用到票务/实物/套票等所有自定义类型商品。
|
||
|
||
```
|
||
Plugin Table (vr_xxx)
|
||
↑
|
||
│ 绑定 spec_value (全局可复用)
|
||
│
|
||
sxo_goods.extension_data (JSON)
|
||
↑
|
||
│ 插件在商品加载时注入
|
||
│
|
||
Frontend — 读 extension_data
|
||
→ 判断 type(票务/实物/套票)
|
||
→ 渲染对应自定义组件
|
||
```
|
||
|
||
### 核心链路
|
||
|
||
1. **插件数据表**(`vr_xxx`)存储自定义业务数据
|
||
2. **绑定 spec_value**:插件 spec_value 关联插件数据(spec_value 全局可复用)
|
||
3. **写入 `goods.extension_data`**:商品保存时,插件将 spec_value 映射写入 ShopXO 扩展字段
|
||
4. **前端按 type 渲染**:前端读取 `extension_data` → 判断 `type` 字段 → 加载对应自定义组件
|
||
|
||
---
|
||
|
||
## 待确认问题(pending-council)
|
||
|
||
详见 `docs/ALIGNMENT.md` Section 二。
|
||
|
||
| 问题 | 说明 |
|
||
|---|---|
|
||
| Q1 | 座位模板与分类的绑定粒度(一个分类 = 一个座位区,还是一个商品可绑定多模板?) |
|
||
| Q2 | spec_base_id_map 生成时机(每个 spec 独立座位,还是所有 spec 共用座位配置?) |
|
||
| Q3 | 观演人信息存储位置(extension_data / vr_tickets / 作为 spec 选项) |
|
||
| Q4 | spec_value 复用粒度 — 全局可复用 vs 商品级私有?(见上方通用扩展方法论) |
|
||
|
||
---
|
||
|
||
## 技术栈
|
||
|
||
- **PHP 8+** / ThinkPHP 8(ShopXO 生态)
|
||
- **MySQL 5.7+**(ShopXO 同一实例)
|
||
- **uni-app**(shopxo-uniapp,已支持微信小程序)
|
||
- **phpqrcode**(`extend/qrcode/phpqrcode.php`,内置)
|
||
- **uni.scanCode**(微信/支付宝等小程序扫码)
|
||
|
||
---
|
||
|
||
## 官方文档(开发前必查)
|
||
|
||
- 官方文档站:https://doc.shopxo.net/
|
||
- 插件开发文档:https://doc.shopxo.net/article/3.html
|
||
- 开发文档索引:https://doc.shopxo.net/article/4.html
|
||
- uniapp 打包教程:https://doc.shopxo.net/article/1/293727233598554112.html
|
||
- 完整索引:[docs/OFFICIAL_DOCS.md](docs/OFFICIAL_DOCS.md)
|
||
|
||
## 开发文档
|
||
|
||
| 文档 | 内容 |
|
||
|---|---|
|
||
| [docs/00_OVERVIEW.md](docs/00_OVERVIEW.md) | 项目总览 |
|
||
| [docs/01_SHOPXO_TECHNICAL_RESEARCH.md](docs/01_SHOPXO_TECHNICAL_RESEARCH.md) | ShopXO 技术能力完整调研 |
|
||
| [docs/02_FRONTEND_CUSTOMIZATION.md](docs/02_FRONTEND_CUSTOMIZATION.md) | shopxo-uniapp 编译与自定义 |
|
||
| [docs/03_VERIFICATION_SYSTEM.md](docs/03_VERIFICATION_SYSTEM.md) | 核销系统设计 |
|
||
| [docs/04_IMPLEMENTATION_ROADMAP.md](docs/04_IMPLEMENTATION_ROADMAP.md) | 实施路线图与 Agent 分工 |
|
||
| [docs/06_SEAT_MAP_INTEGRATION.md](docs/06_SEAT_MAP_INTEGRATION.md) | 选座系统 + 座位地图(v2.0 已精简) |
|
||
| [docs/ALIGNMENT.md](docs/ALIGNMENT.md) | 架构对齐文档(用户需求 vs 文档现状) |
|