vr-shopxo-plugin/docs/PLAN_TREE_API_IMPLEMENTATIO...

247 lines
7.1 KiB
Markdown
Raw Permalink Normal View History

# 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 中包含已售座位
---
*文档状态:规划完成,待实施*