feat: 完成 ShopXO 深度调研文档包

- docs/00_OVERVIEW: 项目总览与双线策略
- docs/01_SHOPXO_TECHNICAL_RESEARCH: DIY/CustomView/30+钩子/插件机制完整分析
- docs/02_FRONTEND_CUSTOMIZATION: shopxo-uniapp 编译到微信小程序+自定义组件
- docs/03_VERIFICATION_SYSTEM: 核销系统设计(QR生成/票夹/B端核销页)
- docs/04_IMPLEMENTATION_ROADMAP: Agent 分工与开发计划
- 更新 ARCHITECTURE.md (v1.1) 整合调研成果
- 更新 README.md
council/backend-reviewer
sileya-ai 2026-04-14 12:01:37 +08:00
parent 0148c32091
commit c8de7d3bff
7 changed files with 2349 additions and 242 deletions

View File

@ -1,273 +1,240 @@
# ShopXO VR票务插件 — 架构文档
> 版本v1.12026-04-14 更新,整合 ShopXO 技术调研成果)
> 源码位置council-research/shopxo-eval/.worktrees/shopxo-evaluator/shopxo-src/
## 项目概述
基于 ShopXO 生态的 VR 演唱会票务插件Plan B
当 vr-ticket-mp 主线项目因维护成本或架构限制无法继续时,此插件作为 Plan B
- **完全复用** ShopXO 已有能力(会员体系/积分/优惠券/微信支付)
- **仅扩展** 票务专属逻辑(场次/观演人/QR核销
- **仅扩展** 票务专属逻辑(场次/座位/观演人/QR核销
- **不修改** ShopXO 核心代码,通过插件机制隔离
### 目标用户
朋友的合作伙伴:需要一个可上线、后期可 AI 改动的轻量商城小程序。
---
## 核心技术发现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. 按商品类型替换模板 ⭐
修改 `Goods.php Index()` 加 1 行判断:
```php
if($goods['item_type'] == 'ticket') {
return MyView('/goods/ticket_detail');
}
```
### 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 商品类型的扩展:
## 整体架构
```
ShopXO items 表
└── item_type = 'ticket' 时,启用插件逻辑
├── 关联 vr_events场次表
├── 关联 vr_sessions场次时间
└── 关联 vr_attendees观演人
ShopXO orders 表
└── 订单项 item_type='ticket' → 插件处理履约
└── 触发 vr_tickets 表写入 → 生成 QR 票
┌─────────────────────────────────────────────────────┐
│ ShopXO PHP 后端 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ 会员/积分 │ │ 商品/订单 │ │ 微信支付 │ │
│ │ coupons │ │ items/orders│ │ payment │ │
│ └──────────────┘ └──────────────┘ └──────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ vr_ticket 插件 │ │
│ │ ┌────────────────────────────────────────┐ │ │
│ │ │ EventService — 场次管理 │ │ │
│ │ │ 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 的边界
| 能力 | 来源 | 说明 |
|------|------|------|
| 商品目录/上下架 | ShopXO | 复用 items 表 |
| 会员体系/充值/积分 | ShopXO | 复用 user/wallet/integral 表 |
| 优惠券 | ShopXO | 复用 coupons 表 |
| 微信支付 | ShopXO | 复用 payment 表 + 回调 |
| 场次/日期管理 | **插件** | 独立表 vr_events/sessions |
| 观演人信息 | **插件** | 独立表 vr_attendees |
| QR 电子票 | **插件** | 独立表 vr_tickets |
| 核销组件 | **插件** | 独立 B 端页面 |
---
## 数据库设计
## 数据模型
### ShopXO 复用表
| 表 | 用途 |
|---|---|
| `sxo_user` | 会员 |
| `sxo_wallet` | 余额钱包 |
| `sxo_integral` | 积分 |
| `sxo_coupon` | 优惠券 |
| `sxo_order` | 订单 |
| `sxo_order_address` | 地址(含身份证) |
| `sxo_payment` | 支付配置 |
| `sxo_goods` | 商品(含票务商品) |
| `sxo_goods_spec_base` | SKU座位=库存1 |
### 插件独立表
```sql
-- 场次表
vr_events (
id, goods_id, -- 关联 ShopXO items.id
name, -- "周杰伦2026巡回演唱会"
venue, -- 场馆
cover_image,
status,
created_at
)
-- 场次时间表
vr_sessions (
id, event_id,
session_time, -- 2026-06-01 20:00
total_stock, -- 总库存
available_stock, -- 可用库存(扣减用 FOR UPDATE SKIP LOCKED
price,
status,
created_at
)
-- 观演人表
vr_attendees (
id, order_id, order_item_id,
session_id,
real_name,
id_card, -- 可选
phone,
ticket_code, -- UUID唯一
qr_data, -- 加密后的 QR 内容
status, -- pending/issued/used/cancelled
created_at
)
-- QR 票表
vr_tickets (
id, attendee_id,
ticket_code,
qr_image_path, -- QR码图片路径
issued_at, -- 支付成功时生成
used_at, -- 核销时间
created_at
)
-- 核销记录表
vr_verifications (
id, ticket_id, verifier_id,
verified_at,
ip_address,
location
)
```
| 表 | 用途 |
|---|---|
| `vr_events` | 活动 |
| `vr_sessions` | 场次时间+库存 |
| `vr_tickets` | 电子票含QR数据 |
| `vr_verifiers` | 核销员 |
| `vr_verifications` | 核销记录 |
---
## 插件目录结构
```
shopxo-vr-ticket-plugin/
app/plugins/vr_ticket/
├── plugin.json # 插件声明
├── app/
│ ├── Admin/ # 管理端
│ │ ├── Controller/
│ │ │ ├── EventController.php
│ │ │ ├── SessionController.php
│ │ │ └── TicketController.php
│ │ ├── Route.php # 管理端路由
│ │ └── View/ # 管理页面
│ │ ├── event_list.html
│ │ ├── session_edit.html
│ │ └── ticket_list.html
│ ├── Api/ # C端API
│ │ ├── EventController.php # 场次详情
│ │ ├── SessionController.php # 可用场次
│ │ ├── CheckoutController.php# 下单钩子(收集观演人)
│ │ └── TicketController.php # 我的票/票夹
│ ├── EventListener.php # 监听 ShopXO 支付成功事件
│ ├── Model/
│ │ ├── VrEvent.php
│ │ ├── VrSession.php
│ │ ├── VrAttendee.php
│ │ └── VrTicket.php
│ └── Service/
│ ├── EventService.php
│ ├── TicketService.php # QR 生成 + 发放
│ └── VerificationService.php# 核销逻辑
├── database/
│ └── migrations/ # 插件数据库迁移
│ ├── 001_create_vr_events.php
│ ├── 002_create_vr_sessions.php
│ ├── 003_create_vr_attendees.php
│ └── 004_create_vr_tickets.php
├── static/
│ └── admin/ # 管理端静态资源
└── README.md
├── service/
│ ├── BaseService.php # 基础配置
│ ├── EventService.php # 活动/场次服务
│ ├── TicketService.php # 票生成/发放
│ └── VerifyService.php # 核销逻辑
├── view/
│ ├── Goods.php # 商品详情页钩子
│ └── User.php # 用户中心钩子
├── Admin/
│ ├── Controller/
│ │ ├── EventController.php
│ │ ├── SessionController.php
│ │ └── TicketController.php
│ └── View/
│ ├── event_list.html
│ ├── event_save.html
│ ├── session_list.html
│ ├── session_save.html
│ ├── ticket_list.html
│ └── verification_list.html
├── Api/
│ └── Controller/
│ ├── EventController.php # 活动API
│ ├── SessionController.php # 场次API
│ └── TicketController.php # 票/核销API
└── EventListener.php # ShopXO事件监听
database/migrations/ # 数据库迁移
static/vr_ticket/ # 静态资源
```
---
## 核心流程
### 1. 票务发布流程
## 完整购票流程
```
商家后台 → 插件菜单 → 新建活动
→ 关联 ShopXO 商品(填写价格/库存)
→ 添加场次(时间+价格+库存)
→ 上架
```
1. 商家后台
→ VR票务插件 → 新建活动关联ShopXO商品
→ 添加场次(时间 + 座位图 + 票价 + 库存)
### 2. 用户购票流程
2. 用户前端
→ 首页浏览 → 进入商品详情页
→ 判断 item_type == 'ticket'
→ 跳转到票务选座页pages/ticket-buy
→ 选择座位 → 填写观演人信息
→ 提交订单 → 微信支付
```
首页浏览 → 商品详情 → 选择场次 → 立即购买
→ 填写观演人信息1-N人场次配置决定
→ 提交订单(插件收集 attendees
→ 微信支付
→ 支付回调 → 插件生成 QR 票
→ 票夹页显示电子票
```
3. 支付成功
→ ShopXO 支付回调
→ TicketService::OnOrderPaid() 触发
→ 生成 QR 票 → 存入 vr_tickets 表
→ 用户可在票夹页查看
### 3. 核销流程
```
B端核销员 → 扫码页面 → 扫描 QR
→ API 校验 ticket_code
→ 标记 used_at
→ 返回核销结果
4. 核销
→ 工作人员打开核销页pages/plugins/vr-ticket-verify
→ 扫描用户 QR 码
→ POST /api/ticket/verify
→ VerifyService 更新核销状态
→ 返回核销结果
```
---
## 关键技术决策
## 与 vr-ticket-mp 对比
### 库存扣减
采用 `FOR UPDATE SKIP LOCKED`(与 vr-ticket-mp 相同策略):
```sql
BEGIN;
SELECT available_stock FROM vr_sessions
WHERE id = ? AND available_stock >= ? FOR UPDATE SKIP LOCKED;
-- 若成功,执行 UPDATE available_stock = available_stock - ?
COMMIT;
```
### QR 票安全
```php
// qr_data = AES_Encrypt(json_encode([
// 'code' => $attendee->ticket_code,
// 'event_id' => $event_id,
// 'exp' => time() + 86400 * 30 // 30天有效期
// ]), $secret_key)
```
核销时解密验证,防止伪造。
### 与 ShopXO 的事件对接
```php
// EventListener.php
class EventListener
{
// 监听 ShopXO 支付成功事件
public function onOrderPaid($order_id)
{
$items = $this->orderService->getOrderItems($order_id);
foreach ($items as $item) {
if ($item->item_type === 'ticket') {
$this->ticketService->issueTickets($item);
}
}
}
}
```
---
## 与 vr-ticket-mp 的对比
| 维度 | vr-ticket-mp主线 | ShopXO 插件Plan B |
|------|---------------------|---------------------|
| 后端 | Go + Gin自建 | PHP + ThinkPHPShopXO |
| 数据库 | Supabase Postgres | ShopXO MySQL |
| 前端 | uni-app自建 | shopxo-uniapp已有 |
| 会员体系 | Supabase Auth | ShopXO 内置 |
| 积分/优惠券 | 自建 | ShopXO 内置 |
| 微信支付 | 自建 | ShopXO 内置 |
| 部署 | Docker | 虚拟主机即可 |
| 开发成本 | 高(全部自建) | 低(复用 ShopXO |
| 适合场景 | 票务为主,商城为辅 | 商城为主,票务为辅 |
| 维度 | vr-ticket-mp主线 | vr-shopxo-pluginPlan B |
|---|---|---|
| **后端** | Go + Gin自建 | PHP + ThinkPHPShopXO |
| **数据库** | Supabase Postgres | ShopXO MySQL |
| **前端** | uni-app自建 | shopxo-uniapp已有 |
| **会员体系** | Supabase Auth | ShopXO 内置 |
| **积分/优惠券** | 自建 | ShopXO 内置 |
| **微信支付** | 自建 | ShopXO 内置 |
| **QR 核销** | 自建 | 基于 ShopXO 已有机制 |
| **部署** | Docker | 虚拟主机即可 |
| **AI 参与度** | 80% | 85-90% |
| **维护成本** | 高 | 低 |
---
## 技术栈
- **语言**PHP 8+
- **框架**ThinkPHP 8ShopXO 生态
- **前端**Vue.js插件管理端+ uni-appShopXO 已有
- **数据库**MySQLShopXO 同一实例
- **QR生成**phpqrcode / endroid/qr-code
- **PHP 8+** / ThinkPHP 8ShopXO 生态)
- **MySQL 5.7+**ShopXO 同一实例)
- **uni-app**shopxo-uniapp已支持微信小程序
- **phpqrcode**`extend/qrcode/phpqrcode.php`,内置)
- **uni.scanCode**(微信/支付宝等小程序扫码)
---
## 开发计划Agent 集群并行)
## 开发文档
| 阶段 | 工作内容 | 负责人 |
|------|---------|--------|
| Phase 0 | 插件骨架 + 本地跑通 demo | 李狗蛋 |
| Phase 1 | 数据库设计 + 迁移脚本 | 妮可 |
| Phase 2 | 场次管理 CRUD + API | 李狗蛋 |
| Phase 3 | 下单钩子 + 观演人收集 | 小老D |
| Phase 4 | 支付回调 + QR 票生成 | 西莉娅 |
| Phase 5 | B端核销页 + 扫码API | 小老D |
| Phase 6 | Uni-app 改造(观演人表单 + 票夹) | 妮可 |
| Phase 7 | 联调 + 测试 + 部署文档 | 西莉娅 |
预计:**2-3 周 MVP****1 个月**完整上线。
| 文档 | 内容 |
|---|---|
| [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 分工 |

View File

@ -1,40 +1,62 @@
# ShopXO VR票务插件
# VR票务插件 for ShopXO
基于 ShopXO 生态的 VR 演唱会票务解决方案Plan B
> Plan B基于 ShopXO 生态的 VR 演唱会票务解决方案
> 当 vr-ticket-mpGo + Supabase + uni-app 主线)不适用时,选用此方案
## 功能特性
## 核心能力
- ✅ 场次管理(多日期/多票价/库存追踪)
- ✅ 观演人信息收集(下单时强制填写)
- ✅ QR 电子票生成(支付成功后自动发放)
- ✅ B 端扫码核销(支持多核销员)
- ✅ 复用 ShopXO 会员/积分/优惠券体系
- ✅ 插件独立部署,不修改 ShopXO 核心代码
| 能力 | 实现方式 |
|---|---|
| 场次管理 | 插件独立表 `vr_events` / `vr_sessions` |
| 商品详情页定制 | 30+ 钩子注入,或 1 行控制器代码替换模板 |
| 选座 UI | 自定义 Vue 组件Fork shopxo-uniapp |
| 观演人收集 | 插件钩子收集,下单时写入 `vr_tickets` 表 |
| QR 电子票 | ShopXO 内置 `\base\Qrcode` + phpqrcode |
| 微信小程序 | shopxo-uniapp 已支持HBuilderX 一键发行 |
| B 端核销 | Fork `realstore/check/check.vue`,完整参考 |
| 会员/积分/优惠券 | 全部复用 ShopXO 内置能力 |
## 快速开始
```bash
# 1. 克隆插件
# 1. 克隆本仓库
git clone http://xmhome.ow-my.com:3000/sileya-ai/vr-shopxo-plugin.git
# 2. 上传至 ShopXO 插件目录
cp -r vr-shopxo-plugin /path/to/shopxo/app/plugins/
# 2. 上传插件到 ShopXO
cp -r vr-shopxo-plugin/app/plugins/vr_ticket /path/to/shopxo/app/plugins/
# 3. 后台安装插件
# 3. 数据库迁移
# 访问 /admin/plugins/vr_ticket/migrate 或手动执行 SQL
# 4. 后台安装
# 管理后台 → 应用中心 → 插件管理 → 安装 VR票务插件
# 4. 数据库迁移
# 访问 /plugins/vr-ticket/admin/migrate
# 5. shopxo-uniapp 改造
# HBuilderX 导入 shopxo-uniapp
# 添加 pages/ticket-buy/ 和 pages/ticket-verify/
# 配置 manifest.json 的 AppID
# 发行 → 微信小程序
```
## 架构文档
## 技术调研文档
详见 [ARCHITECTURE.md](ARCHITECTURE.md)
- [ShopXO 技术能力调研](docs/01_SHOPXO_TECHNICAL_RESEARCH.md) — DIY/CustomView/钩子/插件完整分析
- [uni-app 前端定制](docs/02_FRONTEND_CUSTOMIZATION.md) — 小程序编译与自定义组件
- [核销系统设计](docs/03_VERIFICATION_SYSTEM.md) — QR 生成/核销 API/票夹
- [实施路线图](docs/04_IMPLEMENTATION_ROADMAP.md) — Agent 分工与开发计划
## 开发状态
## 关键发现2026-04-14
🚧 正在规划中,尚未开始编码。
- ✅ ShopXO 内置 **CustomView Ace 编辑器**(全代码自定义页面)
- ✅ 商品详情页 **30+ 插件钩子**,最佳注入点 `plugins_view_goods_detail_base_sku_top`
- ✅ shopxo-uniapp **已支持微信小程序**,条件编译已配置
- ✅ ShopXO 内置 **phpqrcode** QR 码生成库
- ✅ `realstore/check/check.vue` 是 **B 端核销页最佳参考**
---
## 项目状态
**仓库**`http://xmhome.ow-my.com:3000/sileya-ai/vr-shopxo-plugin`
🚧 **调研完成,尚未开始编码**
## 仓库地址
`http://xmhome.ow-my.com:3000/sileya-ai/vr-shopxo-plugin`

79
docs/00_OVERVIEW.md Normal file
View File

@ -0,0 +1,79 @@
# VR票务插件 — 项目总览
## 项目背景
大头的朋友需要一个外卖/包邮/自提 + 会员(充值/积分/优惠券)的小程序商城。
约束条件:**无程序员/无运维/无前端,要直接可用,后期能用 AI 改代码,部署简单,不考虑 Java**。
在 vr-ticket-mp主线Go + Supabase + uni-app之外建立 **Plan B**:基于 ShopXO 生态的票务插件。
---
## 双线策略
| | vr-ticket-mp主线 | vr-shopxo-pluginPlan B |
|---|---|---|
| **定位** | 票务为核心,商城为辅 | 商城为主,票务为辅 |
| **后端** | Go + Gin自建 | PHP + ThinkPHPShopXO |
| **数据库** | Supabase Postgres | ShopXO MySQL |
| **前端** | uni-app自建 | shopxo-uniapp已有 |
| **会员/积分/优惠券** | 自建 | ShopXO 内置 |
| **微信支付** | 自建 | ShopXO 内置 |
| **部署** | Docker | 虚拟主机即可 |
| **维护成本** | 高 | 低 |
| **触发条件** | — | 电商需求 > 票务需求,或 vr-ticket-mp 维护成本过高 |
---
## 为什么选择 ShopXO
ShopXO 在 ShopXO / Bagisto / Saleor / Medusa 四个候选中**断层第一推荐9/10**。
| 能力 | ShopXO | Bagisto | Saleor | Medusa |
|---|---|---|---|---|
| 外卖/自提/包邮 | ✅ 全功能内置 | ❌ 无 | ❌ 无 | ❌ 无 |
| 会员充值/积分/优惠券 | ✅ 全功能内置 | ❌ 无 | ❌ 无 | ❌ 无 |
| 微信支付 | ✅ 内置 | ⚠️ 需配置 | ⚠️ 需配置 | ⚠️ 需配置 |
| 部署难度 | ⭐ 虚拟主机即可 | ⭐ VPS/SSH | ⭐ Docker 门槛高 | ⭐ 云托管推荐 |
| uni-app 前端 | ✅ shopxo-uniapp | ❌ 无 | ❌ | ❌ |
| 票务插件 | ⚠️ 需开发 | ❌ 无 | ❌ 无 | ❌ 无 |
| AI 友好度 | ⭐ 业务层 80% 可AI | ⭐ 差 | ⭐ 差 | ⭐ 差 |
| 协议 | MIT | MIT | MIT | MIT |
---
## 今天调研的关键突破
1. **CustomView Ace 编辑器**ShopXO 内置全代码自定义页面编辑器HTML/CSS/JS 三栏,实时预览
2. **30+ 商品详情页钩子**`plugins_view_goods_detail_base_sku_top` 是票务 UI 最佳注入点
3. **商品详情页按类型替换模板**:修改 `Goods.php Index()` 加 1 行判断即可
4. **shopxo-uniapp 支持微信小程序**条件编译已配置HBuilderX 一键发行
5. **核销机制现成可用**`realstore/check/check.vue` 是 B 端核销页最佳参考
6. **QR 码生成内置**`\base\Qrcode` 类 + phpqrcode 库URL 即用
---
## 文档目录
| 文档 | 内容 |
|---|---|
| [00_OVERVIEW.md](00_OVERVIEW.md) | 项目总览(本文档) |
| [01_SHOPXO_TECHNICAL_RESEARCH.md](01_SHOPXO_TECHNICAL_RESEARCH.md) | ShopXO 技术能力完整调研 |
| [02_FRONTEND_CUSTOMIZATION.md](02_FRONTEND_CUSTOMIZATION.md) | shopxo-uniapp 编译与自定义 |
| [03_VERIFICATION_SYSTEM.md](03_VERIFICATION_SYSTEM.md) | 核销系统设计 |
| [04_IMPLEMENTATION_ROADMAP.md](04_IMPLEMENTATION_ROADMAP.md) | 实施路线图与 Agent 分工 |
| [../ARCHITECTURE.md](../ARCHITECTURE.md) | 核心架构设计(基础版) |
---
## 关键文件路径(源码位置)
ShopXO 源码克隆自 `https://gitee.com/zongzhige/shopxo`,位于:
```
/Users/bigemon/.openclaw/workspace/council-research/shopxo-eval/.worktrees/shopxo-evaluator/shopxo-src/
```
shopxo-uniapp 克隆自 `https://gitee.com/zongzhige/shopxo-uniapp`,位于:
```
/Users/bigemon/.openclaw/workspace/council-research/shopxo-eval/.worktrees/shopxo-evaluator/shopxo-uniapp-src/
```

View File

@ -0,0 +1,603 @@
# ShopXO 技术能力完整调研
> 调研时间2026-04-14上午
> 调研方式:源码分析 + Council 并行 agent 调研ShopXO / Bagisto / Saleor / Medusa
> 源码位置:`council-research/shopxo-eval/.worktrees/shopxo-evaluator/shopxo-src/`
---
## 一、DIY 设计器组件系统
### 1.1 组件定义机制
ShopXO DIY 设计器是 **Vue3 SPA**
- 入口:`public/static/diy/js/entry/index-*.js`
- 组件列表定义在前端 JS 中(组件面板)
- 后端只存储布局配置 JSON`sxo_diy` 表)
### 1.2 渲染层支持的组件类型
模板位置:
`app/module/view/layout/public/common/module_view.html`
| 组件值 (value) | 功能 | AI 参与度 |
|---|---|---|
| `images` | 单图 | ✅ 替换链接即可 |
| `many-images` | 多图(多种布局) | ✅ |
| `images-text` | 图文混排 | ✅ |
| `images-magic-cube` | 图片魔方(复杂宫格) | ✅ |
| `video` | 视频 | ✅ |
| `goods` | 商品列表3 种样式routine/leftright/rolling | ✅ |
| `title` | 标题文字 | ✅ |
| **`custom`** | **自定义 HTML** | **✅✅ 完全自由** |
### 1.3 `custom` 组件渲染逻辑
```php
{{case custom}}
{{if !empty($vss['config']['custom'])}}
{{$vss.config.custom|raw}}
{{/if}}
{{/case}}
```
直接输出原始 HTML无任何过滤。
**⚠️ 注意**`custom` 组件在后端渲染层存在,但 admin DIY Vue3 设计器的组件面板 UI 入口不明确(未在源码中找到 admin 侧配置)。**替代方案**:使用 CustomView 完全弥补(见下节)。
### 1.4 DIY 设计器局限性
- **展示型页面**(首页/专题页重度依赖拖拽装修JSON 数据库存储AI 无法直接参与视觉设计
- **业务型页面**(商品/订单/会员):表格 + 表单AI 参与度高
- **票务插件 UI**:完全走代码,不依赖 DIY 系统 → AI 参与度极高
---
## 二、CustomView 自定义页面系统 ⭐ 重大发现
### 2.1 功能概述
ShopXO 内置全代码自定义页面编辑器Ace Playground Web Component
后台入口:**营销菜单 → 自定义页面管理**
### 2.2 技术细节
| 能力 | 详情 |
|---|---|
| **编辑器** | Ace Playground非 iframe是原生 Web Component |
| **语言** | HTML + CSS + JavaScript 三栏独立编辑 |
| **实时预览** | iframe 渲染,实时刷新 |
| **模板语法** | ThinkPHP `{{$data.xxx|raw}}` 可用(会员信息、商品数据等) |
| **访问地址** | `/index/customview/index?id=xxx` |
| **全屏模式** | 支持 `is_full_screen` 参数 |
### 2.3 存储内容
| 字段 | 渲染方式 |
|---|---|
| `html_content` | `{{$data.html_content\|raw}}` 在 div 内 |
| `css_content` | `<style>{{$data.css_content\|raw}}</style>` 自动包裹 |
| `js_content` | `<script>{{$data.js_content\|raw}}</script>` 自动包裹 |
### 2.4 admin 端 Ace 编辑器实现
文件:`app/admin/view/default/customview/saveinfo.html`
```javascript
class AcePlayground extends HTMLElement {
constructor() {
var shadow = this.attachShadow({mode: 'open'});
dom.buildDom(['div', {id: 'host'},
['div', {id: 'html'}], // HTML 编辑器
['div', {id: 'css'}], // CSS 编辑器
['div', {id: 'js'}], // JS 编辑器
['iframe', {id: 'preview'}] // 实时预览
], shadow);
this.htmlEditor = ace.edit(shadow.querySelector('#html'), {...});
this.cssEditor = ace.edit(shadow.querySelector('#css'), {...});
this.jsEditor = ace.edit(shadow.querySelector('#js'), {...});
this.preview = shadow.querySelector('#preview');
this.updatePreview(); // 实时更新预览
}
}
```
### 2.5 对票务的意义
**CustomView 可以实现完全自定义的票务辅助页面**
- 票夹页面(用户的所有电子票)
- 观演人管理页面
- 活动专题页面
在 DIY 设计器中,通过 `custom` 组件引用 CustomView 页面 URL或直接在插件钩子中链接到 CustomView 页面。
---
## 三、商品详情页钩子系统30+ 钩子)
### 3.1 核心文件
- 控制器:`app/index/controller/Goods.php``PluginsHook()` 方法)
- 模板:`app/index/view/default/goods/module/middle_base/right/` 目录下各模板
### 3.2 钩子完整列表
```
📍 相册区域
plugins_view_goods_detail_photo_within ← 相册内部
plugins_view_goods_detail_photo_bottom ← 相册底部
📍 右侧购买面板
plugins_view_goods_detail_panel_original_price_top ← 原价上方
plugins_view_goods_detail_panel_price_top ← 现价上方
plugins_view_goods_detail_panel_price_bottom ← 现价下方
plugins_view_goods_detail_panel_bottom ← 整个面板底部
📍 规格/库存区域 ← 🎯 票务最佳注入点
plugins_view_goods_detail_base_sku_top ← 规格选择区顶部 ⭐
plugins_view_goods_detail_base_inventory_top ← 库存区域顶部
plugins_view_goods_detail_base_inventory_bottom ← 库存区域底部 ⭐
📍 购买导航
plugins_view_goods_detail_base_buy_nav_min_inside_begin ← 购买区内部前
plugins_view_goods_detail_base_buy_nav_min_inside ← 购买区内部后
📍 底部信息(标签页)
plugins_view_goods_detail_tabs_top / _content / _bottom
plugins_view_goods_detail_tabs_comments_top/bottom
plugins_view_goods_detail_tabs_guess_like_top/bottom
📍 全局
plugins_view_goods_detail_content_top/bottom
plugins_view_goods_detail_left_top
plugins_view_goods_detail_title
```
### 3.3 钩子渲染机制
控制器中:
```php
foreach($hook_arr as $hook_name) {
$assign[$hook_name.'_data'] = MyEventTrigger($hook_name, [
'hook_name' => $hook_name,
'is_backend' => false,
'goods_id' => $goods_id,
'goods' => &$goods,
]);
}
MyViewAssign($assign);
```
模板中渲染:
```php
{{foreach $plugins_view_goods_detail_base_sku_top_data as $hook}}
{{if is_string($hook) or is_int($hook)}}
{{$hook|raw}}
{{/if}}
{{/foreach}}
```
### 3.4 最佳注入点分析
**票务选座 UI 最佳位置**`plugins_view_goods_detail_base_sku_top`
- 位于规格选择区顶部
- 在购买按钮上方
- 可以完全占据购买区域
- 插件返回 HTML 字符串即可渲染
---
## 四、按商品类型完全替换模板
### 4.1 主题机制
`MyView()` 函数(`app/common.php`)支持主题覆盖:
```php
function MyView($view = '', $data = []) {
$theme = DefaultTheme(); // 从配置读取主题名
$file = APP_PATH.$group.DS.'view'.DS.$theme.DS.$view_new.$suffix;
// 如果当前主题有这个文件,就用主题的;否则用 default
}
```
主题目录:`app/index/view/{theme_name}/goods/index.html`
主题切换配置:`sxo_config` 表中 `common_default_theme` 字段。
**⚠️ 限制**:主题是全局的,所有商品共用同一套模板。
### 4.2 按商品类型动态选择模板
`Goods.php Index()` 方法中加 1 行判断:
```php
// Goods.php Index() 方法,约第 440 行
// 在 return MyView(); 之前插入:
if(!empty($goods['item_type']) && $goods['item_type'] == 'ticket') {
return MyView('/goods/ticket_detail'); // 自定义票务模板
}
return MyView(); // 默认模板
```
对应模板文件:`app/index/view/default/goods/ticket_detail.html`
**这是 ShopXO 允许范围内,唯一能让特定类型商品使用独立模板的方式。**
---
## 五、插件系统架构
### 5.1 目录结构
```
app/plugins/{PluginName}/
├── service/
│ └── BaseService.php ← 必须:配置字段 + 安装/卸载逻辑
├── view/
│ ├── User.php ← 用户中心钩子实现
│ ├── Goods.php ← 商品详情页钩子实现
│ └── ...
├── js/
│ └── ...
└── 配置文件
```
### 5.2 BaseService.php 必须实现
```php
namespace app\plugins\{PluginName}\service;
class BaseService {
// 1. 插件配置表单字段
public static function Config($params = []) {
return [
'title' => '插件名称',
'base' => [...], // 基础配置
'items' => [...], // 自定义配置项
];
}
// 2. 安装回调
public static function Install($params = []) { ... }
// 3. 卸载回调
public static function Uninstall($params = []) { ... }
}
```
### 5.3 视图钩子实现示例
插件 `view/Goods.php`
```php
namespace app\plugins\vr_ticket\view;
class Goods {
// 钩子plugins_view_goods_detail_base_sku_top
public static function PluginsViewGoodsDetailBaseSkuTop($params) {
$goods = $params['goods'];
if(empty($goods['item_type']) || $goods['item_type'] != 'ticket') {
return ''; // 非票务商品,不输出
}
return '<div id="ticket-seat-map">...</div>';
}
}
```
ShopXO 事件系统自动发现并调用所有注册该钩子的插件方法。
### 5.4 Service 层钩子(可改业务逻辑)
```
plugins_service_goods_buy_nav_button_handle ← 可动态增减购买按钮
plugins_service_goods_spec_data ← 可改规格/库存数据
plugins_service_buy_order_insert_begin ← 订单创建前
plugins_service_buy_order_insert_success ← 订单创建后
```
---
## 六、用户中心钩子
### 6.1 路由
- 个人资料页:`/index/personal/``Personal.php`)— ❌ 无钩子
- 用户中心:`/index/user/``User.php`)— ✅ 6 个钩子
### 6.2 钩子列表
```
plugins_view_user_center_top ← 用户中心顶部
plugins_view_user_base_bottom ← 用户基础信息底部
plugins_view_user_various_top ← 聚合内容(订单/资产)顶部
plugins_view_user_various_bottom ← 聚合内容底部
plugins_view_user_various_inside_top ← 聚合内容内部顶部 ⭐
plugins_view_user_various_inside_bottom ← 聚合内容内部底部
```
### 6.3 最佳注入点
**票夹**(我的票):注入到 `plugins_view_user_various_inside_top`
在插件 `view/User.php` 中:
```php
public static function PluginsViewUserVariousInsideTop($params) {
return render_ticket_list_html($tickets); // 返回票夹 HTML
}
```
---
## 七、搜索页钩子8 个)
```
plugins_view_search_top ← 搜索区域顶部
plugins_view_search_bottom ← 搜索区域底部
plugins_view_search_inside_top ← 搜索结果区域顶部
plugins_view_search_inside_bottom ← 搜索结果区域底部
plugins_view_search_data_top ← 商品列表顶部
plugins_view_search_data_bottom ← 商品列表底部
plugins_view_search_nav_top ← 筛选导航顶部
plugins_view_search_nav_inside_begin/end ← 筛选导航内部
```
---
## 八、订单系统关键发现
### 8.1 订单状态
| 值 | 含义 |
|---|---|
| 0 | 待确认 |
| 1 | 已确认/待支付 |
| 2 | 已支付/待发货 |
| 3 | 已发货/待收货 |
| 4 | 已完成 |
| 5 | 已取消 |
| 6 | 已关闭 |
### 8.2 支付状态
| 值 | 含义 |
|---|---|
| 0 | 未支付 |
| 1 | 已支付 |
| 2 | 已退款 |
| 3 | 部分退款 |
### 8.3 订单模式
| 值 | 含义 |
|---|---|
| 0 | 快递 |
| 1 | 同城 |
| 2 | 自提 |
| 3 | 虚拟 |
**票务建议**:使用 `order_model = 3`(虚拟),因为不需要物流。
### 8.4 关键表结构
**订单表 `sxo_order`**
- `order_no` — 订单号UNIQUE
- `user_id` — 用户 ID
- `status` — 订单状态
- `pay_status` — 支付状态
- `order_model` — 订单模式
- **`extension_data`** — 扩展数据 JSON可存票务核销信息
- `client_type` — 客户端类型weixin/h5/app 等)
**订单地址表 `sxo_order_address`**
- `name` — 收件人姓名
- `tel` — 收件人电话
- `idcard_name` — 身份证姓名
- `idcard_number` — 身份证号码
**核销码表 `sxo_order_extraction_code`**
```sql
CREATE TABLE `sxo_order_extraction_code` (
`id` int PRIMARY KEY AUTO_INCREMENT,
`order_id` int, -- 关联订单ID
`user_id` int, -- 用户ID
`code` char(30), -- 取货码(可用于票务核销)
`add_time` int,
`upd_time` int
);
```
---
## 九、QR 码生成能力
### 9.1 核心类
位置:`extend/base/Qrcode.php`
使用 phpqrcode 库(`extend/qrcode/phpqrcode.php`
### 9.2 API 接口
```
展示模式GET /?s=index/qrcode/index&content=BASE64_ENCODE(data)
创建模式:\base\Qrcode::Create(['content' => $data, 'root_path' => $path])
```
### 9.3 展示模式参数
| 参数 | 说明 | 默认值 |
|---|---|---|
| `content` | 二维码内容base64 编码) | 当前 URL |
| `level` | 容错率L/M/Q/H | L |
| `size` | 大小1-30 | 6 |
| `mr` | 外边距 | 1 |
### 9.4 生成票务 QR 码
```php
$ticket_data = json_encode([
'code' => $attendee->ticket_code,
'event' => $event->name,
'session' => $session->session_time,
'seat' => $seat,
'exp' => time() + 86400 * 30 // 30天有效期
]);
$qr_url = MyUrl('index/qrcode/index', [
'content' => urlencode(base64_encode($ticket_data)),
'size' => 8,
'level' => 'H', // 高容错率
]);
```
在页面中展示:`<img src="<?= $qr_url ?>" />`
---
## 十、购买流程与库存原子性
### 10.1 Buy API 端点
```
POST /?s=index/buy/add ← 创建订单
GET /?s=index/buy/index ← 获取结算页数据
```
关键参数 `goods_data`(绕过购物车直接下单):
```php
$params['goods_data'] = [
'goods_id' => 123,
'stock' => 1,
'spec' => [['type_id' => $type_id, 'value_id' => $value_id]]
];
```
### 10.2 库存扣减原子性
`BuyService.php` 第 1692-1699 行:
```php
$where = [
'id' => $spec_base_id,
'goods_id' => $goods_id,
'inventory >= ' => $buy_number
];
Db::name('GoodsSpecBase')->where($where)->dec('inventory', $buy_number)->update();
```
- 订单支付时在事务内原子扣减
- WHERE 条件不满足时 `dec()` 返回 0 行 → 事务回滚
- **结论**ShopXO 自身防超卖是安全的,无需额外 Go 锁座层
### 10.3 锁座机制
ShopXO **不支持锁座**。对于演唱会选座:
- 座位作为 SKUinventory = 0 或 1
- 购买时直接扣库存
- 超卖风险由 ShopXO 库存原子性兜底
---
## 十一、自提点核销机制(最佳参考)
### 11.1 核销码表
`sxo_order_extraction_code` — ShopXO 用此表存储自提点取货码
### 11.2 uni-app 核销页面
位置:`pages/plugins/realstore/check/check.vue`
关键代码:
```vue
<!-- 扫码按钮(非 H5 平台显示)-->
<!-- #ifndef H5 -->
<uni-icons type="scan" size="56rpx" @tap="scan_event"></uni-icons>
<!-- #endif -->
<!-- 手动输入核销码 -->
<input type="text" v-model="check_value" />
<!-- 扫码触发 -->
uni.scanCode({
success: (res) => {
self.check_value = res.result;
self.form_submit(); // 自动提交验证
}
});
<!-- 提交验证 -->
uni.request({
url: app.globalData.get_request_url('verification', 'adminorderallot', 'realstore'),
method: 'POST',
data: { extraction_code: this.check_value }
});
```
### 11.3 核销 API
路径:`/?s=admin/orderallot/verification`(插件内)
参数:`extraction_code`(核销码)
返回:
```json
{
"code": 0, // 0=成功,其他=失败
"msg": "核销成功",
"data": {...}
}
```
### 11.4 核销页面 B 端 UI 特性
- 成功/失败结果大字展示(绿/红)
- 核销后清空输入框,可连续扫描
- 底部统计(已核销/待核销/今日核销)
- 支持切换核销门店popup 选择)
---
## 十二、ShopXO 其他关键表
### 12.1 商品表 `sxo_goods`
| 字段 | 说明 |
|---|---|
| `site_type` | 履约类型0-8对应快递/自提/虚拟等) |
| `is_exist_many_spec` | 是否多规格 |
| `inventory` | 主库存 |
| `site_model` | 站点模式 |
### 12.2 规格相关表
- `sxo_goods_spec_type` — 规格维度如区域A区/B区/C区
- `sxo_goods_spec_value` — 规格值映射(如座位:排号+座号)
- `sxo_goods_spec_base` — 每个 SKU 的库存+价格(`inventory` 整数字段)
### 12.3 库存日志表
`sxo_order_goods_inventory_log` — 记录所有库存变动,支持回滚
---
## 十三、综合评估
| 场景 | 能力 | 方式 |
|---|---|---|
| 首页/专题页 DIY | ✅ 完全支持 | DIY 设计器 |
| 自定义 HTML 组件 | ⚠️ 渲染层支持admin UI 不明确 | `custom` 组件 |
| 完全自定义页面 | ✅✅ Ace 编辑器 | CustomView |
| 商品详情页注入票务 UI | ✅✅ 30+ 钩子,最佳注入点 SKU 顶部 | 插件钩子 |
| 商品详情页整体替换 | ✅ 1 行控制器代码 | 修改 Goods.php |
| 用户中心注入票夹 | ✅ 6 个钩子 | 插件 |
| 个人资料页 | ❌ 无法自定义 | 固定模板 |
| 搜索页扩展 | ✅ 8 个钩子 | 插件 |
| 订单详情页 | ❌ 无视图钩子,仅 Service 钩子 | 插件改数据 |
| 购买流程 | ✅ 多个 Service 钩子 | 插件改逻辑 |
| B 端核销页 | ✅✅ 参考 realstore/check.vue | Fork + 定制 |
| QR 码生成 | ✅✅ 内置 phpqrcode | `\base\Qrcode` |
| 插件开发 | ✅ 完整 Hook + Service 机制 | 文档完善 |
**AI 参与度**:票务核心逻辑 95% 可 AI 生成(除 Goods.php 的 1 行判断外)

View File

@ -0,0 +1,471 @@
# shopxo-uniapp 前端编译与自定义
> 调研时间2026-04-14
> 源码位置:`council-research/shopxo-eval/.worktrees/shopxo-evaluator/shopxo-uniapp-src/`
> 官方仓库https://gitee.com/zongzhige/shopxo-uniapp
---
## 一、项目概述
shopxo-uniapp 是 ShopXO 官方出品的 uni-app 前端,支持:
- ✅ 微信小程序MP-WEIXIN
- ✅ QQ 小程序MP-QQ
- ✅ 百度小程序MP-BAIDU
- ✅ 支付宝小程序MP-ALIPAY
- ✅ 抖音/头条小程序MP-TOUTIAO
- ✅ 快手小程序MP-KUAISHOU
- ✅ H5
- ✅ APPiOS/Android
**README 原文**> 已支持小程序微信、QQ、百度、支付宝、头条&抖音、快手)+ H5 + APP
---
## 二、编译到微信小程序
### 2.1 编译工具
**HBuilderX**uni-app 官方 IDE
### 2.2 编译步骤
1. 用 HBuilderX 导入 `shopxo-uniapp-src` 项目
2. 修改 `App.vue` 中的接口地址:
```javascript
// globalData 配置
request_url: 'https://your-shopxo-domain.com/', // 你的 ShopXO 域名
static_url: 'https://your-shopxo-domain.com/static/'
```
3. 修改 `manifest.json` 中的 AppID微信小程序配置
```json
{
"mp-weixin": {
"appid": "wx YOUR APPID",
"setting": {
"urlCheck": false
}
}
}
```
4. 顶部菜单 → **发行** → **微信小程序**
5. 用微信开发者工具打开发行目录
### 2.3 条件编译指令
`.vue` 文件和 `pages.json` 中使用:
```vue
<!-- #ifdef MP-WEIXIN || MP-BAIDU || MP-QQ || MP-KUAISHOU || H5 || APP -->
<view class="top-nav">微信/百度/QQ/快手/H5/APP 专属内容</view>
<!-- #endif -->
<!-- #ifdef MP-ALIPAY -->
<view>支付宝专属内容</view>
<!-- #endif -->
<!-- #ifndef H5 -->
<!-- 非 H5 平台(小程序)专属内容 -->
<uni-icons type="scan" @tap="scan_event"></uni-icons>
<!-- #endif -->
```
`pages.json` 中:
```json
{
"path": "pages/goods-detail/goods-detail",
"style": {
"// #ifdef MP-WEIXIN": "",
"navigationStyle": "custom",
"// #endif": "",
}
}
```
---
## 三、pages.json 路由与全局组件
### 3.1 核心页面
```json
{
"pages": [
{ "path": "pages/index/index" }, // 首页(含 DIY
{ "path": "pages/goods-category/goods-category" }, // 分类
{ "path": "pages/cart/cart" }, // 购物车
{ "path": "pages/user/user" } // 用户中心
],
"subPackages": [
{
"root": "pages/diy", // DIY 页面
"pages": [{ "path": "diy" }]
},
{
"root": "pages/goods-detail", // 商品详情
"pages": [{ "path": "goods-detail" }]
},
{
"root": "pages/goods-search", // 搜索
"pages": [{ "path": "goods-search" }]
},
{
"root": "pages/user-order-detail", // 订单详情
"pages": [{ "path": "user-order-detail" }]
}
]
}
```
### 3.2 全局注册的自定义组件
`pages/index/index``style.usingComponents` 中全局注册:
```json
{
"component-diy": "/pages/diy/components/diy/diy",
"component-form-input": "/pages/form-input/components/form-input/form-input",
"component-layout": "/pages/design/components/layout/layout",
"component-goods-comments": "/pages/goods-detail/components/goods-comments/goods-comments",
"component-coupon-card": "/pages/plugins/coupon/components/coupon-card/coupon-card",
"component-form-input-base": "/pages/form-input/components/form-input/form-input-base"
}
```
其他页面按需引入。
---
## 四、商品详情页改造
### 4.1 商品详情页位置
`pages/goods-detail/goods-detail.vue`
### 4.2 添加票务条件渲染
`<template>` 中添加:
```vue
<template>
<view :class="theme_view">
<!-- 顶部导航(始终显示)-->
<view class="page ...">
<top-nav></top-nav>
<!-- 票务商品:完全自定义页面结构 -->
<block v-if="goods && goods.item_type === 'ticket'">
<ticket-header :goods="goods"></ticket-header>
<ticket-seat-selector
:goods-id="goods.id"
:spec-data="goods_spec_data"
@select="on_seat_select"
></ticket-seat-selector>
<ticket-info :goods="goods"></ticket-info>
<ticket-attendee-form
:max-count="selected_seats.length"
@submit="on_attendee_submit"
></ticket-attendee-form>
<ticket-purchase-bar
:goods="goods"
:selected-seats="selected_seats"
:attendees="attendees"
@buy="ticket_buy"
></ticket-purchase-bar>
</block>
<!-- 普通商品:原有完整 UI -->
<block v-else>
<goods-photo :photos="goods_photo"></goods-photo>
<goods-price :goods="goods"></goods-price>
<goods-spec-select
v-if="goods.is_exist_many_spec == 1"
:spec-data="goods_spec_data"
></goods-spec-select>
<goods-buy-nav :buy-button="buy_button"></goods-buy-nav>
<goods-tabs :tabs="tabs"></goods-tabs>
</block>
</view>
</view>
</template>
```
### 4.3 后端配合(可选)
如果需要完全不同的页面结构,可以:
**方案 A插件钩子替换**
- 在 `plugins_view_goods_detail_base_sku_top` 注入跳转按钮
- 点击后用 `uni.reLaunch` 跳转到 `pages/ticket-buy/ticket-buy`
**方案 B修改 Goods.php 控制器1 行)**
```php
// app/index/controller/Goods.php Index() 方法
if($goods['item_type'] == 'ticket') {
return MyView('/goods/ticket_detail'); // 自定义模板
}
return MyView();
```
**推荐方案 A**(通过插件机制,不修改核心代码)。
---
## 五、新建票务专属页面
### 5.1 在 pages.json 中添加路由
```json
{
"subPackages": [
{
"root": "pages/ticket-buy",
"pages": [
{
"path": "ticket-buy",
"style": {
"navigationBarTitleText": "选择座位",
"navigationStyle": "custom"
}
}
]
},
{
"root": "pages/ticket-verify",
"pages": [
{
"path": "ticket-verify",
"style": {
"navigationBarTitleText": "票务核销",
"enablePullDownRefresh": false
}
}
]
},
{
"root": "pages/ticket-wallet",
"pages": [
{
"path": "ticket-wallet",
"style": {
"navigationBarTitleText": "我的票夹"
}
}
]
}
]
}
```
### 5.2 页面目录结构
```
pages/ticket-buy/
├── ticket-buy.vue # 选座 + 购票主流程
└── components/
├── seat-selector.vue # 座位选择器
├── attendee-form.vue # 观演人表单
└── purchase-bar.vue # 购买栏
pages/ticket-wallet/
├── ticket-wallet.vue # 票夹主页面
└── components/
└── ticket-card.vue # 电子票卡片(含 QR 码)
pages/ticket-verify/
└── ticket-verify.vue # B 端核销页面(参考 realstore/check.vue
```
---
## 六、uni.scanCode 扫码能力
### 6.1 基础用法
```javascript
// 扫码
uni.scanCode({
onlyFromCamera: true, // 只允许相机扫码
success: (res) => {
console.log('扫码结果:', res.result);
// res.result: 扫码内容
// res.scanType: 二维码类型QR_CODE 等)
},
fail: (err) => {
console.error('扫码失败', err);
}
});
```
### 6.2 在 H5 平台的处理
```vue
<!-- #ifndef H5 -->
<uni-icons type="scan" size="56rpx" @tap="scan_event"></uni-icons>
<!-- #endif -->
<!-- H5 平台提示手动输入 -->
<!-- #ifdef H5 -->
<view class="cr-grey text-size-sm" @tap="show_manual_input">
请输入核销码
</view>
<!-- #endif -->
```
### 6.3 参考实现
ShopXO 的 `realstore/check/check.vue` 已完整实现:
- 扫码触发 → 自动提交验证
- 手动输入兼容
- 成功/失败状态显示
- 连续扫描(核销后清空输入框)
---
## 七、QR 码展示
### 7.1 后端生成 QR 码图片 URL
```javascript
// 生成票务 QR 码 URL
function getTicketQrUrl(ticketId) {
const baseUrl = app.globalData.request_url;
const ticketData = JSON.stringify({
id: ticketId,
exp: Date.now() + 30 * 86400 * 1000 // 30天有效期
});
const encoded = encodeURIComponent(
uni.base64ToString(
uni.stringToBase64(ticketData)
)
);
return `${baseUrl}?s=index/qrcode/index&content=${encoded}&size=8&level=H`;
}
```
### 7.2 在页面中展示
```vue
<template>
<view class="ticket-card">
<image :src="qrCodeUrl" mode="aspectFit" class="qr-image" />
<view class="ticket-code">{{ ticket.code }}</view>
</view>
</template>
```
---
## 八、manifest.json 关键配置
### 8.1 多端配置
```json
{
"name": "ShopXO",
"appid": "__UNI__50E3C11",
"description": "ShopXO开源商城...",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueCompiler": "uni-app",
"distribute": {
"android": { ... },
"ios": { ... },
"mp-weixin": {
"appid": "wx YOUR APPID",
"setting": {
"urlCheck": false,
"es6": true,
"minified": true
},
"usingComponents": true
}
}
}
}
```
### 8.2 权限配置(微信小程序)
```json
"mp-weixin": {
"permission": {
"scope.userLocation": {
"desc": "用于场馆定位"
}
}
}
```
---
## 九、自定义组件注册
### 9.1 全局注册(所有页面可用)
`pages/index/index.vue``style.usingComponents` 中添加:
```json
"component-ticket-seat": "/pages/ticket-buy/components/seat-selector/seat-selector",
"component-ticket-card": "/pages/ticket-wallet/components/ticket-card/ticket-card",
"component-attendee-form": "/pages/ticket-buy/components/attendee-form/attendee-form"
```
### 9.2 页面级注册
在单个页面的 `pages.json` 中:
```json
{
"path": "pages/ticket-wallet/ticket-wallet",
"style": {
"usingComponents": {
"component-ticket-card": "/pages/ticket-wallet/components/ticket-card/ticket-card"
}
}
}
```
---
## 十、ShopXO API 调用
### 10.1 请求封装
ShopXO uni-app 使用全局封装的请求方法:
```javascript
uni.request({
url: app.globalData.get_request_url('action', 'controller', 'module'),
method: 'POST',
data: { ... },
success: (res) => {
if (res.data.code == 0) {
// 成功
} else {
// 失败
}
}
});
```
### 10.2 插件 API 路径
对于插件的 API
```javascript
// 格式get_request_url(action, controller, plugins_name)
const url = app.globalData.get_request_url(
'verify', // action
'adminverify', // controller
'vrticket' // 插件名apps 目录下的目录名)
);
```
### 10.3 鉴权
请求自动带上登录态:
```javascript
// 已在全局封装,每次请求自动附加:
// Header: Authorization / Token
// 或者通过 Session/Cookie
```

View File

@ -0,0 +1,544 @@
# 核销系统设计
> 调研时间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 '关联用户IDShopXO 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推荐用户进入票夹时刷新状态
- 方案 BWebSocket 推送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

View File

@ -0,0 +1,421 @@
# 实施路线图
> 规划时间2026-04-14
> 目标:基于 ShopXO 的 VR 演唱会票务 MVP
---
## 一、整体时间估算
| 阶段 | 内容 | 人天 | 可并行 | 累计 |
|---|---|---|---|---|
| Phase 0 | 环境搭建 + 插件骨架 | 2天 | — | 2天 |
| Phase 1 | 数据库设计 + 迁移 | 2天 | ✅ Phase 0 | 2天 |
| Phase 2 | 场次管理 CRUD + API | 3天 | ✅ Phase 1 | 3天 |
| Phase 3 | 下单钩子 + 观演人收集 | 3天 | ✅ Phase 2 | 4天 |
| Phase 4 | 支付回调 + QR 票生成 | 2天 | ✅ Phase 3 | 5天 |
| Phase 5 | uni-app 票务页面 | 3天 | ✅ Phase 3 | 6天 |
| Phase 6 | B 端核销页 + API | 2天 | ✅ Phase 4 | 6天 |
| Phase 7 | 联调 + 测试 + 部署 | 3天 | 需串行 | 9天 |
**预估**Agent 集群并行 **1-2 周 MVP****3 周完整流程**
---
## 二、Phase 0 — 环境搭建
### 目标
本地跑通 ShopXO + shopxo-uniapp 开发环境
### 任务
1. **Docker 部署 ShopXO**(参考 `DEPLOYMENT.md`
- PHP 8.0+ / MySQL 5.7+ / nginx
- 或使用虚拟主机安装包
2. **安装 shopxo-uniapp**
- HBuilderX 导入项目
- 配置 `request_url``static_url`
- 本地 H5 预览验证
3. **创建插件骨架**
```bash
mkdir -p app/plugins/vr_ticket/
cp plugin.json app/plugins/vr_ticket/
mkdir -p app/plugins/vr_ticket/{service,view,Admin/Controller,Api/Controller}
mkdir -p static/vr_ticket/
mkdir -p database/migrations/
```
4. **在 ShopXO 后台安装插件**
- 访问 `/admin/plugins/index`
- 上传插件 zip 或手动放置到 `app/plugins/vr_ticket/`
- 点击安装
### 验收
- ShopXO H5 前端正常访问
- shopxo-uniapp H5 预览正常
- 插件在后台可见
---
## 三、Phase 1 — 数据库设计
### 任务
创建插件迁移文件:
```sql
-- database/migrations/001_create_vr_events.sql
CREATE TABLE `vr_events` (
`id` int UNSIGNED PRIMARY KEY AUTO_INCREMENT,
`goods_id` int UNSIGNED NOT NULL COMMENT 'ShopXO商品ID',
`name` varchar(255) NOT NULL COMMENT '活动名称',
`venue` varchar(255) COMMENT '场馆',
`cover_image` varchar(255) COMMENT '封面图',
`status` tinyint DEFAULT 1 COMMENT '状态0禁用, 1启用',
`created_at` int UNSIGNED DEFAULT 0,
`updated_at` int UNSIGNED DEFAULT 0,
KEY `goods_id` (`goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- database/migrations/002_create_vr_sessions.sql
CREATE TABLE `vr_sessions` (
`id` int UNSIGNED PRIMARY KEY AUTO_INCREMENT,
`event_id` int UNSIGNED NOT NULL,
`session_time` datetime NOT NULL COMMENT '场次时间',
`total_stock` int UNSIGNED DEFAULT 0 COMMENT '总库存',
`available_stock` int UNSIGNED DEFAULT 0 COMMENT '可用库存',
`price` decimal(10,2) UNSIGNED DEFAULT 0 COMMENT '票价',
`status` tinyint DEFAULT 1,
`created_at` int UNSIGNED DEFAULT 0,
`updated_at` int UNSIGNED DEFAULT 0,
KEY `event_id` (`event_id`),
KEY `session_time` (`session_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- database/migrations/003_create_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,
`goods_id` int UNSIGNED NOT NULL,
`user_id` int UNSIGNED NOT NULL,
`event_id` int UNSIGNED NOT NULL,
`session_id` int UNSIGNED NOT NULL,
`ticket_code` char(36) NOT NULL COMMENT 'UUID票码',
`qr_data` text COMMENT '加密QR内容',
`seat_info` varchar(255) COMMENT '座位信息',
`real_name` varchar(60) COMMENT '观演人姓名',
`phone` char(15) COMMENT '手机号',
`verify_status` tinyint DEFAULT 0 COMMENT '0未核销, 1已核销',
`verify_time` int UNSIGNED DEFAULT 0,
`verifier_id` int 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;
-- database/migrations/004_create_vr_verifiers.sql
CREATE TABLE `vr_verifiers` (
`id` int UNSIGNED PRIMARY KEY AUTO_INCREMENT,
`user_id` int UNSIGNED NOT NULL COMMENT 'ShopXO用户ID',
`name` varchar(60) NOT NULL,
`status` tinyint DEFAULT 1,
`created_at` int UNSIGNED DEFAULT 0,
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- database/migrations/005_create_vr_verifications.sql
CREATE TABLE `vr_verifications` (
`id` int UNSIGNED PRIMARY KEY AUTO_INCREMENT,
`ticket_id` int UNSIGNED NOT NULL,
`ticket_code` char(36) NOT NULL,
`verifier_id` int UNSIGNED NOT NULL,
`verifier_name` varchar(60),
`event_id` int UNSIGNED,
`created_at` int UNSIGNED DEFAULT 0,
KEY `ticket_id` (`ticket_id`),
KEY `created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### AI 参与度
**90% AI 可生成**(妮可 agent 主导)
---
## 四、Phase 2 — 场次管理 CRUD
### 任务
1. **管理端页面**(后台)
- `Admin/Controller/EventController.php` — 活动管理
- `Admin/Controller/SessionController.php` — 场次管理
- `Admin/View/event_list.html` — 活动列表
- `Admin/View/session_edit.html` — 场次编辑
2. **C 端 API**
- `Api/Controller/EventController.php` — 活动详情
- `Api/Controller/SessionController.php` — 可用场次列表
### API 设计
```
GET /?s=admin/vrticket/event/list
POST /?s=admin/vrticket/event/save
DELETE /?s=admin/vrticket/event/delete
GET /?s=admin/vrticket/session/list&event_id=8
POST /?s=admin/vrticket/session/save
DELETE /?s=admin/vrticket/session/delete
GET /?s=api/vrticket/event/detail&id=8
GET /?s=api/vrticket/session/available&goods_id=123
```
### AI 参与度
**85% AI 可生成**(标准 CRUD可套模板
---
## 五、Phase 3 — 下单钩子 + 观演人收集
### 5.1 目标
在 ShopXO 下单流程中收集观演人信息
### 5.2 方案
利用 ShopXO 的「订单商品扩展表单」机制(`ordergoodsform` 插件的思路):
`plugins_view_goods_detail_base_sku_top` 注入观演人表单:
```html
<!-- 观演人信息收集(票务商品专用)-->
<div id="vr-ticket-attendee-form" class="vr-form">
<view class="form-title">观演人信息</view>
<view v-for="(item, index) in attendeeList" :key="index">
<input type="text" placeholder="姓名" v-model="item.real_name" />
<input type="idcard" placeholder="身份证号(选填)" v-model="item.id_card" />
<input type="phone" placeholder="手机号" v-model="item.phone" />
</view>
<view class="add-btn" @tap="addAttendee">+ 添加观演人</view>
</div>
```
### 5.3 数据收集流程
1. 用户在前端填写观演人信息
2. 前端将观演人数据存入本地(`uni.setStorage`
3. 点击购买时,将观演人数据通过插件 API 暂存
4. 插件在 `plugins_service_buy_order_insert_begin` 钩子中将观演人数据写入 `vr_tickets`
### 5.4 钩子实现
```php
// 插件 Service/TicketService.php
public static function OnBeforeOrderInsert(&$params, &$order_data) {
// 检查是否有票务商品
$items = $order_data['items'] ?? [];
foreach ($items as $item) {
if (self::IsTicketGoods($item['goods_id'])) {
// 收集观演人信息,生成票码
$attendees = self::CollectAttendees($item);
self::CreateTickets($order_data['order_id'], $item, $attendees);
}
}
}
```
### AI 参与度
**80% AI 可生成**(逻辑稍复杂,需与 ShopXO 订单流程对接)
---
## 六、Phase 4 — 支付回调 + QR 票生成
### 6.1 目标
支付成功后自动发放 QR 电子票
### 6.2 触发点
ShopXO 支付成功 → `plugins_service_buy_order_insert_success` 钩子
### 6.3 任务
```php
// 插件 Service/TicketService.php
public static function OnOrderPaid($order_id, $order_no) {
// 1. 查询该订单的所有票务商品
$tickets = self::GetPendingTickets($order_id);
foreach ($tickets as $ticket) {
// 2. 生成加密票码
$ticket_code = self::GenerateTicketCode(); // UUID v4
// 3. 加密 QR 内容
$qr_data = self::EncryptQrData([
'id' => $ticket['id'],
'code' => $ticket_code,
'event' => $ticket['event_id'],
'exp' => time() + 86400 * 30, // 30天有效期
]);
// 4. 更新数据库
Db::name('vr_tickets')
->where('id', $ticket['id'])
->update([
'ticket_code' => $ticket_code,
'qr_data' => $qr_data,
'issued_at' => time(),
]);
}
// 5. 发送通知(可选)
self::NotifyUser($order_id);
}
```
### AI 参与度
**90% AI 可生成**(标准业务逻辑)
---
## 七、Phase 5 — uni-app 票务页面
### 7.1 任务
| 页面 | 文件 | 说明 |
|---|---|---|
| 选座 + 购票 | `pages/ticket-buy/ticket-buy.vue` | 参考 goods-detail.vue |
| 座位选择组件 | `pages/ticket-buy/components/seat-selector.vue` | SVG/Canvas 座位图 |
| 观演人表单 | `pages/ticket-buy/components/attendee-form.vue` | 动态表单项 |
| 票夹 | `pages/ticket-wallet/ticket-wallet.vue` | 参考 user/order-list |
### 7.2 关键组件实现
**座位选择器**(最简单的 SVG 实现):
```vue
<template>
<view class="seat-map">
<svg viewBox="0 0 800 600" class="seat-svg">
<g v-for="seat in seats" :key="seat.id">
<rect
:x="seat.x"
:y="seat.y"
width="30"
height="30"
:fill="getSeatColor(seat.status)"
@tap="onSeatTap(seat)"
class="seat-rect"
/>
<text :x="seat.x + 15" :y="seat.y + 20" class="seat-label">
{{ seat.label }}
</text>
</g>
</svg>
<view class="seat-legend">
<view class="legend-item"><view class="dot available"></view>可选</view>
<view class="legend-item"><view class="dot selected"></view>已选</view>
<view class="legend-item"><view class="dot sold"></view>已售</view>
</view>
</view>
</template>
```
### 7.3 接入商品详情页
修改 `pages/goods-detail/goods-detail.vue`
- 检测 `goods.item_type === 'ticket'`
- 跳转到 `pages/ticket-buy/ticket-buy?goods_id=xxx`
或通过插件钩子注入选座 UI覆盖原有的规格选择器。
### AI 参与度
**90% AI 可生成**(标准 Vue 组件)
---
## 八、Phase 6 — B 端核销页
### 任务
1. **插件 API**`Api/Controller/TicketController.php`
- `verify()` — 核销验证
2. **uni-app 核销页**
- Fork `pages/plugins/realstore/check/check.vue`
- 改造成 `pages/plugins/vr-ticket-verify/check/check.vue`
- 调整 API 路径和返回处理
3. **后台核销统计**
- `Admin/Controller/TicketController.php`
- `Admin/View/verification_list.html`
### AI 参与度
**90% AI 可生成**(核心逻辑已参考 realstore 完整实现)
---
## 九、Phase 7 — 联调 + 测试 + 部署
### 9.1 联调清单
- [ ] 活动创建 → 商品关联
- [ ] 场次库存 → 商品 SKU 映射
- [ ] 前端选座 → 后端扣库存
- [ ] 微信支付 → 回调 → QR 票生成
- [ ] 票夹显示 → QR 码展示
- [ ] B 端扫码 → 核销状态更新
- [ ] C 端状态实时刷新
### 9.2 测试用例
| 用例 | 预期 |
|---|---|
| 正常购票流程 | 支付成功 → 收到 QR 票 |
| 并发抢票 | 库存不超卖 |
| 核销同一张票两次 | 第二次报错「已核销」 |
| QR 码过期 | 核销时报「票已过期」 |
| 退款后票失效 | 票状态更新为已退款 |
### 9.3 部署
- **PHP 虚拟主机**:上传插件 zip → 后台安装
- **shopxo-uniapp**HBuilderX 发行 → 微信审核
---
## 十、Agent 分工建议
| Agent | 负责任务 |
|---|---|
| **李狗蛋**MacBook Pro VM | Phase 0 + Phase 2场次 CRUD |
| **妮可**Intel MacBook | Phase 1数据库迁移脚本 |
| **小老D**Proxmox Linux | Phase 3 + Phase 6钩子 + 核销) |
| **西莉娅**(本地 Hub | Phase 4QR 生成)+ Phase 7联调+ 文档整合 |
---
## 十一、里程碑
| 里程碑 | 日期 | 交付物 |
|---|---|---|
| M1 | 第 1 周 | 插件跑通、数据库就绪 |
| M2 | 第 2 周 | 场次管理 + 购票流程 + QR 票发放 |
| M3 | 第 3 周 | B 端核销 + 票夹 + 联调测试 |
| M4 | 第 4 周 | 微信审核 + 正式上线 |