# Council 评估报告 — BackendArchitect(Round 3 更新) > 评估日期:2026-05-26 | 角色:后端架构师 | Git: `0d6d20062` --- ## 一、现状评估(2026-05-26 现场核查) ### 1.1 Phase 4 Tree API 设计 **状态:📋 设计文档已提交,代码为零** | 组件 | 状态 | 说明 | |------|------|------| | `docs/PHASE_4_API.md` | ✅ 存在(父仓库 commit `40a9b0ad1`) | Tree API 设计文档 | | `docs/PLAN_TREE_API_IMPLEMENTATION.md` | ✅ 存在(父仓库 commit `40a9b0ad1`) | 实现计划 | | `SeatMapService.php`(设计名) | ❌ **文件不存在** | 实际文件名为 `SeatSkuService.php` | | `SeatSkuService.php`(实际) | ✅ 存在 | 但没有 `buildTree()` 方法 | | `api/` 目录(Worktree) | ❌ **未纳入 Git 追踪** | 父仓库有 `api/Goods.php`,Worktree 为空 | | `api/Goods.php::seatmap()` | ⚠️ 父仓库有,工作树无 | 调用不存在的 `SeatMapService::GetSeatMap()` | **Round 3 修正**:`api/Goods.php` 在父仓库存在,有 `seatmap()` 方法,但父仓库 Worktree 隔离造成版本混淆。Tree API 设计完整,代码仅存在 `SeatSkuService::GetGoodsViewData()` 部分复用逻辑。 ### 1.2 SeatMapService + seatmap API **状态:⚠️ API 断裂,带运行时错误** | 组件 | 状态 | 位置 | |------|------|------| | `index/Index.php::soldSeats` | ⚠️ 存在 | `index/Index.php:43` | | `SeatSkuService::getSoldSeats()` | ❌ **完全缺失** | 被 `Index.php:43` 调用但方法不存在 | | `index/Index.php::soldSeats` | ❌ **运行时崩溃** | PHP Fatal Error | | `SeatSkuService::GetGoodsViewData()` | ✅ 完整 | 返 回 seatSpecMap + specTypeList | | `SeatSkuService::buildSeatSpecMap()` | ✅ 完整 | `SeatSkuService.php:532` | | `SeatSkuService::BatchGenerate()` | ✅ 完整 | 座位 SKU 批量生成 | **严重性澄清**:Round 2 称 H5 绕过了 soldSeats API(正确),但忽略了 UniApp 的 `soldSeats` 端点调用会触发 PHP 致命错误。`ticket_detail.html` 自身正常,但所有直接调用 `soldSeats` API 的客户端都会崩溃。 **新增发现**:`api/Goods.php` 的 `seatmap()` 方法(父仓库)调用 `SeatMapService::GetSeatMap()`,但该类不存在(实际类名为 `SeatSkuService`,方法为 `GetGoodsViewData`)。这是 **命名混淆 Bug**。 ### 1.3 seatSpecMap 注入商品详情 API **状态:❌ Gap 仍成立** | 组件 | 状态 | 说明 | |------|------|------| | `vr_goods_config` 表字段 | ✅ 存在 | `Event.php::Install()` 创建 | | `SeatSkuService::GetGoodsViewData()` | ✅ 存在 | `SeatSkuService.php:369` | | `Hook.php::plugins_service_goods_data` | ❌ **未注册** | `Hook.php:13-28` 无此 case | | H5 `ticket_detail.html` | ✅ 工作正常 | 直接调用 `GetGoodsViewData()` | | UniApp 商品详情 API | ❌ **Gap 1 成立** | Hook 不注册则无法触发 seatSpecMap 注入 | **确认**:Gap 1 完全成立。H5 工作是因为 `ticket_detail.html` 是模板文件,在服务器端直接调用 `SeatSkuService::GetGoodsViewData()`。UniApp 走 API 层,`plugins_service_goods_data` Hook 不注册,则 ShopXO 的商品详情 API 不会触发 VR 座位数据注入。 ### 1.4 CartSave extension_data 多座位链路 **状态:✅ H5 已验证,UniApp 只需复刻** | 组件 | 状态 | 说明 | |------|------|------| | `ticket_detail.html:762` 订单提交 | ✅ 已实现 | `extension_data` 嵌套在 `order_base` | | `TicketService::onOrderPaid()` | ✅ 已实现 | 逐行生成票(多座位支持) | | UniApp CartSave | ✅ 与 H5 相同 JSON 格式 | Gap 2 已消除,只要后端提供端点即可 | **Round 2 修正确认**:Gap 2 已验证可工作,核心问题变为"UniApp 需要相同的 API 端点格式"。 --- ## 二、发现问题(Round 3 修正) ### P0(运行时崩溃,直接阻塞) | # | 问题 | 位置 | 严重度 | Round 2 对比 | |---|------|------|--------|-------------| | P0-1 | `SeatSkuService::getSoldSeats()` **方法缺失** | `SeatSkuService.php`(末行为 `buildSeatSpecMap`,无此方法) | **致命** — `Index.php:43` 触发 PHP Fatal Error | 确认:Round 2 误判为"H5 绕过" | | P0-2 | `plugins_service_goods_data` Hook **未注册** | `Hook.php:13-28`(无对应 case) | **致命** — UniApp 商品详情无法获取 seatSpecMap | 与 Round 2 一致 | ### P1(影响扩展性) | # | 问题 | 位置 | 严重度 | Round 2 对比 | |---|------|------|--------|-------------| | P1-1 | `api/Goods.php::seatmap()` 引用不存在的 `SeatMapService` | `shopxo/app/plugins/vr_ticket/api/Goods.php:241` | **高** — 命名混淆,实际方法在 `SeatSkuService::GetGoodsViewData` | 新发现 | | P1-2 | `api/` 目录未纳入 Git 追踪(Worktree) | 工作树 `shopxo/app/plugins/vr_ticket/api/` | **高** — 无法部署 / 无法协作 | 新发现 | | P1-3 | Phase 4 Tree API 代码为零 | 无 `buildTree()` 方法 | **中** — 设计完备,代码未开始 | 与 Round 2 一致 | --- ## 三、技术方案建议 ### P0-1 Fix:`getSoldSeats()` 实现 **文件**:`SeatSkuService.php`(追加到文件末尾) ```php /** * 获取已售座位列表(seatKey 数组) * * 已售座位定义:inventory = 0 的 GoodsSpecBase 记录 * seatKey 格式:roomId_rowLabel_colNum(如 "room_0_A_3") * * @param int $goodsId * @param int $specBaseId 可选,限定查询 * @return string[] */ public static function getSoldSeats(int $goodsId, int $specBaseId = 0): array { if ($goodsId <= 0) return []; $query = \think\facade\Db::name('goods_spec_base') ->where('goods_id', $goodsId) ->where('inventory', 0); if ($specBaseId > 0) { $query->where('id', $specBaseId); } $soldSpecs = $query->select()->toArray(); if (empty($soldSpecs)) return []; // 从 extends.seat_key 提取 $seatKeys = []; foreach ($soldSpecs as $spec) { $extends = json_decode($spec['extends'] ?? '{}', true); $seatKey = $extends['seat_key'] ?? ''; if (!empty($seatKey)) { $seatKeys[] = $seatKey; } } return $seatKeys; } ``` **触发点**:`index/Index.php:43` 现有调用无需修改。 ### P0-2 Fix:`plugins_service_goods_data` Hook 注册 **文件**:`Hook.php:13` 追加 case: ```php case 'plugins_service_goods_data': $goodsId = $params['goods_id'] ?? 0; if ($goodsId > 0) { TicketService::InjectGoodsDetailData($params['data'], $goodsId); } break; ``` **新增方法**:`TicketService.php`(追加到文件末尾): ```php /** * 注入商品详情 VR 票务数据(通过 plugins_service_goods_data Hook) * * @param array &$data ShopXO 商品详情数据(引用传递) * @param int $goodsId */ public static function InjectGoodsDetailData(array &$data, int $goodsId): void { if ($goodsId <= 0) return; $vrConfig = \think\facade\Db::name('goods') ->where('id', $goodsId) ->value('vr_goods_config'); if (empty($vrConfig)) return; $viewData = SeatSkuService::GetGoodsViewData($goodsId); if (empty($viewData['vr_seat_template'])) return; $seatTemplate = $viewData['vr_seat_template']; // seatSpecMap:seat_key → 完整规格(含 row/col/room/section/price) $data['seatSpecMap'] = $viewData['seatSpecMap'] ?? []; // goods_spec_data:场次列表(含价格) $data['goods_spec_data'] = $viewData['goods_spec_data'] ?? []; // specTypeList:5维规格维度定义 $data['specTypeList'] = $viewData['specTypeList'] ?? []; // seatMap:座位图渲染数据 $data['seatMap'] = $seatTemplate['seat_map'] ?? null; // goods_config:当前生效的配置块 $data['goods_config'] = $viewData['goods_config'] ?? null; } ``` ### P1 Fix:命名统一(SeatSkuService vs SeatMapService) | 当前引用 | 应改为 | 位置 | |---------|-------|------| | `use app\plugins\vr_ticket\service\SeatMapService` | `use app\plugins\vr_ticket\service\SeatSkuService` | `shopxo/app/plugins/vr_ticket/api/Goods.php:12` | | `SeatMapService::GetSeatMap` | `SeatSkuService::GetGoodsViewData` | `shopxo/app/plugins/vr_ticket/api/Goods.php:241` | **注意**:此修复应在 `api/` 目录重新纳入 Git 追踪后执行。 --- ## 四、优先级建议 | 优先级 | 任务 | 预计工时 | 收益 | |--------|------|---------|------| | **P0-1** | 实现 `getSoldSeats()` | 30min | 修复 soldSeats API 崩溃 | | **P0-2** | Hook 注册 + `InjectGoodsDetailData()` | 1h | 解锁 UniApp 完整票务链路 | | **P1-A** | `api/` 目录纳入 Git 追踪 + 命名修复 | 30min | 可部署、可协作 | | **P1-B** | Phase 4 Tree API 实现(`buildTree()`) | 待定 | Phase 4 功能完成 | **决策路径**: 1. 先修 P0-1 + P0-2(2h 内可完成) 2. 再推进 `api/` 目录规范化 3. Phase 4 在 P0/P1 稳定后作为独立任务启动 --- ## 五、投票(Round 3) **议题:下一步主攻方向** **投票:A — 后端优先** **理由**: 1. **最小改动最大收益**:`getSoldSeats()` 实现(30 行)+ Hook 注册(10 行)= 约 40 行代码,修复后 UniApp 票务链路全部解锁。这是最低成本的最高收益修复。 2. **运行时崩溃是根本阻塞**:Round 2 的"H5 已绕过"分析正确,但忽略了 `Index.php:soldSeats` 对所有直接调用 API 的客户端(UniApp / 第三方)都会触发 PHP 致命错误。必须修复。 3. **Hook 注册是 UniApp 的唯一入口**:Gap 1 对 H5 无影响(H5 走模板层直接调用),但对 UniApp 来说是唯一入口。没有 Hook 注册,UniApp 永远无法通过标准 API 获取 seatSpecMap。 4. **Gap 2(CartSave)已消除**:Round 2 确认 H5 已验证 extension_data 链路正确,UniApp 只需复刻同样的 JSON 结构,不存在后端缺口。 5. **Phase 4 不应前置**:Tree API 是体验增强,不是购买流程的基础设施,在核心票务链路(P0)未稳定前启动 Phase 4 是资源浪费。 **补充:对其他提案的评估** - **B(前端优先)**:不可行。UniApp 选座组件需要 seatSpecMap 数据,但 Gap 1 不修则数据不可得。前端等 Hook 注册后再开发效率更高。 - **C(双线并行)**:在 P0 明确可修复的前提下,"双线并行"是浪费:前端等待期间无所事事,不如后端一次性修完再解锁前端。 - **D(Phase 4 优先)**:Phase 4 是锦上添花,不是基础设施。Tree API 失败不影响用户购票核心流程。 --- *报告人:BackendArchitect | 2026-05-26 | Round 3*