# 扁平数据 + 查询管理器 方案

> 讨论日期：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` 参数调整