# 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()` ✅ 已实现（seatSpecMap.inventory 反推已售座位）

---

## 五、后续方向

### 方向 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` |
