6.2 KiB
6.2 KiB
扁平数据 + 查询管理器 方案
状态: ✅ 已完成
讨论日期:2026-05-15
参与:西莉雅、大头
一、核心思想
后端做计算,前端做渲染。
- 动态层级树:根据
group_by参数动态生成层级结构 - 查询管理器(Query Manager):按
group_by参数聚合,生成层级树 - 自底向上聚合:每个层级节点自动计算
inventory、min_price、max_price、has_available - 座位嵌入:座位数据直接嵌入树的最深层
seats属性 - 模板去重池:同一
venue + room + section的模板只返回一份,格式为键值对
二、关键设计决策
2.1 模板去重
原则:同一 venue + room + section 的座位图模板在所有场次下共享,与 session 无关。
例如:测试场馆 + 老展厅 1 + A
→ 多个场次 × 1 个模板 = 1 份模板数据
→ template_key = "测试场馆_老展厅 1_A"
→ 在 seat_templates 中只存 1 份,格式为键值对
2.2 层级顺序由前端控制
group_by=venue,session,room,section → 场馆优先(Joery 场景)
group_by=session,venue,room,section → 场次优先
group_by=section,venue,session,room → 自定义顺序
2.3 座位数据嵌入树
API 返回:
├── tree(层级,含 seats 嵌入在最深层)
├── seat_templates(模板去重池,键值对格式)
└── meta(元数据)
前端选座流程(全程零额外 API):
venue → session → room → section
↓
直接从 tree 最深层获取 seats
↓
查 template_key → 查 seat_templates → 渲染座位图
2.4 移除 flat_inventory
flat_inventory 已被移除,因为:
- 座位数据已嵌入 tree 最深层
- 前端直接从
seats获取座位,无需额外遍历
三、API 接口
GET /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=goods&pluginsaction=tree
?goods_id=118
&group_by=venue,session,room,section
返回结构(当前实现):
{
"code": 0,
"data": {
"goods_id": 118,
"group_by": ["venue", "session", "room", "section"],
"tree": {
"venues": {
"测试场馆": {
"name": "测试场馆",
"min_price": 0,
"max_price": 0,
"has_available": true,
"inventory": 31,
"sessions": {
"07:00-09:59": {
"name": "07:00-09:59",
"inventory": 31,
"rooms": {
"老展厅 1": {
"name": "老展厅 1",
"inventory": 16,
"sections": {
"A": {
"name": "A",
"min_price": 0,
"max_price": 0,
"has_available": true,
"inventory": 10,
"template_key": "测试场馆_老展厅 1_A",
"price": 0,
"seats": {
"1排1座": {
"spec_key": "$vr-分区=A|...",
"venue": "测试场馆",
"session": "07:00-09:59",
"room": "老展厅 1",
"section": "A",
"seat": "1排1座",
"price": 0,
"inventory": 1
}
}
}
}
}
}
}
}
}
}
},
"seat_templates": {
"测试场馆_老展厅 1_A": {
"template_key": "测试场馆_老展厅 1_A",
"name": "测试场馆",
"room_name": "老展厅 1",
"section_name": "A",
"seat_map": { ... },
"layout_cols": 10,
"layout_rows": 10
}
},
"meta": {
"seat_count": 59,
"template_count": 6,
"cache_hit": false,
"computed_at": 1778861766
}
}
}
四、与现有系统衔接
vr_goods_config
↓
SeatSkuService::buildSeatSpecMap() → 扁平 SKU
↓
QueryManager(新增)
├── buildTree():按 group_by 聚合,嵌入 seats
├── buildTemplatePool():模板去重
└── transformTemplatePool():转换为键值对格式
↓
缓存层(TTL=60s)
↓
API Response
现有组件不变:
- SeatMapService / SeatSkuService:继续作为数据源
- vr_seat_templates 表:继续存储座位模板
- vr_goods_config:继续作为商品配置
五、实现状态
| 任务 | 状态 | 说明 |
|---|---|---|
| QueryManager 核心逻辑 | ✅ 已完成 | buildTree, computeStatsRecursive |
| 动态层级树生成 | ✅ 已完成 | 根据 group_by 动态构建 |
| Seats 嵌入最深层 | ✅ 已完成 | seats 属性嵌入 tree 最深层 |
| 自底向上统计 | ✅ 已完成 | inventory, min_price, max_price, has_available |
| seat_templates 键值对 | ✅ 已完成 | transformTemplatePool() |
| 移除 flat_inventory | ✅ 已完成 | seats 已嵌入 tree |
| 缓存机制 | ✅ 已完成 | Cache::set/get + 标签失效 |
| API 文档 | ✅ 已完成 | docs/api/VR_TICKET_TREE_API.md |
| peer_goods 多场次关联 | ✅ 已完成 | 按 coding 关联同演出不同日期商品 |
| session_meta 场次元数据 | ✅ 已完成 | 从 SKU extends 提取停售时间戳 |
| SKU batch_expire_ts 写入 | ✅ 已完成 | BatchGenerate 时写入 extends |
| BuyCheck 停售验证钩子 | ✅ 已完成 | 开场前5分钟禁止下单 |
| 字段修正 | ✅ 已完成 | goods_code→coding, produce_date→batch_number_expire |
六、注意事项
- template_key 生成算法:
venue_room_section,前端不感知算法 - 缓存失效:订单支付成功后同时清除 SeatMapService 缓存和 tree 缓存
- inventory=0 的座位:仍保留在 seats 中,用于前端标记"已售"状态
- 缓存 TTL:默认 60s,通过
cache_ttl参数可调整
七、相关文档
| 文档 | 说明 |
|---|---|
| api/VR_TICKET_TREE_API.md | 完整 API 文档 |
| 14_TREE_API_DESIGN.md | Tree API 设计文档 |