[架构决策] SPEC 设计方向:Zone 级 vs 每个座位一个 SPEC + ShopXO 原生防超卖 #9

Closed
opened 2026-04-15 10:51:22 +00:00 by sileya-ai · 7 comments

架构决策:SPEC 设计方向

创建时间:2026-04-15
状态:待决策
优先级:P0


一、背景与问题来源

本次 P0-2 价格验证修复讨论中,暴露了一个根本性的架构认知偏差:在讨论 ShopXO SPEC 机制时,发现当前 Phase 0-2 的实现与原始设计文档(docs/06_SEAT_MAP_INTEGRATIONATION.md)存在重大不一致。


二、ShopXO SPEC 机制(代码确认)

2.1 核心机制

ShopXO 的商品有多个 SPEC 维度,每个维度有多个 VALUE。

商品:VR演唱会票
  ├── SPEC 维度 1(场馆):[百老汇, 嘉庚体育馆]
  ├── SPEC 维度 2(分区):[VIP区, 普通区]
  ├── SPEC 维度 3(时段):[9点, 11点]
  └── SPEC 维度 4(座位):[a1, a2, b1, b2, ...]

ShopXO 自动计算所有维度的笛卡尔积,生成 SKU:
2场馆 × 2分区 × 2时段 × N座位 = 大量 SKU

关键数据库表:

  • goods_spec_base:每行 = 一个完整 SKU(spec_base_id = SKU ID)
    • 包含:inventory(库存)、pricegoods_id
  • goods_spec_value:(spec_base_id, value) 对
    • 一个 spec_base_id 对应多行(每个 SPEC 维度一个 value)

2.2 购买流程(代码确认)

ShopXO BuyService::BuyGoods() 处理购买:

// BuyService.php:94
$goods["spec"] = self::GoodsSpecificationsHandle($v);
// → 从 goods_data 提取 spec 字段({type, value} 对)

// GoodsService.php:2763-2790
// → 根据 spec 数组去 goods_spec_value 查对应的 spec_base_id
// → 用 spec_base_id 读 goods_spec_base 里的 inventory 和 price

ShopXO 原生防超卖:购买时直接用 spec_base_id 原子扣 goods_spec_base.inventory,无需自己写 FOR UPDATE 锁。


三、原始设计文档(docs/06_SEAT_MAP_INTEGRATIONATION.md 第 1.5 节)

3.1 spec_base_id_map 设计

{
  "spec_base_id_map": {
    "1_1":  { "spec_base_id": 10001, "row": "A", "col": 1, "seat_type": "a", "price": 599 },
    "1_2":  { "spec_base_id": 10002, "row": "A", "col": 2, "seat_type": "a", "price": 599 },
    "3_5":  { "spec_base_id": 10003, "row": "C", "col": 5, "seat_type": "b", "price": 399 }
  }
}
  • key = seat_id(具体座位,如"1_1" = 第1排第1座)
  • spec_base_id = ShopXO SKU ID(每个座位的完整 SPEC 组合)
  • 每个座位的 stock = 1

3.2 原始设计的购买流程

用户选座 seat_id="1_1"
    → 查 spec_base_id_map → spec_base_id=10001
    → ShopXO Buy API: { goods_id, spec }
    → ShopXO 原子扣 goods_spec_base.inventory(stock=1)
    → 完成

3.3 文档中的购买流程(关键注释)

用户在前端选座 seat_id="3_5"
    → 查 spec_base_id_map 拿到 spec_base_id=10003
    → 调 ShopXO Buy API: goods_id + spec_base_id
    → ShopXO 原子扣 spec_base.inventory = 1(FOR UPDATE)
    → 订单完成

四、当前代码实际实现

4.1 submit() 函数(ticket_detail.html:384-421)

var goodsParams = JSON.stringify([{
    goods_id: this.goodsId,
    spec_base_id: this.sessionSpecId,   // ← zone 级别的一个 ID
    stock: this.selectedSeats.length,   // ← 座位数量
    extension_data: { seats: this.selectedSeats }
}]);

所有选中座位(不管在哪个 Zone)共用一个 spec_base_id(zone 级别)。

4.2 实际问题

方面 原始设计 当前代码
spec_base_id_map key 每个座位("1_1") 待确认(测试数据是 {A: id, B: id},疑似 Zone 级)
submit() 传给 ShopXO 每个座位自己的 spec_base_id 所有座位共用 sessionSpecId(zone 级)
防超卖机制 ShopXO 原生 stock=1 我们自己在 onOrderPaid 用 FOR UPDATE 锁
多 Zone 混买 每行 = 一个 spec_base_id(多行 goods_params) 一个 goods_params 行包含所有 Zone

4.3 关键漏洞

当前代码绕过了 ShopXO 的 spec 验证:

// 我们发送的是 spec_base_id(直接 SKU ID)
// 而 ShopXO BuyService::BuyGoods() 期望的是 spec(规格值数组)
// GoodsSpecificationsHandle($v) → 提取 spec 字段
// 如果 spec 字段为空 → ShopXO 内部走 fallback 逻辑

多 Zone 混买场景下(当前代码的 bug):

用户选了:

  • Zone A(¥100)× 2 个座位(a1, a2)
  • Zone B(¥60)× 1 个座位(b1)

前端发给 ShopXO:

[{
    "goods_id": 123,
    "spec_base_id": "Zone-A-session-id",   // ← 只有 Zone A 的 ID
    "stock": 3,
    "extension_data": { "seats": [a1, a2, b1] }
}]

ShopXO 只会检查 Zone A 的库存(A区够不够 3 张),完全不知道 Zone B 座位的存在


五、两种架构方向

方案 A:每个座位一个 SPEC(原始设计)

核心思路: Zone 只是 UI 展示用的样式/价格区;每个具体座位是独立的 SPEC in ShopXO,stock=1。

实现要求:

  1. spec_base_id_map 每个座位一个 key(如 {"1_1": {spec_base_id: 10001}}
  2. 后台管理:创建/编辑座位模板时,自动在 ShopXO 中批量生成 N 个 SPEC(每个座位一个 SKU,inventory=1)
  3. submit():按 spec_base_id 分组,每个 spec_base_id 单独一行 goods_params
  4. ShopXO 原生防超卖(stock=1 原子扣)

优点:

  • 防超卖完全由 ShopXO 处理,无需自己写锁
  • 多 Zone 混买自然支持
  • 每个座位有独立 inventory

缺点:

  • 大型场馆 = 大量 SPEC(5000 座 = 5000 个 SKU)
  • 后台 CRUD 复杂度增加(批量生成 SPEC)
  • ShopXO SPEC 数量上限待确认

方案 B:每个 Zone 一个 SPEC(当前实现)

核心思路: 每个 Zone(A区、B区)= 一个 SPEC,stock = 该 Zone 座位总数。

实现要求:

  1. spec_base_id_map 每个 Zone 一个 key(如 {A: id_A, B: id_B}
  2. 每个 Zone 单独一个 goods_params 行(前端分组提交)
  3. onOrderPaid 校验:seats[] 数量 = 该 Zone 的 stock
  4. 自建 FOR UPDATE 锁(当前已有)

优点:

  • SPEC 数量少(几个 Zone = 几个 SKU)
  • 实现简单

缺点:

  • 绕过 ShopXO spec 验证(当前 bug:发给 ShopXO 的 spec_base_id 是 zone 级别,但 seats[] 包含所有 Zone 的座位)
  • 防超卖依赖我们自己的锁
  • 多 Zone 混买场景需要前端分组逻辑

六、待确认的关键问题

问题 1(最高优先):spec_base_id_map 实际存储格式

数据库里 spec_base_id_map 到底是 Zone 级还是座位级?

  • Zone 级:{"A": {spec_base_id: id_A}, "B": {spec_base_id: id_B}}
  • 座位级:{"1_1": {spec_base_id: id_1_1}, "1_2": {spec_base_id: id_1_2}, ...}

调查方法:

-- 查看 ShopXO 数据库实际内容
SELECT id, goods_id, inventory, price FROM sxo_goods_spec_base WHERE goods_id = ? ORDER BY id LIMIT 20;

问题 2:ShopXO SPEC 数量上限

  • ShopXO 对单个商品的 SPEC 数量有没有限制?
  • 5000 座 × 多 Zone × 多时段 = 多少 SPEC?
  • 需要在 ShopXO 社区或代码中确认

问题 3:Zone 级 SPEC 的合法性

  • 如果 Zone = SPEC(每个 Zone 一个 SPEC),那么多 Zone 混买时如何处理?
  • 当前 submit() 把所有 Zone 座位塞进一个 goods_params 行是否合法?

七、行动项

  • 问题 1:调研 ShopXO 数据库,确认 spec_base_id_map 实际存储格式
  • 问题 2:调研 ShopXO SPEC 数量上限
  • ���策:选择方案 A 还是方案 B
  • 根据决策,重新实现 spec_base_id_map 生成逻辑 + submit() 逻辑

八、关联 Issue

  • Issue #6:P0-2 价格验证(相关)
  • Issue #7:M-04 loadSoldSeats 未实现(需要先确认 SPEC 设计方向)
# 架构决策:SPEC 设计方向 > 创建时间:2026-04-15 > 状态:**待决策** > 优先级:**P0** --- ## 一、背景与问题来源 本次 P0-2 价格验证修复讨论中,暴露了一个根本性的架构认知偏差:在讨论 ShopXO SPEC 机制时,发现当前 Phase 0-2 的实现与原始设计文档(`docs/06_SEAT_MAP_INTEGRATIONATION.md`)存在重大不一致。 --- ## 二、ShopXO SPEC 机制(代码确认) ### 2.1 核心机制 ShopXO 的商品有**多个 SPEC 维度**,每个维度有多个 VALUE。 ``` 商品:VR演唱会票 ├── SPEC 维度 1(场馆):[百老汇, 嘉庚体育馆] ├── SPEC 维度 2(分区):[VIP区, 普通区] ├── SPEC 维度 3(时段):[9点, 11点] └── SPEC 维度 4(座位):[a1, a2, b1, b2, ...] ``` ShopXO 自动计算所有维度的笛卡尔积,生成 SKU: `2场馆 × 2分区 × 2时段 × N座位 = 大量 SKU` **关键数据库表:** - `goods_spec_base`:每行 = 一个完整 SKU(spec_base_id = SKU ID) - 包含:`inventory`(库存)、`price`、`goods_id` - `goods_spec_value`:(spec_base_id, value) 对 - 一个 spec_base_id 对应多行(每个 SPEC 维度一个 value) ### 2.2 购买流程(代码确认) ShopXO `BuyService::BuyGoods()` 处理购买: ```php // BuyService.php:94 $goods["spec"] = self::GoodsSpecificationsHandle($v); // → 从 goods_data 提取 spec 字段({type, value} 对) // GoodsService.php:2763-2790 // → 根据 spec 数组去 goods_spec_value 查对应的 spec_base_id // → 用 spec_base_id 读 goods_spec_base 里的 inventory 和 price ``` **ShopXO 原生防超卖**:购买时直接用 `spec_base_id` 原子扣 `goods_spec_base.inventory`,无需自己写 FOR UPDATE 锁。 --- ## 三、原始设计文档(`docs/06_SEAT_MAP_INTEGRATIONATION.md` 第 1.5 节) ### 3.1 spec_base_id_map 设计 ```json { "spec_base_id_map": { "1_1": { "spec_base_id": 10001, "row": "A", "col": 1, "seat_type": "a", "price": 599 }, "1_2": { "spec_base_id": 10002, "row": "A", "col": 2, "seat_type": "a", "price": 599 }, "3_5": { "spec_base_id": 10003, "row": "C", "col": 5, "seat_type": "b", "price": 399 } } } ``` - **key = seat_id**(具体座位,如"1_1" = 第1排第1座) - **spec_base_id = ShopXO SKU ID**(每个座位的完整 SPEC 组合) - **每个座位的 stock = 1** ### 3.2 原始设计的购买流程 ``` 用户选座 seat_id="1_1" → 查 spec_base_id_map → spec_base_id=10001 → ShopXO Buy API: { goods_id, spec } → ShopXO 原子扣 goods_spec_base.inventory(stock=1) → 完成 ``` ### 3.3 文档中的购买流程(关键注释) > ``` > 用户在前端选座 seat_id="3_5" > → 查 spec_base_id_map 拿到 spec_base_id=10003 > → 调 ShopXO Buy API: goods_id + spec_base_id > → ShopXO 原子扣 spec_base.inventory = 1(FOR UPDATE) > → 订单完成 > ``` --- ## 四、当前代码实际实现 ### 4.1 submit() 函数(ticket_detail.html:384-421) ```javascript var goodsParams = JSON.stringify([{ goods_id: this.goodsId, spec_base_id: this.sessionSpecId, // ← zone 级别的一个 ID stock: this.selectedSeats.length, // ← 座位数量 extension_data: { seats: this.selectedSeats } }]); ``` **所有选中座位(不管在哪个 Zone)共用一个 `spec_base_id`**(zone 级别)。 ### 4.2 实际问题 | 方面 | 原始设计 | 当前代码 | |------|---------|---------| | spec_base_id_map key | 每个座位("1_1") | **待确认**(测试数据是 {A: id, B: id},疑似 Zone 级) | | submit() 传给 ShopXO | 每个座位自己的 spec_base_id | 所有座位共用 sessionSpecId(zone 级) | | 防超卖机制 | ShopXO 原生 stock=1 | 我们自己在 onOrderPaid 用 FOR UPDATE 锁 | | 多 Zone 混买 | 每行 = 一个 spec_base_id(多行 goods_params) | 一个 goods_params 行包含所有 Zone | ### 4.3 关键漏洞 **当前代码绕过了 ShopXO 的 spec 验证:** ```javascript // 我们发送的是 spec_base_id(直接 SKU ID) // 而 ShopXO BuyService::BuyGoods() 期望的是 spec(规格值数组) // GoodsSpecificationsHandle($v) → 提取 spec 字段 // 如果 spec 字段为空 → ShopXO 内部走 fallback 逻辑 ``` **多 Zone 混买场景下(当前代码的 bug):** 用户选了: - Zone A(¥100)× 2 个座位(a1, a2) - Zone B(¥60)× 1 个座位(b1) 前端发给 ShopXO: ```json [{ "goods_id": 123, "spec_base_id": "Zone-A-session-id", // ← 只有 Zone A 的 ID "stock": 3, "extension_data": { "seats": [a1, a2, b1] } }] ``` ShopXO 只会检查 Zone A 的库存(A区够不够 3 张),**完全不知道 Zone B 座位的存在**。 --- ## 五、两种架构方向 ### 方案 A:每个座位一个 SPEC(原始设计) **核心思路:** Zone 只是 UI 展示用的样式/价格区;每个具体座位是独立的 SPEC in ShopXO,stock=1。 **实现要求:** 1. `spec_base_id_map` 每个座位一个 key(如 `{"1_1": {spec_base_id: 10001}}`) 2. 后台管理:创建/编辑座位模板时,自动在 ShopXO 中批量生成 N 个 SPEC(每个座位一个 SKU,inventory=1) 3. `submit()`:按 spec_base_id 分组,每个 spec_base_id 单独一行 goods_params 4. ShopXO 原生防超卖(stock=1 原子扣) **优点:** - 防超卖完全由 ShopXO 处理,无需自己写锁 - 多 Zone 混买自然支持 - 每个座位有独立 inventory **缺点:** - 大型场馆 = 大量 SPEC(5000 座 = 5000 个 SKU) - 后台 CRUD 复杂度增加(批量生成 SPEC) - ShopXO SPEC 数量上限待确认 ### 方案 B:每个 Zone 一个 SPEC(当前实现) **核心思路:** 每个 Zone(A区、B区)= 一个 SPEC,stock = 该 Zone 座位总数。 **实现要求:** 1. `spec_base_id_map` 每个 Zone 一个 key(如 `{A: id_A, B: id_B}`) 2. 每个 Zone 单独一个 goods_params 行(前端分组提交) 3. `onOrderPaid` 校验:seats[] 数量 = 该 Zone 的 stock 4. 自建 FOR UPDATE 锁(当前已有) **优点:** - SPEC 数量少(几个 Zone = 几个 SKU) - 实现简单 **缺点:** - 绕过 ShopXO spec 验证(当前 bug:发给 ShopXO 的 spec_base_id 是 zone 级别,但 seats[] 包含所有 Zone 的座位) - 防超卖依赖我们自己的锁 - 多 Zone 混买场景需要前端分组逻辑 --- ## 六、待确认的关键问题 ### 问题 1(最高优先):spec_base_id_map 实际存储格式 **数据库里 `spec_base_id_map` 到底是 Zone 级还是座位级?** - Zone 级:`{"A": {spec_base_id: id_A}, "B": {spec_base_id: id_B}}` - 座位级:`{"1_1": {spec_base_id: id_1_1}, "1_2": {spec_base_id: id_1_2}, ...}` **调查方法:** ```sql -- 查看 ShopXO 数据库实际内容 SELECT id, goods_id, inventory, price FROM sxo_goods_spec_base WHERE goods_id = ? ORDER BY id LIMIT 20; ``` ### 问题 2:ShopXO SPEC 数量上限 - ShopXO 对单个商品的 SPEC 数量有没有限制? - 5000 座 × 多 Zone × 多时段 = 多少 SPEC? - 需要在 ShopXO 社区或代码中确认 ### 问题 3:Zone 级 SPEC 的合法性 - 如果 Zone = SPEC(每个 Zone 一个 SPEC),那么多 Zone 混买时如何处理? - 当前 `submit()` 把所有 Zone 座位塞进一个 goods_params 行是否合法? --- ## 七、行动项 - [ ] **问题 1**:调研 ShopXO 数据库,确认 spec_base_id_map 实际存储格式 - [ ] **问题 2**:调研 ShopXO SPEC 数量上限 - [ ] **���策**:选择方案 A 还是方案 B - [ ] 根据决策,重新实现 spec_base_id_map 生成逻辑 + submit() 逻辑 --- ## 八、关联 Issue - Issue #6:P0-2 价格验证(相关) - Issue #7:M-04 loadSoldSeats 未实现(需要先确认 SPEC 设计方向)
Poster
Owner

补充评论 1(2026-04-15 18:55 CST)

数据库实际状态确认

-- 商品 111/112 规格配置
SELECT id, title, is_exist_many_spec FROM vrt_goods WHERE id IN (111,112);
-- 结果:is_exist_many_spec = 0(单规格模式,无多规格)

-- goods_spec_type 表
SELECT COUNT(*) FROM vrt_goods_spec_type WHERE goods_id IN (111,112);
-- 结果:0 条

-- goods_spec_base 表(SKU)
SELECT COUNT(*) FROM vrt_goods_spec_base WHERE goods_id IN (111,112);
-- 结果:0 条

-- vr_seat_templates.spec_base_id_map
-- {"A": 1001, "B": 1002, "C": 1003} ← 这些 ID 在 spec_base 表里根本不存在!

问题严重程度比原 Issue 描述的还要高

当前商品 112 的状态:

  • is_exist_many_spec=0 → ShopXO 关闭多规格校验
  • spec_base 表为空 → 没有 SKU
  • spec_base_id_map 引用的 1001/1002/1003 → 不存在
  • ShopXO 防超卖机制:完全没启用

现在的购买流程:ShopXO 直接走裸商品逻辑,库存校验和扣减完全绕过。防超卖全靠 onOrderPaid 里的自建 FOR UPDATE 锁。


昨天讨论(spec_value per-goods COPY)与 Issue #9 的关系

昨天大头补充的结论(spec_value 是 per-goods COPY,不是全局可复用)影响的是「绑定 vr_venues 到 ShopXO 规格」的匹配逻辑。

对方案 A 的影响:不大。

  • 方案 A 的本质是:创建座位模板时,在该商品下批量生成 SPEC(每个座位一个 SKU,stock=1)
  • per-goods COPY 只影响「如何按名字/ID 关联 vr_venues」,不影响「能否为每个座位创建 SKU」

对方案 B 的影响:反而更糟糕。

  • 方案 B 需要多 Zone 混买时前端拆单,但 Zone 级别的 SKU 根本不存在
  • 自建 FOR UPDATE 锁是现在的唯一防线——ShopXO 原生防超卖一条都没生效

行动建议

必须决策:选方案 A 还是方案 B。

无论选哪个,当前 is_exist_many_spec=0 + spec_base=空的局面必须立即修正。现在的状态是「ShopXO 层面完全裸奔」。

建议流程:

  1. 大头拍板方向
  2. Council 审议实现路径
  3. 先修正商品规格配置(is_exist_many_spec=1 + 正确生成 SKU)
  4. 再修复 submit() 逻辑
## 补充评论 1(2026-04-15 18:55 CST) ### 数据库实际状态确认 ```sql -- 商品 111/112 规格配置 SELECT id, title, is_exist_many_spec FROM vrt_goods WHERE id IN (111,112); -- 结果:is_exist_many_spec = 0(单规格模式,无多规格) -- goods_spec_type 表 SELECT COUNT(*) FROM vrt_goods_spec_type WHERE goods_id IN (111,112); -- 结果:0 条 -- goods_spec_base 表(SKU) SELECT COUNT(*) FROM vrt_goods_spec_base WHERE goods_id IN (111,112); -- 结果:0 条 -- vr_seat_templates.spec_base_id_map -- {"A": 1001, "B": 1002, "C": 1003} ← 这些 ID 在 spec_base 表里根本不存在! ``` ### 问题严重程度比原 Issue 描述的还要高 当前商品 112 的状态: - `is_exist_many_spec=0` → ShopXO 关闭多规格校验 - spec_base 表为空 → 没有 SKU - `spec_base_id_map` 引用的 1001/1002/1003 → 不存在 - ShopXO 防超卖机制:完全没启用 **现在的购买流程**:ShopXO 直接走裸商品逻辑,库存校验和扣减完全绕过。防超卖全靠 `onOrderPaid` 里的自建 FOR UPDATE 锁。 --- ### 昨天讨论(spec_value per-goods COPY)与 Issue #9 的关系 昨天大头补充的结论(spec_value 是 per-goods COPY,不是全局可复用)影响的是「绑定 vr_venues 到 ShopXO 规格」的匹配逻辑。 **对方案 A 的影响**:不大。 - 方案 A 的本质是:创建座位模板时,在该商品下批量生成 SPEC(每个座位一个 SKU,stock=1) - per-goods COPY 只影响「如何按名字/ID 关联 vr_venues」,不影响「能否为每个座位创建 SKU」 **对方案 B 的影响**:反而更糟糕。 - 方案 B 需要多 Zone 混买时前端拆单,但 Zone 级别的 SKU 根本不存在 - 自建 FOR UPDATE 锁是现在的唯一防线——ShopXO 原生防超卖一条都没生效 --- ### 行动建议 **必须决策**:选方案 A 还是方案 B。 无论选哪个,**当前 `is_exist_many_spec=0` + spec_base=空的局面必须立即修正**。现在的状态是「ShopXO 层面完全裸奔」。 建议流程: 1. 大头拍板方向 2. Council 审议实现路径 3. 先修正商品规格配置(is_exist_many_spec=1 + 正确生成 SKU) 4. 再修复 submit() 逻辑
Poster
Owner

补充评论 2(2026-04-15 19:10 CST)

解决方案:$vr- 命名空间前缀(已确认方案)

为避免用户手动添加的普通规格与插件专用规格冲突,采用 $vr- 前缀做命名空间隔离:

插件专用规格(Ticket 相关):
  $vr-场馆     → 场馆名称(如 $vr-场馆 = "鸟巢")
  $vr-分区     → 座位分区(如 $vr-分区 = "VIP区")
  $vr-时段     → 场次时间(如 $vr-时段 = "14:00")

用户普通规格(无前缀):
  场馆、颜色、尺码 等普通商品规格

为什么不冲突:插件票务商品使用自定义模板 ticket_detail.html,前端 UI 不走 ShopXO 默认规格选择器。用户无法通过默认界面触碰到 $vr- 规格。

特殊字符支持确认

ShopXO spec name 无字符过滤,从源码确认:

前端(goods/spec.html):

<input type="text" name="specifications_name_..." required />

只有 required 验证,无 regex 无字符限制。

后端(GoodsService::GoodsSpecificationsInsert):

$v["name"] = $vs; // 直接存储,无过滤

结论:$vr-场馆 完全合法。$、-、中文均可用。


两天讨论综合结论

关键结论
昨天傍晚 spec_value 是 per-goods COPY;ShopXO spec 是模板级复制;可用 $vr- 前缀隔离
今天 18:55 商品 112 实际状态:is_exist_many_spec=0,spec_base 表为空,ShopXO 防超卖完全未启用
今天 19:10 $vr- 前缀方案确认,特殊字符无限制

当前唯一待决策:方案 A vs 方案 B

方案 A(每个座位一个 SPEC):

  • 正确路径,ShopXO 原生防超卖
  • 需要后台实现:批量在 ShopXO 生成每个座位的 SKU
  • $vr- 前缀规格在商品下生成座位级 SKU

方案 B(每个 Zone 一个 SPEC):

  • 简单但有漏洞(多 Zone 混买 + ShopXO spec 校验失效)
  • 目前靠自建锁撑着,但 ShopXO 原生机制全未启用

需要 Council 评议的核心问题:方案 A 的「后台批量生成 SKU」实现路径是否可行?以及当前 broken 状态下的紧急修复优先级。

## 补充评论 2(2026-04-15 19:10 CST) ### 解决方案:$vr- 命名空间前缀(已确认方案) 为避免用户手动添加的普通规格与插件专用规格冲突,采用 `$vr-` 前缀做命名空间隔离: ``` 插件专用规格(Ticket 相关): $vr-场馆 → 场馆名称(如 $vr-场馆 = "鸟巢") $vr-分区 → 座位分区(如 $vr-分区 = "VIP区") $vr-时段 → 场次时间(如 $vr-时段 = "14:00") 用户普通规格(无前缀): 场馆、颜色、尺码 等普通商品规格 ``` **为什么不冲突**:插件票务商品使用自定义模板 `ticket_detail.html`,前端 UI 不走 ShopXO 默认规格选择器。用户无法通过默认界面触碰到 `$vr-` 规格。 ### 特殊字符支持确认 ✅ ShopXO spec name 无字符过滤,从源码确认: **前端**(goods/spec.html): ```html <input type="text" name="specifications_name_..." required /> ``` 只有 required 验证,无 regex 无字符限制。 **后端**(GoodsService::GoodsSpecificationsInsert): ```php $v["name"] = $vs; // 直接存储,无过滤 ``` 结论:`$vr-场馆` 完全合法。$、-、中文均可用。 --- ### 两天讨论综合结论 | 天 | 关键结论 | |---|---| | 昨天傍晚 | spec_value 是 per-goods COPY;ShopXO spec 是模板级复制;可用 `$vr-` 前缀隔离 | | 今天 18:55 | 商品 112 实际状态:is_exist_many_spec=0,spec_base 表为空,ShopXO 防超卖完全未启用 | | 今天 19:10 | $vr- 前缀方案确认,特殊字符无限制 | --- ### 当前唯一待决策:方案 A vs 方案 B 方案 A(每个座位一个 SPEC): - 正确路径,ShopXO 原生防超卖 - 需要后台实现:批量在 ShopXO 生成每个座位的 SKU - $vr- 前缀规格在商品下生成座位级 SKU 方案 B(每个 Zone 一个 SPEC): - 简单但有漏洞(多 Zone 混买 + ShopXO spec 校验失效) - 目前靠自建锁撑着,但 ShopXO 原生机制全未启用 **需要 Council 评议的核心问题**:方案 A 的「后台批量生成 SKU」实现路径是否可行?以及当前 broken 状态下的紧急修复优先级。
Poster
Owner

补充评论 3(2026-04-15 19:38 CST)

甲方新需求已整理

完整需求文档:docs/10_NEW_REQUIREMENTS_20260415.md

4 项新需求摘要

  1. 多座位单订单(一个订单可包含多个座位,各生成独立核销码)
  2. 订单详情核销码卡夹展示(手动滑切 + 核销状态灰掉 + 自动切换下一张)
  3. 商品 ext.required_fields 必填字段配置(身份证/手机号等)
  4. 手机号自动填充微信认证手机号(订单级,同单多座位只需填一份)

ShopXO 多 spec_base_id 同单购买验证结果

源码证据(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() → 每个 goods_params 条目 → 一个 $data[] 元素
  2. OrderSplitService::Run() → 按 warehouse 分组(非 goods_id 合并),不同 spec_base_id 保留为不同 goods_items[]
  3. OrderInsert()foreach($v["goods_items"] as $vs) → 每个条目 → 一行 order_goods

结论:ShopXO 原生支持同一 goods_id + 不同 spec_base_id 各买 1 个,各生成独立 order_goods 行。


方案 A 完全兼容甲方全部新需求

甲方需求 方案 A 实现
多座位单订单 每座位 = 独立 spec_base_id
多核销码 order_goods × N → vr_tickets × N
ext 必填字段 goods.extension_data.required_fields
手机号订单级 联系信息挂 order 备注,非 goods_params

当前数据库 broken 状态(需紧急修复)

is_exist_many_spec = 0   -- ShopXO 认为无多规格
spec_base  = 空的
spec_base_id_map  {A:1001, B:1002, C:1003} -- 这些 ID 不存在

ShopXO 防超卖机制完全未启用。


需要 Council 补充评议

  • 新需求是否改变了方案 A vs B 的推荐?
  • 紧急修复(is_exist_many_spec → 1 + 生成 SKU)的最小步骤是什么?
## 补充评论 3(2026-04-15 19:38 CST) ### 甲方新需求已整理 完整需求文档:`docs/10_NEW_REQUIREMENTS_20260415.md` **4 项新需求摘要**: 1. 多座位单订单(一个订单可包含多个座位,各生成独立核销码) 2. 订单详情核销码卡夹展示(手动滑切 + 核销状态灰掉 + 自动切换下一张) 3. 商品 ext.required_fields 必填字段配置(身份证/手机号等) 4. 手机号自动填充微信认证手机号(订单级,同单多座位只需填一份) --- ### ShopXO 多 spec_base_id 同单购买验证结果 ✅ **源码证据(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()` → 每个 goods_params 条目 → 一个 `$data[]` 元素 2. `OrderSplitService::Run()` → 按 warehouse 分组(非 goods_id 合并),不同 spec_base_id 保留为不同 goods_items[] 3. `OrderInsert()` → `foreach($v["goods_items"] as $vs)` → 每个条目 → **一行 order_goods** 结论:ShopXO 原生支持同一 goods_id + 不同 spec_base_id 各买 1 个,各生成独立 order_goods 行。 --- ### 方案 A 完全兼容甲方全部新需求 | 甲方需求 | 方案 A 实现 | |---------|------------| | 多座位单订单 | 每座位 = 独立 spec_base_id ✅ | | 多核销码 | order_goods × N → vr_tickets × N ✅ | | ext 必填字段 | goods.extension_data.required_fields ✅ | | 手机号订单级 | 联系信息挂 order 备注,非 goods_params ✅ | --- ### 当前数据库 broken 状态(需紧急修复) ```sql is_exist_many_spec = 0 -- ShopXO 认为无多规格 spec_base 表 = 空的 spec_base_id_map → {A:1001, B:1002, C:1003} -- 这些 ID 不存在 ``` ShopXO 防超卖机制完全未启用。 --- **需要 Council 补充评议**: - 新需求是否改变了方案 A vs B 的推荐? - 紧急修复(is_exist_many_spec → 1 + 生成 SKU)的最小步骤是什么?
Poster
Owner

补充评论 4(2026-04-15 19:49 CST)— Council 评议结果

Council 最终结论(全票通过:方案 A)

完整报告:council-output/ARCHITECTURE_DECISION.md


Q1:批量 SKU 生成路径

结论: 可行,但必须旁路 GoodsSpecificationsInsert()

  • GoodsSpecificationsInsert() 每次保存时 DELETE 所有 spec 后重建,1 万座会删崩
  • 正确路径:直接 SQL INSERT 三表(goods_spec_type / goods_spec_base / goods_spec_value
  • 性能:10000 座位 ≈ 3-4 秒(分批 500 条/批)
  • 初始化一次,座位模板绑定时生成,后续不变

Q2:商品 112 broken 状态修复

结论:推荐方案乙(最小修复集),紧急程度中等

-- Step 1: 启用多规格
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: 幂等保护(在 TicketService::issueTicket() 中添加 spec_base_id=0 fallback)

Q3:$vr- 前缀安全性

结论: 低风险,确认安全

  • ThinkPHP {$var} 默认 HTML 转义,不作为 PHP 变量解析
  • parseVar 正则 \$["']*[a-zA-Z_]$vr-场馆 中无法匹配有效变量名
  • DB varchar 类型允许 $ 字符
  • 唯一注意:后台规格管理页可能显示不当(纯展示问题,不影响安全)

Q4:最终推荐

结论: 方案 A 全票通过(三方一致)

每个座位 = 一个 ShopXO SKU(inventory=1),ShopXO 原生原子扣库存防超卖。

维度 方案 A 方案 B
防超卖 ShopXO 原生 dec(),DB 层保证 自建 FOR UPDATE
多 Zone 混买 seat-level 原子处理,体验流畅 共享 Zone 库存,复杂
1 万座场景 可行(Hook 隐藏 SKU 行) 优势消失

行动项

优先级 行动 负责
P0 Q2 最小修复集 + SeatSkuService::BatchGenerate() BackendArchitect
P1 submit() 重构:session-level → seat-level 逐座提交 FrontendDev
P2 loadSoldSeats():查询各座位库存状态 FrontendDev
P3 Hook 隐藏插件 SKU + 独立 SKU 管理页面 FrontendDev
## 补充评论 4(2026-04-15 19:49 CST)— Council 评议结果 ### Council 最终结论(全票通过:方案 A) 完整报告:`council-output/ARCHITECTURE_DECISION.md` --- ### Q1:批量 SKU 生成路径 **结论:✅ 可行,但必须旁路 GoodsSpecificationsInsert()** - `GoodsSpecificationsInsert()` 每次保存时 DELETE 所有 spec 后重建,1 万座会删崩 - 正确路径:直接 SQL INSERT 三表(`goods_spec_type` / `goods_spec_base` / `goods_spec_value`) - 性能:10000 座位 ≈ 3-4 秒(分批 500 条/批) - 初始化一次,座位模板绑定时生成,后续不变 --- ### Q2:商品 112 broken 状态修复 **结论:推荐方案乙(最小修复集),紧急程度中等** ```sql -- Step 1: 启用多规格 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: 幂等保护(在 TicketService::issueTicket() 中添加 spec_base_id=0 fallback) ``` --- ### Q3:$vr- 前缀安全性 **结论:✅ 低风险,确认安全** - ThinkPHP `{$var}` 默认 HTML 转义,不作为 PHP 变量解析 - `parseVar` 正则 `\$["']*[a-zA-Z_]` 在 `$vr-场馆` 中无法匹配有效变量名 - DB `varchar` 类型允许 `$` 字符 - 唯一注意:后台规格管理页可能显示不当(纯展示问题,不影响安全) --- ### Q4:最终推荐 **结论:✅ 方案 A 全票通过(三方一致)** 每个座位 = 一个 ShopXO SKU(inventory=1),ShopXO 原生原子扣库存防超卖。 | 维度 | 方案 A ✅ | 方案 B | |------|----------|--------| | 防超卖 | ShopXO 原生 dec(),DB 层保证 | 自建 FOR UPDATE | | 多 Zone 混买 | seat-level 原子处理,体验流畅 | 共享 Zone 库存,复杂 | | 1 万座场景 | 可行(Hook 隐藏 SKU 行) | 优势消失 | --- ### 行动项 | 优先级 | 行动 | 负责 | |--------|------|------| | **P0** | Q2 最小修复集 + SeatSkuService::BatchGenerate() | BackendArchitect | | **P1** | submit() 重构:session-level → seat-level 逐座提交 | FrontendDev | | **P2** | loadSoldSeats():查询各座位库存状态 | FrontendDev | | **P3** | Hook 隐藏插件 SKU + 独立 SKU 管理页面 | FrontendDev |
Poster
Owner

补充评论 5(2026-04-15 20:15 CST)— P0/P1 实现确认 + 设计分歧讨论

P0/P1 实现状态

任务 状态 文件
P0-A BaseService::initGoodsSpecs() 完成 shopxo/app/plugins/vr_ticket/service/BaseService.php:164
P0-B SeatSkuService::BatchGenerate() 完成 shopxo/app/plugins/vr_ticket/service/SeatSkuService.php:31
P1 submit() seat-level 重构 完成 shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html

已推送 commit:5e9c11137(BackendArchitect) + 93b70d4d5(FrontendDev)


当前 admin 管理端现状

现有管理页面(Phase 2 已实现):

  • SeatTemplate(座位模板管理)— 已存在
  • Ticket(电子票管理)— 已存在
  • Verifier(核销员管理)— 已存在
  • Verification(核销记录)— 已存在

目前没有场馆管理页面


当前 vr_seat_templates 表结构 vs 你的描述

当前表结构

vr_seat_templates
  ├── name          — 模板名称(如 "Bird Nest - Zone A")
  ├── category_id   — 绑定 ShopXO 商品分类
  ├── seat_map      — JSON(含 zones/rows/seats 全部内嵌)
  └── spec_base_id_map — seatId → spec_base_id

seat_map 示例:
{
  "zones": {
    "A": { "price": 899, "color": "#e74c3c", "label": "VIP区" },
    "B": { "price": 599, "color": "#3498db", "label": "看台区" }
  },
  "rows": [...],
  "seats": { "A_1": {...}, "A_2": {...} }
}

当前 BatchGenerate() 中的 $vr- spec 维度

  • $vr-场馆 → 硬编码为 "国家体育馆"(无独立 venue 表)
  • $vr-分区 → 从 seat_map.zones 读取(Zone A/B/C 区)
  • $vr-时段 → placeholder,后续 UpdateSessionSku 替换
  • $vr-座位号 → seatId(如 "A_1")

你的描述 vs 当前设计 — 对比分歧

你描述的管理维度 当前实际 分歧点
场馆管理(名称/地理位置/图片) 无独立 venue 表,$vr-场馆 硬编码 ⚠️ 需要新增 vr_venues 表 + admin 页面
座位分区模板(布局/价格/名称) vr_seat_templates.seat_map 内嵌了 zones 部分重叠,可能需要拆分
spec 组合 BatchGenerate() 自动生成 符合方向

关键问题需要讨论

  1. 场馆是否需要独立表? 还是说 vr_seat_templatesname 字段就够用?
  2. zones(分区)和座位布局是否需要分开管理? 还是 seat_map JSON 一体化管理即可?
  3. $vr-场馆 的值应该来自哪里?
## 补充评论 5(2026-04-15 20:15 CST)— P0/P1 实现确认 + 设计分歧讨论 ### P0/P1 实现状态 | 任务 | 状态 | 文件 | |------|------|------| | P0-A BaseService::initGoodsSpecs() | ✅ 完成 | shopxo/app/plugins/vr_ticket/service/BaseService.php:164 | | P0-B SeatSkuService::BatchGenerate() | ✅ 完成 | shopxo/app/plugins/vr_ticket/service/SeatSkuService.php:31 | | P1 submit() seat-level 重构 | ✅ 完成 | shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html | 已推送 commit:`5e9c11137`(BackendArchitect) + `93b70d4d5`(FrontendDev) --- ### 当前 admin 管理端现状 现有管理页面(Phase 2 已实现): - SeatTemplate(座位模板管理)— 已存在 - Ticket(电子票管理)— 已存在 - Verifier(核销员管理)— 已存在 - Verification(核销记录)— 已存在 **目前没有场馆管理页面**。 --- ### 当前 vr_seat_templates 表结构 vs 你的描述 **当前表结构**: ``` vr_seat_templates ├── name — 模板名称(如 "Bird Nest - Zone A") ├── category_id — 绑定 ShopXO 商品分类 ├── seat_map — JSON(含 zones/rows/seats 全部内嵌) └── spec_base_id_map — seatId → spec_base_id seat_map 示例: { "zones": { "A": { "price": 899, "color": "#e74c3c", "label": "VIP区" }, "B": { "price": 599, "color": "#3498db", "label": "看台区" } }, "rows": [...], "seats": { "A_1": {...}, "A_2": {...} } } ``` **当前 BatchGenerate() 中的 $vr- spec 维度**: - `$vr-场馆` → 硬编码为 "国家体育馆"(无独立 venue 表) - `$vr-分区` → 从 seat_map.zones 读取(Zone A/B/C 区) - `$vr-时段` → placeholder,后续 UpdateSessionSku 替换 - `$vr-座位号` → seatId(如 "A_1") --- ### 你的描述 vs 当前设计 — 对比分歧 | 你描述的管理维度 | 当前实际 | 分歧点 | |----------------|---------|--------| | 场馆管理(名称/地理位置/图片) | 无独立 venue 表,$vr-场馆 硬编码 | ⚠️ 需要新增 vr_venues 表 + admin 页面 | | 座位分区模板(布局/价格/名称) | vr_seat_templates.seat_map 内嵌了 zones | 部分重叠,可能需要拆分 | | spec 组合 | BatchGenerate() 自动生成 | ✅ 符合方向 | --- ### 关键问题需要讨论 1. **场馆是否需要独立表?** 还是说 `vr_seat_templates` 的 `name` 字段就够用? 2. **zones(分区)和座位布局是否需要分开管理?** 还是 seat_map JSON 一体化管理即可? 3. **$vr-场馆 的值应该来自哪里?**
Poster
Owner

补充评论 6(2026-04-15 21:13 CST)— Phase 3 完整设计方案

完整方案文档:docs/11_EDITOR_AND_INJECTION_DESIGN.md

核心结论

后台编辑器:在场馆配置管理页面做表单可视化编辑器(layui + Vue3 CDN),不碰 ShopXO 原生 Spec 界面。

商品发布注入:利用 plugins_view_admin_goods_save 钩子,在商品发布页注入票务配置面板(选场馆 + 分区多选)。商户点发布 → 插件静默生成海量 Spec。

整体流程

商户操作:插件后台建场馆配置(venue + zones + 座位布局)
    ↓
发布商品:选场馆 + 分区
    ↓
插件静默:BatchGenerate() 为每座位生成 SKU(每座 inventory=1)
    ↓
用户前台:看到票务选座 UI(无感知 Spec)
    ↓
购买选座:每座位 → 1 个 order_goods → 1 张 QR

关键设计决策

决策点 选择 理由
场馆+座位数据存在哪 JSON 单字段(seat_map) 不拆表,编辑灵活,扩展方便
Spec 谁创建 插件在发布时自动生成 商户无感,不需要懂 Spec
Spec 注入到哪 商品发布钩子静默注入 不碰 ShopXO 后台 Spec 管理
编辑页处理 优先级低,先跑通创建 琳琅满目的 Spec 列表问题后续解决

实施步骤

Phase 3-1:后台场馆配置管理(admin 页面)
Phase 3-2:商品发布页注入(钩子 + 表单面板)
Phase 3-3:Spec 自动生成接入 BatchGenerate()
Phase 3-4:商品编辑回显(低优先级)

## 补充评论 6(2026-04-15 21:13 CST)— Phase 3 完整设计方案 完整方案文档:`docs/11_EDITOR_AND_INJECTION_DESIGN.md` ### 核心结论 **后台编辑器**:在场馆配置管理页面做表单可视化编辑器(layui + Vue3 CDN),不碰 ShopXO 原生 Spec 界面。 **商品发布注入**:利用 `plugins_view_admin_goods_save` 钩子,在商品发布页注入票务配置面板(选场馆 + 分区多选)。商户点发布 → 插件静默生成海量 Spec。 ### 整体流程 ``` 商户操作:插件后台建场馆配置(venue + zones + 座位布局) ↓ 发布商品:选场馆 + 分区 ↓ 插件静默:BatchGenerate() 为每座位生成 SKU(每座 inventory=1) ↓ 用户前台:看到票务选座 UI(无感知 Spec) ↓ 购买选座:每座位 → 1 个 order_goods → 1 张 QR ``` ### 关键设计决策 | 决策点 | 选择 | 理由 | |--------|------|------| | 场馆+座位数据存在哪 | JSON 单字段(seat_map) | 不拆表,编辑灵活,扩展方便 | | Spec 谁创建 | 插件在发布时自动生成 | 商户无感,不需要懂 Spec | | Spec 注入到哪 | 商品发布钩子静默注入 | 不碰 ShopXO 后台 Spec 管理 | | 编辑页处理 | 优先级低,先跑通创建 | 琳琅满目的 Spec 列表问题后续解决 | ### 实施步骤 Phase 3-1:后台场馆配置管理(admin 页面) Phase 3-2:商品发布页注入(钩子 + 表单面板) Phase 3-3:Spec 自动生成接入 BatchGenerate() Phase 3-4:商品编辑回显(低优先级)
Poster
Owner

补充评论 7(2026-04-15 21:45 CST)— v3.0 设计确认

文档:docs/11_EDITOR_AND_INJECTION_DESIGN.md(v3.0)

三个核心决策(已确认)

决策 确认结果
venue 信息放哪 存入 seat_map.venue 顶层,不独立建表
是否支持按分区过滤 必须支持,Phase 3-3 改造 BatchGenerate(enabledZones)
数据库扩展兼容性 新增 sxo_goods.vr_ticket_config JSON 字段,老商品 NULL 兼容

新增内容

  • 美化版 seat_map JSON 示例(演唱会/剧场/电影院 3 种场景)
  • cinema 场景示例:5 区只开放 A+B(对应「电影院每次开放场馆不一样」需求)
  • BatchGenerate v3.0 签名:BatchGenerate(int $goodsId, int $templateId, array enabledZones=[])
  • vr_ticket_config 字段设计(template_id / selected_zones / spec_base_id_map / created_at)

PM Auditor 10 项纠错(v2.0)

  • hook 注册格式修正(hooks 数组,非 backend_hook)
  • seat_map 字段名修正(seats/sections,非 zones)
  • 表前缀修正(sxo_ 原生表,vrt_ 插件表)
  • BatchGenerate 参数签名修正(当前全量,v3.0 后支持过滤)
  • vr_ticket_config 字段修正(待新增,非已有)
  • AdminGoodsSave 文件状态修正(待新建,非已有)

下一步

Phase 3-1 开工 → 新建 Venue.php + 表单编辑器 + seat_map 升级迁移

## 补充评论 7(2026-04-15 21:45 CST)— v3.0 设计确认 文档:`docs/11_EDITOR_AND_INJECTION_DESIGN.md`(v3.0) ### 三个核心决策(已确认) | 决策 | 确认结果 | |------|---------| | venue 信息放哪 | 存入 `seat_map.venue` 顶层,不独立建表 | | 是否支持按分区过滤 | **必须支持**,Phase 3-3 改造 `BatchGenerate(enabledZones)` | | 数据库扩展兼容性 | 新增 `sxo_goods.vr_ticket_config` JSON 字段,老商品 NULL 兼容 | ### 新增内容 - 美化版 seat_map JSON 示例(演唱会/剧场/电影院 3 种场景) - cinema 场景示例:5 区只开放 A+B(对应「电影院每次开放场馆不一样」需求) - BatchGenerate v3.0 签名:`BatchGenerate(int $goodsId, int $templateId, array enabledZones=[])` - vr_ticket_config 字段设计(template_id / selected_zones / spec_base_id_map / created_at) ### PM Auditor 10 项纠错(v2.0) - hook 注册格式修正(hooks 数组,非 backend_hook) - seat_map 字段名修正(seats/sections,非 zones) - 表前缀修正(sxo_ 原生表,vrt_ 插件表) - BatchGenerate 参数签名修正(当前全量,v3.0 后支持过滤) - vr_ticket_config 字段修正(待新增,非已有) - AdminGoodsSave 文件状态修正(待新建,非已有) ### 下一步 Phase 3-1 开工 → 新建 Venue.php + 表单编辑器 + seat_map 升级迁移
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: sileya-ai/vr-shopxo-plugin#9
There is no content yet.