vr-shopxo-plugin/docs/TASK_TREE_API_IMPLEMENTATIO...

220 lines
7.5 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.

# Task BriefVR Tree API 实现
> 目标:实现参数化层级生成器 API
> 参考文档:`docs/14_TREE_API_DESIGN.md`
> 状态:规划完成,待实现
---
## 背景
当前 `SeatMapService::GetSeatMap()` 返回扁平 `seatSpecMap`,前端需要 O(n²) 重建层级,且 session 作为顶层不符合业务逻辑。
新 API `/api/goods/tree` 返回:
1. **层级树**(按 `group_by` 参数动态生成,叶节点含 `template_key`
2. **模板去重池**`seat_templates_flat`,每个模板只存一份)
3. **扁平 SKU 列表**(用于前端本地 spec_key 前缀匹配)
---
## 实现步骤
### Step 1验证现有数据结构
**验证文件**
- `service/SeatSkuService.php` — 确认 `SPEC_DIMS` 顺序和 `makeSpecKey` 算法
- `service/SeatMapService.php` — 确认 `buildSeatSpecMap` 输出结构
**验证内容**
1. `spec_key` 格式:维度按固定顺序排序,用 `|` 连接
2. `template_key` 生成:当前代码中是否有 venue+room+section 组合的模板 key格式是什么
3. seat_specMap 中的 `inventory=0` 座位是否被过滤?
```php
// 在 ShopXO Docker 容器里验证
docker exec shopxo-php bash -c "php -r '
// 读取 goods_id=118 的 seatSpecMap
// 打印前 3 条记录的 spec_key 格式
// 打印 template_snapshot 的 venue+rooms 结构
'"
```
### Step 2实现 QueryManager 服务
**文件**`service/QueryManager.php`
**核心方法**
```php
class QueryManager
{
// 主入口:生成层级树
public static function buildTree(int $goodsId, array $groupBy): array
// 分组聚合(核心逻辑)
private static function aggregate(array $flatInventory, array $groupBy): array
// 生成模板去重池
private static function buildTemplatePool(array $flatInventory, array $tree): array
// 生成扁平 SKU 列表(用于前端本地筛选)
private static function buildFlatInventory(array $seatSpecMap): array
}
```
**聚合逻辑**
```
输入flatInventory (seatSpecMap), groupBy = ['venue', 'session', 'room', 'section']
输出:层级树
遍历每个 SKU
1. 解析 spec_key提取各维度值
2. 按 groupBy 顺序构建嵌套路径(如 tree[venue][session][room][section]
3. 累加 inventory计算 min_price
4. 生成 template_key = venue_venueName_room_roomName_section_sectionChar
5. 标记 has_available = 累计 inventory > 0
模板去重:
每个 section 节点只存 template_key不存完整模板
模板池在最后统一挂载(从 vr_seat_templates 表读取,或从 seatMap 数据提取)
```
### Step 3实现 API 接口
**文件**`api/Goods.php`
```php
/**
* 获取层级树(含模板去重池 + 扁平 SKU
* 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'); // 默认 venue-first
$groupBy = array_filter(array_map('trim', explode(',', $groupBy)));
if ($goodsId <= 0) {
return ['code' => -1, 'msg' => 'goods_id 无效'];
}
// 缓存 keygoods_id + group_by 组合
$cacheKey = 'vr_tree_' . $goodsId . '_' . md5(implode(',', $groupBy));
$cached = \think\facade\Cache::get($cacheKey);
if ($cached !== null) {
$data = $cached;
$data['meta']['cache_hit'] = true;
return ['code' => 0, 'msg' => 'success', 'data' => $data];
}
// 1. 读取现有 seatSpecMap复用 SeatMapService
$seatSpecMap = SeatMapService::GetSeatMap($goodsId)['seatSpecMap'] ?? [];
// 2. 调用 QueryManager 构建树
$tree = QueryManager::buildTree($goodsId, $groupBy, $seatSpecMap);
// 3. 构建模板去重池
$templates = QueryManager::buildTemplatePool($goodsId, $tree);
// 4. 构建扁平 SKU 列表
$flatInventory = QueryManager::buildFlatInventory($seatSpecMap);
// 5. 组装响应
$data = [
'goods_id' => $goodsId,
'group_by' => $groupBy,
'tree' => $tree,
'seat_templates_flat' => $templates,
'flat_inventory' => $flatInventory,
'meta' => [
'flat_count' => count($flatInventory),
'template_count' => count($templates),
'cache_hit' => false,
'computed_at' => time(),
],
];
// 6. 写入缓存TTL = 60s
\think\facade\Cache::set($cacheKey, $data, 60);
return ['code' => 0, 'msg' => 'success', 'data' => $data];
}
```
### Step 4模板去重池生成逻辑
```php
private static function buildTemplatePool(int $goodsId, array $tree): array
{
$pool = [];
// 遍历 tree找到所有 template_key
// 从 vr_seat_templates 表读取对应模板数据
// key = template_key如 "鸟巢_主厅_A"
// value = 模板的 map + sections + seats
// 或者从 seatMap 数据直接提取(如果模板数据已在 seatMap 中)
// 取决于当前数据结构中模板数据的存储位置
return $pool;
}
```
**关键**:模板只按 `venue + "_" + room + "_" + section` 存储一份,不管有多少个 session。
### Step 5前端适配ticket_detail.html
**改动点**
1. 初始请求改用 `/api/goods/tree`(替换现有的 `GetGoodsViewData` PHP 渲染)
2. 从返回中提取 `tree` + `seat_templates_flat` + `flat_inventory`
3.`tree` 渲染选择器venue → session → room → section
4. 用户选到 section 时:
-`tree[venue][session][room][section]['template_key']`
-`seat_templates_flat[template_key]` 获取座位图
-`flat_inventory` 做 spec_key 前缀匹配,标记可选座位
**spec_key 前缀匹配(前端 JS**
```javascript
function filterSeatsBySpec(flatInventory, selections) {
// selections = { venue: '鸟巢', session: '15:00-16:59', room: '主厅', section: 'A' }
// 构造前缀
const prefix = '$vr-场馆=' + selections.venue +
'|$vr-场次=' + selections.session +
'|$vr-演播室=' + selections.room +
'|$vr-分区=' + selections.section + '|';
return flatInventory.filter(sku =>
sku.spec_key.startsWith(prefix) && sku.inventory > 0
);
}
```
---
## 验证清单
实现完成后,用 curl 测试:
```bash
# 测试 venue-firstJoery 的场景)
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)['data']
print('flat_count:', d['meta']['flat_count'])
print('template_count:', d['meta']['template_count'])
print('venues:', list(d['tree'].keys()))
"
# 测试 session-first
curl "http://localhost:10000/api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=goods&pluginsaction=tree&goods_id=118&group_by=session,venue,room,section" ...
```
---
## 注意事项
1. **不要删除现有代码**SeatMapService / SeatSkuService它们是数据源新 API 依赖它们
2. **模板去重**:确保同一个 venue+room+section 的模板只出现一次在 `seat_templates_flat`
3. **缓存失效**:订单支付成功后调用 `SeatMapService::ClearCache()` 时,同时清除 tree 缓存
4. **inventory=0 的座位**:需要确认是否包含在 `flat_inventory` 里(前端的座位图需要知道哪些是已售的)
5. **spec_key 排序**:必须与前端使用的排序规则一致,否则前缀匹配失败