vr-shopxo-plugin/docs/DEVELOPMENT_LOG.md

679 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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+ 钩子清单
**插件核心机制:**
1. config.json — 插件元数据(名称/版本/依赖/菜单/权限/静态资源)
2. BaseService — 插件业务服务基类GetDb / 参数校验 / 日志)
3. EventListener.php — 生命周期钩子Install/Uninstall/Upgrade/Index
4. URL 路由 — 后台控制器 plugins_admin 前缀,前台 plugins 前缀
5. 视图 — 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 票务详情页方案抉择
方案 AURL 劫持 ❌ 放弃
- 缺点:无法继承商品详情页基础样式,改动 ShopXO 核心代码量大
方案 BCSS 隐藏标准 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` 名称。
**示例配置:**
```php
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 112VR演唱会电子票 2024item_type=ticket
- 分类 ID 911VR演唱会
- 座位模板 ID 1Bird Nest - Zone A绑定 category_id=911
- 3排座位A排AAAAAA红色VIP区/ B排BBBBBB蓝色看台区/ C排CCCCCC绿色普通区
---
## 五、Phase 1Goods.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 前端票务详情页渲染结果
URLhttp://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
- Q2spec 座位共用 vs 独立 → 确认方案
- Q4spec 复用粒度 → 确认粒度
### 6.2 PM Round 2已合并
- 解决 plan.md 合并冲突
### 6.3 待 Council 审议的遗留问题
| 问题 | 状态 | 说明 |
|------|------|------|
| Q2spec座位共用vs独立 | ✅ 已解决 | 见 ARCHITECTURE.md |
| Q3观演人存储位置 | ⏳ 待 Council | 尚未最终确认 |
| Q4spec复用粒度 | ✅ 已解决 | 见 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 4B端扫码核销 | ⏳ 待开始 | 核销员管理 + 扫码 API |
### 8.3 关键文件路径
ShopXO 容器:
源码:~/.openclaw/workspace/council-research/shopxo-eval/.worktrees/shopxo-evaluator/shopxo-src/
插件shopxo-src/app/plugins/vr_ticket/
Goods.phpshopxo-src/app/index/controller/Goods.php
vr-shopxo-plugin 仓库:
插件代码app/plugins/vr_ticket/
ShopXO 修改shopxo-modifications/app/index/controller/Goods.php
文档docs/
---
## 九、下一步计划
### Phase 2后台管理页面
1. 座位模板管理admin/controller/SeatTemplate.php
- Layui 列表页(已生成 view
- 创建/编辑/删除操作
2. 电子票管理admin/controller/Ticket.php
- 票列表(支持按订单号/手机号搜索)
- 票详情(显示 QR 码)
- 导出功能
3. 核销员管理admin/controller/Verifier.php
- 增删改查
4. 核销记录admin/controller/Verification.php
- 核销历史列表
### Phase 3支付回调 + QR 票生成
1. 实现 TicketService::onOrderPaid() → 支付成功时生成票
2. Hookplugins_service_order_pay_success_handle_end
3. AES 加密 QR 数据
4. ShopXO 站内通知或 Realtime 推送
### Phase 4B 端扫码核销
1. 核销 APIB 端小程序调用)
- POST /api/ticket/verify扫码枪调用
- RLS 策略profiles.role = 'staff' 可核销
2. 核销员注册
- 后台添加核销员(手机号)
- 绑定 user_id
---
## 十、已知问题与待验证项
| 问题 | 优先级 | 状态 |
|------|--------|------|
| 后台插件菜单无权限 | P1 | admin 已有 340/341vr_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**
1. `{include}` / `{:}` ThinkTemplate 标签 → `<?php echo ModuleInclude(...) ?>` 原生 PHP
2. `{$var|default='...'}``<?php echo $var ?? '...' ?>`
3. `{json_decode(...)|raw}``<?php echo json_encode(...) ?>`
4. 复制 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: 替换为票务专用精简 footer449行→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.md` v2.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 JSrooms[] 结构) | 待定 | GetGoodsViewData() 输出确定后 |
| AdminGoodsSaveHandle SKU 生成 | 待定 | 新格式已确认 |
| loadSoldSeats() 实现 | 待定 | vr_tickets 有数据后 |
---
## 十三、模板渲染修复 + vr_goods_config v3.0 格式确认2026-04-20 上午)
### 13.1 模板渲染修复
**问题**ThinkTemplate 的 `{include file="public/head"}` 在 Linux 下因 `view_depr=/` 导致路径拼接错误。
**方案(路线 B**ThinkTemplate → PHP ModuleInclude
- `{include file=...}``<?php echo ModuleInclude(...) ?>`
- `{:Config()} / {:IsMobileLogin()}``<?php echo Config() ?> / <?php echo IsMobileLogin() ?>`
- `{$var|default='...'}``<?php echo $var ?? '...' ?>`
- `{if}...{/if}``<?php if():?>...<?php endif;?>`
复制 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: 替换为票务专用精简 footer449行→53行
```
### 13.2 vr_goods_config JSON 格式 v3.0 最终确认
**变更历程**
- v2.0rooms 直接嵌入,但 selected_sections 格式不确定
- v3.0(最终):增加 `template_snapshot` 字段selected_sections 确认为对象格式
**最终 v3.0 结构**
```json
{
"version": 3.0,
"template_id": 4,
"selected_rooms": ["room_id_xxx"],
"selected_sections": { "room_id_xxx": ["A", "B"] },
"sessions": [{ "start": "15:00", "end": "16:59" }],
"template_snapshot": {
"venue": { ... },
"rooms": [{ "id": "...", "map": [...], "sections": [...], "seats": {...} }]
}
}
```
**核心设计决策**
- `template_id`:发布/编辑时读取最新 vr_seat_templates 的依据
- `template_snapshot`:发布时从 vr_seat_templates 读取并存储的快照,前端渲染数据来源
- `selected_sections`:对象格式 `{ room_id: ["A","B"] }`(每个房间各自的选择)
- `spec_base_id_map`不入库GetGoodsViewData 从 `goods_spec_base.extends->seat_key` 动态构建
- `seat_key` 格式:`{roomId}_{rowLabel}_{colNum}`(无 MD5
- 现有前端编辑体验**完全不受影响**前端只提交选择项template_snapshot 由后端保存时填充)
### 13.3 spec_base_id_map 断路问题
**根因**BatchGenerate 生成 GoodsSpecBase.id 后,从未写入 spec_base_id_map。
**解决方案**
- BatchGenerate 写入 `goods_spec_base.extends.seat_key = "roomId_rowLabel_colNum"`
- GetGoodsViewData 从 `extends.seat_key` 动态构建 `spec_base_id_map`
### 13.4 Git 状态
```
741f25451 docs: v3.0 最终规格 - template_snapshot 字段 + selected_sections 对象格式
6daa33232 docs: new vr_goods_config spec + Phase 2 v3.0 plan
1b0ac3276 fix: 替换为票务专用精简 footer
c894e7018 fix: 复制 ShopXO public 模板
349ec063c fix: 替换 ThinkTemplate 标签为 PHP ModuleInclude
7bd896764 feat(Phase 2): 完成票务商品前端展示层
```
### 13.5 Issue 记录
- **Issue #13**[P0] vr_goods_config v3.0 落地实现
- Step 1AdminGoodsSaveHandle 填充 template_snapshot
- Step 2BatchGenerate 写入 extends.seat_key
- Step 3GetGoodsViewData 重写
- Step 4ticket_detail.html JS seatKey 格式更新
### 13.6 AdminGoodsSaveHandle template_snapshot 填充逻辑澄清
**template_snapshot 的前端职责 vs 后端职责**
- **前端**Admin 编辑页打开时):用 `template_id` 读最新 vr_seat_templates填充 `template_snapshot` 到表单数据,一并提交
- **后端**AdminGoodsSaveHandle save_thing_end检测 `template_snapshot` 是否缺失,若缺失则从 vr_seat_templates 读表填充,再写回 goods 表,然后 BatchGenerate
这意味着:
- `template_snapshot` 主要由**前端**在编辑页加载时填充
- 后端只是兜底(兼容旧商品、或前端未传的情况)
- BatchGenerate 仍读 `vr_seat_templates` 表(实时数据),不受 template_snapshot 影响
- 前端展示用 `template_snapshot`SKU 生成用 `vr_seat_templates`(数据层和展示层分离)
**提交**
```
bbea35d83 feat(AdminGoodsSaveHandle): 保存时自动填充 template_snapshot
```
---
### 13.7 Issue #13 根因修复 — template_snapshot.rooms 为空2026-04-20
**问题现象**:商品保存后 `vr_goods_config``template_snapshot.rooms = []`,但 `selected_rooms = ["room_0"]` 有值。
**三层根因及修复**
#### 根因 1前端不发送 template_snapshot
`outputBase64` 的 JSON 结构里不包含 `template_snapshot`,后端 `save_thing_end` 拿到的数据里 `template_snapshot` 可能是旧的或空的。
**修复**AdminGoodsSaveHandle.php`save_thing_end` 时改为**永远从 DB 重建** template_snapshot而非等它为空才读。条件`selected_rooms 有值 || template_snapshot 为空 || template_snapshot.rooms 为空`。
#### 根因 2`save_thing_end` 的 `$params['data']` 是值拷贝
ShopXO 的 `save_thing_end` 传入 `$data` 是事务快照(值传递),不是引用。`$params['data']['vr_goods_config']` 里的值可能和 DB 里不一致。
**修复**:改用 `Db::name('Goods')->find($goodsId)` 直接从 DB 读,加 fallback 到 `$params['data']`
#### 根因 3room.id 为空导致 ID 匹配失败
模板5的 `rooms[0].id = ""`(空字符串),前端发 `selected_rooms = ["room_0"]`filter 里的匹配逻辑找不到对应房间空id无法通过前缀匹配或直接匹配
**修复**AdminGoodsSaveHandle.php空 id 时用数组索引替代 `room_N`N 从0开始
#### 根因 4幽灵配置软删除场馆仍出现在表单
模板软删除后,前端 `getRooms()` 返回 `[]`,但已保存配置里的 template_snapshot.venue 信息还在,导致 checkbox 选中却无法操作。
**修复**AdminGoodsSave.php加载时用 `Set(validTemplateIds)` 过滤掉 `status=0` 模板的配置。
**提交**
```
05b69588f chore: remove debug logging from AdminGoodsSaveHandle
c03737308 fix(Admin): 改用 random_int() CSPRNG修正 UUID v4 版本/变体位
1244adfaa feat(Admin): SeatTemplateSave 时为无 id 的 room 生成 UUID
8a33e7fa2 fix(AdminGoodsSaveHandle): 空id房间用数组索引匹配 room_0/room_1
da001797a fix(vr_ticket): template_snapshot 重建逻辑重写 + 幽灵配置过滤
```
---
### 13.8 room.id 生成逻辑2026-04-20
**问题**:早期模板数据的 `room.id` 为空(老格式无 id 字段),导致前端 selected_rooms 无法正确匹配。
**修复**Admin.php → `SeatTemplateSave`):保存场馆模板时,若 `room.id` 为空则生成 UUID v4 格式。
```php
if (empty($room['id'])) {
$room['id'] = sprintf('%08x-%04x-%04x-%04x-%04x%08x',
time(),
random_int(0, 0xffff),
random_int(0, 0xffff),
(random_int(0, 0x3fff) & 0x0fff) | 0x4000, // 版本4 + 变体10xx
random_int(0, 0xffff),
random_int(0, 0xffffffff));
}
```
- `random_int()`PHP 7+ CSPRNG优于 `mt_rand()`(可被种子预测)
- 版本 nibble = 4UUID v4变体 = 10xxRFC 4122
- 已有房间编辑保存时会自动补上 id不影响已有数据
**已有兜底兼容逻辑(无需改动)**
- AdminGoodsSave.php 第36-40行`room.id` 为空时用 `room_N` 索引兼容
- SeatSkuService.php 第100行`id` 为空时用 `room_{index}` 兼容
---
### 13.9 Debug 代码清理2026-04-20
移除了 AdminGoodsSaveHandle.php 中所有调试日志代码vr_debug.log 写入),不影响正常功能。