diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c4d3015..5eb9551 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,6 +1,6 @@ # ShopXO VR票务插件 — 架构文档 -> 版本:v1.2(2026-04-14 下午更新,座位地图 + 场馆绑定架构确认) +> 版本:v1.3(2026-04-14 更新,venue_data 直接写入 sxo_goods,vr_sessions 职责明确) > 源码位置:council-research/shopxo-eval/.worktrees/shopxo-evaluator/shopxo-src/ ## 项目概述 @@ -18,17 +18,21 @@ 详见 `docs/06_SEAT_MAP_INTEGRATION.md` -**核心发现**: +**核心架构(2026-04-14 更新)**: 1. **字符地图是行业标准**:场馆平面图 → 字符串地图(如 `aaa___aaa`)→ 前端渲染为 SVG/DOM -2. **ShopXO 分类 = Venue Type 绑定**:每个"场馆类型"对应一个 ShopXO 分类,商品挂分类 = 绑定 venue type -3. **vr_venues 表**:商家在插件后台管理场馆,上传/编辑座位图 JSON -4. **vr_sessions.seat_map_json**:每个场次存一份座位图配置 -5. **spec_base_id_map**:seat_id(如 `"3_5"`)→ spec_base_id(如 10003)映射,绑定 ShopXO 购买流程 -6. **ShopXO spec 系统无硬限制**:规格种类数、单规格选项数、SKU 组合数均无限制(3场馆×2票种×500座位=3000 SKU 完全可行) +2. **venue_data 直接存在 sxo_goods**:每个票务商品 = 1 场演出,`sxo_goods.venue_data`(LONGTEXT)存完整配置 +3. **venue_data JSON 内容**:`venue`(场馆+座位图)+ `sessions[]`(场次列表)+ `spec_base_id_map`(座位→SKU映射) +4. **vr_venues**:场馆主数据(名称/地址/座位图),多个商品/场次可复用同一个场馆 +5. **vr_sessions**:每个演出场次(日期+时间),共用 venue 的座位图,独立库存 +6. **spec_base_id_map**:seat_id(如 `"3_5"`)→ spec_base_id → ShopXO 购买流程 +7. **ShopXO spec 系统无硬限制**:3场馆×2票种×500座位=3000 SKU 完全可行 -**绑定链路**: +**数据流**: ``` -ShopXO 分类(venue type)← → vr_venues ← → vr_sessions ← → spec_base_id_map ← → ShopXO spec_base +sxo_goods.venue_data JSON + ├── venue.seat_map → 前端渲染座位图 + ├── sessions[] → 场次选择器 + └── spec_base_id_map → 选座 → Buy API → 原子扣 spec_base.inventory ``` ## 核心技术发现(2026-04-14 调研) diff --git a/docs/06_SEAT_MAP_INTEGRATION.md b/docs/06_SEAT_MAP_INTEGRATION.md index a36eeef..efa862e 100644 --- a/docs/06_SEAT_MAP_INTEGRATION.md +++ b/docs/06_SEAT_MAP_INTEGRATION.md @@ -102,93 +102,164 @@ --- -## 二、ShopXO 后台集成方案 +## 二、核心架构:venue_data 直接写入 sxo_goods -### 2.1 核心设计:复用 ShopXO 分类作为 Venue Type +### 2.1 为什么不用 ShopXO 分类? -**每个 venue type = 一个 ShopXO 分类** +ShopXO 分类是**商品类型**(演唱会/话剧/周边),不是**具体场馆**。 +多场馆 × 每个场馆不同座位配置 → 分类不够用。 -ShopXO 已有完整的商品多级分类系统(`sxo_goods_category`),我们直接复用: +**最优解:直接在 sxo_goods 表加字段,完整配置存在商品里。** -``` -sxo_goods_category -├── 演唱会 -│ ├── 杭州大剧院-演唱会场 -│ ├── 北京鸟巢-演唱会场 ← 绑定 seat_map_json -│ └── 上海梅赛德斯-演唱会场 -├── 话剧 -│ ├── 人艺剧院 -│ └── 国家大剧院 -└── 电影(可选) +### 2.2 数据库改动:sxo_goods 新增 venue_data 字段 + +```sql +ALTER TABLE sxo_goods +ADD COLUMN venue_data LONGTEXT COMMENT '票务插件:场馆+场次+座位配置JSON'; ``` -**优点**: -- 不需要改 `sxo_goods` 表结构 -- 直接用 ShopXO 原生"分类选择器"(后台商品编辑已有,无需开发) -- 商家在 ShopXO 后台创建商品时,分类 = venue type 绑定一步到位 +`LONGTEXT` ≈ 4GB,存完整座位图配置绑绑有余。 +ShopXO 已有先例:`sxo_order.extension_data` 和 `sxo_goods_spec_base.extends` 都是 `LONGTEXT`。 -### 2.2 场馆表(vr_venues) +### 2.3 venue_data JSON 结构 + +```json +{ + "venue": { + "id": 1, + "name": "北京鸟巢", + "address": "北京市朝阳区国家体育场南路1号", + "seat_map": { + "map": ["aaaaaaaaaaaa", "aaaaaaaaaaaa", "bbbbbb__bb", "bbbbbbbbbbbb"], + "row_labels": ["A", "B", "C", "D"], + "seats": { + "a": { "price": 599, "label": "VIP区", "classes": "seat-vip" }, + "b": { "price": 399, "label": "普通区", "classes": "seat-normal" }, + "_": null + }, + "sections": [ + { "name": "VIP区", "color": "#FF6B6B", "rows": [0, 1] }, + { "name": "普通区", "color": "#4ECDC4", "rows": [2, 3] } + ] + } + }, + "sessions": [ + { + "id": 1, + "datetime": "2026-06-01 19:30", + "price_overrides": { "a": 699, "b": 399 } + }, + { + "id": 2, + "datetime": "2026-06-02 19:30", + "price_overrides": { "a": 599, "b": 299 } + } + ], + "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 } + } +} +``` + +**每个商品 = 1 个演出场次 = 完整的票务配置** +- 商家创建"周杰伦北京鸟巢演唱会2026-06-01场次"商品时,venue_data 包含:venue 信息 + 座位图 + spec_base_id_map +- 用户打开商品详情 → `sxo_goods.venue_data` 已经在商品数据里 → 直接渲染选座 UI + +### 2.4 vr_sessions 是什么? + +`vr_sessions`(场次表)用于管理**同一场演出在不同时间的多场次**: + +``` +商品:周杰伦北京鸟巢2026演唱会 + ├── vr_sessions[1]:2026-06-01 19:30 第一场 + ├── vr_sessions[2]:2026-06-02 19:30 第二场 + └── vr_sessions[3]:2026-06-03 14:00 第三场(下午场,价格不同) + +每个 session: + - id / datetime(具体时间) + - price_overrides(该场次的价格覆盖,优先级高于 venue.seat_map.seats) + - 复用 venue.seat_map 和 spec_base_id_map(座位布局不变) + - 独立库存(每个场次的 spec_base.inventory 独立追踪) +``` + +**如果一个商品只有单场演出**,vr_sessions 可以简化为 venue_data 里的 sessions 数组。 + +**vr_sessions 独立表的价值**: +- 多场次共用同一个 venue(座位图不变) +- 每个 session 有独立的 spec_base.inventory(第一场售罄≠第三场售罄) +- 每个 session 可以有不同的 price_overrides(早鸟票/周末票等) + +### 2.5 vr_venues(场馆表) ```sql CREATE TABLE vr_venues ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - category_id INT UNSIGNED NOT NULL COMMENT 'ShopXO分类ID,绑定venue type', - name VARCHAR(180) NOT NULL COMMENT '场馆名称', - address VARCHAR(255) NOT NULL DEFAULT '' COMMENT '场馆地址', - seat_map_json LONGTEXT COMMENT '座位地图JSON(map[] + seats配置)', + id BIGINT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(180) NOT NULL COMMENT '场馆名称', + address VARCHAR(255) NOT NULL DEFAULT '' COMMENT '场馆地址', + seat_map_json LONGTEXT COMMENT '座位地图JSON', seat_base_price INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '基础票价', - status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '0下架 1上架', - add_time INT UNSIGNED NOT NULL DEFAULT 0, - upd_time INT UNSIGNED NOT NULL DEFAULT 0, - INDEX idx_category_id (category_id) + status TINYINT UNSIGNED NOT NULL DEFAULT 1, + add_time INT UNSIGNED NOT NULL DEFAULT 0, + upd_time INT UNSIGNED NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='VR演唱会场馆'; ``` -**关键字段**: -- `category_id`:ShopXO 分类 ID,ShopXO 后台商品编辑时的"分类选择"结果直接对应 -- `seat_map_json`:座位地图配置,商家在插件后台导入/编辑 -- `seat_base_price`:基础票价 +**vr_venues 的作用**: +- 场馆基础信息统一管理(名称/地址) +- seat_map_json 是场馆的"硬件配置"(场地座位布局是固定的) +- 多个 session 共用一个 venue,venue_data 在每个 session/goods 里存一份副本 -### 2.3 插件后台:场馆 + 座位图编辑器 +### 2.6 插件后台:场馆 + 商品票务配置编辑器 商家在 ShopXO 后台插件入口管理: -1. **场馆管理**:创建场馆,关联 ShopXO 分类,上传/绘制座位图 -2. **座位图编辑器**:输入字符串地图(如 `aaaaaaaaaaaa`)或上传 SVG -3. **场次管理**:创建演出场次,选择场馆,设定时间、价格 -### 2.4 插件 Hook:向 ShopXO 商品保存/读取注入 venue 信息 +1. **场馆管理**:`vr_venues` CRUD,上传/编辑座位图(字符串地图编辑器) +2. **商品票务配置**:选择 venue → 选择/创建 session → 系统将 venue.seat_map_json + session 信息整合写入 `sxo_goods.venue_data` +3. **SKU 批量生成**:根据 seat_map 生成/更新 `sxo_goods_spec_base` 中的座位 SKU(spec_base_id_map) + +### 2.7 插件 Hook ```php -// plugins_service_goods_handle_begin — 保存商品时 +// plugins_service_goods_handle_begin — 保存商品时,拦截 venue_data public static function GoodsSaveHandle(&$params, &$goods, $goods_id) { - // venue_id 由插件后台单独保存,不依赖 ShopXO 商品表 - // 关联:vr_venues ← (goods_id) → ShopXO sxo_goods + if (!empty($params['venue_data'])) { + // ShopXO 会自动将 venue_data 写入 sxo_goods.venue_data 字段 + } } -// plugins_service_goods_data — 读取商品时,注入 venue 信息 -public static function GoodsDataHandle(&$data, $goods_id) +// plugins_service_goods_data — 读取商品时,注入票务配置 +public static function GoodsDataHandle(&$data, &$goods_id) { - // 读取该商品关联的 venue + 场次列表 - $data['vr_venues'] = VrVenueService::GetVenueByGoodsId($goods_id); - $data['vr_sessions'] = VrSessionService::GetSessionsByGoodsId($goods_id); + // venue_data 已存在 sxo_goods.venue_data,直接可用 + // 前端模板通过 $goods.venue_data 直接访问 } ``` -### 2.5 商品详情页加载流程 +### 2.8 商品详情页加载流程 ``` 用户打开票务商品详情页 │ - → 触发 Goods.php Hook 判断 item_type=ticket + → site_type=3(虚拟),触发票务 Hook 注入选座区 │ - → 插件读取 goods_id 对应的 vr_venues + vr_sessions + → 前端读取 $goods.venue_data + │ ├── venue.seat_map → 渲染座位图 SVG + │ ├── venue.sessions → 显示场次选择器 + │ └── spec_base_id_map → 选座后查 SKU │ - → 前端展示: - │ 步骤1:选择场次(日期+时间) - │ 步骤2:加载该场次座位图(从 seat_map_json 渲染 SVG) - │ 步骤3:用户点击座位 → 获取 spec_base_id - │ 步骤4:调 ShopXO Buy API 购买该 SKU + → 步骤1:用户选场次(datetime) + │ └── 从 sessions[] 取 price_overrides 渲染座位图价格 + │ + → 步骤2:用户点击座位 → 获取 seat_id(如 "3_5") + │ └── 查 spec_base_id_map → 拿到 spec_base_id + │ + → 步骤3:调 ShopXO Buy API → spec_base_id + goods_id + │ └── ShopXO BuyService::OrderInsertHandle() 原子扣库存 + │ + → 步骤4:支付成功 → 插件生成 ticket_code + QR ``` --- @@ -211,15 +282,16 @@ public static function BindSessionToSpecBase($session_id) ``` **绑定关系**: -- `vr_sessions.spec_base_id_map` ← JSON 映射(seat_id → spec_base_id) +- `sxo_goods.venue_data.spec_base_id_map` ← JSON 映射(seat_id → spec_base_id),完整配置存在商品表 - `sxo_goods_spec_base` ← 每个座位一个 SKU(inventory=1,price=座位价格) - ShopXO `BuyService::OrderInsertHandle` ← 原子扣 inventory,天然防超卖 -### 3.2 场次变更时的 SKU 联动 +### 3.2 场次/座位变更时的 SKU 联动 -- **场次新增座位**:调用 `GoodsSpecificationsInsert` 新增 spec_base -- **场次删除座位**:将对应 spec_base.inventory 置为 0(软删除) +- **新增座位**:调用 ShopXO `GoodsSpecificationsInsert()` 新增 spec_base +- **删除座位**:将对应 spec_base.inventory 置为 0(软删除) - **价格变更**:更新 `sxo_goods_spec_base.price` +- **配置更新后**:重新生成 `sxo_goods.venue_data.spec_base_id_map` 并保存 --- @@ -227,7 +299,7 @@ public static function BindSessionToSpecBase($session_id) | 阶段 | 内容 | 工作量 | |---|---|---| -| **Phase A** | vr_venues / vr_sessions 表 + CRUD | 小 | +| **Phase A** | sxo_goods 加 venue_data LONGTEXT 字段 + vr_venues/vr_sessions 表 + CRUD | 小 | | **Phase B** | 场馆座位图编辑器(字符串地图) | 中 | | **Phase C** | Vue 3 选座组件(渲染 + 交互) | 中 | | **Phase D** | spec_base_id_map 绑定逻辑 | 中 |