154 lines
4.5 KiB
Markdown
154 lines
4.5 KiB
Markdown
|
|
# 扁平数据 + 查询管理器 方案
|
|||
|
|
|
|||
|
|
> 讨论日期:2026-05-15
|
|||
|
|
> 参与:西莉雅、大头
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、核心思想
|
|||
|
|
|
|||
|
|
**后端做计算,前端做渲染。**
|
|||
|
|
|
|||
|
|
- 扁平数据层(Flat Inventory):所有 SKU 以 spec_key 为索引,不预设层级
|
|||
|
|
- 查询管理器(Query Manager):按 `group_by` 参数聚合,生成层级树
|
|||
|
|
- 缓存层:结果缓存,避免重复计算
|
|||
|
|
- 前端:通过 `group_by` 参数指定想要的层级顺序,收到树后直接渲染
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、关键设计决策
|
|||
|
|
|
|||
|
|
### 2.1 模板去重
|
|||
|
|
|
|||
|
|
**原则**:同一 `venue + room + section` 的座位图模板在所有场次下共享,与 session 无关。
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
例如:鸟巢 + 主厅 + A区
|
|||
|
|
→ 6 个场次 × 1 个模板 = 1 份模板数据(不是 6 份)
|
|||
|
|
→ template_key = "鸟巢_主厅_A"
|
|||
|
|
→ 在 seat_templates_flat 中只存 1 份
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**优势**:
|
|||
|
|
- 消除跨场次的模板数据冗余
|
|||
|
|
- 前端只需按 key 引用,无需处理重复模板
|
|||
|
|
|
|||
|
|
### 2.2 层级顺序由前端控制
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
group_by=venue,session,room,section → 场馆优先(Joery 场景)
|
|||
|
|
group_by=session,venue,room,section → 场次优先(当前实现)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
后端不在意顺序,只负责聚合。
|
|||
|
|
|
|||
|
|
### 2.3 扁平数据全量返回
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
API 返回:
|
|||
|
|
├── tree(层级,含 section 聚合)
|
|||
|
|
├── seat_templates_flat(模板去重池)
|
|||
|
|
└── flat_inventory(所有 SKU,前端本地筛选)
|
|||
|
|
|
|||
|
|
前端选座流程(全程零额外 API):
|
|||
|
|
venue → session → room → section
|
|||
|
|
↓
|
|||
|
|
查 template_key → 查 seat_templates_flat → 渲染座位图
|
|||
|
|
↓
|
|||
|
|
spec_key 前缀匹配 flat_inventory → 标记可选座位
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.4 实时查询接口(复用查询管理器)
|
|||
|
|
|
|||
|
|
查询管理器同时支持实时查询:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
GET /api/goods/inventory?goods_id=118&spec=venue:鸟巢,session:15:00-16:59
|
|||
|
|
→ 返回该条件下的总库存
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、API 接口
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
GET /api/goods/tree
|
|||
|
|
?goods_id=118
|
|||
|
|
&group_by=venue,session,room,section
|
|||
|
|
&cache_ttl=120
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
返回结构:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"goods_id": 118,
|
|||
|
|
"title": "VR演唱会",
|
|||
|
|
"group_by": ["venue", "session", "room", "section"],
|
|||
|
|
"tree": {
|
|||
|
|
"鸟巢": {
|
|||
|
|
"name": "鸟巢",
|
|||
|
|
"min_price": 280,
|
|||
|
|
"has_available": true,
|
|||
|
|
"rooms": { "主厅": { "sections": { "A": { "template_key": "鸟巢_主厅_A", "price": 680, "inventory": 12 } } } },
|
|||
|
|
"sessions": { "15:00-16:59": { "min_price": 380, "has_available": true } }
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"seat_templates_flat": {
|
|||
|
|
"鸟巢_主厅_A": { "map": [...], "sections": [...], "seats": {...} }
|
|||
|
|
},
|
|||
|
|
"flat_inventory": [
|
|||
|
|
{ "spec_key": "$vr-场次=15:00-16:59|...|$vr-座位号=1排1座", "price": 680, "inventory": 1, "seat_key": "room_0_A_1" }
|
|||
|
|
],
|
|||
|
|
"meta": { "flat_count": 120, "template_count": 4, "cache_hit": false }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、与现有系统衔接
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
vr_goods_config
|
|||
|
|
↓
|
|||
|
|
SeatSkuService::buildSeatSpecMap() → 扁平 SKU(不预设层级)
|
|||
|
|
↓
|
|||
|
|
QueryManager(新增)
|
|||
|
|
├── buildTree():按 group_by 聚合
|
|||
|
|
├── buildTemplatePool():模板去重
|
|||
|
|
└── buildFlatInventory():返回扁平 SKU 列表
|
|||
|
|
↓
|
|||
|
|
缓存层(TTL=60s)
|
|||
|
|
↓
|
|||
|
|
API Response
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**现有组件不变**:
|
|||
|
|
- SeatMapService / SeatSkuService:继续作为数据源
|
|||
|
|
- vr_seat_templates 表:继续存储座位模板
|
|||
|
|
- vr_goods_config:继续作为商品配置
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、与"层级 JSON 树方案"的关系
|
|||
|
|
|
|||
|
|
| 维度 | 层级 JSON 树(方案 A) | 扁平 + 查询管理器(方案 B,采纳) |
|
|||
|
|
|------|---------------------|--------------------------------|
|
|||
|
|
| 前端复杂度 | 低(直接用树) | 中(本地筛选 + 模板引用) |
|
|||
|
|
| 后端复杂度 | 中(预计算固定结构) | 中(查询管理器,按参数聚合) |
|
|||
|
|
| 灵活性 | 差(固定层级) | 好(前端指定 group_by) |
|
|||
|
|
| 模板去重 | 支持(但实现复杂) | ✅ 原生支持 |
|
|||
|
|
| 数据冗余 | 高(模板重复) | 低(去重池) |
|
|||
|
|
| 实时查询 | 不支持 | ✅ 支持 |
|
|||
|
|
|
|||
|
|
**结论**:Joery 的方案更优,采纳方案 B。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、注意事项
|
|||
|
|
|
|||
|
|
1. **template_key 生成算法**:由后端统一生成,格式为 `venue_room_section`,前端不感知算法
|
|||
|
|
2. **spec_key 排序**:前端匹配时必须使用与后端相同的排序规则(固定顺序,用 `|` 连接)
|
|||
|
|
3. **缓存失效**:订单支付成功后同时清除 SeatMapService 缓存和 tree 缓存
|
|||
|
|
4. **inventory=0 的座位**:flat_inventory 需要包含已售座位(用于前端标记"已售"状态)
|
|||
|
|
5. **缓存 TTL**:默认 60s,可通过 `cache_ttl` 参数调整
|