# 扁平数据 + 查询管理器 方案 > 讨论日期: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` 参数调整