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