# ShopXO VR票务插件 — 架构文档

> 版本：v2.2（2026-04-14 更新，Q4 spec 绑定方案已确认 — $vr- 前缀隔离）
> 源码位置：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` 字段 → 加载对应自定义组件

---

### Q4 结论（spec 绑定方案 — 已确认）

ShopXO spec 是模板级复制模式，用 `$vr-` 前缀做命名空间隔离：

```
插件初始化时：
  → 在 sxo_goods_spec_template 创建 "$vr-场馆" 模板
  → 商家在商品里应用此模板，spec_value COPY 到商品 spec_type

插件商品加载时：
  → 遍历 sxo_goods_spec_type
  → 识别 name 以 "$vr-" 开头的类型
  → 按 spec_value.name 去 vr_venues 表查 seat_map
  → 写入 extension_data
```

**这个方案的优点：**
- 用户自己添加的普通规格永远不会冲突（没有 `$vr-` 前缀）
- 插件完全掌控自己的 spec 命名空间
- 模板可批量导入，不需要每个商品手动配置

---

## 待确认问题（pending-council）

详见 `docs/ALIGNMENT.md` Section 二。

| 问题 | 说明 |
|---|---|
| Q1 | 座位模板与分类的绑定粒度（一个分类 = 一个座位区，还是一个商品可绑定多模板？） |
| Q2 | spec_base_id_map 生成时机（每个 spec 独立座位，还是所有 spec 共用座位配置？） |
| Q3 | 观演人信息存储位置（extension_data / vr_tickets / 作为 spec 选项） |
| Q4 | spec_value 复用粒度 — 全局可复用 vs 商品级私有？ | ✅ **已解决** — `$vr-` 前缀隔离方案（见上方 Q4 结论） |

---

## 技术栈

- **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 文档现状） |
