7.1 KiB
7.1 KiB
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(新建)
核心方法:
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 示例):
[
'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
实现:
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 缓存:
// 清除所有 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 后)
docker exec shopxo-php bash -c "php -r '
// 读取 goods_id=118 的 spec_key 样本
// 验证排序是否正确
'"
4.2 API 测试(Step 3 后)
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> 0template_count<<flat_count(模板去重生效)cache_hit: false(首次),true(后续请求)
五、风险评估
| 风险 | 概率 | 影响 | 缓解 |
|---|---|---|---|
| 前端 spec_key 生成逻辑不一致 | 中 | 高 | 前端必须使用与后端相同的字母排序规则 |
| 缓存失效时机 | 低 | 中 | 先实现缓存清除逻辑 |
| 大数据量性能 | 低 | 中 | 缓存 TTL=60s,定期失效 |
六、后续工作
- 前端适配: 修改
ticket_detail.html的 spec_key 生成逻辑 - 多模板支持: 当前 design 支持多场馆×多模板,需要扩展
- inventory=0 处理: 确认是否在 flat_inventory 中包含已售座位
文档状态:规划完成,待实施