131 lines
5.2 KiB
Markdown
131 lines
5.2 KiB
Markdown
|
|
# vr-shopxo-plugin 架构决策报告
|
|||
|
|
|
|||
|
|
> 关联:Issue #9
|
|||
|
|
> 日期:2026-04-15
|
|||
|
|
> 参与:council/BackendArchitect + SecurityEngineer + FrontendDev
|
|||
|
|
> 轮次:Round 3(最终)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 背景
|
|||
|
|
|
|||
|
|
vr-shopxo-plugin 是 ShopXO 票务插件,核心场景:VR 演唱会票务小程序,用户选座→下单→QR 核销。
|
|||
|
|
|
|||
|
|
Phase 0/1/2 已完成基础骨架,暴露 P0 架构问题:ShopXO SPEC 与 SKU 的绑定方案。
|
|||
|
|
|
|||
|
|
**当前商品 112 实测状态(问题根因):**
|
|||
|
|
```sql
|
|||
|
|
is_exist_many_spec = 0 -- ShopXO 认为无多规格
|
|||
|
|
spec_base 表 = 空 -- 没有任何 SKU
|
|||
|
|
spec_base_id_map → {A:1001, B:1002, C:1003} -- 这些 ID 在 DB 里不存在!
|
|||
|
|
```
|
|||
|
|
ShopXO 防超卖机制完全未启用,购买走裸商品逻辑。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 核心问题与结论
|
|||
|
|
|
|||
|
|
### Q1:方案 A 后台批量生成 SKU 路径是否可行?
|
|||
|
|
|
|||
|
|
**结论:可行,但必须旁路 GoodsSpecificationsInsert()**
|
|||
|
|
|
|||
|
|
- ShopXO GoodsService::GoodsSpecificationsInsert() 每次商品保存时 DELETE 所有现有 spec 后重建,10K+ 座位场景不可用
|
|||
|
|
- 可行路径:直接 SQL INSERT 到 sxo_goods_spec_type、sxo_goods_spec_base、sxo_goods_spec_value 三表
|
|||
|
|
- 性能:10000 座位约 3-4 秒(分批 500 条/批提交)
|
|||
|
|
- 关键:spec_base_id_map[seat_id] → actual_db_id 映射必须在 INSERT 后即时重建
|
|||
|
|
|
|||
|
|
### Q2:商品 112 broken 状态是否需要紧急修复?
|
|||
|
|
|
|||
|
|
**结论:推荐方案乙(最小修复集)**
|
|||
|
|
|
|||
|
|
1. UPDATE sxo_goods SET is_exist_many_spec = 1 WHERE id = 112
|
|||
|
|
2. INSERT $vr- spec_type(场馆/分区、时段三个维度)
|
|||
|
|
3. TicketService::issueTicket() 添加 spec_base_id = 0 的幂等保护 fallback
|
|||
|
|
|
|||
|
|
紧急程度中等,不影响当前票务逻辑运行,但应在 Phase 3 批量 SKU 生成前完成。
|
|||
|
|
|
|||
|
|
### Q3:$vr- 前缀方案是否有隐患?
|
|||
|
|
|
|||
|
|
**结论:低风险(SecurityEngineer + FrontendDev 确认)**
|
|||
|
|
|
|||
|
|
- ThinkPHP {$var} 默认做 HTML 转义,$vr- 不会被解析为 PHP 变量
|
|||
|
|
- |raw 仅跳过 HTML 转义,不会执行变量插值
|
|||
|
|
- ThinkPHP parseVar 正则对连字符 - 的处理会阻断 $vr- 的完整解析
|
|||
|
|
- ShopXO spec name 数据库字段无字符过滤,存储层安全
|
|||
|
|
|
|||
|
|
### Q4:方案 A vs 方案 B 最终推荐
|
|||
|
|
|
|||
|
|
**结论:一致推荐方案 A(每个座位一个 SKU)**
|
|||
|
|
|
|||
|
|
三方一致理由汇总:
|
|||
|
|
|
|||
|
|
| 维度 | 方案 A(每座位一个 SKU) | 方案 B(每个 Zone 一个 SKU) |
|
|||
|
|
|------|--------------------------|------------------------------|
|
|||
|
|
| 防超卖 | ShopXO 原生 dec() 原子扣减 | 自建 FOR UPDATE 锁(死锁风险) |
|
|||
|
|
| 数据一致性 | 每个座位 inventory=1,事务保护 | Zone 库存需插件自己维护 |
|
|||
|
|
| 购买流程 | ShopXO 原生流程完整走通 | 需 Hook 旁路购买逻辑 |
|
|||
|
|
| 票务链路 | spec_base_id 直接映射座位 | 需反向解析 seat_id |
|
|||
|
|
| SKU 数量 | 10000+(插件自管,ShopXO 后台隐藏) | 10-50(Zone 数量) |
|
|||
|
|
| 并发安全 | InnoDB 行锁 + 选座低并发窗口 | 自建锁,高并发风险 |
|
|||
|
|
| 可维护性 | 依赖 ShopXO 原生机制,有据可查 | 插件黑盒,故障排查困难 |
|
|||
|
|
|
|||
|
|
方案 B 的"SKU 少"优势在演唱会 10K+ 场景不成立:插件自管 SKU,通过 Hook 隐藏插件专用规格,不走 ShopXO 原生规格管理页面。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 最终推荐
|
|||
|
|
|
|||
|
|
### 采纳方案:方案 A — 每个座位一个 ShopXO SKU(stock=1)
|
|||
|
|
|
|||
|
|
### 实现路径
|
|||
|
|
|
|||
|
|
1. Phase 3 批量 SKU 生成(BackendArchitect 负责)
|
|||
|
|
- 创建 SeatSkuService::BatchGenerate():直接 SQL INSERT 批量生成 SKU
|
|||
|
|
- 旁路 GoodsSpecificationsInsert(),避免每次商品保存清空重建
|
|||
|
|
- 分批提交(500 条/批),初始化一次
|
|||
|
|
|
|||
|
|
2. 紧急修复(BackendArchitect 负责)
|
|||
|
|
- UPDATE is_exist_many_spec = 1
|
|||
|
|
- INSERT $vr- spec_type(场馆/分区/时段)
|
|||
|
|
- TicketService::issueTicket() 的 spec_base_id=0 幂等保护
|
|||
|
|
|
|||
|
|
3. 插件规格隔离(FrontendDev 负责)
|
|||
|
|
- 通过 Hook 隐藏插件专用 SKU(不出现在 ShopXO 规格管理页)
|
|||
|
|
- 建立独立"座位 SKU 管理"页面
|
|||
|
|
|
|||
|
|
### 技术细节:ShopXO 原生防超卖机制
|
|||
|
|
|
|||
|
|
BuyService.php:1677-1681:
|
|||
|
|
```php
|
|||
|
|
$where = [
|
|||
|
|
['id', '=', $base['data']['spec_base']['id']],
|
|||
|
|
['goods_id', '=', $v['goods_id']],
|
|||
|
|
['inventory', '>=', $v['buy_number']],
|
|||
|
|
];
|
|||
|
|
Db::name('GoodsSpecBase')->where($where)->dec('inventory', $v['buy_number'])->update();
|
|||
|
|
```
|
|||
|
|
翻译为 SQL:UPDATE goods_spec_base SET inventory = inventory - N WHERE inventory >= N
|
|||
|
|
|
|||
|
|
这是 MySQL 层面的条件原子扣减,TOCTOU 窗口极小(选座模式已在前端锁定具体座位,并发窗口远低于总库存),推荐接受此风险。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 行动项
|
|||
|
|
|
|||
|
|
| 优先级 | 行动项 | 负责 |
|
|||
|
|
|--------|--------|------|
|
|||
|
|
| P0 | 创建 SeatSkuService::BatchGenerate() — 直接 SQL INSERT 批量生成 SKU | BackendArchitect |
|
|||
|
|
| P0 | 执行 Q2 最小修复集:UPDATE is_exist_many_spec + INSERT $vr- spec_type | BackendArchitect |
|
|||
|
|
| P1 | TicketService::issueTicket() 添加 spec_base_id=0 幂等保护 | BackendArchitect |
|
|||
|
|
| P2 | 通过 Hook 隐藏插件专用 SKU,建立独立座位 SKU 管理页面 | FrontendDev |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 共识
|
|||
|
|
|
|||
|
|
- BackendArchitect: CONSENSUS: YES — 推荐方案 A,Round 2/3 分析完成
|
|||
|
|
- SecurityEngineer: CONSENSUS: YES — $vr- 前缀低风险,方案 A 推荐
|
|||
|
|
- FrontendDev: CONSENSUS: YES — 方案 A 推荐,前端配合方案清晰
|
|||
|
|
|
|||
|
|
全票通过:采纳方案 A
|