diff --git a/docs/11_EDITOR_AND_INJECTION_DESIGN.md b/docs/11_EDITOR_AND_INJECTION_DESIGN.md
index 2597116..ed56afc 100644
--- a/docs/11_EDITOR_AND_INJECTION_DESIGN.md
+++ b/docs/11_EDITOR_AND_INJECTION_DESIGN.md
@@ -1,29 +1,33 @@
# 后台编辑器 + 商品发布注入方案设计
-> 版本:v2.0 | 日期:2026-04-15 | 状态:**待大头确认后执行**
->
-> v2.0 更新:经 PM Auditor 代码级核查,修正 10 处与实际代码不符的描述(见文末勘误表)
+> 版本:v3.0 | 日期:2026-04-15 21:45 | 状态:**已确认,待执行**
+>
+> v3.0 更新(2026-04-15 21:45 大头确认):
+> 1. venue 信息放入 seat_map 顶层(不独立建表)
+> 2. BatchGenerate() 必须支持按分区过滤(enabledZones 参数)
+> 3. 新增 sxo_goods.vr_ticket_config JSON 字段,老商品兼容
+> 4. 增加美化版 seat_map 示例 JSON
---
## 一、整体架构一句话
-**插件在 ShopXO 后台建一套"场馆配置"管理界面,用户发布票务商品时,选场馆 → 插件自动生成海量 Spec 并注入商品,用户全程不碰 ShopXO 原生 Spec 管理。**
+**插件在 ShopXO 后台建一套"场馆配置"管理界面,用户发布票务商品时,选场馆 → 选分区 → 插件自动只生成这些分区的海量 Spec 并注入商品,用户全程不碰 ShopXO 原生 Spec 管理。**
---
## 二、三大核心区域
```
-┌─────────────────────────────────────────────────────────────────┐
-│ ShopXO 原生后台 │
-│ │
-│ ┌─────────────────┐ ┌──────────────────────┐ │
-│ │ 商品管理 │ │ 插件专属后台(新增) │ │
-│ │ 发布/编辑商品 │ ←→ │ 场馆配置管理 │ │
-│ │ (注入点) │ │ 座位分区模板编辑器 │ │
-│ └─────────────────┘ └──────────────────────┘ │
-└─────────────────────────────────────────────────────────────────┘
+┌─────────────────────────────────────────────────────────────┐
+│ ShopXO 原生后台 │
+│ │
+│ ┌──────────────────┐ ┌────────────────────────┐ │
+│ │ 商品管理 │ ←→ │ 插件专属后台(新增) │ │
+│ │ 发布/编辑商品 │ │ 场馆配置管理 │ │
+│ │ (注入点) │ │ 座位分区模板编辑器 │ │
+│ └──────────────────┘ └────────────────────────┘ │
+└─────────────────────────────────────────────────────────────┘
```
### 区域 A:插件专属后台(新增)
@@ -32,66 +36,62 @@
```
VR票务
- ├── 场馆配置 ← Phase 3-1 新增(当前不存在)
+ ├── 场馆配置 ← Phase 3-1 新增
├── 座位模板 ← 已存在(Phase 2)
├── 电子票管理 ← 已存在
├── 核销员管理 ← 已存在
└── 核销记录 ← 已存在
```
-> ⚠️ **当前状态**:`Venue.php` 控制器不存在。`vr_seat_templates` 表只有 `name` / `category_id` / `seat_map` / `spec_base_id_map` 字段,`venue` 信息目前不存在于数据库中。
+> ⚠️ **当前状态**:`Venue.php` 控制器不存在,Phase 3-1 从零新建。
### 区域 B:ShopXO 商品发布页(注入点)
-> ⚠️ **待核实**:商品发布页 Tab 名称需对照 `saveinfo.html` 模板确认。当前描述"规格型号"Tab 可能不准确。
+> ⚠️ **待核实**:商品发布页 Tab 名称需对照 `saveinfo.html` 模板确认。
商户在 ShopXO 后台「商品管理 → 添加商品」:
```
添加商品
[商品名称] [商品分类]
-
- ▼ 票务配置 ← ShopXO 原生区域,我们注入票务选择器
- [请选择场馆 ▼] ← 场馆下拉(来自 vr_seat_templates 表)
- [请选择分区 ▼] ← 分区多选(根据场馆联动)
-
+
+ ▼ 票务配置 ← ShopXO 原生区域,插件注入票务选择器
+ 场馆:[请选择场馆 ▼] ← 来自 vr_seat_templates 表
+ 分区:[□VIP区 □看台区 □普通区] ← 多选,根据场馆联动
+
[商品详情 富文本编辑器]
▼ 其他Tab(参数/图片等)← ShopXO 原生
```
### 区域 C:插件商品详情页(已存在)
-用户在前台看到票务商品详情页,选座下单,这个已实现。
+用户在前台看到票务商品详情页,选座下单,已实现。
---
## 三、场馆配置管理(区域 A)
-### 3.1 数据结构(当前 vs 目标)
-
-> ⚠️ **当前 seat_map 实际结构**(从 `SeatSkuService::BatchGenerate` 代码反推):
-> - **无** `venue` 顶层字段
-> - **无** `zones` 顶层字段
-> - 顶层字段为:`map`、`seats`、`sections`、`row_labels`
-> - `$vr-场馆` 的值目前**硬编码**为"国家体育馆"
-
-**目标结构(Phase 3-1 需调整)**:
+### 3.1 seat_map 数据结构(v3.0 最终版)
```json
{
- "venue": { // ← Phase 3-1 新增:从 venue 表读取或直接存这里
+ "venue": {
"name": "国家体育馆",
- "address": "北京市朝阳区",
- "image": "/uploads/vr/venue/1.jpg"
+ "address": "北京市朝阳区奥体中心",
+ "image": "/uploads/vr/venue/national-stadium.jpg"
},
- "map": ["AAAAAA", "BBBBBB", "CCCCCC"],
- "seats": { // ← 实际字段名,不是 zones
+ "map": [
+ "AAAAAA",
+ "BBBBBB",
+ "CCCCCC"
+ ],
+ "seats": {
"A": { "price": 899, "color": "#e74c3c", "label": "VIP区" },
"B": { "price": 599, "color": "#3498db", "label": "看台区" },
"C": { "price": 299, "color": "#2ecc71", "label": "普通区" }
},
- "sections": [ // ← 实际字段名,存储分区元信息
- { "char": "A", "name": "VIP区", "color": "#e74c3c" },
+ "sections": [
+ { "char": "A", "name": "VIP区", "color": "#e74c3c" },
{ "char": "B", "name": "看台区", "color": "#3498db" },
{ "char": "C", "name": "普通区", "color": "#2ecc71" }
],
@@ -99,59 +99,135 @@ VR票务
}
```
-**关键理解**:
-- `seats` = 每行(row char)的默认价格/颜色/标签(整行统一)
-- `sections` = 每个分区的元信息(char → name/color 映射)
-- `venue` 信息目前**不存在**于 seat_map,Phase 3-1 需要决策:是加到 seat_map 里,还是新建 `vr_venues` 表
+**字段说明**:
-### 3.2 场馆配置管理页面(表单可视化编辑器)
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `venue` | object | 场馆信息。**v3.0 新增**。name 即 `$vr-场馆` spec 值,不再硬编码 |
+| `map` | string[] | 每排座位数字符串(A=6座,B=6座,C=6座),决定总座位数 |
+| `seats` | object | **字段名是 seats 不是 zones**。每行(char)的默认 price/color/label |
+| `sections` | object[] | 每个分区的元信息(char → name/color 映射),前端分区选择器用此渲染多选框 |
+| `row_labels` | string[] | 行标签,用于前端渲染座位坐标("A_1", "A_2"...)|
-> ⚠️ **待实现**:当前 `Venue.php` 控制器不存在,Phase 3-1 需要新建。
+**`$vr-场馆` spec 值来源**:从 `seat_map.venue.name` 读取,Phase 3-1 改造 `BatchGenerate`。
-商户在插件后台「场馆配置 → 添加」,看到以下表单:
+**与旧数据的兼容性**:
+- 存量 seat_map 无 `venue` 顶层 → 降级取 `vr_seat_templates.name` 作为 `$vr-场馆` 值
+- 存量 seat_map 无 `sections` 顶层 → 降级从 `seats` 推导
+
+### 3.2 美化版 seat_map 示例(Phase 3-1 表单填充示例)
+
+**示例 1:大型演唱会(3 区,每区 6 座)**
+```json
+{
+ "venue": {
+ "name": "鸟巢",
+ "address": "北京市朝阳区国家体育场南路1号",
+ "image": "/uploads/vr/venue/bird-nest.jpg"
+ },
+ "map": ["AAAAAA", "BBBBBB", "CCCCCC"],
+ "seats": {
+ "A": { "price": 1299, "color": "#e74c3c", "label": "内场VIP" },
+ "B": { "price": 899, "color": "#9b59b6", "label": "内场A区" },
+ "C": { "price": 599, "color": "#3498db", "label": "内场B区" }
+ },
+ "sections": [
+ { "char": "A", "name": "内场VIP", "color": "#e74c3c" },
+ { "char": "B", "name": "内场A区", "color": "#9b59b6" },
+ { "char": "C", "name": "内场B区", "color": "#3498db" }
+ ],
+ "row_labels": ["A", "B", "C"]
+}
+```
+
+**示例 2:小型剧场(2 区,每区 4 座)**
+```json
+{
+ "venue": {
+ "name": "上海音乐厅",
+ "address": "上海市黄浦区延安东路523号",
+ "image": "/uploads/vr/venue/shanghai-concert-hall.jpg"
+ },
+ "map": ["AAAA", "BBBB"],
+ "seats": {
+ "A": { "price": 499, "color": "#f39c12", "label": "前排" },
+ "B": { "price": 299, "color": "#1abc9c", "label": "后排" }
+ },
+ "sections": [
+ { "char": "A", "name": "前排", "color": "#f39c12" },
+ { "char": "B", "name": "后排", "color": "#1abc9c" }
+ ],
+ "row_labels": ["A", "B"]
+}
+```
+
+**示例 3:开放部分分区(电影院场景)**
+```json
+{
+ "venue": {
+ "name": "IMAX 影城",
+ "address": "北京市朝阳区建国路88号",
+ "image": "/uploads/vr/venue/imax-cinema.jpg"
+ },
+ "map": ["AAAAAA", "BBBBBB", "CCCCCC", "DDDDDD", "EEEEE"],
+ "seats": {
+ "A": { "price": 99, "color": "#e74c3c", "label": "情侣座" },
+ "B": { "price": 69, "color": "#3498db", "label": "VIP厅" },
+ "C": { "price": 49, "color": "#2ecc71", "label": "普通座" },
+ "D": { "price": 49, "color": "#2ecc71", "label": "普通座" },
+ "E": { "price": 39, "color": "#95a5a6", "label": "边座" }
+ },
+ "sections": [
+ { "char": "A", "name": "情侣座", "color": "#e74c3c" },
+ { "char": "B", "name": "VIP厅", "color": "#3498db" },
+ { "char": "C", "name": "普通座", "color": "#2ecc71" },
+ { "char": "D", "name": "普通座", "color": "#2ecc71" },
+ { "char": "E", "name": "边座", "color": "#95a5a6" }
+ ],
+ "row_labels": ["A", "B", "C", "D", "E"]
+}
+```
+> 示例 3 场景:发布商品时只选 A+B 分区,C/D/E 暂不开放。`BatchGenerate(enabledZones=['A','B'])` 只生成 12 个座位 SKU。
+
+### 3.3 场馆配置管理页面
+
+> ⚠️ **待实现**:当前 `Venue.php` 控制器不存在。
+
+商户在插件后台「场馆配置 → 添加」看到的表单:
```
【场馆基本信息】
- 场馆名称:[________________________]
- 场馆地址:[________________________]
+ 场馆名称:[_______________________________]
+ 场馆地址:[_______________________________]
场馆图片:[上传按钮]
-
-【分区配置】(可以加/减分区)
+
+【分区配置】(增删分区,拖拽排序)
┌──────────────────────────────────────────────────┐
- │ Char │ 标签:VIP区 │ 单价:899元 │ 颜色:[红] │
+ │ Char │ 标签 │ 单价(元)│ 颜色 │ │
├──────────────────────────────────────────────────┤
- │ Char │ 标签:看台区 │ 单价:599元 │ 颜色:[蓝] │
+ │ A │ 内场VIP │ 1299 │ [■红 ▼]│ [删除] │
├──────────────────────────────────────────────────┤
- │ Char │ 标签:普通区 │ 单价:299元 │ 颜色:[绿] │
+ │ B │ 内场A区 │ 899 │ [■紫 ▼]│ [删除] │
+ ├──────────────────────────────────────────────────┤
+ │ C │ 内场B区 │ 599 │ [■蓝 ▼]│ [删除] │
└──────────────────────────────────────────────────┘
- [+ 添加分区] [- 删除分区]
-
-【座位排布预览】
- A A A A A A
- B B B B B B
- C C C C C C
-
- 每排座位数:[6___] 排数自动生成
-
+ [+ 添加分区]
+
+【座位排布预览】(实时渲染)
+ 🔴 🔴 🔴 🔴 🔴 🔴 ← A区(VIP,1299元)
+ 🟣 🟣 🟣 🟣 🟣 🟣 ← B区(内场A区,899元)
+ 🔵 🔵 🔵 🔵 🔵 🔵 ← C区(内场B区,599元)
+
+ 每排座位数:[6___] 总座位数:18
+
【保存】 【取消】
```
**技术实现**:
-- layui 表单 + Vue3 CDN(轻量,不破坏 ShopXO 后台已有的 jQuery/layui 结构)
-- 约 500 行前端代码,1-1.5 人天
-- 保存时:表单数据 → 编码成 JSON → 写入 `vr_seat_templates.seat_map`
-
-### 3.3 venue 信息的设计决策(待讨论)
-
-**选项 A**:venue 信息存入 `seat_map.venue`(JSON 顶层)
-- 优点:简单,不改表结构
-- 缺点:一个模板只能关联一个 venue
-
-**选项 B**:新建 `vr_venues` 表,`vr_seat_templates.venue_id` 外键关联
-- 优点:一个 venue 可对应多个模板(如鸟巢主场地 + 鸟巢室外场)
-- 缺点:多一张表,多一套 CRUD
-
-> 当前 `vr_seat_templates` 表只有 `name` 字段同时承载"模板名"和"场馆名",暂未分离。
+- layui 表单 + Vue3 CDN(轻量,不破坏 ShopXO 后台 jQuery/layui 结构)
+- ~500 行前端代码,1-1.5 人天
+- 保存:表单 → seat_map JSON → 写入 `vr_seat_templates.seat_map`
+- 颜色选择器:预设色盘(不引入额外依赖)
---
@@ -160,19 +236,18 @@ VR票务
### 4.1 注入原理
```
-ShopXO admin Goods::SaveInfo()
+ShopXO admin Goods::SaveInfo()
→ 调用 hook plugins_view_admin_goods_save(Goods.php:159)
- → 触发 vr_ticket/hook/AdminGoodsSave.php(Phase 3-2 新建)
+ → 触发 vr_ticket/hook/AdminGoodsSave.php
→ 返回票务配置面板 HTML
- → 插入 saveinfo.html 的 base tab 内(
容器)
+ → 插入 saveinfo.html base tab
内
```
### 4.2 钩子注册方式
-> ⚠️ **重要修正**:ShopXO 插件系统**不支持** `backend_hook` 字段。
-> `plugin.json` 实际使用 `hooks` 数组。
+> ⚠️ **重要**:ShopXO **不支持** `backend_hook` 字段,实际使用 `hooks` 数组。
-**plugin.json 正确格式**:
+**plugin.json 追加**(Phase 3-2):
```json
{
"hooks": [
@@ -184,178 +259,207 @@ ShopXO admin Goods::SaveInfo()
}
```
-> 当前 `plugin.json` 只有前两个钩子,后两个是 **Phase 3-2 需要追加的**。
-
### 4.3 AdminGoodsSave.php(待新建)
-> ⚠️ **当前不存在**:`vr_ticket/` 目录下无 `AdminGoodsSave.php` 文件。
+> ⚠️ **待实现**:`vr_ticket/` 下当前无此文件。
-Phase 3-2 需要新建:
-- 文件路径:`plugins/vr_ticket/hook/AdminGoodsSave.php`
-- `plugins_view_admin_goods_save` 钩子返回 HTML 注入票务表单
-- `plugins_service_goods_save_handle` 钩子处理票务数据保存
+**文件**:`plugins/vr_ticket/hook/AdminGoodsSave.php`
-### 4.4 场馆下拉数据来源
-
-`AdminGoodsSave.php` 查询 `vr_seat_templates` 表:
+职责:
+- 查询 `vr_seat_templates` 表,构造场馆下拉 + 分区多选 HTML
+- 返回 layui + Vue3 CDN 的票务配置面板
+**数据来源**:
```php
$templates = Db::name('vr_seat_templates')
- ->field('id, name, seat_map')
+ ->field('id, name, seat_map, category_id')
->where(['status' => 1])
->select();
+
+foreach ($templates as &$t) {
+ $seatMap = json_decode($t['seat_map'], true);
+ // v3.0: venue.name 优先,否则降级取模板 name
+ $t['venue_name'] = $seatMap['venue']['name'] ?? $t['name'];
+ // sections 用于渲染分区多选框
+ $t['zones'] = $seatMap['sections'] ?? [];
+}
```
-当前 `seat_map` 中无 `venue.name`,所以下拉显示的是 `vr_seat_templates.name` 字段(如 "Bird Nest - Zone A")。
+### 4.4 AdminGoodsSaveHandle.php(待新建)
-> ⚠️ Phase 3-1 需要决策:venue 独立后,下拉应显示 venue.name 而非模板 name。
+**文件**:`plugins/vr_ticket/hook/AdminGoodsSaveHandle.php`
+
+触发时机:`plugins_service_goods_save_handle`(GoodsService.php:1550)
+
+职责:
+```php
+// 接收 POST: { template_id, selected_zones: ['A', 'C'] }
+$templateId = $params['vr_template_id'] ?? 0;
+$zones = $params['vr_zones'] ?? []; // e.g. ['A', 'C']
+
+// 1. 生成 SKU(只生成选中分区)
+$result = SeatSkuService::BatchGenerate($goodsId, $templateId, $zones);
+// goodsId 从 $params['id'] 或新建商品的返回值中获取
+
+// 2. 写入 vr_ticket_config 快照(Phase 3-3 新增字段)
+$snapshot = [
+ 'template_id' => $templateId,
+ 'selected_zones' => $zones,
+ 'spec_base_id_map' => $result['data']['spec_base_id_map'],
+ 'created_at' => time(),
+];
+// Db::name('goods')->where('id', $goodsId)->update(['vr_ticket_config' => json_encode($snapshot)]);
+```
---
## 五、商品发布完整流程
-### 场景:商户发布一张票务商品
-
-**Step 1**:商户进入 ShopXO 后台 → 商品管理 → 添加商品
-
-**Step 2**:填写基础信息
-```
-商品名称:周杰伦 VR 虚拟演唱会
-商品分类:VR演出
-```
-
-**Step 3**:在注入的票务配置面板选场馆和分区
-```
-票务配置
- 场馆:[Bird Nest - Zone A ▼] ← AdminGoodsSave 注入的下拉
- 分区:[✓VIP区 ✓看台区] ← 多选,根据场馆联动
-```
-
-**Step 4**:点击发布
+**场景**:商户发布一张票务商品(只开放 A 区和 C 区)
```
-商户点击"发布商品"
- ↓
-Goods::Save() 调用 GoodsService::GoodsSave()(Goods.php:187)
- ↓
-GoodsService::GoodsSave() 执行标准商品保存逻辑
- ↓
-触发钩子 plugins_service_goods_save_handle(GoodsService.php:1550)
- ↓
-AdminGoodsSaveHandle() 收到 POST 数据
- ├── 提取 template_id 和选中的分区 chars
- ├── 调用 SeatSkuService::BatchGenerate($goodsId, $templateId)
- │ └── 注意:BatchGenerate() 签名是 (int $goodsId, int $seatTemplateId)
- │ └── 当前版本:按 template 全量生成(不支持按 zones 过滤)
- │ └── 为每个座位生成一行 sxo_goods_spec_base(inventory=1)
- │ └── 同时写入 sxo_goods_spec_value($vr-场馆/$vr-分区/$vr-时段/$vr-座位号)
- ├── 更新 vr_seat_templates.spec_base_id_map(持久化,非内存)
- └── 返回(让标准保存流程继续)
- ↓
-商品保存完成
- ↓
-订单后续流程不变(已有实现)
+Step 1:商户进入 ShopXO 后台 → 商品管理 → 添加商品
+
+Step 2:填写基础信息
+ 商品名称:周杰伦 VR 虚拟演唱会
+ 商品分类:VR演出
+
+Step 3:在票务配置面板选场馆和分区
+ 票务配置
+ 场馆:[鸟巢 ▼]
+ 分区:[✓VIP区(A) □看台区(B) ✓普通区(C)]
+
+Step 4:点击发布
+ Save() → GoodsService::GoodsSave()
+ → 触发 plugins_service_goods_save_handle
+ → AdminGoodsSaveHandle 收到 POST
+ → BatchGenerate(goodsId=123, templateId=1, enabledZones=['A','C'])
+ → 只生成 A 区(6座)和 C 区(6座)= 12 个 SKU
+ → 写入 sxo_goods_spec_base
+ → 写入 vr_ticket_config JSON 快照
+ → 商品保存完成
+
+Step 5:用户前台看到选座页
+ → 用户点击 A_1 座位
+ → specBaseIdMap['A_1'] = 2001(来自 vr_ticket_config.snapshot)
+ → goodsParams: [{goods_id:123, spec_base_id:2001, stock:1}]
+ → 下单
```
---
-## 六、Spec 生成后的内部结构(技术细节)
+## 六、Spec 生成后内部结构(技术细节)
-> ⚠️ **表前缀**:ShopXO 原生表使用 `sxo_` 前缀,插件自定义表使用 `{$prefix}vrt_`。
-
-以"国家体育馆 + VIP区(A) + 看台区(B)"为例:
+> ⚠️ **表前缀**:ShopXO 原生表用 `sxo_`,插件自定义表用 `{$prefix}vrt_`。
### sxo_goods_spec_base(每行 = 一个座位 SKU)
| spec_base_id | goods_id | inventory | price |
|---|---|---|---|
-| 2001 | 123 | 1 | 899 | ← A_1 座位
-| 2002 | 123 | 1 | 899 | ← A_2 座位
-| ... | 123 | 1 | 899 | ← A_6 座位
-| 3001 | 123 | 1 | 599 | ← B_1 座位
-| ... | 123 | 1 | 599 | ← B_6 座位
+| 2001 | 123 | 1 | 1299 | ← A_1 座位
+| 2002 | 123 | 1 | 1299 | ← A_2 座位
+| ... | 123 | 1 | 1299 | ← A_6 座位
+| 5001 | 123 | 1 | 599 | ← C_1 座位
+| ... | 123 | 1 | 599 | ← C_6 座位
+> 注:B 区座位未生成(商户只选了 A + C)
### sxo_goods_spec_type(每行 = 一个规格维度)
| id | goods_id | name | value |
|---|---|---|---|
-| 1 | 123 | $vr-场馆 | `[{"name":"国家体育馆"}]` |
-| 2 | 123 | $vr-分区 | `[{"name":"VIP区"},{"name":"看台区"}]` |
+| 1 | 123 | $vr-场馆 | `[{"name":"鸟巢"}]` |
+| 2 | 123 | $vr-分区 | `[{"name":"VIP区"},{"name":"普通区"}]` |
| 3 | 123 | $vr-时段 | `[{"name":"待选场次"}]` |
-| 4 | 123 | $vr-座位号 | `[{"name":"A_1"},{"name":"A_2"},...,{"name":"B_6"}]` |
+| 4 | 123 | $vr-座位号 | `[{"name":"A_1"},...{"name":"C_6"}]` |
-### vr_seat_templates.spec_base_id_map(持久化字段)
-
-> ⚠️ **修正**:`spec_base_id_map` 不是内存/缓存,是持久化到 `vr_seat_templates` 表的字段。
-
-商户在前台选座时,前端根据 seatKey(如 `"A_1"`)查表得到对应的 spec_base_id:
+### vr_ticket_config(sxo_goods 表新增字段)
```json
{
- "A_1": 2001, "A_2": 2002, ..., "A_6": 2006,
- "B_1": 3001, "B_2": 3002, ..., "B_6": 3012
+ "template_id": 1,
+ "selected_zones": ["A", "C"],
+ "spec_base_id_map": {
+ "A_1": 2001, "A_2": 2002, ..., "A_6": 2006,
+ "C_1": 5001, "C_2": 5002, ..., "C_6": 5006
+ },
+ "created_at": 1744659200
}
```
---
-## 七、BatchGenerate 实际接口(已实现代码)
+## 七、BatchGenerate 接口(含 v3.0 改造)
-> ⚠️ **关键修正**:文档之前描述的参数签名与实际代码不符。
+### 7.1 当前已实现版本(全量,v2.0)
+
+> ⚠️ 以下为当前 main 分支代码,**不支持**按分区过滤(Phase 3-3 需改造):
-**实际签名**(`SeatSkuService.php:34`):
```php
public static function BatchGenerate(int $goodsId, int $seatTemplateId): array
+// 行为:按模板全部座位生成 SKU
```
-**返回格式**:
+### 7.2 Phase 3-3 改造版本(按分区过滤)
+
```php
-[
- 'code' => 0,
- 'data' => [
- 'total' => 18,
- 'generated' => 12,
- 'spec_base_id_map' => ['A_1' => 2001, 'A_2' => 2002, ...]
- ]
-]
+public static function BatchGenerate(
+ int $goodsId,
+ int $seatTemplateId,
+ array $enabledZones = [] // e.g. ['A', 'C'] — 空=全量
+): array
+
+// 内部逻辑改造:
+// foreach ($seatMap['map'] as $rowChar => $rowSeats) {
+// if (!empty($enabledZones) && !in_array($rowChar, $enabledZones)) {
+// continue; // 跳过未选中的分区
+// }
+// foreach (range(1, strlen($rowSeats)) as $col) {
+// // 生成该座位 SKU...
+// }
+// }
```
-**与文档描述的差异**:
-- 文档说:`(goods_id, venue_id, zones[])` — ❌ 错误
-- 实际是:`(int $goodsId, int $seatTemplateId)` — 全量按模板生成,不支持按 zones 过滤
+**与旧版差异**:
-> ⚠️ 如果 Phase 3-2 需要支持"按分区选择生成",需要修改 `BatchGenerate()` 增加 zones 过滤逻辑。
+| 维度 | 旧版(全量) | 新版(v3.0) |
+|------|------------|-------------|
+| 参数 | `(goodsId, templateId)` | `(goodsId, templateId, enabledZones)` |
+| 行为 | 所有座位都生成 | 只生成 enabledZones 内的分区座位 |
+| 兼容性 | — | enabledZones 为空时退化为全量(向后兼容)|
---
-## 八、商品编辑时的处理(优先级低)
+## 八、vr_ticket_config 字段(v3.0 新增)
-**问题**:商户编辑已发布票务商品时,ShopXO 后台会显示琳琅满目的 Spec 列表(几千个座位 SKU)。
+### 8.1 数据库迁移
-**解决方案**(暂不实现,先让创建流程跑通):
+```sql
+-- EventListener.php 中追加(Phase 3-3)
+ALTER TABLE {$prefix}goods
+ADD COLUMN vr_ticket_config JSON COMMENT '票务配置快照:template_id/selected_zones/spec_base_id_map' AFTER extension_data;
+```
-**方案 A(推荐)**:商品表新增 `vr_ticket_config` JSON 字段
-- 保存时:只存 `{ template_id, zones[], created_at }`
-- 编辑时:从此字段还原venue + zone 选择状态,不从 spec_base 反推
-- **需要迁移**:新增 `sxo_goods.vr_ticket_config` 字段
+**老商品兼容**:存量商品 `vr_ticket_config = NULL`,编辑页检测到为空时提示"请重新选择"。
-**方案 B**:直接用 `sxo_goods.extension_data`(如果 ShopXO 支持)
+### 8.2 生命周期
-> ⚠️ 当前商品表**无** `vr_ticket_config` 字段。方案 A 需要新建数据库迁移。
+| 时机 | 操作 |
+|------|------|
+| 发布商品时 | `AdminGoodsSaveHandle` 写入 `vr_ticket_config` |
+| 编辑商品时 | 从此字段还原"选择了哪些场馆/分区" → 不展示琳琅满目的 spec_base 列表 |
+| 删除商品时 | 钩子 `plugins_service_order_delete_success` 同步清理关联 spec |
---
## 九、与 ShopXO 原生 Spec 管理的关系
-| 维度 | ShopXO 原生 Spec | 我们的票务 Spec |
-|------|----------------|----------------|
-| 谁创建 | 商户在商品编辑页手动添加 | 插件在发布时自动生成 |
+| 维度 | ShopXO 原生 Spec | 票务 Spec |
+|------|----------------|----------|
+| 谁创建 | 商户手动添加 | 插件自动生成(静默) |
| 表前缀 | `sxo_` | `sxo_goods_spec_base` / `sxo_goods_spec_value` |
-| 商户感知 | 在后台规格管理看到 | 无感(我们注入的表单已覆盖场景) |
-| 用户在前台看到 | 购物车/下单流程 | 票务选座 UI(ticket_detail.html)|
-| 核销 | 不涉及 | 每座位一个 QR(vr_tickets 表)|
-
-**商户永远不需要进入 ShopXO 原生的"规格管理"界面来管理票务座位。**
+| 商户感知 | 在后台规格管理看到 | 无感(注入的表单已覆盖场景) |
+| 用户在前台 | 购物车/下单流程 | 票务选座 UI |
+| 核销 | 不涉及 | 每座位一个 QR(vrt_vr_tickets)|
---
@@ -363,45 +467,59 @@ public static function BatchGenerate(int $goodsId, int $seatTemplateId): array
### Phase 3-1:后台场馆配置管理(新建 admin 页面)
-- [ ] 决策:venue 信息存在 seat_map 内还是独立表?
+- [x] 决策已确认:venue 信息存入 seat_map 顶层
- [ ] 新建 `admin/controller/Venue.php`
- [ ] 新建 `admin/view/venue/list.html`(场馆列表)
-- [ ] 新建 `admin/view/venue/save.html`(表单编辑器:venue + zone + 座位排布)
+- [ ] 新建 `admin/view/venue/save.html`(表单编辑器)
- [ ] 升级 `vr_seat_templates.seat_map` JSON 结构(加入 venue 顶层)
- [ ] 将现有测试数据迁移为带 venue 的格式
+- [ ] `BatchGenerate` 读取 `seat_map.venue.name`(不再硬编码)
### Phase 3-2:商品发布页注入
-- [ ] 在 `plugin.json` 的 `hooks` 数组中追加钩子(**不用** `backend_hook`)
-- [ ] 新建 `hook/AdminGoodsSave.php`(注入票务配置面板 HTML)
+- [ ] `plugin.json` 的 `hooks` 数组追加 2 个钩子
+- [ ] 新建 `hook/AdminGoodsSave.php`(注入票务配置 HTML)
- [ ] 新建 `hook/AdminGoodsSaveHandle.php`(处理保存数据)
-- [ ] 场馆下拉联动分区多选(Vue3,轻量)
-- [ ] 确认商品发布页实际 Tab 名称(待对照 `saveinfo.html`)
+- [ ] 场馆下拉 + 分区多选联动(Vue3,轻量)
+- [ ] 确认商品发布页实际 Tab 名称(对照 `saveinfo.html`)
-### Phase 3-3:Spec 自动生成接入
+### Phase 3-3:Spec 自动生成接入 + 按分区过滤
-- [ ] `BatchGenerate()` 增加按 zones 过滤参数(如需要)
-- [ ] `extension_data` 写入 order_goods(选座信息追溯)
+- [ ] `BatchGenerate()` 增加 `enabledZones` 参数
+- [ ] `AdminGoodsSaveHandle` 从 POST 提取选中分区,传给 `BatchGenerate`
+- [ ] 数据库迁移:`sxo_goods` 新增 `vr_ticket_config` JSON 字段
+- [ ] 发布时写入 `vr_ticket_config` 快照
### Phase 3-4(优先级低):商品编辑回显
-- [ ] 新建 `sxo_goods.vr_ticket_config` 字段迁移
-- [ ] 编辑页加载时解析 `vr_ticket_config`,还原 venue + zone
+- [ ] 编辑页加载时解析 `vr_ticket_config`,还原场馆 + 分区状态
- [ ] 编辑页票务配置面板回显
+- [ ] 若修改了 venue/zone:提示重新生成 spec 或自动重建
---
-## 十一、勘误表(v1.0 → v2.0)
+## 十一、v3.0 确认决策
-| # | v1.0 描述 | 实际代码 | v2.0 修正 |
-|---|---------|---------|---------|
-| 1 | `plugin.json` 用 `backend_hook` 注册 | ShopXO 用 `hooks` 数组 | 改为追加到 `hooks` 数组 |
-| 2 | `AdminGoodsSave.php` 已存在或待实现 | 文件不存在 | 明确为 Phase 3-2 新建任务 |
-| 3 | seat_map 顶层有 `venue` 字段 | 不存在,`$vr-场馆` 硬编码 | 新增设计决策:venue 存在哪 |
-| 4 | seat_map 顶层有 `zones` 字段 | 实际是 `seats` 和 `sections` | 全文修正字段名 |
-| 5 | `BatchGenerate(goods_id, venue_id, zones[])` | `(int $goodsId, int $seatTemplateId)` 全量生成 | 修正签名,注明全量/过滤差异 |
-| 6 | `goods_spec_base` 前缀是 `vrt_` | ShopXO 原生表是 `sxo_` | 修正前缀,说明 `sxo_` vs `vrt_` |
-| 7 | `vr_ticket_config` 字段存在 | 不存在,需新建迁移 | 明确为 Phase 3-4 新建任务 |
-| 8 | 场馆配置是 Phase 2 已完成 | `Venue.php` 不存在,Phase 3-1 待实现 | 修正当前状态标注 |
-| 9 | `spec_base_id_map` 是内存/缓存 | 持久化到 `vr_seat_templates.spec_base_id_map` | 修正存储位置描述 |
-| 10 | 商品分类需绑定票务配置 | 代码中未强制校验 | 删除此约束描述 |
+| 决策点 | 确认结果 |
+|--------|---------|
+| venue 信息存在哪 | 存入 `seat_map.venue` 顶层,不独立建表 |
+| 是否支持按分区过滤 | **必须支持**,Phase 3-3 改造 BatchGenerate 增加 enabledZones 参数 |
+| 数据库扩展兼容性 | 新增 `sxo_goods.vr_ticket_config` JSON 字段,老商品留 NULL |
+| 老 seat_map 兼容性 | 降级处理:无 venue → 取模板 name;无 sections → 从 seats 推导 |
+
+---
+
+## 十二、勘误表(v1.0 → v2.0 → v3.0)
+
+| # | v1.0 描述 | 实际/修正 |
+|---|---------|---------|
+| 1 | `plugin.json` 用 `backend_hook` 注册 | ShopXO 用 `hooks` 数组 ✅ v2.0 已修正 |
+| 2 | seat_map 有 `venue` 顶层字段 | 不存在,v3.0 决策新增 ✅ |
+| 3 | seat_map 有 `zones` 顶层字段 | 实际是 `seats` + `sections` ✅ v2.0 已修正 |
+| 4 | BatchGenerate 全量生成 | v3.0 决策:必须支持按分区过滤 ✅ |
+| 5 | `goods_spec_base` 前缀是 `vrt_` | 原生表是 `sxo_` ✅ v2.0 已修正 |
+| 6 | `vr_ticket_config` 字段存在 | 不存在,v3.0 决策新增 ✅ |
+| 7 | 场馆配置管理是 Phase 2 已完成 | 待实现,Phase 3-1 ✅ v2.0 已修正 |
+| 8 | spec_base_id_map 是内存/缓存 | 持久化到 `vr_seat_templates.spec_base_id_map` ✅ v2.0 已修正 |
+| 9 | 商品分类需绑定票务配置 | 代码未强制校验,删除此约束 ✅ v2.0 已修正 |
+| 10 | 无 vr_ticket_config 导致编辑页 spec 堆满 | v3.0 决策:新增字段解决 ✅ |