18 KiB
VR 票务插件开发日志
vr-shopxo-plugin 项目全量记录 仓库:http://xmhome.ow-my.com:3000/sileya-ai/vr-shopxo-plugin 最后更新:2026-04-15
一、项目背景与决策
1.1 需求来源(2026-04-13)
大头受朋友委托,为其合作伙伴调研轻量级商城小程序解决方案。
核心需求:
- 订单:外卖配送 / 包邮 / 自提
- 会员:充值、积分、优惠券
- 约束:无程序员/无前端/无后端,要求直接可用,后期能用 AI 改动,架构清晰,部署简单
调研结论(4 个方案对比):
| 项目 | Stars | 功能 | 部署 | 会员体系 | AI友好 | 综合 |
|---|---|---|---|---|---|---|
| ShopXO | 8.5k | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | 9/10 |
| Bagisto | 14k | ⭐⭐ | ⭐⭐ | ⭐ | ⭐⭐⭐ | 5/10 |
| Saleor | 22.4k | ⭐⭐ | ⭐ | ⭐⭐ | ⭐⭐⭐ | 5/10 |
| Medusa | 23k+ | ⭐⭐ | ⭐ | ⭐ | ⭐⭐⭐ | 4/10 |
ShopXO 断层第一,原因:功能完整 + 虚拟主机可部署 + 有 uni-app 前端配套 + MIT 协议可商用。
票务插件定位: 票务 = ShopXO 商品类型扩展。item_type = 'ticket' 时走插件逻辑处理座位图/场次/QR票。
1.2 技术栈决策
| 层 | 技术选型 | 说明 |
|---|---|---|
| 商城底座 | ShopXO v6.8.0 | ThinkPHP 8,虚拟主机可部署 |
| 前端 | uni-app | 微信小程序 + H5 |
| 票务插件 | PHP 原生 | 插件机制 + Hook 系统 |
| 票务详情页 | 独立 HTML 模板 | 完全独立 UI,绕过 ShopXO 主题限制 |
| 数据库 | MySQL(与 ShopXO 共用) | 表前缀 vrt_ |
| QR 票 | AES 加密 | 防伪造 |
| 核销 | 扫码枪 + RLS | B 端小程序扫码核销 |
核心原则(已固化):
怎么快怎么来,怎么方便怎么来,少改动少复杂度,完全允许改 ShopXO 核心代码(自己部署)。
二、技术调研(2026-04-13 白天)
2.1 ShopXO 插件机制调研
调研文件:
- docs/07_SHOPXO_PLUGIN_MECHANISM.md — 插件开发机制完整手册
- docs/08_SHOPXO_REQUIREMENTS_MAPPING.md — 票务需求 → ShopXO 机制对照矩阵
- docs/09_SHOPXO_HOOKS_REFERENCE.md — 100+ 钩子清单
插件核心机制:
- config.json — 插件元数据(名称/版本/依赖/菜单/权限/静态资源)
- BaseService — 插件业务服务基类(GetDb / 参数校验 / 日志)
- EventListener.php — 生命周期钩子(Install/Uninstall/Upgrade/Index)
- URL 路由 — 后台控制器 plugins_admin 前缀,前台 plugins 前缀
- 视图 — admin/view/default/plugins_admin/ + view/default/plugins/ 目录
关键发现(票务用途):
| 钩子 | 用途 |
|---|---|
| plugins_service_order_pay_success_handle_end | 支付成功 → 生成 QR 票 |
| plugins_view_goods_detail_base_sku_top | 商品详情页顶部(选座 UI) |
| plugins_view_user_various_inside_top | 用户中心(票夹) |
| plugins_service_goods_delete_end | 商品删除 → 清理票务数据 |
| plugins_admin_goods_info_init_end | 后台商品编辑 → 加载票务字段 |
2.2 票务详情页方案抉择
方案 A:URL 劫持 ❌ 放弃
- 缺点:无法继承商品详情页基础样式,改动 ShopXO 核心代码量大
方案 B:CSS 隐藏标准 SKU ❌ 放弃
- 缺点:Hook 链过长,不可控
方案 C:插件模板替换 ❌ 不可行
- 调研结论:MyView() 源码确认 ShopXO 插件系统是纯钩子系统,config.json 无权覆盖 Goods 控制器模板路径,goods/detail.html 写死在控制器里
方案 D(最终):Goods.php 1 行判断 ✅
- 在 app/index/controller/Goods.php 的 return MyView(); 前插入判断
- item_type == 'ticket' → 加载插件模板路径 + 预查询座位模板数据
- 改动量:1 行条件判断 + ~10 行数据注入
- 实测验证:浏览器访问商品详情页,座位图渲染正常
三、ShopXO 环境配置(2026-04-15 凌晨)
3.1 Docker 环境
| 服务 | 端口 | 说明 |
|---|---|---|
| shopxo-web | :10000 | Nginx 前端 |
| shopxo-mysql | :10001 | MySQL 8.0 |
| shopxo-php | :9000 | PHP-FPM |
3.2 关键配置
- 后台入口:
adminufgeyw.php(安装时随机生成6位字符串) - 表前缀:
vrt_ - 数据库名:
vrticket - 数据库凭证:root=shopxo_root_2024 / user=shopxo_user / pass=shopxo_pass_2024
- 源码路径:
/Users/bigemon/WorkSpace/vr-shopxo-plugin/shopxo/ - is_develop:
true(config/shopxo.php 第41行) - 自定义侧边栏配置:
config/vrt_custom_menu.php(菜单入口,不依赖插件系统)
3.3 自定义侧边栏快速入口(2026-04-16)
ShopXO 后台 sidebar 菜单通过 AdminPowerService.php 的 AdminPowerMenuData() 生成(第 598 行 return 前)。
配置文件: shopxo/config/vrt_custom_menu.php
机制: 直接 include 配置文件,遍历 $custom_menu_config['menus'],追加到 $admin_left_menu[]。菜单项的 icon 字段只填图标名(无需 iconfont 前缀,模板会自动加)。
图标来源: shopxo/public/static/common/iconfont/iconfont.css,选 534 个中的任意 .icon-xxx:before 名称。
示例配置:
return [
'menus' => [
[
'id' => 'my-test-plugin-menu',
'name' => '我的测试插件',
'icon' => 'icon-label', // 模板自动渲染为 iconfont icon-label
'url' => '/adminufgeyw.php?s=plugins/index/pluginsname/my_test_plugin/pluginscontrol/admin/pluginsaction/index.html',
'control' => 'plugins',
'action' => 'index',
'is_show' => 1,
],
],
];
生效方式: 修改配置文件后,重启 PHP 容器即可(docker restart shopxo-php)。
3.3 后台权限修复
admin 用户(role_id=1)默认缺少插件权限。手动写入 38 条权限到 vrt_role_power:
- 应用管理链路:340 / 341 及子权限 342-591
3.4 模板目录冲突
ThinkPHP 路由用 plugins_admin(下划线格式),但实际目录为 pluginsadmin(无下划线)。通过创建符号链接解决。
四、Phase 0:插件骨架(2026-04-15 04:36 起)
4.1 完成内容
数据库建表(手动 SQL):
-- 座位模板 CREATE TABLE vrt_vr_seat_templates ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '模板名称', category_id INT DEFAULT 0 COMMENT '绑定分类ID', spec_base JSON COMMENT '座位规格基数据', qr_data VARCHAR(64) NOT NULL COMMENT 'QR数据前缀', is_enable TINYINT DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );
-- 电子票 CREATE TABLE vrt_vr_tickets ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, order_no VARCHAR(64), goods_id INT NOT NULL, user_id INT NOT NULL, qr_code TEXT NOT NULL COMMENT 'AES加密QR数据', status ENUM('pending','active','used','cancelled') DEFAULT 'pending', qr_data VARCHAR(128), seat_label VARCHAR(32), verified_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );
-- 核销员 CREATE TABLE vrt_vr_verifiers ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, name VARCHAR(50), status TINYINT DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );
-- 核销记录 CREATE TABLE vrt_vr_verifications ( id INT PRIMARY KEY AUTO_INCREMENT, ticket_id INT NOT NULL, verifier_id INT NOT NULL, verified_at DATETIME DEFAULT CURRENT_TIMESTAMP, location VARCHAR(200) );
插件文件结构:
app/plugins/vr_ticket/ ├── plugin.json # 插件配置(3个子菜单) ├── EventListener.php # 生命周期(Install建表/Uninstall/Upgrade) ├── service/ │ ├── BaseService.php # 工具(AES/QrData/UUID/日志) │ ├── TicketService.php # onOrderPaid/verifyTicket/getUserTickets │ └── SeatTemplateService.php ├── admin/ │ ├── controller/ │ │ ├── SeatTemplate.php # 座位模板 CRUD │ │ ├── Ticket.php # 电子票列表+详情+导出 │ │ ├── Verifier.php # 核销员管理 │ │ └── Verification.php # 核销记录 │ └── view/default/ # Layui 列表页 └── view/goods/ └── ticket_detail.html # 前端票务详情页(独立UI)
测试数据:
- 商品 ID 112:VR演唱会电子票 2024,item_type=ticket
- 分类 ID 911:VR演唱会
- 座位模板 ID 1:Bird Nest - Zone A,绑定 category_id=911
- 3排座位:A排(AAAAAA,红色VIP区)/ B排(BBBBBB,蓝色看台区)/ C排(CCCCCC,绿色普通区)
五、Phase 1:Goods.php 改法 + 前端验证(2026-04-15 白天)
5.1 修改内容
文件:app/index/controller/Goods.php 位置:detail() 方法,return MyView(); 前约第 137-139 行
代码改动:
// --- VR 票务处理 start --- $goods = $result['data']['goods']; if (!empty($goods['item_type']) && $goods['item_type'] === 'ticket') { // 加载座位模板 $spec_base = Db::table('vr_seat_templates') ->where('category_id', $goods['category_id']) ->where('is_enable', 1) ->find(); $goods['vr_seat_template'] = $spec_base; // 加载 goods_spec_data(座位动态价格) $goods_spec_data = empty($goods['spec_base']) ? [] : json_decode($goods['spec_base'], true); $goods['vr_spec_data'] = $goods_spec_data; // 使用票务专用模板 $this->set_title($goods['title'].' - VR电子票'); return MyView('public/../../../plugins/vr_ticket/view/goods/ticket_detail', [ 'common' => $common, 'header' => $header, 'goods' => $goods, ]); } // --- VR 票务处理 end ---
5.2 前端票务详情页渲染结果
URL:http://localhost:10000/?s=index/goods/index/id/1(商品1改为 item_type=ticket 测试)
渲染效果:
- 舞台(舞 台)
- 座位图(三行):A排(AAAAAA,红色VIP区)/ B排(BBBBBB,蓝色看台区)/ C排(CCCCCC,绿色普通区)
- 图例(VIP区 / 看台 / 普通)
- 选座 UI(已选座位计数 + 合计价格)
- 场次选择
- 观演人表单(姓名+手机号)
六、Council 审议记录(2026-04-14)
6.1 Architect Round 1(已合并)
评审结论(Q2+Q4):
- Q2:spec 座位共用 vs 独立 → 确认方案
- Q4:spec 复用粒度 → 确认粒度
6.2 PM Round 2(已合并)
- 解决 plan.md 合并冲突
6.3 待 Council 审议的遗留问题
| 问题 | 状态 | 说明 |
|---|---|---|
| Q2(spec座位共用vs独立) | ✅ 已解决 | 见 ARCHITECTURE.md |
| Q3(观演人存储位置) | ⏳ 待 Council | 尚未最终确认 |
| Q4(spec复用粒度) | ✅ 已解决 | 见 ARCHITECTURE.md |
七、关键决策固化
| 决策 | 结论 | 备注 |
|---|---|---|
| 改 ShopXO 核心可以吗 | ✅ 可以,自己部署 | 原则已写入 README |
| 票务详情页方案 | ✅ Goods.php 1行判断 → ticket_detail.html | 已验证 |
| spec = 场次 | ✅ 确认 | 无需 vr_sessions 表 |
| 座位模板绑定分类 | ✅ 确认 | Q1 已解决 |
| item_type 字段 | ✅ ticket / normal | 触发票务逻辑开关 |
| 座位图渲染 | ✅ HTML Table + CSS Grid | 不依赖第三方库 |
| QR 安全 | ✅ AES_Encrypt | 防伪造 |
| AI 介入程度 | 90%+ | 模板/Hook/PHP/Vue 均为标准技术 |
八、当前状态快照(2026-04-15 09:00 CST)
8.1 Git Commit 历史
7508bed docs: 追加 vr-shopxo-plugin Phase 0/1 状态记录
0f5a82d feat(Phase 1): ShopXO Goods.php 修改(实际验证通过)
34f7045 feat(Phase 0): vr_ticket plugin skeleton complete
d5edb76 docs: add guiding principle + Goods.php modification guide
1c6d32b docs: add ShopXO hooks reference (v6.8.0) - extracted from source
e7b7bf9 docs: add plugin mechanism + requirements mapping docs
536ef9e docs: add 项目启动报告 REPORT-KICKOFF.md (issue #5)
8c6878e council(draft): Architect - 合并 Round 1 架构评审结论
9eae259 council(draft): Architect - Round 1 架构评审结论 (Q2+Q4)
8.2 Phase 完成度
| Phase | 状态 | 说明 |
|---|---|---|
| Phase 0:骨架 | ✅ 完成 | 14个文件,4张表,插件已注册 |
| Phase 1:前端票务详情页 | ✅ 完成 | Goods.php验证通过,座位图渲染正常 |
| Phase 2:后台管理页面 | ⏳ 待开始 | 场次管理/座位管理/票务订单列表 |
| Phase 3:支付回调 + 发票 | ⏳ 待开始 | 钩子联调 + QR 票生成 |
| Phase 4:B端扫码核销 | ⏳ 待开始 | 核销员管理 + 扫码 API |
8.3 关键文件路径
ShopXO 容器: 源码:~/.openclaw/workspace/council-research/shopxo-eval/.worktrees/shopxo-evaluator/shopxo-src/ 插件:shopxo-src/app/plugins/vr_ticket/ Goods.php:shopxo-src/app/index/controller/Goods.php
vr-shopxo-plugin 仓库: 插件代码:app/plugins/vr_ticket/ ShopXO 修改:shopxo-modifications/app/index/controller/Goods.php 文档:docs/
九、下一步计划
Phase 2:后台管理页面
-
座位模板管理(admin/controller/SeatTemplate.php)
- Layui 列表页(已生成 view)
- 创建/编辑/删除操作
-
电子票管理(admin/controller/Ticket.php)
- 票列表(支持按订单号/手机号搜索)
- 票详情(显示 QR 码)
- 导出功能
-
核销员管理(admin/controller/Verifier.php)
- 增删改查
-
核销记录(admin/controller/Verification.php)
- 核销历史列表
Phase 3:支付回调 + QR 票生成
- 实现 TicketService::onOrderPaid() → 支付成功时生成票
- Hook:plugins_service_order_pay_success_handle_end
- AES 加密 QR 数据
- ShopXO 站内通知或 Realtime 推送
Phase 4:B 端扫码核销
-
核销 API(B 端小程序调用)
- POST /api/ticket/verify(扫码枪调用)
- RLS 策略:profiles.role = 'staff' 可核销
-
核销员注册
- 后台添加核销员(手机号)
- 绑定 user_id
十、已知问题与待验证项
| 问题 | 优先级 | 状态 |
|---|---|---|
| 后台插件菜单无权限 | P1 | admin 已有 340/341,vr_ticket 控制器权限未单独分配 |
| 观演人存储位置(Q3) | P2 | 待 Council 审议 |
| spec_base JSON 结构最终版 | P2 | 已确认 Q4 方案,待落地 |
| 支付回调联调 | P2 | 等待 Phase 2 后台完成后测试 |
| 核销 API RLS | P2 | 待实现 |
十一、Phase 2 前台展示层完成(2026-04-20)
11.1 完成内容
Commit 7bd896764:
Goods.php:item_type=ticket → 绝对路径 View::fetch(ticket_detail.html) + 数据注入SeatSkuService.php:新增 GetGoodsViewData(),从 goods.vr_goods_config JSON + ShopXO 原生平表读取座位图+场次TicketService.php:onOrderPaid 改用 sxo_order_detail + JSON spec 解析座位号,幂等改为 seat_info
docs/14 修正:
- 修正了数据流描述(vrt_vr_goods_config → goods.vr_goods_config)
- 修正了表名(vrt_order_detail → sxo_order_detail)
- 删除了错误的 Think 驱动修改说明
新文档:
docs/PHASE2_PLAN.md(本文件):Phase 2 当前状态 + 下一步计划
11.2 模板渲染问题说明
Goods.php 绝对路径方案已实现,但 {include file="public/head"} 标签是否能在容器内正确解析待实测。
详见 docs/PHASE2_PLAN.md 第二章。
11.3 当前 Git 状态
7bd896764 feat(Phase 2): 完成票务商品前端展示层 ← HEAD
dc63cff77 chore: clean up my_test_plugin residual hooks
11.4 Phase 2 剩余工作
| 任务 | 状态 |
|---|---|
| 模板渲染实测(容器内) | ⚠️ 待大头操作 |
| loadSoldSeats() 实现 | ❌ 未开始 |
| vr_ticket Hook.php 补充 | ❌ 未开始 |
| 4 个后台控制器联调 | ❌ 未开始 |
| 核销 API | ❌ 未开始 |
11.5 清理记录(2026-04-20)
shopxo/test_ticket.php→ 移至_backup_20260420/test_ticket.php(临时测试脚本,不入仓库)docs/14_TEMPLATE_RENDER_INVESTIGATION.md→ 重写修正版(删除错误信息,保留调查价值)- 核心代码(Goods.php / SeatSkuService.php / TicketService.php)→ 全部提交推送
十二、模板渲染修复 + JSON 格式升级(2026-04-20 白天)
12.1 模板渲染修复(v2.0 路线 B)
问题:ThinkTemplate 的 {include file="public/head"} 标签在 Linux 下因 view_depr=/ 导致路径拼接错误,页面以纯文本输出。
解决方案(路线 B):
{include}/{:}ThinkTemplate 标签 →<?php echo ModuleInclude(...) ?>原生 PHP{$var|default='...'}→<?php echo $var ?? '...' ?>{json_decode(...)|raw}→<?php echo json_encode(...) ?>- 复制 ShopXO
app/index/view/default/public/→plugins/vr_ticket/view/goods/public/
提交记录:
349ec063c fix: 替换 ThinkTemplate 标签为 PHP ModuleInclude
c894e7018 fix: 复制 ShopXO public 模板 + 修复 footer_page 不存在问题
1b0ac3276 fix: 替换为票务专用精简 footer(449行→53行)
渲染结果:✅ 商品详情页正常渲染,但场次为空(待适配新 JSON 格式)。
12.2 vr_goods_config JSON 格式重新设计(重大变更)
背景:大头 + Gemini 重新设计了 vr_goods_config 规格,从依赖 vr_seat_templates 表实时查询,改为商品发布时快照模式。
新格式核心:
goods.vr_goods_config包含完整的rooms[]快照(座位图+sections+seats)- 不再需要实时查
vr_seat_templates表 spec_base_id_map格式:{room_id}_{row}_{colNum}→spec_base_id
设计原则:
- 商品发布时快照 → 已发布商品与
vr_seat_templates解耦 - 修改模板不影响已发布商品 → 绝对一致性
- SKU 和 config 一起过时、一起更新
新文档:
docs/VR_GOODS_CONFIG_SPEC.md— 新 JSON 格式完整规格说明(已确认)docs/PHASE2_PLAN.mdv2.0 — 同步更新,下一步工作计划
12.3 当前 Git 状态
1b0ac3276 fix: 替换为票务专用精简 footer ← HEAD
c894e7018 fix: 复制 ShopXO public 模板
349ec063c fix: 替换 ThinkTemplate 标签为 PHP ModuleInclude
7bd896764 feat(Phase 2): 完成票务商品前端展示层
12.4 接下来需要实现
| 任务 | 负责人 | 依赖 |
|---|---|---|
| 重写 GetGoodsViewData() 适配新格式 | 待定 | VR_GOODS_CONFIG_SPEC.md 已确认 |
| 更新 ticket_detail.html JS(rooms[] 结构) | 待定 | GetGoodsViewData() 输出确定后 |
| AdminGoodsSaveHandle SKU 生成 | 待定 | 新格式已确认 |
| loadSoldSeats() 实现 | 待定 | vr_tickets 有数据后 |