vr-shopxo-plugin/council-output/ARCHITECTURE_DECISION.md

131 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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-50Zone 数量) |
| 并发安全 | InnoDB 行锁 + 选座低并发窗口 | 自建锁,高并发风险 |
| 可维护性 | 依赖 ShopXO 原生机制,有据可查 | 插件黑盒,故障排查困难 |
方案 B 的"SKU 少"优势在演唱会 10K+ 场景不成立:插件自管 SKU通过 Hook 隐藏插件专用规格,不走 ShopXO 原生规格管理页面。
---
## 最终推荐
### 采纳方案:方案 A — 每个座位一个 ShopXO SKUstock=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();
```
翻译为 SQLUPDATE 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 — 推荐方案 ARound 2/3 分析完成
- SecurityEngineer: CONSENSUS: YES — $vr- 前缀低风险,方案 A 推荐
- FrontendDev: CONSENSUS: YES — 方案 A 推荐,前端配合方案清晰
全票通过:采纳方案 A