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