# VR Tree API 实现计划 > 创建时间:2026-05-15 > 负责人:大头(代码实施) > 参考文档:`docs/14_TREE_API_DESIGN.md`、`docs/TASK_TREE_API_IMPLEMENTATION.md` --- ## 一、设计确认 ### 1.1 设计决策(已与大头确认) | 决策项 | 结论 | 原因 | |--------|------|------| | spec_key 排序 | **字母顺序** | 未来扩展新维度时自动融入正确位置,无需修改排序逻辑 | | tree 分组维度 | **4 层**(不含座位号) | 座位号是最底层扁平元素,与 SKU/库存绑定,通过 spec_key 前缀匹配查找 | | inventory=0 | **保留并返回** | 便于前端座位图标记已售状态,与座位模板一一对应 | ### 1.2 spec_key 格式(已确定) ``` $vr-场次=15:00-16:59|$vr-场馆=鸟巢|$vr-演播室=主厅|$vr-分区=A|$vr-座位号=1排1座 (按字母顺序排序) ``` > **重要**:前端生成 spec_key 时必须使用相同的字母排序规则。 --- ## 二、实现任务清单 ### Task 1: 创建 QueryManager 服务 ✅ **文件**: `service/QueryManager.php`(新建) **核心方法**: ```php class QueryManager { /** * 主入口:生成层级树 * @param int $goodsId * @param array $groupBy e.g. ['venue', 'session', 'room', 'section'] * @param array $seatSpecMap * @return array ['tree' => [...], 'template_keys' => [...]] */ public static function buildTree(int $goodsId, array $groupBy, array $seatSpecMap): array /** * 构建扁平 SKU 列表 * @param array $seatSpecMap * @return array flat_inventory */ public static function buildFlatInventory(array $seatSpecMap): array /** * 构建模板去重池 * @param array $templateKeys * @return array seat_templates_flat */ public static function buildTemplatePool(array $templateKeys): array } ``` **分组维度映射**: | group_by 值 | spec_key 前缀 | 对应维度 | |-------------|---------------|----------| | `venue` | `$vr-场馆=` | venueName | | `session` | `$vr-场次=` | sessionName | | `room` | `$vr-演播室=` | roomName | | `section` | `$vr-分区=` | sectionName | **层级树结构**(venue-first 示例): ```php [ 'tree' => [ '鸟巢' => [ 'name' => '鸟巢', 'min_price' => 280, 'has_available' => true, 'rooms' => [ '主厅' => [ 'name' => '主厅', 'min_price' => 380, 'has_available' => true, 'sections' => [ 'A' => [ 'template_key' => '鸟巢_主厅_A', 'price' => 680, 'inventory' => 12, 'has_available' => true ], // ... ] ] ], 'sessions' => [ '15:00-16:59' => ['min_price' => 380, 'has_available' => true], '20:00-21:59' => ['min_price' => 280, 'has_available' => false] ] ] ] ] ``` --- ### Task 2: 实现 tree() API 接口 ✅ **文件**: `api/Goods.php` **路由**: ``` GET /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=goods&pluginsaction=tree&goods_id=118&group_by=venue,session,room,section ``` **实现**: ```php public function tree() { $goodsId = input('goods_id', 0, 'intval'); $groupBy = input('group_by', 'venue,session,room,section', 'trim'); $groupBy = array_filter(array_map('trim', explode(',', $groupBy))); if ($goodsId <= 0) { return self::error('goods_id 无效'); } // 1. 缓存检查 $cacheKey = 'vr_tree_' . $goodsId . '_' . md5(implode(',', $groupBy)); $cached = \think\facade\Cache::get($cacheKey); if ($cached !== null) { $cached['meta']['cache_hit'] = true; return self::success($cached); } // 2. 读取数据源 $seatMapData = SeatMapService::GetSeatMap($goodsId); $seatSpecMap = $seatMapData['seatSpecMap'] ?? []; // 3. 调用 QueryManager $treeData = QueryManager::buildTree($goodsId, $groupBy, $seatSpecMap); $flatInventory = QueryManager::buildFlatInventory($seatSpecMap); $templates = QueryManager::buildTemplatePool($treeData['template_keys'] ?? []); // 4. 组装响应 $result = [ 'goods_id' => $goodsId, 'group_by' => $groupBy, 'tree' => $treeData['tree'], 'seat_templates_flat' => $templates, 'flat_inventory' => $flatInventory, 'meta' => [ 'flat_count' => count($flatInventory), 'template_count' => count($templates), 'cache_hit' => false, 'computed_at' => time(), ], ]; // 5. 写入缓存(TTL = 60s) \think\facade\Cache::set($cacheKey, $result, 60); return self::success($result); } ``` --- ### Task 3: 处理缓存失效 **位置**: 订单支付成功回调 在 `SeatMapService::ClearCache()` 被调用时,同时清除 tree 缓存: ```php // 清除所有 group_by 组合的 tree 缓存(可以存储一个 set 记录 key) // 或简单清除带前缀的缓存 \think\facade\Cache::delete('vr_tree_' . $goodsId . '_'); ``` --- ## 三、实现顺序 | Step | 任务 | 文件 | 优先级 | |------|------|------|--------| | 1 | **创建 QueryManager** | QueryManager.php | 🔴 核心 | | 2 | **实现 tree() API** | Goods.php | 🔴 核心 | | 3 | 处理缓存失效 | 订单回调处 | 🟡 后续 | | 4 | 前端适配 | ticket_detail.html | 🟡 后续 | --- ## 四、测试验证 ### 4.1 修复验证(Step 1 后) ```bash docker exec shopxo-php bash -c "php -r ' // 读取 goods_id=118 的 spec_key 样本 // 验证排序是否正确 '" ``` ### 4.2 API 测试(Step 3 后) ```bash curl "http://localhost:10000/api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=goods&pluginsaction=tree&goods_id=118&group_by=venue,session,room,section" \ -H "X-Requested-With: XMLHttpRequest" | python3 -c " import json,sys d = json.load(sys.stdin) if d['code'] == 0: data = d['data'] print('✅ flat_count:', data['meta']['flat_count']) print('✅ template_count:', data['meta']['template_count']) print('✅ venues:', list(data['tree'].keys())) print('✅ cache_hit:', data['meta']['cache_hit']) else: print('❌ error:', d['msg']) " ``` **预期结果**: - `flat_count` > 0 - `template_count` << `flat_count`(模板去重生效) - `cache_hit: false`(首次),`true`(后续请求) --- ## 五、风险评估 | 风险 | 概率 | 影响 | 缓解 | |------|------|------|------| | 前端 spec_key 生成逻辑不一致 | 中 | 高 | 前端必须使用与后端相同的字母排序规则 | | 缓存失效时机 | 低 | 中 | 先实现缓存清除逻辑 | | 大数据量性能 | 低 | 中 | 缓存 TTL=60s,定期失效 | --- ## 六、后续工作 1. **前端适配**: 修改 `ticket_detail.html` 的 spec_key 生成逻辑 2. **多模板支持**: 当前 design 支持多场馆×多模板,需要扩展 3. **inventory=0 处理**: 确认是否在 flat_inventory 中包含已售座位 --- *文档状态:规划完成,待实施*