docs: 修正 docs/14 + 新增 PHASE2_PLAN.md

- docs/14: 修正数据流/表名/Think驱动等3处错误,删除过时状态
- PHASE2_PLAN.md: Phase 2 当前状态 + 模板渲染待验证项 + 下一步计划
- DEVELOPMENT_LOG.md: 追加 Phase 2 完成记录
council/SecurityEngineer
Council 2026-04-20 05:24:26 +08:00
parent 7bd8967648
commit 914e2a0fc3
3 changed files with 376 additions and 0 deletions

View File

@ -0,0 +1,182 @@
# VR-Ticket 模板渲染问题调查报告(已修正)
> 生成时间2026-04-19 20:39 CST
> 修正时间2026-04-20 CST
> 背景Phase 0/1 完成后,开始 Phase 2 前台模板渲染调试
---
## ⚠️ 重要修正说明2026-04-20
本文档为 2026-04-19 的调查记录,**部分内容有误**,已在此版本中修正:
| 章节 | 原错误 | 修正 |
|------|--------|------|
| 2.2 数据流 | 误以为读取 `vrt_vr_goods_config` 等独立表 | 实际:`goods.vr_goods_config` + ShopXO 原生表 `goods_spec_value` / `goods_spec_base` |
| 2.3 表名 | `vrt_order_detail` | 实际:`sxo_order_detail`ShopXO 原生平表) |
| 2.4 Think驱动 | 认为需要修改 vendor 文件 | Goods.php 绝对路径方案无需改 Think 驱动 |
| 6.决策 | "先不提交" | 代码已提交commit 7bd896764文档状态过时 |
---
## 一、问题描述
访问票务商品详情页 `http://localhost:10000/?s=goods/index/id/118.html` 时,模板**完全没有渲染**
浏览器源码中显示原始模板标签:
```
{include file="public/head" /}
{$goods.title|default='VR演唱会'}
选择场次
该商品暂无场次信息
...
```
ThinkTemplate 语法标签(`{include}`、`{$...}`、`{if}`)均以原文形式输出,页面 UI 无法正常显示。
---
## 二、已完成的代码修改(已验证)
### 2.1 Goods.php前台商品详情控制器
**文件:** `shopxo/app/index/controller/Goods.php`
**改动内容:** 在 `Goods::Index()` 方法中,`$this->PluginsHook()` 之后、`return MyView()` 之前,新增以下判断:
```php
// 票务商品:加载自定义模板并注入座位数据
if (($goods['item_type'] ?? '') === 'ticket') {
$viewData = \app\plugins\vr_ticket\service\SeatSkuService::GetGoodsViewData($goods_id);
MyViewAssign([
'vr_seat_template' => $viewData['vr_seat_template'] ?? null,
'goods_spec_data' => $viewData['goods_spec_data'] ?? [],
]);
// 使用绝对路径 + fetch() 方法
$tplFile = ROOT . 'app' . DS . 'plugins' . DS . 'vr_ticket' . DS . 'view' . DS . 'goods' . DS . 'ticket_detail.html';
return \think\facade\View::fetch($tplFile, $assign);
}
```
**状态:** ✅ 已提交7bd896764
---
### 2.2 SeatSkuService.php座位 SKU 服务层)
**文件:** `shopxo/app/plugins/vr_ticket/service/SeatSkuService.php`
**新增方法:** `GetGoodsViewData(int $goodsId): array`
**实际数据流(修正版):**
1. 从 `goods.vr_goods_config` JSON 字段读取商品配置(不是独立的 vrt_vr_goods_config 表)
2. 解析 JSON取出第一个配置块的 `template_id``sessions` 场次列表
3. 从 `vr_seat_templates` 表查询座位模板(含 seat_map / spec_base_id_map JSON
4. 从 ShopXO 原生表 `goods_spec_value` + `goods_spec_base` 联查,生成场次列表
5. 返回 `['vr_seat_template' => [...], 'goods_spec_data' => [...], 'goods_config' => [...]]`
**状态:** ✅ 已提交7bd896764
---
### 2.3 TicketService.php票务核心服务层
**文件:** `shopxo/app/plugins/vr_ticket/service/TicketService.php`
**修复内容(修正版):**
- `onOrderPaid()` 表名:`OrderGoods` → `order_detail`(映射到 ShopXO 原生平表 `sxo_order_detail`
- `sxo_order_detail.spec` 是 JSON 格式,需要 `json_decode()` 解析座位号
- 幂等保护:改用 `seat_info` 而非 `spec_base_id`(同一订单+同一座位名只发一张票)
- 观演人信息从 `order.extension_data` JSON 中读取
**状态:** ✅ 已提交7bd896764
---
## 三、调查过程中的关键发现
### 3.1 ThinkTemplate include 标签解析机制
**流程追溯:**
1. `View::fetch($absPath)` → Think 驱动的 `fetch()`
2. Think 驱动调用 `$this->template->fetch($template, $data)`ThinkTemplate 实例)
3. ThinkTemplate `fetch()` → 读取文件内容 → `parse($content)``parseInclude($content)`
4. `parseInclude()``parseTemplateName($file)``parseTemplateFile($template)`
5. `parseTemplateFile()``$template = $this->config['view_path'] . $template . '.' . $view_suffix`
**关键问题:** 使用绝对路径时ThinkTemplate 需要知道 `view_path` 应指向插件模板所在目录,否则 `{include file="public/head"}` 会找不到 `/var/www/html/app/index/view/default/public/head.html`
### 3.2 ShopXO 原生模板不使用 `{include}` 标签
**发现:** ShopXO 原生模板编译缓存使用 `ModuleInclude()` 而非 ThinkTemplate 原生的 `{include}` 标签:
```php
<?php echo ModuleInclude('public/header'); ?>
```
这说明 ShopXO 用自己的 `ModuleInclude()` 函数替代了部分 include 机制。
**但票务模板仍用 `{include}`** —— 因为:
- `ticket_detail.html` 是完全独立的票务 UI不继承 ShopXO 商品页基础布局
- 使用 `{include}` 可以让 ThinkTemplate 正常解析 `{$...}` 变量和 `{if}` 标签
- 最终解决方案Goods.php 绝对路径 + `View::fetch()`)使 `{include}` 能正确定位
### 3.3 Linux 路径分隔符问题
**发现:** ThinkTemplate 在 Linux 下 `view_depr``/``str_replace` 将 `/` 替换为 `\`,导致路径拼接错误。
**解决:** Goods.php 直接传绝对路径给 `View::fetch()`,绕过了 ThinkTemplate 的 `view_path` 拼接逻辑。
---
## 四、模板渲染现状
| 项目 | 状态 | 说明 |
|------|------|------|
| Goods.php 绝对路径 | ✅ 正常 | View::fetch($tplFile) 带绝对路径 |
| `{include file="public/head"}` | ⚠️ 待验证 | 理论上可以工作,需在容器内实测 |
| `{$vr_seat_template.seat_map\|raw}` | ⚠️ 待验证 | ThinkTemplate 变量解析 |
| `ModuleInclude()` | ❌ 未使用 | 票务模板不需要 |
**已知待解决问题P1**
1. `{include}` 标签在容器内是否能正确解析到 ShopXO 公共模板
2. `plugins_service_goods_spec_data` 钩子 vr_ticket 未实现
3. `loadSoldSeats()` 函数为空TODO
---
## 五、后续方向
### 方向 A推荐实测 `{include}` 标签
在容器内访问票务商品详情页,观察 `{include file="public/head"}` 是否被正确解析为 ShopXO 公共头部。
若解析失败,切换到方向 B。
### 方向 B`ModuleInclude()` 替换
`{include file="public/head" /}` 改为 `<?php echo ModuleInclude('public/head'); ?>`
### 方向 C纯内联
将所需 CSS/JS 直接写在 `ticket_detail.html` 中,完全去掉 include。
---
## 六、关联提交
- **7bd896764** `feat(Phase 2): 完成票务商品前端展示层`
- Goods.php: item_type=ticket 时加载 ticket_detail.html
- SeatSkuService.php: 新增 GetGoodsViewData()
- TicketService.php: onOrderPaid 用 sxo_order_detail + JSON spec 解析
---
## 附录:相关文件路径
| 文件 | 路径 |
|------|------|
| 票务商品模板 | `shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html` |
| Goods 控制器 | `shopxo/app/index/controller/Goods.php` |
| SeatSku 服务 | `shopxo/app/plugins/vr_ticket/service/SeatSkuService.php` |
| Ticket 服务 | `shopxo/app/plugins/vr_ticket/service/TicketService.php` |

View File

@ -397,3 +397,51 @@ vr-shopxo-plugin 仓库:
| spec_base JSON 结构最终版 | P2 | 已确认 Q4 方案,待落地 | | spec_base JSON 结构最终版 | P2 | 已确认 Q4 方案,待落地 |
| 支付回调联调 | P2 | 等待 Phase 2 后台完成后测试 | | 支付回调联调 | P2 | 等待 Phase 2 后台完成后测试 |
| 核销 API RLS | P2 | 待实现 | | 核销 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→ 全部提交推送

146
docs/PHASE2_PLAN.md Normal file
View File

@ -0,0 +1,146 @@
# Phase 2 — 计划与当前状态
> 版本v1.0 | 日期2026-04-20 | 状态:执行中
> 关联提交7bd896764
---
## 一、Phase 2 完成情况
### ✅ 已完成
| 任务 | 文件 | 说明 |
|------|------|------|
| Goods.php 改法 | `app/index/controller/Goods.php` | item_type=ticket → ticket_detail.html + 数据注入 |
| GetGoodsViewData() | `SeatSkuService.php` | 为前端模板提供座位图+场次数据 |
| onOrderPaid() 修复 | `TicketService.php` | sxo_order_detail + JSON spec 解析 |
| 模板渲染调研 | `docs/14_TEMPLATE_RENDER_INVESTIGATION.md` | 已修正,记录完整调查过程 |
### ⚠️ 待验证(容器内)
| 任务 | 优先级 | 说明 |
|------|--------|------|
| `{include}` 标签实测 | P0 | ticket_detail.html 中 `{include file="public/head"}` 是否能正确解析 |
| ModuleInclude 备选方案 | P1 | 若 `{include}` 失败,切换 `ModuleInclude()` |
| loadSoldSeats() | P1 | 查询已售座位,前端座位图需显示已售状态 |
### ❌ 未开始
| 任务 | 说明 |
|------|------|
| vr_ticket Hook.php 钩子补充 | 缺失 `plugins_service_goods_spec_data` 处理 |
| 后台座位模板管理 | admin/controller/SeatTemplate.php已生成未调试 |
| 后台电子票列表 | admin/controller/Ticket.php已生成未调试 |
| 后台核销员管理 | admin/controller/Verifier.php已生成未调试 |
| 后台核销记录 | admin/controller/Verification.php已生成未调试 |
| 核销 API | B 端扫码核销 REST 接口 |
---
## 二、模板渲染问题现状
### 问题
票务商品详情页 ThinkTemplate 标签未解析(`{$...}` / `{include}` / `{if}` 以原文输出)。
### 根因
Goods.php 原来用 `MyView()` 加载主题模板,票务商品需要加载插件独立模板 `ticket_detail.html`
### 解决路径Goods.php 绝对路径方案)
```
Goods::Index()
→ $goods['item_type'] === 'ticket'
→ SeatSkuService::GetGoodsViewData($goods_id)
→ MyViewAssign([vr_seat_template, goods_spec_data])
→ View::fetch($tplFile) [$tplFile = 绝对路径]
→ ThinkTemplate 渲染 ticket_detail.html含 {include} 标签)
```
### 待实测项(容器内操作)
```bash
# 在 shopxo-php 容器内
docker exec -it shopxo-php bash
cd /var/www/html
curl "http://localhost:10000/?s=goods/index/id/118.html"
# 检查:
# 1. {include file="public/head"} 是否被解析为 HTML 内容
# 2. {$goods.title} 是否显示商品标题
# 3. 座位图是否正常渲染
```
### 失败备选
`{include}` 标签失败,修改 `ticket_detail.html`
```php
<!-- 替换 -->
{include file="public/head" /}
<!-- 改为 -->
<?php echo ModuleInclude('public/head'); ?>
```
---
## 三、Phase 2 接下来的工作
### Step 1模板渲染实测容器内
**操作人:** 大头(容器在本机)
```bash
docker ps | grep shopxo-php # 确认容器运行中
curl -s "http://localhost:10000/?s=goods/index/id/118.html" | head -50
```
**成功标准:** HTML 源码中不再有 ThinkTemplate 标签(`{include}` / `{$` / `{if}`),座位图 div 正常显示。
### Step 2座位图已售状态
SeatSkuService 需要补充 `loadSoldSeats()` 方法,查询 `vr_tickets` 表中该商品+场次已生成的票,返回已售座位 ID 列表,前端据此灰化已售座位。
### Step 3后台管理页面联调
4 个后台控制器SeatTemplate / Ticket / Verifier / Verification均已生成需要
1. 确认路由可访问(后台 URL 格式)
2. 验证 CRUD 操作正常
3. 确认 RLS 策略
### Step 4核销 API
`POST /api/vr_ticket/verify` — B 端小程序扫码调用。
---
## 四、数据库表结构(当前)
| 表名 | 用途 | 状态 |
|------|------|------|
| `vrt_vr_seat_templates` | 座位模板 | ✅ |
| `vrt_vr_tickets` | 电子票 | ✅ |
| `vrt_vr_verifiers` | 核销员 | ✅ |
| `vrt_vr_verifications` | 核销记录 | ✅ |
| `vrt_vr_audit_log` | 审计日志 | ✅ |
| `goods.vr_goods_config` | 商品配置JSON | ✅ |
| `sxo_order_detail` | 订单明细ShopXO | ✅ |
---
## 五、已知风险
| 风险 | 影响 | 缓解 |
|------|------|------|
| `{include}` 标签容器内解析失败 | P0页面无样式 | 切换 ModuleInclude 方案 |
| shopxo-php 容器未启动 | 无法验证 | 每次操作前 `docker ps` 确认 |
| Admin 控制器鉴权链不完整 | 后台无法访问 | 确认继承 Common 并调用 IsLogin/IsPower |
| 座位模板与商品分类绑定逻辑 | 需确认 1:N 还是 1:1 | 实测验证 |
---
## 六、决策点(待大头确认)
1. **模板 include 方案**:先试 `{include}`,失败后换 `ModuleInclude()`,还是直接内联 CSS/JS
2. **loadSoldSeats()**:是否需要实时查库,还是前端纯靠 JS 状态管理?
3. **后台前端框架**Layui 是否继续使用,还是改用其他方案?