From 111063d785bbb121460ac6156a92857bba4594ea Mon Sep 17 00:00:00 2001 From: Council Date: Fri, 17 Apr 2026 21:49:21 +0800 Subject: [PATCH] docs: add 13_GOODS_ADD_HOOK_RESEARCH.md - goods add hook survey - Code-verified: vr_seat_templates full field list + seat_map v3 JSON structure - Confirmed: no goods_save hook exists yet, no vr_goods_config table - Critical bug found: SeatSkuService.php has corrupted \t\think\xacade strings (P0 fix) - Two-hook design: plugins_view_admin_goods_save + plugins_service_goods_save_handle - goods.vr_goods_config JSON field (no new table needed) - BatchGenerate extension: room/section filtering --- docs/13_GOODS_ADD_HOOK_RESEARCH.md | 333 +++++++++++++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 docs/13_GOODS_ADD_HOOK_RESEARCH.md diff --git a/docs/13_GOODS_ADD_HOOK_RESEARCH.md b/docs/13_GOODS_ADD_HOOK_RESEARCH.md new file mode 100644 index 0000000..bfd8faf --- /dev/null +++ b/docs/13_GOODS_ADD_HOOK_RESEARCH.md @@ -0,0 +1,333 @@ +# 商品发布钩子调研报告 — 票务商品配置注入 + +> 调研时间:2026-04-17 21:40–22:00 GMT+8 +> 调研人:西莉雅 + 子 Agent 代码扫描 +> 文档版本:v1.0.0 +> 状态:**调研完成,待执行** + +--- + +## 一、当前现状(代码实测) + +### 1.1 vr_seat_templates 表完整字段 + +来源:`database/migrations/001_vr_tables.sql` + 代码交叉验证 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | BIGINT UNSIGNED | 模板ID,主键 | +| `name` | VARCHAR(180) | 场馆简短名称(搜索用) | +| `category_id` | BIGINT UNSIGNED | 绑定分类ID(**已废弃**,VenueSave 强制置 0) | +| `seat_map` | LONGTEXT | 核心 JSON,完整座位配置 | +| `spec_base_id_map` | LONGTEXT | 座位ID → spec_base_id 映射(SKU生成后填充) | +| `status` | TINYINT UNSIGNED | 状态:0=禁用 1=启用 | +| `add_time` | INT UNSIGNED | 创建时间戳 | +| `upd_time` | INT UNSIGNED | 更新时间戳 | + +### 1.2 seat_map JSON 实际完整结构(v3 版) + +```json +{ + "venue": { + "name": "北京国家体育馆", + "address": "北京市朝阳区天辰东路9号", + "location": { "lng": "116.397128", "lat": "39.916527" }, + "images": ["https://..."] + }, + "rooms": [ + { + "id": "room_1", + "name": "主要展厅A", + "sections": [ + { "char": "A", "name": "VIP区", "price": 999.00, "color": "#ff4d4f" }, + { "char": "B", "name": "普通区", "price": 299.00, "color": "#1677ff" } + ], + "seats": { + "A": { "char": "A", "name": "VIP区", "price": 999.00, "color": "#ff4d4f", "label": "VIP区" }, + "B": { "char": "B", "name": "普通区", "price": 299.00, "color": "#1677ff", "label": "普通区" } + }, + "map": [ + "AAAAA", + "AAAAA", + "BBBBB", + "BBBBB", + "CCCCC" + ] + }, + { + "id": "room_2", + "name": "次展厅B", + "sections": [ + { "char": "C", "name": "观众区", "price": 199.00, "color": "#52c41a" } + ], + "seats": { "C": { ... } }, + "map": ["CCCCCC", "CCCCCC"] + } + ] +} +``` + +**关键结构说明**: +- `venue` → 场馆信息顶层,一个模板只有一个 venue +- `rooms[]` → **复数演播厅节点**(核心!一个场馆可有多个厅) +- 每个 room 内 `sections[]` → **座位分区配置**(char/label/price/color) +- 每个 room 内 `map[]` → 字符地图(每排一个字符串,每字符=一个座位) +- `_` 和 `-` → 通道/空白,不算座位 +- `seats{}` 字典由**后端 VenueSave 保存时自动从 sections 生成**(前端不用管) + +### 1.3 当前商品关联机制 + +**当前:无 vr_goods_config 表,无商品发布钩子。** + +商品与票务的关联仅通过 `goods.item_type` 字段(Event.php 安装时追加): +```sql +ALTER TABLE {prefix}goods ADD COLUMN item_type VARCHAR(20) NOT NULL DEFAULT 'normal' +COMMENT '商品类型:normal=普通 goods ticket=票务 physical=周边' +``` + +**当前没有商品编辑页注入**,插件 Hook.php 只注册了: +- `plugins_service_admin_menu_data`(后台菜单) +- `plugins_service_order_pay_success_handle_end`(支付成功) +- `plugins_service_order_delete_success`(订单删除,空实现) + +### 1.4 SeatSkuService.php 严重 Bug(⚠️ 需修复) + +文件中有大量畸形字符串 `\t hink\xacade\...`(疑似编辑器损坏或文件编码问题): + +```php +// ❌ 错误写法(文件里大量存在) +$template = hink\xacade\Db::name(self::table('seat_templates')) +// ✅ 正确写法应为: +$template = \think\facade\Db::name(self::table('seat_templates')) +``` + +涉及所有 `Db::` 调用(startTrans/commit/rollback/insertGetId/insertAll 等),会导致 `BatchGenerate()` **完全无法运行**。这是 Phase 0 阶段必须先修复的问题。 + +### 1.5 BatchGenerate 当前签名(待扩展) + +```php +public static function BatchGenerate(int $goodsId, int $seatTemplateId): array +// 目前只接受 goodsId + seatTemplateId,不支持按 rooms/sections 过滤 +``` + +--- + +## 二、设计方案(待确认执行) + +### 2.1 两大钩子的职责分工 + +``` +用户进入商品发布页(/admin/goods/saveinfo) + │ + └─► Hook[1] plugins_view_admin_goods_save + └─► 注入票务配置面板(隐藏状态) + └─► 附"是否为票务商品"勾选框 + └─► 用户勾选后,票务配置面板解除禁用/展开 + │ +用户点击「发布商品」 + │ + └─► Hook[2] plugins_service_goods_save_handle + └─► 读取 goods.vr_goods_config(JSON) + └─► 对每个 templateId 调用 BatchGenerate(带节点过滤) + └─► 写入/更新 sku(goods_spec_base + goods_spec_value) +``` + +### 2.2 Hook[1] 注入面板设计 + +**注入位置**:商品发布页 `saveinfo.html`,在规格区域之前或商品基本信息区底部 + +**UI 交互**: +``` +添加商品 + [商品名称] [商品分类 ▼] + ☑ 是否为票务商品 ← 勾选框,解锁票务配置 + ───────────────────────── + ▼ 票务配置(勾选后展开) + 场馆模板:[请选择 ▼] + └─ 场馆名称预览 + 地址 + + 演播厅选择(多选): + [✓] 主要展厅A(3区×6座 = 18座) + [✓] 次展厅B(1区×6座 = 6座) + + 分区选择(每个演播厅内多选): + 主要展厅A: + [✓] VIP区(A) — 999元 🔴 + [ ] 普通区(B) — 299元 🔵 + [ ] 后排区(C) — 199元 🟢 + + 次展厅B: + [✓] 观众区(C) — 199元 🟢 +``` + +**隐藏方案**:票务配置面板初始 `display: none`,由勾选框的 `@change` 事件控制展开。 + +**数据来源**: +```php +// 查询所有可用模板(status=1) +$templates = Db::name('vr_seat_templates') + ->field('id, name, seat_map') + ->where('status', 1) + ->select(); +// seat_map JSON 里已有 venue + rooms + sections,无需额外查询 +``` + +### 2.3 Hook[2] 数据处理流程 + +用户提交后,`params` 里携带: +```json +{ + "vr_is_ticket": "1", + "vr_goods_config": [ + { + "template_id": 1, + "selected_rooms": ["room_1", "room_2"], + "selected_sections": { + "room_1": ["A", "B"], + "room_2": ["C"] + } + } + ] +} +``` + +**存储**:新增 `goods.vr_goods_config` LONGTEXT 字段(JSON 数组),直接存这个结构,不建新表。 + +### 2.4 BatchGenerate 扩展(按分区过滤) + +```php +public static function BatchGenerate( + int $goodsId, + int $seatTemplateId, + array $selectedRooms = [], // e.g. ['room_1', 'room_2'],空=全部 + array $selectedSections = [] // e.g. ['A','B'] per room,空=全部 +): array +``` + +**扩展逻辑**: +```php +// 遍历每个 room +foreach ($seatMap['rooms'] as $room) { + // 跳过未选中的房间 + if (!empty($selectedRooms) && !in_array($room['id'], $selectedRooms)) { + continue; + } + + foreach ($room['map'] as $rowIndex => $rowStr) { + $chars = mb_str_split($rowStr); + foreach ($chars as $colIndex => $char) { + // 跳过通道/空白 + if ($char === '_' || $char === '-') continue; + + // 跳过未选中的座位类型 + if (!empty($selectedSections[$room['id']]) + && !in_array($char, $selectedSections[$room['id']])) { + continue; + } + // 生成 SKU... + } + } +} +``` + +**向后兼容**:当 `selectedRooms` 和 `selectedSections` 均为空时,退化为全量生成(兼容旧逻辑)。 + +### 2.5 数据库迁移 + +```sql +-- 在 goods 表新增字段(Event.php 中执行) +ALTER TABLE {prefix}goods +ADD COLUMN vr_goods_config LONGTEXT COMMENT '票务配置:[{template_id, selected_rooms, selected_sections}]' +AFTER item_type; +``` + +老商品 `vr_goods_config = NULL`,编辑页检测到时展示为空表单。 + +### 2.6 钩子注册(config.json 更新) + +```json +{ + "hook": { + "plugins_service_admin_menu_data": ["app\\plugins\\vr_ticket\\Hook"], + "plugins_service_order_pay_success_handle_end": ["app\\plugins\\vr_ticket\\Hook"], + "plugins_service_order_delete_success": ["app\\plugins\\vr_ticket\\Hook"], + "plugins_view_admin_goods_save": ["app\\plugins\\vr_ticket\\hook\\AdminGoodsSave"], + "plugins_service_goods_save_handle": ["app\\plugins\\vr_ticket\\hook\\AdminGoodsSaveHandle"] + } +} +``` + +### 2.7 新增文件清单 + +| 文件 | 职责 | +|------|------| +| `hook/AdminGoodsSave.php` | Hook[1]:注入票务配置面板 HTML(Vue3 CDN) | +| `hook/AdminGoodsSaveHandle.php` | Hook[2]:保存时读取 vr_goods_config → 调用 BatchGenerate | +| `service/SeatSkuService.php` | **修复**畸形字符串 + **扩展**按房间/分区过滤 | +| `Event.php` | 安装时追加 `goods.vr_goods_config` 字段 | + +--- + +## 三、大头确认的设计决策 + +| 决策点 | 结论 | +|--------|------| +| vr_goods_config 存在哪 | 直接存 `goods.vr_goods_config`(JSON 数组),不建新表 | +| 表名 | MySQL 数据库 `vrticket`(不是 `shopxo` 默认前缀) | +| 票务配置面板是否默认显示 | **否**,默认隐藏,用户勾选"是否为票务商品"后展开 | +| 勾选框名称 | "是否为票务商品"(checkbox) | +| 多模板支持 | 支持(vr_goods_config 是 JSON 数组,每个元素=一个模板配置) | +| 座位模板表内字段废弃 | `category_id` 绑定字段已废弃,不使用 | +| category_id 废弃后的模板查询 | 直接查询 `vr_seat_templates.status=1`,不再依赖 category_id | + +--- + +## 四、与前端商品详情的联动 + +``` +商品发布(钩子注入配置)→ goods.vr_goods_config 写入 + ↓ +商品详情页加载 → BaseService::isTicketGoods() 判断 + ↓ +item_type === 'ticket' → 渲染 ticket_detail.html + ↓ +前端读取 goods.vr_goods_config → 获取 templateId → API 查询最新座位状态 + ↓ +用户在前台看到选座界面(每个座位独立 SKU) +``` + +--- + +## 五、实施优先级 + +| 优先级 | 任务 | 原因 | +|--------|------|------| +| **P0** | 修复 SeatSkuService.php 畸形字符串 | BatchGenerate 完全无法运行 | +| **P0** | 新增 `goods.vr_goods_config` 字段迁移 | 所有后续逻辑依赖此字段 | +| **P0** | 注册两个钩子到 config.json | 钩子不注册 = 功能不触发 | +| **P1** | 实现 AdminGoodsSave.php(Hook[1]) | 面板注入,核心交互入口 | +| **P1** | 实现 AdminGoodsSaveHandle.php(Hook[2]) | 保存时生成 SKU | +| **P1** | 扩展 BatchGenerate 增加按房间/分区过滤 | 核心算法升级 | +| **P2** | 商品编辑页回显(读取 vr_goods_config 还原勾选状态) | 提升体验 | + +--- + +## 六、关键技术参考 + +### 场馆配置页 Vue3 实现参考 +`view/venue/save.html` 使用 Vue 3 CDN(`vue@3.3.4.prod`),`delimiters: ['[[', ']]']` 避免与 ThinkPHP 模板冲突。 + +### Base64 传输方案 +Vue 数据通过 Base64 编码后由 `` 传输(避免 JSON 被框架 URL 净化截断)。商品配置同理可用同样模式。 + +### 商品发布页模板位置 +ShopXO 后台商品发布页:`app/admin/view/default/goods/saveinfo.html` +钩子注入点:`plugins_view_admin_goods_save` 标记处 + +--- + +## 七、版本历史 + +| 版本 | 日期 | 修改内容 | +|------|------|---------| +| v1.0.0 | 2026-04-17 | 初始版本,代码实测 + 大头确认设计 | \ No newline at end of file