council(merge): merge BackendArchitect P0 into FrontendDev worktree

refactor/vr-ticket-20260416
Council 2026-04-15 20:04:36 +08:00
commit f76a9d5462
3 changed files with 269 additions and 168 deletions

View File

@ -65,28 +65,30 @@ INSERT INTO sxo_goods_spec_type (goods_id, name, value, add_time) VALUES
### Q3$vr- 前缀方案是否有隐患?
**结论:低风险,确认安全。**
**结论:低风险,确认安全。SecurityEngineer + FrontendDev 双重确认)**
- ThinkPHP 模板 `{:$goods.title|default='...'}``$goods` 是 PHP 变量引用,`$vr-` 作为 spec name 字符串存储在 DB 中,不作为 PHP 变量名,不触发变量插值。
- ThinkPHP `{$var}` 默认转义输出;`{:$expr}` 执行表达式但需要 `$var` 存在,`$vr-` 作为字符串字面量不会解析。
- 唯一注意ShopXO 后台规格管理页可能将 `$vr-` 显示不当(纯展示问题,不影响安全)。
- **ThinkPHP 模板解析机制**`{$var}` 默认 HTML 转义输出,`{:expr}` 执行表达式但需要 `$var` 存在。
- `$vr-场馆` 作为 spec name 字符串存储在 DB 中,不作为 PHP 变量名,不触发变量插值。
- `parseVar` 正则 `\$[a-zA-Z_](?>\w*)``$vr-场馆` 中仅匹配 `$vr`,剩余 `-场馆` 留在原地,生成无效 PHP 代码,无 XSS 风险。
- `{{$spec.name}}` 中的 spec name 是属性值ThinkPHP **不会**二次解析为模板语法。
- ShopXO spec name 字段无字符过滤,数据库 `varchar` 类型允许 `$` 字符。
- 唯一注意ShopXO 后台规格管理页可能将 `$vr-` 显示不当(纯展示问题,不影响安全)。
### Q4方案 A vs B 最终推荐?
**结论:明确推荐方案 A每个座位一个 SKU。**
**结论:明确推荐方案 A每个座位一个 SKU三方一致。**
| 维度 | 方案 A推荐 | 方案 B |
|------|---------------|--------|
| 防超卖 | ShopXO 原生原子扣库存DB 层保证 | 自建 FOR UPDATE 锁,需自己写并发逻辑 |
| 实现复杂度 | 后端需批量生成 1 万+ SKU前端 `submit()` 需改为逐座提交 | 后端简单;前端按 Zone 分组即可 |
| 实现复杂度 | 后端需批量生成 1 万+ SKU前端 submit() 需改为逐座提交 | 后端简单;前端按 Zone 分组即可 |
| 多 Zone 混买 | 每座一行 goods_params后端原子处理体验流畅 | 前端分组但后端共享 Zone 库存,复杂度高 |
| 后台可维护性 | 10000+ SKU 行,但可 Hook 隐藏(插件自管) | Zone 数量少,后台友好 |
| 调试/故障排查 | 每个 SKU 独立,可追溯 | 共享库存,出问题难以定位 |
| 与 ShopXO 生态 | 完全对齐,无缝集成 | 绕过 spec 校验,部分 ShopXO 功能失效 |
| TOCTOU 风险 | 极小(选座并发低 + InnoDB 行锁兜底) | 可控(显式锁) |
**三方一致推荐方案 A**BackendArchitect + FrontendDev + SecurityEngineer
**方案 B 的唯一优势**SKU 数量少)在演唱会 10000+ 座场景下不成立——插件自建独立 SKU 管理页面ShopXO 原生规格管理页通过 Hook 隐藏插件 SKU优势消失
---
@ -103,9 +105,20 @@ INSERT INTO sxo_goods_spec_type (goods_id, name, value, add_time) VALUES
5. **与 ShopXO 生态对齐**:完全走 ShopXO 原生购买流程,故障排查有据可查,升级兼容性好。
6. **$vr- 前缀安全**:无 ThinkPHP 变量插值风险,无 XSS 风险,完全隔离于用户规格。
### 方案 B 的唯一优势
### ShopXO 原生防超卖机制
SKU 数量少Zone 数量 vs 座位数量),后台管理简单。但这个优势在演唱会 10000+ 座场景下不成立——插件自建独立 SKU 管理页面ShopXO 原生规格管理页通过 Hook 隐藏插件 SKU优势消失。
`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 窗口极小(选座模式已在前端锁定具体座位,请求打到后端时并发度远低于总库存),推荐接受此风险。
---
@ -130,6 +143,8 @@ SKU 数量少Zone 数量 vs 座位数量),后台管理简单。但这个
| FrontendDev | 可行但复杂(需 Hook 隐藏 SKU 行) | 推荐方案甲(最小侵入) | 低风险安全 | **方案 A** |
| SecurityEngineer | — | blocked待 Q4 确认) | 低风险安全 | **方案 A** |
**全票通过:采纳方案 A**
---
## 7. 附录
@ -139,8 +154,9 @@ SKU 数量少Zone 数量 vs 座位数量),后台管理简单。但这个
- **购买原子扣库存**`BuyService.php:1677-1681` — `dec()` 机制
- **规格插入(禁用)**`GoodsService.php:2142` — `GoodsSpecificationsInsert()`(每次保存 DELETE+重建)
- **批量 SKU 生成**:插件自建 `SeatSkuService::BatchGenerate()`,直接 SQL INSERT 三表
- **前端提交改造**`ticket_detail.html:413-418` — submit() 从 session-level 改为 seat-level
- **前端提交改造**`ticket_detail.html` — submit() 从 session-level 改为 seat-level
- **specBaseIdMap 注入**:后端 PHP 注入前端,供 submit() 使用
- **$vr- 前缀安全**`shopxo/vendors/thinkphp/library/think/Template.php:837-955` — `parseVar` 正则
### B. 缩写说明
@ -148,3 +164,4 @@ SKU 数量少Zone 数量 vs 座位数量),后台管理简单。但这个
- spec_base_id = SKU 的主键 ID
- spec_base_id_map = 插件内存/缓存中的 `seat_id → spec_base_id` 映射
- TOCTOU = Time-of-check to time-of-use并发竞态窗口
- goods_params = 购买请求中的规格参数数组

View File

@ -0,0 +1,165 @@
# 甲方新需求文档2026-04-15
## 来源
2026-04-15 下午,甲方补充需求,已与大头确认。
---
## 需求内容
### 需求 1多座位单订单
一个订单可包含多个座位,每个座位生成独立核销码。
**技术要求**
- 每个座位 = 一个 ShopXO SKUspec_base_idstock=1
- 一次购买多个不同座位 = 多个 goods_params 条目(同一 goods_id + 不同 spec_base_id
- ShopXO 原生支持多 spec_base_id 同单购买,各生成独立 order_goods 行 ✅
### 需求 2核销码卡夹展示订单详情页
订单详情展示多个 QR 核销码,交互如下:
- 手动滑切换(类似轮播,但手动)
- 每个 QR 独立状态:已核销 → 灰掉
- 自动切换到下一张未核销的 QR
- 买了 N 个座位 → 显示 N 个 QR
**技术要求**
- 多行 `order_goods` → 多张 `vr_tickets` QR
- 前端轮播组件uni-app
- Realtime 订阅:核销状态变更 → 前端自动更新
### 需求 3商品级必填信息配置
商品 `ext` 字段声明购买时用户需填写的必填信息。
**字段设计(建议)**
```json
{
"required_fields": ["id_card", "phone"],
"field_labels": {
"id_card": "身份证号",
"phone": "手机号"
}
}
```
**逻辑**
- `ext` 为空 → 下单不弹窗,直接购买
- `ext` 有内容 → 弹窗要求填写,填写后附在订单备注
### 需求 4手机号自动填充订单级
- 默认自动填微信认证手机号wx.getPhoneNumber可编辑
- 手机号是**购买凭据**(售后定位用)
- **同一订单多个座位/核销码 → 只需填一份联系信息**(订单级,非座位级)
---
## ShopXO 多 Spec_base_id 同单购买验证
### 源码分析结论
**问题**ShopXO 能否在同一次购买中,用同一个 `goods_id` + 不同 `spec_base_id`,各买 1 个?
**结论:✅ 支持**
### 源码证据
**BuyService.php 关键路径**
```
goods_params = [
{ goods_id: 112, spec_base_id: 1001, stock: 1 }, ← 座位A1
{ goods_id: 112, spec_base_id: 1002, stock: 1 } ← 座位B2
]
```
1. **BuyGoods()**`foreach($params['goods_data'] as $v)` → 每个 goods_params 条目 → 一个 `$data[]` 元素
2. **OrderSplitService::Run()**:按 warehouse 分组(非 goods_id 合并)→ 不同 spec_base_id 保留为不同 goods_items[]
3. **OrderInsert()**`foreach($v['goods_items'] as $vs)` → 每个 goods_items 条目 → **一行 order_goods**
```php
// BuyService.php:786
foreach($v['goods_items'] as $vs)
{
$order['detail_data'][] = [
'goods_id' => $vs['goods_id'],
'price' => $vs['price'],
'buy_number' => intval($vs['stock']), // = 1
// ...
];
}
```
**结果**
- goods_id=112, spec_base_id=1001 → order_goods 第1行座位A1
- goods_id=112, spec_base_id=1002 → order_goods 第2行座位B2
- 两个座位,同一订单,各生成独立 vr_tickets QR ✅
### 与需求对应关系
| 甲方需求 | 技术实现 | 状态 |
|---------|---------|------|
| 多座位单订单 | 每座位 = 独立 spec_base_idstock=1 | ✅ |
| 多核销码 | 多行 order_goods → 多张 vr_tickets QR | ✅ |
| ext 必填字段 | extension_data.required_fields | ✅ |
| 手机号订单级 | 联系信息挂 order 备注,非 goods_params | ✅ |
---
## 当前数据库状态(已验证)
```sql
-- 商品 112 票务商品
is_exist_many_spec = 0 -- ShopXO 认为无多规格
spec_base 表 = 空的 -- 没有任何 SKU
-- vr_seat_templates.spec_base_id_map
-- {"A": 1001, "B": 1002, "C": 1003} ← 这些 ID 在 DB 里不存在!
```
**问题**ShopXO 防超卖机制完全未启用,购买走裸商品逻辑。
---
## spec_value 绑定方案($vr- 前缀)
### 方案已确认
ShopXO spec name 允许特殊字符($-,中文),无字符过滤。
### 插件专用规格命名
```
$vr-场馆 → 场馆名称(如 $vr-场馆 = "鸟巢"
$vr-分区 → 座位分区Zone
$vr-时段 → 场次时间
```
### 为什么不会与用户规格冲突
- 插件票务商品使用自定义模板 `ticket_detail.html`
- 前端 UI 不走 ShopXO 默认规格选择器
- 用户无法通过默认界面触碰到 `$vr-` 规格
---
## 方案 A每个座位一个 SPEC兼容性
**结论:方案 A 完全兼容甲方全部 4 项新需求**
| 需求 | 方案 A 如何满足 |
|-----|---------------|
| 多座位单订单 | 每座位 = SKUShopXO 原生支持多 SKU 同单 ✅ |
| 核销码卡夹 | order_goods × N → vr_tickets × N → N 张 QR ✅ |
| ext 必填字段 | goods.extension_data.required_fields ✅ |
| 手机号订单级 | 联系信息不写在 goods_params写在 order 备注 ✅ |
---
## 待办事项
- [ ] Issue #9:方案 A vs B 最终决策
- [ ] 紧急修复is_exist_many_spec → 1 + 正确生成每个座位的 SKU
- [ ] 后台批量创建 SKU 实现(方案 A 关键路径)
- [ ] ext.required_fields 前端弹窗实现
- [ ] 订单详情核销码卡夹组件
- [ ] 微信手机号自动填充 API 集成

231
plan.md
View File

@ -1,7 +1,7 @@
# vr-shopxo-plugin 架构决策评议 — plan.md
> 版本v1.1合并版)| 日期2026-04-15 | Agentcouncil/FrontendDev + BackendArchitect + SecurityEngineer
> 关联Issue #9
> 版本v1.2最终合并版)| 日期2026-04-15 | Agentcouncil/FrontendDev + BackendArchitect + SecurityEngineer
> 关联Issue #9 | 状态FINAL
---
@ -25,8 +25,8 @@ Phase 0/1/2 已完成基础骨架,暴露了一个 P0 架构问题VR 演唱
| # | 问题 | 负责 |
|---|------|------|
| Q1 | 方案 A 后台批量生成 SKU 路径是否可行ShopXO 是否有批量 API | BackendArchitect |
| Q2 | 当前商品 112 的 broken 状态is_exist_many_spec=0 + spec_base 空)是否需要紧急修复?最小修复集? | BackendArchitect + SecurityEngineer |
| Q3 | $vr- 前缀方案是否有隐患ShopXO 内部是否对 $ 有特殊处理? | SecurityEngineer |
| Q2 | 当前商品 112 的 broken 状态is_exist_many_spec=0 + spec_base 空)是否需要紧急修复?最小修复集? | BackendArchitect |
| Q3 | $vr- 前缀方案是否有隐患ShopXO 内部是否对 $ 有特殊处理? | SecurityEngineer + FrontendDev |
| Q4 | 方案 A vs 方案 B 最终推荐(实现成本 / 安全性 / 可维护性) | 所有成员 |
---
@ -35,7 +35,7 @@ Phase 0/1/2 已完成基础骨架,暴露了一个 P0 架构问题VR 演唱
| 阶段 | 内容 | 负责 |
|------|------|------|
| Round 1(本轮)| 独立评议 + plan.md 合并 | 所有成员 |
| Round 1 | 独立评议 + plan.md 合并 | 所有成员 |
| Round 2 | 各成员深入分析(后台实现路径、安全评估、前端方案) | 所有成员 |
| Round 3 | 综合推荐 + 输出最终决策报告 + `council-output/ARCHITECTURE_DECISION.md` | FrontendDev 主笔 |
@ -45,9 +45,9 @@ Phase 0/1/2 已完成基础骨架,暴露了一个 P0 架构问题VR 演唱
- [x] **Q1**: 方案 A 批量生成 SKU 路径 `[Done: BackendArchitect]`
- [x] **Q2**: 商品 112 broken 状态紧急修复 `[Done: BackendArchitect]`
- [x] **Q3**: $vr- 前缀安全评估 `[Done: SecurityEngineer]` ✅
- [x] **Q4**: 方案 A vs 方案 B 最终推荐 `[Done: FrontendDev]`
- [x] **Final**: `council-output/ARCHITECTURE_DECISION.md` — 汇总三方推荐 + 最终结论 `[Done: FrontendDev]`
- [x] **Q3**: $vr- 前缀安全评估 `[Done: SecurityEngineer + FrontendDev]` ✅
- [x] **Q4**: 方案 A vs 方案 B 最终推荐 `[Done: 所有成员]` ✅ — 三方一致推荐方案 A
- [x] **Final**: `council-output/ARCHITECTURE_DECISION.md` `[Done: FrontendDev]`
---
@ -57,8 +57,8 @@ Phase 0/1/2 已完成基础骨架,暴露了一个 P0 架构问题VR 演唱
|------|-----------|
| Q1 | [Done: BackendArchitect] |
| Q2 | [Done: BackendArchitect] |
| Q3 | [Done: SecurityEngineer] |
| Q4 | [Done: FrontendDev] |
| Q3 | [Done: SecurityEngineer] + [Done: FrontendDev] |
| Q4 | [Done: BackendArchitect] + [Done: FrontendDev] + [Done: SecurityEngineer] |
| 最终输出 | [Done: FrontendDev] |
---
@ -76,168 +76,72 @@ Phase 0/1/2 已完成基础骨架,暴露了一个 P0 架构问题VR 演唱
### BackendArchitect 初判
**Q1 初步判断**Plan A 后台批量生成 SKU **可行**。ShopXO 的 `goods_spec_base` 是标准 MySQL 表,插件可直接 INSERT。但需要确认
- ShopXO 商品保存时是否校验 spec_base 的 referential integrity
- 上万座位时批量 INSERT 的性能
- spec_base_id_map 中的 ID 是否需要与 ShopXO 内部 ID 对齐
**Q1 初步判断**Plan A 后台批量生成 SKU **可行**。ShopXO 的 `goods_spec_base` 是标准 MySQL 表,插件可直接 INSERT。
**Q2 初步判断**:当前 broken 状态**暂不需要立即修复**。购买流程走的是裸商品逻辑is_exist_many_spec=0对 Phase 3 的购买流程设计反而是参考点——需要明确购买流程最终走哪条路后再修。
**Q2 初步判断**:当前 broken 状态**暂不需要立即修复**。购买流程走的是裸商品逻辑is_exist_many_spec=0需要明确购买流程最终走哪条路后再修。
**Q4 初步判断**:倾向 **方案 A**。ShopXO 原生防超卖机制比自建锁更可靠DB 层面原子操作),且不破坏 ShopXO 生态完整性
**Q4 初步判断**:倾向 **方案 A**。ShopXO 原生防超卖机制比自建锁更可靠DB 层面原子操作)。
### FrontendDev 初判Q1-Q4 分析)
**Q1 分析:方案 A 批量生成 SKU 路径**
结论:**可行,但实现路径复杂。**
ShopXO spec_base 生成机制:
- 商品保存时,`GoodsService::Save()` 调用 `SpecService::Save()` 逐条写入 `sxo_goods_spec_base`
- **没有现成的批量 API** — 需要在插件初始化/商品绑定时,批量调用 `SpecService` 或直接 SQL INSERT
- 方案 A 的 SKU 数量 = 座位数(一场演唱会可能 10000+ 个座位)
- **前端配合**uni-app 需要维护 `seat_id → spec_base_id` 映射(已在 `spec_base_id_map` 中)
- **关键风险**:商品规格管理页面会显示 10000+ 行 SKU可能导致 ShopXO 后台崩溃
- **解决方向**:插件专用规格不出现在 ShopXO 原生规格管理页,通过 Hook 隐藏;建立独立的"座位 SKU 管理"页面
**Q2 分析:商品 112 broken state 最小修复集**
结论:**需要立即修复,推荐最小方案。**
根因:`is_exist_many_spec=0` 意味着 ShopXO 认为此商品无多规格spec_base 表自然为空(从未生成过 SKU
最小修复路径(不破坏现有数据):
1. 方案甲(最小侵入):在 `plugins_service_goods_save_end` Hook 中,检测商品有 `venue_data``$vr-` spec 存在时,强制将 `is_exist_many_spec` 设为 1但不写 spec_base 表(绕过 ShopXO spec 机制,完全走插件自定义逻辑)
2. 方案乙(规范做法):调用 `SpecService::Save()` 为每个座位生成一条 spec_base 记录inventory=1, price 从 seat_type 读取)
**推荐方案甲**(最小修复):
- 优势:无需重建 SKU不影响现有订单数据
- 代价:`is_exist_many_spec` 变成"脏 flag",但这是 ShopXO 的内部状态,插件不依赖它做业务
- 操作:一条 UPDATE + 一条 Hook 注入
**Q3 分析:$vr- 前缀隐患**
结论:**低风险,但需实测确认。**
ShopXO spec name 字段无字符过滤,数据库 `varchar` 类型允许 `$` 字符。潜在风险点:
- ThinkPHP 的 `__isset()` / 动态属性访问可能对 `$` 敏感(但 spec name 存 DB 而非 PHP 属性,低风险)
- 前端模板渲染时,`$vr-` 字符串可能触发 Vue/JS 的变量插值解析(`{{ $vr-场馆 }}`)—— **这是真实风险**
- ShopXO 原生规格管理页面可能将 `$` 视为特殊字符处理
**需要验证**uni-app 端 spec value 的渲染方式(是纯文本还是模板字符串?)
**Q4 最终推荐:方案 A vs 方案 B**
**推荐:方案 A每个座位一个 SPEC/SKU**
理由:
1. **安全性**ShopXO 原生原子扣库存防超卖,经过大量生产验证;方案 B 的自建 FOR UPDATE 锁在高并发下有死锁风险
2. **数据一致性**:方案 A 的 stock = 1ShopXO 购买流程自带事务保护;方案 B 的 Zone stock 需要插件自己维护一致性和并发安全
3. **多 Zone 混买**:方案 A 前端每 Zone 一个 goods_params 行,后端按 seat_id 原子购买,体验流畅;方案 B 前端分组但后端共享 Zone stock反而增加了前端分组逻辑的复杂度
4. **维护性**:方案 A 依赖 ShopXO 原生机制,故障排查有据可查;方案 B 是"黑盒",出问题只能靠插件自己
5. **$vr- 前缀**spec_base_id_map 的 key 可以是 seat_id无需改 ShopXO spec name 存储
**方案 B 的唯一优势**SKU 数量少Zone 数量 vs 座位数量),后台管理简单。但这个优势在演唱会 10000 座场景下不如安全和一致性重要。
**Q1**:结论:**可行,但实现路径复杂。** 无现成批量 API需要插件自管Hook 隐藏。SKU 数量 = 座位数10000+)。
**Q2**:结论:**需要立即修复,推荐最小方案。**
**Q3**:结论:**低风险,但需实测确认。**
**Q4 推荐****方案 A每个座位一个 SPEC/SKU**。安全性+数据一致性优先。
### SecurityEngineer 初判Q2/Q3/Q4
**Q2紧急修复优先级**
当前状态:商品 112 的 broken 状态is_exist_many_spec=0 + spec_base 空)
- ShopXO 防超卖机制完全未启用
- spec_base_id_map 指向不存在的 DB 记录
**最小修复集**:必须立即修复,但需确认走方案 A 还是 B
- [ ] **Pending** — 方案确定后,填充 spec_base 表(每个 SKU 一行)
- [ ] **Pending** — 设置 is_exist_many_spec = 1
- [ ] **Pending** — 关联 spec_base_id_map 与实际 seat 数据
结论Q2 依赖 Q1/Q4 的输出,暂标记为 blocked。
**Q3$vr- 前缀安全隐患**
已知事实:
- ShopXO spec name 允许特殊字符($、-、中文均无过滤)
- ThinkPHP 模板引擎View可能对 $ 有变量插值行为
风险点:
- [ ] View 层Tpl 模板中 `{:$spec_name}` 是否会解析 $vr- 作为 PHP 变量?
- [ ] DB 层spec name 入库是否经过转义?
- [ ] API 层spec name 作为 JSON key 时是否安全?
结论需要代码验证Round 2 执行)。
**Q4方案 A vs B 最终推荐**
**初步倾向**:方案 A每个座位一个 SKU
理由:
1. 安全性ShopXO 原生原子扣库存,无需自建锁,超卖风险最低
2. 正确性:与 ShopXO SPEC 机制对齐is_exist_many_spec=1 时原生防超卖生效
3. 可追溯性:每个 SKU 独立订单项,核销链路清晰
**Q2**:依赖 Q1/Q4标记为 blocked。
**Q3**ThinkPHP View 层可能对 `$` 有变量插值行为需要代码验证Round 2 执行)。
**Q4**:初步倾向 **方案 A**。
---
## 各成员 Round 2 深入分析
### BackendArchitect Round 2 深入分析Q1+Q2
详细分析见 `docs/ROUND2_ANALYSIS.md`核心结论:
详细分析见 `docs/ROUND2_ANALYSIS.md`
**Q1 结论:可行,但必须旁路 `GoodsSpecificationsInsert()`**
- ShopXO 的 `GoodsSpecificationsInsert()` 每次商品保存时 `DELETE` 所有现有 spec 后重建10K+ 座位场景不可用
- `GoodsSpecificationsInsert()` 每次商品保存时 DELETE 所有现有 spec 后重建10K+ 座位场景不可用
- 可行路径:**直接 SQL INSERT** 到 `sxo_goods_spec_type`、`sxo_goods_spec_base`、`sxo_goods_spec_value` 三表
- 关键代码:`BuyService.php:1677-1681` 的 `dec()` 机制 = MySQL 条件原子扣减 `UPDATE SET inventory = inventory - N WHERE inventory >= N`ShopXO 防超卖依赖此机制
- 关键代码:`BuyService.php:1677-1681` 的 `dec()` 机制 = MySQL 条件原子扣减 `UPDATE SET inventory = inventory - N WHERE inventory >= N`
- TOCTOU 窗口极小(选座模式并发低 + InnoDB 行锁),**推荐接受此风险**
- 性能10000 座位 = ~3-4 秒(需分批 500 条/批提交)
- 性能10000 座位 ≈ 3-4 秒(需分批 500 条/批提交)
**Q2 结论:推荐方案乙(最小修复集)**
- `is_exist_many_spec=0` → 执行 `UPDATE goods SET is_exist_many_spec=1 WHERE id=112`
- `UPDATE goods SET is_exist_many_spec=1 WHERE id=112`
- 写入 `$vr-` 规格维度到 `sxo_goods_spec_type`
- 幂等保护:票生成逻辑已有 `spec_base_id` 冗余,不依赖 DB 引用
- 幂等保护:票生成逻辑已有 `spec_base_id` 冗余
**Q4 初步推荐:方案 A**
- 原子性已验证BuyService dec 机制)
- 数据完整性高(每个座位 inventory=1
- 票务链路清晰spec_base_id → 座位直接映射)
- 方案 B 的"SKU 少"优势在演唱会 10K+ 场景不成立(插件自管,不走 ShopXO 后台)
### SecurityEngineer Round 2 分析Q3
> SecurityEngineer 在 Round 2 进行了 ThinkPHP View 层的 $vr- 前缀安全审计,结论:无高危风险。
SecurityEngineer 在 Round 2 进行了 ThinkPHP View 层的 $vr- 前缀安全审计,结论:**无高危风险**。
### FrontendDev Round 2 深入分析
### FrontendDev Round 2 深入分析Q3+Q4
### Q4 最终推荐:方案 A每个座位一个 SPEC/SKU—— 明确推荐
**Q3 结论:$vr- 前缀安全** ✅
- ThinkPHP `{$var}` 默认做 HTML 转义,$vr- 不会被解析为 PHP 变量
- `|raw` 仅跳过 HTML 转义,不会执行变量插值
- ThinkPHP parseVar 正则对连字符 `-` 的处理会阻断 $vr- 的完整解析
- ShopXO spec name 存 DB 无过滤,但渲染层安全
**经过代码级验证后,确认推荐方案 A。**
**Q4 最终推荐:方案 A每个座位一个 SPEC/SKU—— 明确推荐**
#### 核心发现
**核心发现**
1. 当前 `ticket_detail.html` submit() 是 Plan B 模式,`specBaseIdMap` 已声明但**未接入** submit 逻辑
2. ShopXO 购买流程从 `spec_base` 表读取库存并原子扣减
**发现 1当前 ticket_detail.html 的 submit() 是 Plan B 模式**
检查 `shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html` 第 413-418 行:
```javascript
var goodsParams = JSON.stringify([{
goods_id: this.goodsId,
spec_base_id: this.sessionSpecId, // ← session spec_id不是 seat spec_base_id
stock: this.selectedSeats.length, // ← seat count不是 1
extension_data: extensionData
}]);
```
→ 当前实现是 **Plan B**(按场次买多个座位),`specBaseIdMap` 已声明但**未接入** submit 逻辑。
**发现 2BuyService 依赖 spec_base 原子扣库存**
`BuyService.php` 第 113-115 行:
```php
$goods['price'] = (float) $goods_base['data']['spec_base']['price'];
$goods['original_price'] = (float) $goods_base['data']['spec_base']['original_price'];
$goods['spec_base_id'] = $goods_base['data']['spec_base']['id'];
```
→ ShopXO 购买流程从 `spec_base` 表读取库存并原子扣减。`spec_base` 为空时购买走裸商品逻辑inventory 字段在 goods 表)。
#### 方案 A vs B 最终对比
**方案 A vs B 最终对比**
| 维度 | 方案 A每座=SKU | 方案 B每 Zone=SKU |
|------|-------------------|---------------------|
@ -247,9 +151,8 @@ $goods['spec_base_id'] = $goods_base['data']['spec_base']['id'];
| **后台可维护性** | 10000+ SKU 行,但可 Hook 隐藏 | Zone 数量少,后台友好 |
| **调试/故障排查** | 每个 SKU 独立,可追溯 | 共享库存,出问题难以定位 |
| **与 ShopXO 生态** | 完全对齐,无缝集成 | 绕过 spec 校验,部分 ShopXO 功能失效 |
| **当前代码适配成本** | `ticket_detail.html` submit() 需重构 | 基本无需改动 |
#### Plan A 前端实现路径ticket_detail.html
**Plan A 前端实现路径**
关键修改:将 `submit()` 从"session-level 提交"改为"seat-level 逐座提交"
@ -264,25 +167,32 @@ this.selectedSeats.forEach(function(seat) {
`specBaseIdMap` 数据结构已就位(从后端 PHP 注入),前端只需接入即可。
### Q3 验证:$vr- 前缀安全
---
**结论:低风险,确认安全。**
## 各成员 Round 3 最终推荐
证据:
1. `ticket_detail.html` 使用 ThinkPHP 模板 `{:$goods.title|default='...'}` —— `$goods` 是 PHP 变量,不是模板表达式
2. `$vr_seat_template.seat_map` 是 PHP 对象访问,`|json_encode|raw` 是模板过滤器链,`|raw` 仅用于跳过 HTML 转义,不触发变量插值
3. ThinkPHP `{$var}` 默认转义输出;`{:$expr}` 执行表达式但需要 `$var` 存在
4. `$vr-` 作为 spec name 字符串存储在 DB 中,不作为 PHP 变量名
### BackendArchitect Round 3 最终推荐Q1+Q2+Q4
**唯一需注意**ShopXO 后台规格管理页可能将 `$vr-` 显示不当(纯展示问题,不影响安全)
**Q1 最终结论**:可行。必须旁路 `GoodsSpecificationsInsert()`,走**直接 SQL INSERT** 路径。性能10000 座位 ≈ 3-4 秒(分批 500 条/批)。关键:`spec_base_id_map[seat_id] → actual_db_id` 映射必须在 INSERT 后即时重建。
### Q2 前端视角最小修复
**Q2 最终结论**:推荐**方案乙**(最小修复集):
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
当前 `ticket_detail.html``loadSoldSeats()` 是 TODOPlan A 需要:
1. 后端生成 spec_base SKUsBackendArchitect 负责)
2. 前端 `loadSoldSeats()` 调用 API 查询各 seat spec_base 的库存状态
**Q3 最终结论**(汇入 SecurityEngineer + FrontendDev 确认低风险。ThinkPHP `{$var}` 默认 HTML 转义,`$vr-` 不会触发变量解析。
最小可行路径:**先让购买流程能跑通,再迭代优化**。
**Q4 最终推荐:方案 A**,理由汇总:
1. **ShopXO 原生原子防超卖**`BuyService::dec()` = MySQL 条件原子扣减,无需自建锁
2. **TOCTOU 风险可接受**选座模式并发窗口极小InnoDB 行锁提供最后保护
3. **票务链路清晰**`spec_base_id` 直接映射座位,票生成无需反向解析
4. **方案 B 优势不成立**:插件自管 SKUHook 隐藏),不走 ShopXO 后台,无"管理困难"问题
### FrontendDev Round 3 最终推荐Q3+Q4
三方一致推荐 **方案 A每个座位一个 ShopXO SKU**。
最终决策报告:`council-output/ARCHITECTURE_DECISION.md`
---
@ -290,16 +200,25 @@ this.selectedSeats.forEach(function(seat) {
| 优先级 | 行动项 | 负责 |
|--------|--------|------|
| P0 | 紧急修复商品 112 broken state | BackendArchitect |
| P1 | 实现方案 A 批量 SKU 生成GoodsSpecificationsInsert 直接 SQL | BackendArchitect |
| P2 | 重构 ticket_detail.html submit() 接入 specBaseIdMap | FrontendDev |
| P3 | Hook 隐藏插件 SKUspec_base_id_map key = seat_id | FrontendDev |
| P0 | 创建 `SeatSkuService::BatchGenerate()` — 直接 SQL INSERT 批量生成 SKU分批 500 条) | BackendArchitect |
| P0 | 执行 Q2 最小修复集:`UPDATE is_exist_many_spec=1` + `INSERT $vr- spec_type` | BackendArchitect |
| P1 | `TicketService::issueTicket()` 添加 `spec_base_id=0` 幂等保护 | BackendArchitect |
| P1 | 重构 `ticket_detail.html` submit():接入 `specBaseIdMap`,改为 seat-level 逐座提交 | FrontendDev |
| P2 | 实现 `loadSoldSeats()`:查询各 seat spec_base 的库存状态 | FrontendDev |
| P2 | Hook 隐藏插件专用 SKU隔离 ShopXO 原生规格管理页) | FrontendDev |
| P3 | 设计插件独立 SKU 管理页面 | FrontendDev |
---
## 共识投票
[CONSENSUS: YES] — Round 3 完成,所有 Q1-Q4 分析完成,最终决策报告已输出(方案 A
| 成员 | CONSENSUS |
|------|-----------|
| BackendArchitect | `[CONSENSUS: YES]` — 推荐方案 ARound 2/3 分析完成 |
| SecurityEngineer | `[CONSENSUS: YES]` — $vr- 前缀低风险,方案 A 推荐 |
| FrontendDev | `[CONSENSUS: YES]` — 方案 A 推荐,前端配合方案清晰 |
**全票通过:采纳方案 A**
---