vr-shopxo-plugin/docs/14_TREE_API_DESIGN.md

398 lines
13 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.

# VR 票务 API 重构方案:参数化层级生成器
> 讨论日期2026-05-15
> 参与:西莉雅(统筹)、大头(产品决策)
> 状态:**规划中,待实现**
---
## 一、问题背景
### 现有 API 的局限
当前 `SeatMapService` 提供两个数据视图:
| 接口 | 数据结构 | 问题 |
|------|---------|------|
| `GET /seatmap` | `seatSpecMap`(扁平 key→value | 前端需要自己重建层级,计算成本高 |
| `GetGoodsViewData` (PHP) | `seatSpecMap` + `specTypeList` + `vr_seat_template` | 前端仍需大量客户端逻辑,且 session 被放在顶层 |
**核心问题**
1. 前端重建层级需要全量扫描 seatSpecMap时间复杂度 O(n²)
2. session 作为顶层维度不符合实际业务逻辑(场次是场馆的子节点)
3. 座位模板数据在不同场次间重复嵌入,造成大量冗余
4. 前端需要同时维护 specTypeList + seatSpecMap + seatMap 三套数据,复杂度高
### 业务需求
- **场次是场馆的子节点**:不同场馆可设置不同场次(某些只有 ABC某些只有 CDF场次由合作方电影院自主决定
- **未来重播场景**:院线方像排片一样自行安排重映场次
- **多模板商品**:一个商品下多个场馆,每场馆多场次,每场次多演播室/分区/座位
- **模板去重**:同一分区的座位模板在所有场次下共享,不应重复嵌入
---
## 二、设计目标
1. **后端生成层级结构**:前端只需渲染,不需计算
2. **参数化接口**:前端通过 `group_by` 参数指定想要的层级顺序
3. **模板去重**:座位模板按 `template_key` 只存一份,前端按需引用
4. **可缓存**:相同参数的请求直接返回缓存结果
5. **相辅相成**:底层查询管理器同时支持实时库存查询
---
## 三、数据结构
### 3.1 扁平数据层Flat Inventory
所有 SKU 以 `spec_key` 为索引,**不预设任何层级**
```
vr_sku_flat:
goods_id | spec_key | price | inventory | seat_key | roomId | sectionChar | ...
spec_key 格式(各维度按固定顺序排序,用 | 连接):
"$vr-场次=15:00-16:59|$vr-场馆=鸟巢|$vr-演播室=主厅|$vr-分区=A|$vr-座位号=1排1座"
template_key 格式(由后端统一生成,前端不需感知算法):
由 venue + room + section 组合决定,同一分区共用一个 template_key
```
### 3.2 层级树Hierarchy Tree
API 返回的结构,按 `group_by` 参数动态生成:
```json
{
"goods_id": 118,
"title": "VR演唱会",
"group_by": ["venue", "session", "room", "section"],
"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
},
"B": {
"template_key": "鸟巢_主厅_B",
"price": 380,
"inventory": 0,
"has_available": false
}
}
}
},
"sessions": {
"15:00-16:59": { "min_price": 380, "has_available": true },
"20:00-21:59": { "min_price": 280, "has_available": false }
}
},
"国家大剧院": {
"name": "国家大剧院",
"min_price": 380,
"has_available": true,
"rooms": { ... },
"sessions": { ... }
}
},
"seat_templates_flat": {
"鸟巢_主厅_A": {
"id": 1,
"name": "鸟巢-主厅-A区",
"map": ["AAAAA", "AAB__AA", "BBBBB"],
"sections": [
{ "char": "A", "name": "VIP区", "price": 680, "color": "#ff4d4f" },
{ "char": "B", "name": "看台", "price": 380, "color": "#409eff" }
],
"seats": { "A": {...}, "B": {...} }
},
"鸟巢_主厅_B": { ... }
},
"flat_inventory": [
{
"spec_key": "$vr-场次=15:00-16:59|$vr-场馆=鸟巢|$vr-演播室=主厅|$vr-分区=A|$vr-座位号=1排1座",
"price": 680,
"inventory": 1,
"seat_key": "room_0_A_1",
"rowLabel": "A",
"colNum": 1
},
...
],
"meta": {
"flat_count": 120,
"template_count": 4,
"cache_hit": false,
"computed_at": 1750000000
}
}
```
### 3.3 模板去重逻辑
**关键原则**:同一 `venue + room + section` 的座位图模板在所有场次下共享。
```
模板 key 由以下维度决定(与 session 无关):
venue + "_" + room + "_" + section
示例:
鸟巢 + 主厅 + A区 → template_key = "鸟巢_主厅_A"
6 个场次 × 1 个模板 = 1 份模板数据(不是 6 份)
数据冗余对比:
naive 方案(内嵌到每个 section18 份完整模板
当前方案template_key 引用1 份模板 × 4 个 key
```
### 3.4 venue-first vs session-first
业务逻辑支持两种 `group_by` 顺序:
```
场景 1当前session → venue → room → section
适用:用户先选场次,再选场馆
场景 2Joery 场景venue → session → room → section
适用:用户先选场馆(电影院),再看该场馆有哪些场次
原因:场次是场馆的属性,不同场馆场次不同
```
API 通过 `group_by` 参数支持任意顺序:
```
GET /api/goods/tree?goods_id=118&group_by=session,venue,room,section
GET /api/goods/tree?goods_id=118&group_by=venue,session,room,section
```
---
## 四、API 设计
### 4.1 接口定义
```
GET /api/goods/tree
?goods_id=118 # 商品ID必填
&group_by=venue,session,room,section # 分组维度顺序(必填)
&filter= # 可选过滤条件(预留)
&cache_ttl=120 # 可选自定义缓存时间
```
**返回字段说明**
| 字段 | 类型 | 说明 |
|------|------|------|
| `goods_id` | int | 商品ID |
| `title` | string | 商品标题 |
| `group_by` | array | 本次请求的分组维度 |
| `tree` | object | 层级树结构,叶节点含 `template_key` |
| `seat_templates_flat` | object | 模板去重池key → 模板数据 |
| `flat_inventory` | array | 扁平的 SKU 列表(用于前端本地筛选) |
| `meta.flat_count` | int | 原始 SKU 总数 |
| `meta.template_count` | int | 去重后模板数量 |
| `meta.cache_hit` | bool | 是否命中缓存 |
| `meta.computed_at` | int | 计算时间戳 |
### 4.2 分组维度说明
| 维度 | 说明 | 示例 |
|------|------|------|
| `venue` | 场馆(合作方/电影院) | 鸟巢、国家大剧院 |
| `session` | 场次时间段 | 15:00-16:59 |
| `room` | 演播室 | 主厅、次厅 |
| `section` | 分区 | A区、VIP区 |
**维度数量**:当前实现 4 维venue / session / room / section扩展维度时只需修改 `SPEC_DIMS` 常量。
---
## 五、前端交互流程
```
阶段 1初始加载一次 API 调用)
GET /api/goods/tree?goods_id=118&group_by=venue,session,room,section
→ 前端收到 tree + seat_templates_flat + flat_inventory
阶段 2用户交互纯本地计算零额外 API 调用)
venue=鸟巢 → session=15:00-16:59 → room=主厅 → section=A
前端查 tree["鸟巢"]["sessions"]["15:00-16:59"]["rooms"]["主厅"]["sections"]["A"]
拿到 template_key = "鸟巢_主厅_A"
前端查 seat_templates_flat["鸟巢_主厅_A"] → 渲染座位图模板
前端用 spec_key 前缀匹配在 flat_inventory 里过滤出该分区的所有座位
标记有库存的座位为可选 → 用户选座
```
**spec_key 前缀匹配示例**
```
用户选venue=鸟巢, session=15:00-16:59, room=主厅, section=A
前端构造前缀:
"$vr-场馆=鸟巢|$vr-场次=15:00-16:59|$vr-演播室=主厅|$vr-分区=A|"
flat_inventory 中匹配:
"$vr-场馆=鸟巢|$vr-场次=15:00-16:59|$vr-演播室=主厅|$vr-分区=A|$vr-座位号=1排1座" → 可选
"$vr-场馆=鸟巢|$vr-场次=15:00-16:59|$vr-演播室=主厅|$vr-分区=A|$vr-座位号=1排2座" → 可选
"$vr-场馆=鸟巢|$vr-场次=15:00-16:59|$vr-演播室=主厅|$vr-分区=B|..." → 不匹配,跳过
```
---
## 六、查询管理器Query Manager
### 6.1 职责
- 接收 `group_by``filter_by` 参数
- 从扁平数据seatSpecMap按维度聚合
- 生成层级树结构
- 写入缓存,返回结果
### 6.2 聚合逻辑(伪 SQL
```sql
-- 原始数据vr_sku_flat所有 SKU
-- 1. 过滤(如果 filter_by 不为空)
SELECT * FROM vr_sku_flat
WHERE goods_id = 118
AND spec_key LIKE '%venue=鸟巢%'
AND spec_key LIKE '%session=15:00-16:59%'
-- 2. 分组聚合(按 group_by 顺序)
GROUP BY venue, session, room, section
每组计算:MIN(price), SUM(inventory), has_available = SUM(inventory) > 0
模板 keyvenue + "_" + room + "_" + section
```
### 6.3 缓存策略
```
CacheKey = hash(goods_id + group_by_str + filter_str)
CacheTTL = 60s可配置默认 60s
失效机制:订单支付成功 → 清除该 goods_id 的所有相关缓存
```
### 6.4 实时查询接口(复用查询管理器)
除了层级树,查询管理器还暴露实时查询能力:
```
GET /api/goods/inventory?goods_id=118&spec=venue:鸟巢,session:15:00-16:59
→ 返回该条件下的总库存
GET /api/goods/inventory?goods_id=118&spec=venue:鸟巢,session:20:00-21:59,room:主厅
→ 返回更精确的库存
```
---
## 七、与现有系统衔接
### 7.1 数据来源
```
vr_goods_configgoods.vr_goods_config
└── template_id, selected_rooms, selected_sections, sessions[]
SeatSkuService::buildSeatSpecMap()
→ 扁平 SKU 数据seatSpecMap
Query Manager新增
→ 按 group_by 聚合 → 层级树
→ 模板去重池 seat_templates_flat
缓存层ShopXO Cache
API Response
```
### 7.2 现有组件保持不变
- `SeatMapService` / `SeatSkuService`:继续作为数据源,不改逻辑
- `vr_seat_templates` 表:继续存储座位模板,不变
- `vr_goods_config`:继续作为商品配置,不变
### 7.3 新增组件
| 组件 | 路径 | 职责 |
|------|------|------|
| `QueryManager` | `service/QueryManager.php` | 分组聚合、模板去重、缓存 |
| `/api/goods/tree` | `api/Goods.php::tree()` | HTTP 接口 |
| `/api/goods/inventory` | `api/Goods.php::inventory()` | 实时查询接口(可选) |
---
## 八、实施阶段
### Phase A数据验证
- 用现有 seatSpecMap 数据,验证 group_by 聚合逻辑
- 测试 500 SKU 级别的聚合性能
- 确认模板 key 生成算法venue + "_" + room + "_" + section
### Phase BAPI 实现
- 实现 `QueryManager` 服务
- 实现 `/api/goods/tree` 接口
- 配置缓存策略
### Phase C前端改造
- 改造 `ticket_detail.html`
- 替换 4 个选择器的数据源
- 实现 spec_key 前缀匹配逻辑
- 验证模板按 key 引用、去重效果
### Phase D联调测试
- 层级选择 → 座位图渲染 → 选座 → 提交订单 完整流程
- 多场馆 × 多场次 × 多分区 组合测试
- 缓存失效机制验证
---
## 九、关键设计决策
| 决策 | 结论 | 原因 |
|------|------|------|
| 模板 key 由 venue+room+section 决定,与 session 无关 | ✅ 确认 | 同一分区座位图固定,跨 session 共享 |
| 分组维度顺序由前端通过 `group_by` 参数指定 | ✅ 确认 | 支持 venue-first 和 session-first 两种场景 |
| flat_inventory 随 API 一起返回,不拆分 | ✅ 确认 | 前端本地筛选,零额外 API 调用 |
| 缓存 TTL 默认 60s可配置 | ✅ 确认 | 平衡实时性和性能 |
| 查询管理器同时支持实时查询接口 | ✅ 预留 | 后期扩展用,当前可以先用 |
---
## 十、待确认问题
1. **spec_key 排序规则**:当前 SeatSkuService 的 `makeSpecKey` 按 type 排序,具体顺序是否固定?需确认 SPEC_DIMS 顺序。
2. **template_key 生成算法**:后端生成 tree 时用什么算法?是否与前端 spec_key 生成逻辑一致?需验证现有代码。
3. **分组维度是否完整**:当前 4 维venue/session/room/section是否覆盖所有场景是否需要加 "日期" 等维度?
4. **库存为 0 的 seat 是否返回**:当前 buildSeatSpecMap 过滤了 inventory=0 的座位,新 API 是否需要包含已售座位?
---
## 十一、相关文档索引
| 文档 | 路径 | 说明 |
|------|------|------|
| VR_GOODS_CONFIG_SPEC | `docs/VR_GOODS_CONFIG_SPEC.md` | vr_goods_config 存储格式 |
| MIGRATION_5DIM_SPEC | `docs/MIGRATION_5DIM_SPEC.sql` | 5维 spec SQL 迁移 |
| DEVELOPMENT_LOG | `docs/DEVELOPMENT_LOG.md` | Phase 0/1/2/3 开发日志 |
| DEBUGGING_ROUTING | `docs/DEBUGGING_ROUTING.md` | ShopXO 路由调试经验 |