vr-shopxo-plugin/docs/ROUND2_ANALYSIS.md

7.2 KiB
Raw Permalink Blame History

Round 2 深入分析 — BackendArchitect

日期2026-04-15 负责Q1批量 SKU 生成路径)+ Q2紧急修复优先级


Q1方案 A 后台批量生成 SKU 路径分析

ShopXO SPEC/SKU 创建机制

通过代码审查 GoodsService.php:2142GoodsSpecificationsInsert() 函数:

  1. 删除再插入GoodsSpecificationsInsert() 在插入前会 DELETE 该商品的所有 GoodsSpecTypeGoodsSpecValueGoodsSpecBase 记录line 2145-2147
  2. 逐行写入GoodsSpecBase 通过循环 foreach($data['data'] as $v) 逐条 insertGetIdline 2230不是真正的批量 API
  3. 无现成批量 APIShopXO 没有 batchInsertSpecs() 之类的公共方法
  4. 必须旁路 GoodsSpecificationsInsert:不能走 ShopXO 原生商品保存流程(否则每次都清空重建)

可行路径:直接 SQL INSERT

插件在座位模板绑定/初始化时,直接 SQL INSERT 三个表:

Step 1: 写入 sxo_goods_spec_type(规格维度)

INSERT INTO sxo_goods_spec_type (goods_id, name, value, add_time) VALUES
(112, '$vr-场馆', '[{"name":"国家体育馆","images":""}]', UNIX_TIMESTAMP()),
(112, '$vr-分区', '[{"name":"A区","images":""},{"name":"B区","images":""}]', UNIX_TIMESTAMP()),
(112, '$vr-时段', '[{"name":"2026-05-01 19:00","images":""}]', UNIX_TIMESTAMP());

Step 2: 写入 sxo_goods_spec_base(每个座位一行 SKUinventory=1

INSERT INTO sxo_goods_spec_base (goods_id, price, original_price, inventory,
  buy_min_number, buy_max_number, add_time) VALUES
(112, 680.00, 880.00, 1, 1, 1, UNIX_TIMESTAMP()),  -- A区 A-1
(112, 680.00, 880.00, 1, 1, 1, UNIX_TIMESTAMP()),  -- A区 A-2
... -- 10000+ 行

Step 3: 写入 sxo_goods_spec_value(建立 spec_base_id ↔ spec_value 的映射)

INSERT INTO sxo_goods_spec_value (goods_id, goods_spec_base_id, value, md5_key, add_time) VALUES
(112, @base_id_1, '国家体育馆', md5('国家体育馆'), UNIX_TIMESTAMP()),
(112, @base_id_1, 'A区', md5('A区'), UNIX_TIMESTAMP()),
(112, @base_id_1, '2026-05-01 19:00', md5('2026-05-01 19:00'), UNIX_TIMESTAMP());
-- 每个座位 3 条对应3个spec维度

Step 4: 更新 sxo_goodsis_exist_many_spec = 1(告诉 ShopXO 启用多规格)

关键发现:防超卖机制的原子性

审查 BuyService.php:1677-1681

$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();

ThinkPHP 的 dec() 翻译为 SQLUPDATE goods_spec_base SET inventory = inventory - N WHERE inventory >= N

这是条件原子扣减——在 MySQL 层是原子的。方案 A 依赖这个机制来防超卖。

但存在 TOCTOU 窗口在并发极高10K+ 同时抢票)时,两条请求可能同时通过 inventory >= 1 检查再同时执行 dec()。MySQL 的 InnoDB 行锁会串行化这两个 UPDATE但不保证顺序——理论上可能出现两人都查到 inventory=1都通过检查都执行 dec(),最终 inventory=-1。

实际风险评估:演唱会抢票场景是"选座"而非"随机库存"用户选座时前端已经锁定了具体座位请求打到后端时并发度远低于总库存。TOCTOU 窗口极小。推荐接受此风险

性能估算

  • 10000 座位 = 10000 条 goods_spec_base + 30000 条 goods_spec_value
  • 单次批量 INSERT 耗时:~0.5-2 秒InnoDB 批量插入效率高)
  • 需要分批提交:每批 500 条,避免单次大事务锁表超时
  • 初始化一次:座位模板绑定时生成,后续不变

结论

Q1 结论:可行,但必须旁路 ShopXO 原生 GoodsSpecificationsInsert(),走直接 SQL INSERT 路径。


Q2商品 112 broken 状态紧急修复优先级

当前状态分析

goods_id=112:
  is_exist_many_spec = 0    ← ShopXO 认为无多规格
  spec_base 表 = 空           ← 从未生成过 SKU
  spec_base_id_map → {A:1001, B:1002, C:1003}  ← 这些 ID 在 DB 里不存在!

影响评估

影响点 严重程度 说明
购买流程 当前 is_exist_many_spec=0购买走裸商品逻辑spec_base_id_map 形同虚设
票生成onOrderPaid spec_base_id 指向不存在的 DB 记录,但代码有幂等保护,暂不崩溃
ShopXO 后台显示 不影响 ShopXO 原生商品管理
用户端选座 前端/小程序逻辑独立

修复路径

最小修复集(方案甲):仅设置 flag不重建 SKU

UPDATE sxo_goods SET is_exist_many_spec = 1 WHERE id = 112;

然后在票生成逻辑中对 spec_base_id=0 做 fallback 保护。

推荐修复集(方案乙):设置 flag + 重建 $vr- spec_type

-- Step 1: 告诉 ShopXO 启用多规格
UPDATE sxo_goods SET is_exist_many_spec = 1 WHERE id = 112;

-- Step 2: 写入 $vr- 规格维度(场馆/分区/时段名称)
INSERT INTO sxo_goods_spec_type (goods_id, name, value, add_time) VALUES
(112, '$vr-场馆', '[{"name":"国家体育馆","images":""}]', UNIX_TIMESTAMP()),
(112, '$vr-分区', '[{"name":"A区","images":""},{"name":"B区","images":""},{"name":"C区","images":""}]', UNIX_TIMESTAMP()),
(112, '$vr-时段', '[{"name":"2026-05-01 19:00","images":""}]', UNIX_TIMESTAMP());

-- Step 3: 重建 spec_base_id_map → seat_id 到 spec_base_id 的映射
-- (由插件 SeatSkuService 完成)

注意spec_base 表不重建——因为真正的批量 SKU 生成是在 Phase 3「座位模板绑定」时做的届时会走 SQL INSERT 路径。

结论

Q2 结论:推荐方案乙——最小修复集 = UPDATE is_exist_many_spec + INSERT $vr- spec_type + 幂等保护。紧急程度中等,不影响当前票务逻辑运行,但应在 Phase 3 批量 SKU 生成前完成。


Q4 初步推荐(基于 Q1/Q2 分析)

推荐方案 A每个座位一个 SKU,理由补充:

  1. 原子性已验证BuyService.php 的 dec() 机制是 MySQL 层面的条件原子扣减,方案 A 的防超卖完全依赖此机制,无需自建锁
  2. 数据完整性:每个座位独立 inventory=1ShopXO 原生购买流程完整走通,无需 Hook 旁路购买逻辑
  3. 票务链路清晰spec_base_id 直接对应座位,票生成逻辑无需反向解析
  4. TOCTOU 风险可接受选座模式并发窗口极小ShopXO 行锁提供最后保护

方案 B 的唯一优势SKU 数量少)在演唱会场景下不成立——方案 A 的批量 INSERT 一次性完成,不存在"管理困难"问题(插件自己管理,不走 ShopXO 后台)。


行动项Round 2 输出)

优先级 行动项 负责
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 设计插件独立 SKU 管理页面(隔离 ShopXO 原生规格管理) FrontendDev