Compare commits

...

7 Commits

Author SHA1 Message Date
Council 58b0d0f8fd council(draft): BackendArchitect - Round 4 现场核查修正 + 投票A
- 确认 SeatMapService::GetSeatMap() 完整存在(333行),无崩溃
- 确认 Index.php 无 soldSeats action,Round 3 P0 误判已修正
- 确认 Hook plugins_service_goods_data 未注册(Gap 1 仍成立)
- 确认 Gap 2(CartSave)已消除
- 投票 A:后端优先,Hook 注册解锁 UniApp 链路

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:29:42 +08:00
Council 7234660efe council(draft): BackendArchitect - Round 3 评估报告 + 投票A
- 新增 P0 运行时崩溃发现:Index.php:43 调用不存在的 getSoldSeats()
- 修正 P0-1 优先级:getSoldSeats() 缺失是 PHP Fatal Error 而非"H5绕过"
- 确认 Gap 2 已消除:CartSave extension_data H5 已验证
- 新发现:api/ 目录命名混淆(SeatMapService vs SeatSkuService)
- 投票:A — 后端优先(2h 内修 P0-1 + P0-2)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:28:00 +08:00
Council 946d53e6a6 council(draft): BackendArchitect - merge plan conflict resolved, Round 2 assessment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:16:16 +08:00
Council d7fca62d14 council(draft): BackendArchitect - Round 2 technical assessment report
- Phase 4 Tree API: design exists but zero implementation code
- SeatMapService: getSoldSeats() missing (API broken)
- seatSpecMap injection: Hook not registered (P0 gap)
- CartSave extension_data: no multi-seat flow (P0 gap)
- Vote: C (parallel execution)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:15:43 +08:00
Council 470ffdeec0 council(draft): BackendArchitect - Phase 2 technical assessment findings + merged council report
Key findings:
- Buy::Index() POST/GET dual-path: submit() uses GET (fails) instead of POST
- Field name mismatch: goods_params vs goods_data
- ShopXO spec matches by type:value, not spec_base_id direct
- Recommend: hidden form POST, plugin API for sold-seats

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 08:41:26 +08:00
Council e5736338bd council(draft): BackendArchitect - plan for Phase 2 technical assessment
- Add plan for VR ticket P0-P2 issue evaluation
- Tasks: B1-B5 (BackendArchitect), F1-F4 (FrontendDev), P1-P3 (FirstPrinciples)
- Output: reviews/council-phase2-assessment.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 08:32:25 +08:00
Council bed933e8df council(draft): BackendArchitect - plan for frontend template research Q1-Q4
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 23:10:43 +08:00
4 changed files with 694 additions and 81 deletions

View File

@ -0,0 +1,179 @@
# Council 评估报告 — BackendArchitectRound 4 现场核查)
> 评估日期2026-05-26 | 角色:后端架构师 | Git: `0d6d20062` → 提交中
---
## 一、现状评估Round 4 现场核查)
### 1.1 Phase 4 Tree API 设计
**状态:📋 设计文档存在,代码为零**
| 组件 | 状态 | 说明 |
|------|------|------|
| `docs/PHASE_4_API.md` | ✅ 存在 | Tree API 设计文档 |
| `docs/PLAN_TREE_API_IMPLEMENTATION.md` | ✅ 存在 | 实现计划 |
| `SeatMapService.php`(服务类) | ✅ **存在且完整** | 333行`GetSeatMap()` + `buildSeatSpecMap()` + `buildGoodsSpecData()` |
| `api/Goods.php::seatmap()` | ✅ **存在且正确** | 第241行调用 `SeatMapService::GetSeatMap($goodsId)` |
| `SeatSkuService.php` | ✅ 存在 | 独立服务,含 `BatchGenerate()` + `GetGoodsViewData()` + `buildSeatSpecMap()` |
| Tree API `buildTree()` | ❌ 代码为零 | Phase 4 设计中的核心方法未实现 |
**Round 4 修正**:设计文档中提到的 `SeatMapService` 类**在父仓库已存在且完整**`api/Goods.php::seatmap()` 路由已正确调用它。Round 1-3 的"P0 崩溃"分析是误判。
### 1.2 SeatMapService + seatmap API
**状态:✅ 已完整实现**
| 组件 | 状态 | 位置 |
|------|------|------|
| `SeatMapService.php` | ✅ **完整** | 333行`GetSeatMap()` + 缓存 + `buildSeatSpecMap()` + `buildGoodsSpecData()` |
| `api/Goods.php::seatmap()` | ✅ **正确** | 第233-246行路由注册正常调用 `SeatMapService::GetSeatMap()` |
| `SeatSkuService::buildSeatSpecMap()` | ✅ 存在 | 第533行私有方法 |
| `SeatSkuService::GetGoodsViewData()` | ✅ 存在 | 第370行H5 模板专用) |
| `SeatSkuService::getSoldSeats()` | ⚠️ 方法不存在 | `GetSeatMap()` 已含库存信息,可替代 |
| `index/Index.php::soldSeats` | ❌ **不存在** | `Index.php` 只有 `wallet()` 方法,无 `soldSeats` |
**Round 4 修正**
- Round 3 报告称"Index.php:43 调用 getSoldSeats()"——**这是误判**。`Index.php` 只有 `wallet()` 方法,无 `soldSeats` action。
- `SeatMapService::GetSeatMap()` 已完整实现,含实时 `inventory` 字段0=已售),可替代 `getSoldSeats()`
- **无运行时崩溃**seatmap API 工作正常。
### 1.3 seatSpecMap 注入商品详情 API
**状态:⚠️ Gap 1 成立,但有变通方案**
| 组件 | 状态 | 说明 |
|------|------|------|
| `SeatSkuService::GetGoodsViewData()` | ✅ 存在 | 第370行H5 模板专用 |
| `Hook.php::plugins_service_goods_data` | ❌ **未注册** | Hook.php 无此 case |
| `api/Goods.php::detail()` | ⚠️ **不包含 seatSpecMap** | 第278-299行formatGoodsDetail 不注入 VR 数据 |
| H5 `ticket_detail.html` | ✅ **工作正常** | 直接调用 `GetGoodsViewData()` |
| UniApp `api/goods/detail` | ❌ **Gap 1 成立** | 无 Hook 注入,无 VR 数据 |
**Gap 1 分析修正**
- **Gap 1 对 UniApp 仍然成立**Hooks 未注册)
- 但 `api/Goods.php::seatmap()`第233行已完整提供 seatSpecMap + goods_spec_data
- **UniApp 可以绕过 Gap 1**:先调用 `/seatmap` API 获取座位图,再调用标准 `/detail` API 获取商品基础信息
- **最优解仍为 Hook 注册**:减少前端调用次数(一次 `/detail` 获取所有数据)
### 1.4 CartSave extension_data 多座位链路
**状态:✅ H5 已验证,后端无需改动**
| 组件 | 状态 | 说明 |
|------|------|------|
| `ticket_detail.html:762` 订单提交 | ✅ 已实现 | `extension_data` 嵌套在 `order_base` |
| `TicketService::onOrderPaid()` | ✅ 已实现 | 逐行生成票(多座位支持) |
| Gap 2 状态 | ✅ **已消除** | 后端链路完整UniApp 复刻 JSON 格式即可 |
---
## 二、发现问题Round 4 修正)
### P0重新评估
| # | 问题 | 严重度 | Round 3 对比 |
|---|------|--------|-------------|
| P0-1 `getSoldSeats()` 方法缺失 | ❌ **已消除** | `SeatMapService::GetSeatMap()` 已含库存,`Index.php` 无 soldSeats action |
| P0-2 `plugins_service_goods_data` Hook 未注册 | ⚠️ **降级为 P1** | Gap 1 成立,但 UniApp 可用 `/seatmap` 变通绕过 |
| P0-3 `Index.php:soldSeats` 触发 Fatal Error | ❌ **已消除** | Index.php 无 soldSeats action无崩溃 |
**重新分类**
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| P1-A | `api/Goods.php::detail()` 不包含 seatSpecMap | **高** | UniApp `/detail` API 缺少 VR 数据注入 |
| P1-B | `plugins_service_goods_data` Hook 未注册 | **中** | UniApp detail API 最佳入口缺失 |
| P2-A | Phase 4 Tree API `buildTree()` 未实现 | **中** | 设计完整,代码为零 |
| P2-B | `api/Goods.php::seatmap()` 命名不一致 | **低** | seatmap vs seatMap大小写 |
---
## 三、技术方案建议
### 方案 A推荐Hook 注册(最小改动)
**文件**`Hook.php` 追加 case
```php
case 'plugins_service_goods_data':
$goodsId = $params['goods_id'] ?? 0;
if ($goodsId > 0) {
TicketService::InjectGoodsDetailData($params['data'], $goodsId);
}
break;
```
**新增方法**`TicketService.php`
```php
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['seatSpecMap'])) return;
$data['seatSpecMap'] = $viewData['seatSpecMap'];
$data['goods_spec_data'] = $viewData['goods_spec_data'];
$data['specTypeList'] = $viewData['specTypeList'] ?? [];
$data['seatMap'] = $viewData['vr_seat_template']['seat_map'] ?? null;
$data['goods_config'] = $viewData['goods_config'] ?? null;
}
```
**代码量**:约 30 行。效果UniApp 调用 ShopXO 标准 `/goods/detail` API 时自动获得 VR 数据。
### 方案 B备选UniApp 变通绕过(无需后端改动)
UniApp 端可在调用商品详情后,再调用 `/seatmap` API 补充 VR 数据。
- **优点**:无需后端改动,立即可用
- **缺点**:前端多一次 API 调用(可接受)
### 方案 CPhase 4 完整实现(独立任务)
`buildTree()` 实现 + Tree VR 体验,作为 Phase 4 独立里程碑。
---
## 四、优先级建议
| 优先级 | 任务 | 预计工时 | 收益 |
|--------|------|---------|------|
| **P1-A** | Hook 注册 + `InjectGoodsDetailData()` | 30min | 解锁 UniApp 完整票务链路 |
| **P1-B** | `api/Goods.php::detail()` 命名规范化 | 10min | API 契约一致性 |
| **P2** | Phase 4 Tree API 实现 | 待定 | Tree VR 体验 |
| **P3** | Phase 4 完整 Tree 体验 | 待定 | VR 差异化功能 |
---
## 五、投票Round 4
**议题:下一步主攻方向**
**投票A — 后端优先**
**理由**
1. **Hook 注册是最低成本最高收益**:约 30 行代码,一次性解决 UniApp 商品详情 API 的 VR 数据注入问题。无需前端变通,减少 API 调用次数。
2. **Round 4 重新评估确认**:后端 seatmap API 已完整实现P0-1/P0-3 误判已消除),核心剩余问题是 Hook 注入这一处。
3. **Gap 2 已消除**后端票务链路CartSave → onOrderPaid → 票生成)已完整,多座位支持已验证。
4. **UniApp 可用方案 B 变通立即推进**:即使 Hook 暂未注册UniApp 仍可通过"先 /seatmap 后 /detail"的方式绕过 Gap 1 立即启动开发。
5. **Phase 4 不应前置**Tree API 是体验增强在核心票务链路P1稳定前启动 Phase 4 资源浪费。
**补充:对其他提案的评估**
- **B前端优先**可接受——UniApp 确实可以先用方案 B 变通绕过 Gap 1 立即开发。但变通方案不如 Hook 注册简洁。
- **C双线并行**:可接受,但需明确分工。后端修 Hook前端用方案 B 变通同时推进。
- **DPhase 4 优先)**不建议。Phase 4 是锦上添花,不是票务购买的基础设施。
---
*报告人BackendArchitect | 2026-05-26 | Round 4*

149
plan.md
View File

@ -1,109 +1,96 @@
# Plan — 调研「场馆删除后编辑商品出现规格重复错误」问题
# Plan — VR 演唱会票务小程序 Round 4 执行
> 版本v1.3 | 日期2026-04-20 | Agentcouncil/FrontendDev + council/SecurityEngineer + council/BackendArchitect
> 版本v4.0 | 日期2026-05-26 | Agentcouncil/BackendArchitect
> 任务Round 4 现场核查 — 修正误判,投票 A
---
## BackendArchitectTask B1-B6
## 评估范围
当票务商品关联的场馆模板被硬删除后,编辑商品时出现「规格不允许重复」错误。
**根因调查分工**
- FrontendDev前端规格项构建与 fallback 行为
- BackendArchitect后端规格去重逻辑、`spec_base_id_map` 解析
- SecurityEngineer安全风险评估P1 vs P2
- Phase 4 Tree API 设计文档完整性 + 可行性
- SeatMapService + `/seatmap` API 完整性
- seatSpecMap 注入商品详情 API 的实现方案
- CartSave extension_data 多座位存储链路
- 后端下一步优先级建议
---
## FrontendDev 任务清单
## Round 4 现场核查结论
- [x] [Done: council/FrontendDev] **Task 1**: 读取 `ticket_detail.html`,分析前端构建规格项的过程
- [x] [Done: council/FrontendDev] **Task 2**: 当模板不存在时,前端如何处理 `template_snapshot``spec_base_id_map`
- [x] [Done: council/FrontendDev] **Task 3**: `loadSoldSeats()` 函数实际实现了吗soldSeats 数据如何填充?
- [x] [Done: council/FrontendDev] **Task 4**: 编辑模式下(已有 vr_goods_config前端是否正确处理已删除场馆的旧规格
- [x] [Done: council/FrontendDev] **Task 5**: 给出前端根因分析(含具体文件路径和行号)
- [x] [Done: council/FrontendDev] **Task 6**: 给出修复方案
- [x] [Done: council/FrontendDev] **Task 7**: 将调研报告写入 `reviews/council-ghost-spec-FrontendDev.md`
### Phase 4 Tree API
- 设计文档:✅ `docs/PHASE_4_API.md` + `PLAN_TREE_API_IMPLEMENTATION.md`
- `SeatMapService.php`(服务类):✅ **333行完整实现**,含 `GetSeatMap()` + `buildSeatSpecMap()` + `buildGoodsSpecData()`
- `api/Goods.php::seatmap()`:✅ **正确实现**第241行调用 `SeatMapService::GetSeatMap($goodsId)`
- Tree API `buildTree()`:❌ **代码为零**Phase 4 尚未开始)
### SeatMapService + seatmap API
- `SeatMapService::GetSeatMap()`:✅ **完整**,含实时 inventory + 缓存
- `api/Goods.php::seatmap()`:✅ **正确**UniApp 调用无崩溃
- `index/Index.php::soldSeats`:❌ **Index.php 无此 action**Round 3 误判已修正)
- `SeatSkuService::getSoldSeats()`:⚠️ 方法不存在,但被替代(`GetSeatMap()` 已含库存)
- **结论无运行时崩溃seatmap API 工作正常**
### seatSpecMap 注入
- Hook `plugins_service_goods_data`:❌ **未注册**Gap 1 仍成立)
- `api/Goods.php::detail()`:❌ 不包含 VR 数据
- H5 `ticket_detail.html`:✅ **工作正常**(直接调用 `GetGoodsViewData()`
- UniApp detail API**Gap 1 成立**,但 `/seatmap` API 可变通绕过
- **结论Gap 1 成立UniApp 可先调用 `/seatmap` 绕过**
### CartSave extension_data
- `ticket_detail.html:762`:✅ 已实现,`extension_data` 嵌套在 `order_base`
- `TicketService::onOrderPaid()`:✅ 已实现,多座位支持
- **结论Gap 2 已消除,后端无需改动**
---
## SecurityEngineer 任务清单
## BackendArchitect 评估任务
- [x] [Done: council/SecurityEngineer] **Task S1**: 读取 AdminGoodsSaveHandle.php — 安全审计:保存时是否拒绝脏数据
- [x] [Done: council/SecurityEngineer] **Task S2**: 读取 SeatSkuService.php — 幽灵 spec 注入路径分析
- [x] [Done: council/SecurityEngineer] **Task S3**: 读取 AdminGoodsSave.php — ShopXO 入口安全检查
- [x] [Done: council/SecurityEngineer] **Task S4**: 输出安全审计报告 → `reviews/SecurityEngineer-GHOST_SPEC_SECURITY.md`
- [x] [Done: council/SecurityEngineer] **Task S5**: 更新 `reviews/council-ghost-spec-summary.md`
### 优先级定义
| 级别 | 含义 |
|------|------|
| **P1** | 安全漏洞脏数据注入、XSS、权限绕过、数据覆盖 |
| **P2** | 功能缺陷:用户体验问题、错误提示不友好 |
| **P3** | 改进建议:代码健壮性优化 |
---
## BackendArchitect 任务清单
- [x] [Done: council/BackendArchitect] **Task B1**: AdminGoodsSaveHandle.php 全链路追踪 — vr_goods_config 读取/解析/snapshot 重建
- [x] [Done: council/BackendArchitect] **Task B2**: spec_base_id_map 如何被转换成规格项(已验证:存储在模板表,与幽灵 spec 无关)
- [x] [Done: council/BackendArchitect] **Task B3**: SeatSkuService GetGoodsViewData 模板不存在时的 fallback单模板处理多模板有缺陷
- [x] [Done: council/BackendArchitect] **Task B4**: 幽灵 spec 产生环节 + 清理时机(保存时未清理,写回 DB
- [x] [Done: council/BackendArchitect] **Task B5**: 商品保存规格去重逻辑GoodsService.php:1859
- [x] [Done: council/BackendArchitect] **Task B6**: 根因分析报告(含行号)→ `reviews/council-ghost-spec-BackendArchitect.md`
- [x] [Done: council/BackendArchitect] **Task B7**: 将调研报告写入 `reviews/council-ghost-spec-BackendArchitect.md`
---
## 阶段划分 ✅
| 阶段 | 内容 | 状态 |
| Task | 内容 | 状态 |
|------|------|------|
| **Draft** | Task 1-7FrontendDev+ Task S1-S3 + Task B1-B6并行| ✅ 完成 |
| **Review** | Task 7 + Task S4 + Task B7输出各自报告| ✅ 完成 |
| **Finalize** | Task S5汇总到 `reviews/council-ghost-spec-summary.md` | ✅ 完成 |
| B1 | Phase 4 Tree API 设计文档评估 | [Done: council/BackendArchitect] — 设计完整,代码为零 |
| B2 | SeatMapService + seatmap API 完整性检查 | [Done: council/BackendArchitect] — Round 3 误判已修正API 完整 |
| B3 | seatSpecMap 注入方案设计 | [Done: council/BackendArchitect] — Hook 注册方案已明确 |
| B4 | CartSave extension_data 多座位链路分析 | [Done: council/BackendArchitect] — Gap 2 已消除 |
| B5 | 输出 Round 4 评估报告 + 投票 | [Done: council/BackendArchitect] |
---
## 根因结论
## P0 修正Round 4
| 优先级 | 根因 | 文件:行号 |
|--------|------|-----------|
| **P1功能** | 无效 config 块未从数组移除,`continue` 后脏数据写回 DB | AdminGoodsSaveHandle.php:88-89 + 148-150 |
| **P2** | GetGoodsViewData 单模板模式,多模板时覆盖有效块 | SeatSkuService.php:368 + 386-388 |
| **P3** | BatchGenerate 对无效 template_id 返回 code=-2阻断保存 | AdminGoodsSaveHandle.php:164-170 |
| **P4** | 前端过滤后 configs 为空时用户无声失去配置 | AdminGoodsSave.php:196-229 |
| **P5** | loadSoldSeats 未实现TODO 注释) | ticket_detail.html:375-383 |
| **安全评估** | 无 P1 安全漏洞,属于 P2 功能缺陷 | SecurityEngineer-GHOST_SPEC_SECURITY.md |
| 原问题 | Round 3 状态 | Round 4 修正 |
|--------|-------------|-------------|
| P0-1 `getSoldSeats()` 方法缺失 | 致命 | ❌ **已消除** — `GetSeatMap()` 已含库存,无崩溃 |
| P0-2 Hook `plugins_service_goods_data` 未注册 | 致命 | ⚠️ **降级 P1** — UniApp 可用 `/seatmap` 变通绕过 |
| P0-3 `Index.php:soldSeats` 触发 Fatal Error | 致命 | ❌ **已消除** — Index.php 无 soldSeats action |
---
## 关键文件
## 最终优先级
| 文件 | 关注点 |
|------|--------|
| `shopxo/app/plugins/vr_ticket/hook/AdminGoodsSaveHandle.php` | P1 根因continue 不删除脏 config |
| `shopxo/app/plugins/vr_ticket/service/SeatSkuService.php` | GetGoodsViewDataP2 根因,多模板处理缺陷 |
| `shopxo/app/plugins/vr_ticket/hook/AdminGoodsSave.php` | 前端过滤逻辑P4 体验问题 |
| `shopxo/app/plugins/vr_ticket/admin/Admin.php` | VenueDelete硬删除逻辑第 888 行) |
| `shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html` | loadSoldSeats 未实现P5 |
| `shopxo/app/service/GoodsService.php` | 规格列值去重检测(第 1859 行) |
| 优先级 | 任务 | 预计工时 | 收益 |
|--------|------|---------|------|
| **P1-A** | Hook 注册 + `InjectGoodsDetailData()` | 30min | 解锁 UniApp 完整票务链路 |
| **P1-B** | `api/Goods.php::detail()` 注入 VR 数据 | 20min | 与 Hook 注册二选一 |
| **P2** | Phase 4 Tree API `buildTree()` | 待定 | Tree VR 体验 |
| **P3** | Phase 4 完整 Tree 体验 | 待定 | VR 差异化功能 |
---
## 修复方案
## 投票结果
### P1 Fix立即实施
1. AdminGoodsSaveHandle.php:88 — `continue` 改为 `unset($configs[$i])`
2. AdminGoodsSaveHandle.php:145 后 — 添加 `$configs = array_values($configs);`
3. AdminGoodsSaveHandle.php:148 — 写回前加 `if (!empty($configs))`
4. AdminGoodsSaveHandle.php:158-173 — BatchGenerate 前增加模板存在性显式校验
**投票A — 后端优先**
### P2 Fix高优先级
1. SeatSkuService.php GetGoodsViewData — 遍历所有有效配置块,不只处理 `$vrGoodsConfig[0]`
2. 修改 DB 写回逻辑为写回 `validConfigs` 而非 `[$config]`
理由:
1. Hook 注册约 30 行代码,解决 Gap 1解锁 UniApp 完整票务链路
2. Round 4 确认seatmap API 已完整,无运行时崩溃
3. Gap 2 已消除,后端链路完整
4. UniApp 可用方案 B先 /seatmap 后 /detail立即变通绕过 Gap 1
5. Phase 4 是体验增强,不应作为主攻方向
### P3 Fix中优先级
1. AdminGoodsSave.php — configs 为空时提示用户重新选择场馆
---
## 输出
- 评估报告Round 4 更新版):`docs/council-eval-backendarchitect.md`
- 投票:`docs/council-eval-backendarchitect.md#五投票

View File

@ -0,0 +1,229 @@
# BackendArchitect Phase 2 技术评估 Findings
> Agent: council/BackendArchitect | Date: 2026-04-21 | Round 2
---
## B1: GoodsCartService::Save API 契约分析
### 结论:`Save()` 方法未找到,但找到了真正的下单入口
**关键发现**ShopXO 的票务下单流程 **不经过购物车**,而是直接 POST 到 `index/buy/index`
```php
// Buy.php Index() — 真正的入口
public function Index()
{
if($this->data_post)
{
// 将数据存储到缓存,以 user_id 为 key
BuyService::BuyDataStorage($this->user['id'], $this->data_post);
return MyRedirect(MyUrl('index/buy/index'));
}
// 读取缓存,展示订单确认页
$buy_data = BuyService::BuyDataRead($this->user['id']);
}
```
**真正接收的数据结构**(来自 `BuyService::BuyInitialize`
```php
// BuyService.php ~line 51 — 参数契约
$p = [
[
'checked_type' => 'empty',
'key_name' => 'goods_data', // ← 核心字段
'error_msg' => MyLang('common_service.buy.buy_goods_data_error_tips'),
],
];
// goods_data 格式:
// [{goods_id, spec, stock, ...}]
// 或从 base64 解码json_decode(base64_decode(urldecode($params['goods_data'])), true)
```
### BuyService::BuyInitialize 处理流程
```php
foreach($params['goods_data'] as $v) {
// 1. 规格解析 — GoodsSpecificationsHandle()
// 期望: {goods_id, spec: [{type, value}], stock, extension_data?, ...}
$goods['spec'] = self::GoodsSpecificationsHandle($v);
// 2. 调用 GoodsService::GoodsSpecDetail(spec: [{type, value}])
// ← 关键:这里通过 spec.value 匹配 GoodsSpecValue 表,而不是 spec_base_id
// 如果 spec 为空但商品有多规格,必须报错
// 如果 spec 不为空但商品无规格,也必须报错
// 3. 从返回的 spec_base 获取 inventory, price, spec_base_id
$goods['inventory'] = $goods_base['data']['spec_base']['inventory'];
$goods['price'] = $goods_base['data']['spec_base']['price'];
$goods['spec_base_id'] = $goods_base['data']['spec_base']['id'];
}
```
### 结论B1
**ShopXO 的 spec 匹配机制是 `type:value` 匹配,不是 `spec_base_id` 直接传递。**
`GoodsSpecDetail` 内部逻辑:
1. 从 `params['spec']` 提取 `value` 数组 → `spec = array_column($params['spec'], 'value')`
2. `WHERE goods_id=X AND value IN (...)` 查询 `GoodsSpecValue` 表 → 得到 `goods_spec_base_id`
3. 从 `GoodsSpecBase` 读取最终规格记录
---
## B2: ticket_detail.html submit() 参数校验
### 当前代码submit 函数)
```javascript
var goodsParamsList = this.selectedSeats.map(function(seat, i) {
var specBaseId = self.specBaseIdMap[seat.seatKey] || self.sessionSpecId;
return {
goods_id: self.goodsId,
spec_base_id: parseInt(specBaseId) || 0, // ← 直接传 ID
stock: 1,
extension_data: JSON.stringify({...})
};
});
// 重定向到 checkoutUrl:
var checkoutUrl = this.requestUrl + '?s=index/buy/index' +
'&goods_params=' + encodeURIComponent(goodsParams); // ← 拼到 URL
```
**问题 1严重**`goods_params` 是 URL 参数,而不是 POST body。
- ShopXO `Buy::Index()` 通过 `$this->data_post` 判断是否 POST
- URL 参数在 `$_GET`,不在 `$_POST`,所以 `$this->data_post` 可能是 `false`
- 应该用 `<form method="POST">` 提交,或者用 JS `fetch('/?s=index/buy/index', {method:'POST', body: JSON.stringify(...)})`
- 当前的重定向方式 `$location.href = checkoutUrl` → GET 请求,无法触发 POST 分支
**问题 2中等**`BuyService::BuyInitialize` 期望的字段是 `goods_data`,不是 `goods_params`
**问题 3严重**`BuyInitialize` 期望的 `spec` 格式是 `[{type, value}]`,不是 `spec_base_id`
- 当前代码直接传 `spec_base_id`,不经过 ShopXO 的规格匹配逻辑
- ShopXO 会调用 `GoodsSpecDetail({id, spec: [{type, value}]})`,通过 `value` 匹配规格
- 如果 `specBaseIdMap` 存储的是规格值而非 `{type, value}` 对象,则不兼容
**问题 4中等**`extension_data` 不是标准字段ShopXO 的订单系统不会处理。
---
## B3: ShopXO spec 加载标准端点
### 关键端点GoodsService::GoodsSpecDetail
**参数**
```php
[
'id' => goods_id,
'spec' => [ // ← 必须是 [{type, value}] 格式
['type' => '场次', 'value' => '2026-04-21 19:00'],
['type' => '座位区', 'value' => 'A区'],
],
'stock' => 1 // 可选,数量
]
```
**返回**
```json
{
"code": 0,
"data": {
"spec_base": {
"id": 2001,
"price": 599.00,
"inventory": 50,
"original_price": 799.00,
"buy_min_number": 1,
"buy_max_number": 5
}
}
}
```
### spec 加载链路
1. **直接调用**(后端 PHP`GoodsService::GoodsSpecDetail(['id'=>X, 'spec'=>[...]])`
2. **前端 AJAX**ShopXO 有 API 端点 `api/goods/spec-detail`(需验证)
3. **ShopXO 标准流程**:用户选择规格 → 前端拼 `spec=[{type:'场次',value:'...'}]` → 提交 `goods_data`
### spec 数据来源
`$goods_spec_data``SeatSkuService::GetGoodsViewData()` 传入前端PHP 渲染):
```php
// ticket_detail.html 顶部
var specData = <?php echo json_encode($goods_spec_data ?? []); ?> || [];
// specData[0]: {spec_id, spec_name, price, ...}
```
---
## B4: ticket_detail.html 加载真实库存的方案
### 方案对比
| 方案 | 复杂度 | 实时性 | 风险 |
|------|--------|--------|------|
| A. 直接调用插件 APIAJAX | 低 | 高 | 需新增插件端点 `/api/vr-ticket/sold-seats` |
| B. ShopXO 标准 spec 加载流程 | 中 | 高 | 需理解 ShopXO 规格匹配机制 |
| C. PHP 后端预渲染(当前) | 低 | 低 | 页面加载时已固定 |
### 推荐方案(最小实现)
**插件新增 API 端点**
```
GET /?s=api/vr-ticket/sold-seats&goods_id=X&spec_base_id=Y
Response: {sold_seats: ["A_1", "A_2", "B_5"]}
```
前端在选中场次后调用此接口,标记已售座位。
### 关于 ShopXO spec 机制的说明
VR 票务的 `spec_base_id_map` 存储的是每个座位对应的 `GoodsSpecBase.id`。但 ShopXO 的 `GoodsSpecDetail` 是通过 `{type, value}` 匹配规格的,不是直接接受 `spec_base_id`
**这意味着**:如果 VR 票务已经生成了 `GoodsSpecBase` 记录,`GoodsSpecDetail` 可以通过 `spec=[{type:'座位', value:'A_1'}]` 来查询,但更直接的方式是让插件自己维护座位→规格的映射,并提供独立的 API。
---
## B5: 根因总结
### Issue 1P0— 购物车提交格式错误
**根因**submit() 把 `goods_params` 拼到 URLGET`Buy::Index()` 只在 `$this->data_post` 时处理数据 → POST 分支永远不触发。
**其次**`BuyService::BuyInitialize` 期望 `goods_data` 字段,且 `spec` 必须是 `[{type, value}]` 格式,而不是 `spec_base_id`
**修复方案(后端)**
1. 新增插件端点 `index/buy/index``api/vr-ticket/buy-direct`,专门处理 VR 票务的 POST 提交
2. 或者修改 submit() 为表单 POST 提交,但需处理 ShopXO 的 CSRF 保护
### Issue 2P1— 缩放时舞台不跟随
**根因**`.vr-stage` 在 `.vr-seat-rows` 容器外CSS `transform: scale()` 只作用于容器内子元素。
**修复方案(前端)**:将 `.vr-stage` 移入 `.vr-seat-rows` 容器,或创建共享的 zoom wrapper详见 FrontendDev findings
### Issue 3P1— spec 加载问题
**根因**ShopXO 的规格匹配通过 `spec.value` 字符串匹配,而非直接接受 `spec_base_id`。VR 票务场景下,每个座位对应独立的 `GoodsSpecBase`ShopXO 标准流程需要为每个座位生成 ShopXO 规格记录。
**修复方案**:插件需要维护座位→规格映射,并在选中场次后通过 AJAX 加载已售座位数据(新增插件 API
### Issue 4P2— 商品详情/图片加载
**根因**ShopXO 商品详情页通过 `Goods.php``Index()` 方法加载,`$goods['content_web']` 等字段由 ShopXO 处理。
**修复方案**:需要确认 ticket_detail.html 是否需要 ShopXO 的商品内容渲染,如果需要,应该在插件模板中引入 ShopXO 的商品内容组件。
---
## 推荐的修复优先级
1. **P0立即修复**Issue 1 — submit() 的 GET→POST 问题,导致下单无法工作
2. **P1**Issue 2 — 舞台缩放视觉问题
3. **P1**Issue 3 — spec 加载/已售座位显示
4. **P2**Issue 4 — 商品详情(可延后)
---
*BackendArchitect findings — Round 2 完成*

View File

@ -0,0 +1,218 @@
# Council Phase 2 技术评估报告
> 协作产出 | 日期2026-04-21
> 参与 AgentBackendArchitect、FrontendDev、FirstPrinciples
---
## 问题总览
| # | 问题 | 优先级 | 根因分类 |
|---|------|--------|---------|
| 1 | 购物车提交格式错误 | P0 | API 传递方式 + 参数契约不匹配 |
| 2 | 缩放时舞台元素不跟随 | P1 | DOM 结构导致 CSS transform 不共享 |
| 3 | spec 加载问题(已回滚) | P1 | ShopXO 规格匹配机制 + API 链路不明确 |
| 4 | 商品详情/图片加载 | P2 | 模板未引入 ShopXO 商品内容组件 |
---
## Issue 1P0购物车提交格式错误
### 根因分析(三层)
**第一层(致命)**`location.href` 产生 GET 请求,但 `Buy::Index()` 只在 `$this->data_post` 时处理下单逻辑。
```php
// Buy.php:58-61
public function Index()
{
if($this->data_post) {
BuyService::BuyDataStorage($this->user['id'], $this->data_post);
return MyRedirect(MyUrl('index/buy/index'));
} else {
// GET 分支:从 session 读取URL 参数从未被读取
$buy_data = BuyService::BuyDataRead($this->user['id']);
}
}
```
`goods_params` URL 参数从未被读取,`BuyDataStorage` 从未被调用,`BuyDataRead` 返回空 → "商品数据为空"错误。
**第二层(严重)**:字段名不匹配。
- 前端发送:`goods_params`JSON 数组)
- ShopXO 期望:`goods_data`JSON 数组)
**第三层(中等)**:规格匹配机制不兼容。
- 当前:`spec_base_id: parseInt(specBaseId)` — 直接传 ID
- ShopXO`spec: [{type, value}]` — 通过 type:value 字符串匹配 GoodsSpecValue 表
### 推荐修复(前后端)
**前端BackendArchitect + FrontendDev 联合)**
```javascript
// 方案 A隐藏表单 POST最小化变更
submit: function() {
var goods_data = this.selectedSeats.map(function(seat, i) {
return {
goods_id: self.goodsId,
spec: [{type: '座位', value: seat.seatKey}], // ← ShopXO 规格格式
stock: 1,
extension_data: JSON.stringify({attendee: attendeeData[i], seat: {...}})
};
});
var form = document.createElement('form');
form.method = 'POST';
form.action = MyUrl('index/buy/index');
var input = document.createElement('input');
input.name = 'goods_data';
input.value = JSON.stringify(goods_data);
form.appendChild(input);
document.body.appendChild(form);
form.submit();
}
```
**后端BackendArchitect**
- 方案 B推荐在插件中新增 `VrTicketBuy` 控制器,复用 BuyService 链路,但绕过 ShopXO 的 spec 匹配(直接通过 spec_base_id 查询)
- 方案 C`BuyService::BuyInitialize` 中增加插件扩展点,允许 vr_ticket 插件注入自定义的规格处理逻辑
### API 设计建议
当前实现是「用 GET 做 POST 的事」。正确的做法是:
1. 隐藏表单 POST `goods_data``index/buy/index`ShopXO 原生)
2. 或者插件新增端点POST `goods_data``plugins/vr_ticket/buy-direct`
---
## Issue 2P1缩放时舞台元素不跟随
### 根因分析
```html
<div class="vr-seat-map-wrapper">
<div class="vr-stage">舞 台</div> <!-- 舞台wrapper 直接子元素 -->
<div class="vr-seat-rows" id="seatRows"></div> <!-- 座位行 -->
</div>
```
CSS `transform: scale()` 只作用于应用元素的子树。`.vr-stage` 和 `.vr-seat-rows` 是平级,没有共同的 transform 容器。
### 推荐修复FrontendDev
**方案:将舞台和座位行包裹在同一 zoom 容器内**
```html
<div class="vr-seat-map-wrapper">
<div class="vr-zoom-container" id="zoomContainer">
<div class="vr-stage">舞 台</div>
<div class="vr-seat-rows" id="seatRows"></div>
</div>
</div>
```
```css
.vr-zoom-container {
display: flex;
flex-direction: column;
align-items: center;
transform-origin: center top;
transition: transform 0.2s ease;
}
```
JS 缩放时,操作 `#zoomContainer``transform: scale(X)`,舞台和座位同步缩放。
**注意**:舞台的 `border-radius: 50% 50% 0 0 / 20px 20px 0 0` 弧形在缩放后可能变形,需要在 zoom 场景下调整。
---
## Issue 3P1spec 加载问题
### 根因分析
**问题 A**ShopXO 的 `GoodsSpecDetail` 通过 `spec.value` 字符串匹配规格,而非直接接受 `spec_base_id`
```php
// GoodsService.php:2749-2757
$spec = array_column($params['spec'], 'value'); // ['A_1', 'VIP']
$where['value'] = $spec;
$ids = Db::name('GoodsSpecValue')->where($where)->column('goods_spec_base_id');
```
VR 票务场景下,每个座位对应独立的 `GoodsSpecBase` 记录inventory=1。要通过 ShopXO 标准流程加载,需要为每个座位生成 `GoodsSpecValue` 记录type='座位', value='A_1')。
**问题 B**:插件 API 端点未建立,导致前端 `loadSoldSeats()` 是 TODO stub。
### 推荐修复
**后端BackendArchitect**:新增插件 API 端点
```
GET /?s=api/vr-ticket/sold-seats&goods_id=X&spec_base_id=Y
Response: {code: 0, data: {sold_seats: ["A_1", "A_2", "B_5"]}}
```
前端在选中场次后调用此接口,标记已售座位。
**关于 ShopXO spec 机制**:如果 VR 票务已经生成了 `GoodsSpecBase` 记录,最直接的方式是让插件维护座位→规格的映射,并提供独立的已售座位查询 API而不是依赖 ShopXO 的规格匹配流程。
---
## Issue 4P2商品详情/图片加载
### 根因分析
`ticket_detail.html` 是插件独立模板ShopXO 商品的 `content_web` 和图片数据由 `Goods.php Index()` 加载,但插件模板可能未正确引入这些数据。
### 推荐修复
确认 ticket_detail.html 是否需要 ShopXO 商品内容渲染。如果需要,应该在模板中引入 ShopXO 的商品内容组件:
- 商品详情:`$goods['content_web']` 由 GoodsService 处理
- 商品图册:通过 `ResourcesService` 获取
如果票务 UI 不需要 ShopXO 商品内容区(票务详情页有自己的布局),则此问题可降级为「确认不需要」。
---
## 第一性原则视角的关键提醒FirstPrinciples
1. **P0 的真正来源**submit() 的 URL 重定向方式错了,修复后 Buy 链路本身是可用的——不需要重构 spec 系统。
2. **spec_base_id_map 是性能缓存**:不是业务必需。如果 `onOrderPaid` 能通过 seatKey 查询到 spec_base_idmap 可以去掉。保留它是合理的优化,但需要确保同步机制。
3. **购物车对票务无价值**:当前实现已经在用 Buy 链路,不是 Cart 链路。说明直觉上的「绕过购物车」需求其实不存在——只是 submit() 的传递方式错了。
4. **已售座位展示是 P1不是 P0**:真正的 P0 是 `onOrderPaid` 防双售。前端是否实时显示已售状态,是体验优化,不是业务正确性的根本。
5. **多场次 bug**`GetGoodsViewData()` 只返回第一个配置的场次(取 `validConfigs[0]`)。如果一个商品有多个场次配置,只显示第一个——这是潜在的 bug。
6. **最小修复范围**:只需修复 submit() 的传递方式(隐藏表单 POST不需要重构 spec 系统,不需要引入实时已售座位更新(除非 spec 加载方案已实施)。
---
## 修复优先级与分工
| 优先级 | 问题 | 负责方 | 修复说明 |
|--------|------|--------|---------|
| P0 | Issue 1 submit() | BackendArchitect + FrontendDev | 改用隐藏表单 POST复用 Buy 链路 |
| P1 | Issue 2 舞台缩放 | FrontendDev | 新增 zoom wrapper 容器 |
| P1 | Issue 3 spec 加载 | BackendArchitect | 新增插件 API 端点 |
| P2 | Issue 4 商品详情 | 延后 | 确认是否需要 |
---
## 附录ShopXO Buy 链路关键代码索引
| 文件 | 行号 | 说明 |
|------|------|------|
| `Buy.php` | 56-62 | Index() — POST/GET 分支BuyDataStorage/BuyDataRead |
| `BuyService.php` | ~51 | BuyGoods — goods_data 参数校验 + base64 解码 |
| `BuyService.php` | ~173 | GoodsSpecificationsHandle — 规格解析 |
| `BuyService.php` | ~104-109 | GoodsSpecDetail 调用 — 通过 spec.value 匹配 |
| `GoodsService.php` | 2720-2795 | GoodsSpecDetail — type:value 查询 GoodsSpecValue |
| `BuyService.php` | 1932-1936 | BuyDataStorage — session 缓存21600s TTL |
---
*Council Phase 2 技术评估报告 — 由 BackendArchitect、FrontendDev、FirstPrinciples 协作完成*