Merge origin/main into fix/venue-hard-delete-p0 (resolve plan.md + reviews conflict with origin/main)

pull/19/head
Council 2026-04-22 01:06:11 +08:00
commit 8c38484c58
3 changed files with 349 additions and 163 deletions

215
plan.md
View File

@ -1,184 +1,109 @@
<<<<<<< HEAD
# Plan — ShopXO 酷炫前端模板调研
# Plan — 调研「场馆删除后编辑商品出现规格重复错误」问题
> 版本v1.0 | 日期2026-04-20 | Agentcouncil/FirstPrinciples + council/FrontendDev + council/BackendArchitect + council/ProductManager
=======
# Plan — ShopXO 酷炫前端模板实现方案调研
> 版本v1.0 | 日期2026-04-20 | Agentcouncil/ProductManager + council/FrontendDev + council/BackendArchitect + council/FirstPrinciples
>>>>>>> main
> 版本v1.3 | 日期2026-04-20 | Agentcouncil/FrontendDev + council/SecurityEngineer + council/BackendArchitect
---
## 任务概述
## BackendArchitectTask B1-B6
<<<<<<< HEAD
vr-shopxo-plugin 项目 Phase 0/1/2 后台开发已完成,现需调研票务商品详情页(`ticket_detail.html`)的酷炫前端模板实现方案。
当票务商品关联的场馆模板被硬删除后,编辑商品时出现「规格不允许重复」错误。
**4个调研方向**
- Q1ShopXO 自定义模板最佳实践
- Q2单订单多 SKU 支持(多座位选择前提)
- Q3第三方无代码构建服务提示词策略
- Q4uni-app 兼容性技术栈选型
**输出文件**`docs/council-research-output.md`
**根因调查分工**
- FrontendDev前端规格项构建与 fallback 行为
- BackendArchitect后端规格去重逻辑、`spec_base_id_map` 解析
- SecurityEngineer安全风险评估P1 vs P2
---
## 依赖关系分析
## FrontendDev 任务清单
```
Q2多SKU ──→ Q4uni-app选型的下单流程基础
Q3无代码 ──→ 依赖 Q1 的 ShopXO 模板约束
Q1最佳实践 ──→ 基础,供 Q3/Q4 引用
```
**结论**Q1 + Q2 可并行调研Q3 依赖 Q1Q4 依赖 Q2。
- [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`
---
## Agent 任务分工
## SecurityEngineer 任务清单
### Q1 — ShopXO 自定义模板最佳实践
**负责人**council/FrontendDev
**任务清单**
- [ ] [Claimed: council/FrontendDev] **Task Q1-1**: 读取现有 `ticket_detail.html`,分析当前实现状态
- [ ] [Claimed: council/FrontendDev] **Task Q1-2**: 研究 ShopXO view/goods/ 模板机制,原生组件/API 清单
- [ ] [ ] [Claimed: council/FrontendDev] **Task Q1-3**: 前端技术栈选型建议(原生 / Vue CDN / Tailwind / 其他)
- [ ] [ ] [Claimed: council/FrontendDev] **Task Q1-4**: H5 预览与 uni-app 兼容性保障方案
- [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`
### Q2 — 单订单多 SKU 支持
**负责人**council/BackendArchitect
**任务清单**
- [ ] [Claimed: council/BackendArchitect] **Task Q2-1**: 研究 ShopXO 标准订单模型是否支持单订单多 SKU 行项目
- [ ] [ ] [Claimed: council/BackendArchitect] **Task Q2-2**: 分析多 SKU 下单流程触发条件
- [ ] [ ] [Claimed: council/BackendArchitect] **Task Q2-3**: 若不支持,给出最小改动方案
### 优先级定义
### Q3 — 第三方无代码构建提示词策略
**负责人**council/ProductManager
**任务清单**
- [ ] [Claimed: council/ProductManager] **Task Q3-1**: 调研 Google App Build 等无代码服务的能力边界
- [ ] [ ] [Claimed: council/ProductManager] **Task Q3-2**: 设计 ShopXO 模板约束的 prompt 工程策略
- [ ] [ ] [Claimed: council/ProductManager] **Task Q3-3**: 生成代码后处理(集成到 ShopXO 的步骤清单)
### Q4 — uni-app 兼容性技术栈选型
**负责人**council/FirstPrinciples由 BackendArchitect + FrontendDev 提供输入后 FirstPrinciples 汇总)
**前置条件**Task Q2-3 完成
**任务清单**
- [ ] [Claimed: council/FirstPrinciples] **Task Q4-1**: "一套代码双端"方案评估H5 + 微信小程序)
- [ ] [ ] [Claimed: council/FirstPrinciples] **Task Q4-2**: ShopXO H5 模板与 uni-app 项目桥接方案
### FirstPrinciples 最终拍板
**负责人**council/FirstPrinciples
**任务清单**
- [ ] [Claimed: council/FirstPrinciples] **Task FP-1**: 汇总 Q1-Q4 输出,写入 `docs/council-research-output.md`
- [ ] [ ] [Claimed: council/FirstPrinciples] **Task FP-2**: 明确优先级、依赖关系、技术风险
- [ ] [ ] [Claimed: council/FirstPrinciples] **Task FP-3**: 给出"最小可行方案 vs 理想方案"对比
=======
vr-shopxo-plugin 项目推进 Phase 3 前端模板调研,聚焦 4 个方向:
- Q1ShopXO 自定义模板最佳实践
- Q2单订单多 SKU 支持(多座位选择前提)
- Q3第三方无代码构建服务提示词策略
- Q4uni-app 兼容性技术栈选型
**输出目标**`docs/council-research-output.md`
| 级别 | 含义 |
|------|------|
| **P1** | 安全漏洞脏数据注入、XSS、权限绕过、数据覆盖 |
| **P2** | 功能缺陷:用户体验问题、错误提示不友好 |
| **P3** | 改进建议:代码健壮性优化 |
---
## 任务清单
## BackendArchitect 任务清单
### 全体 Round 1规划并行限时 2-3 分钟)
- [ ] [Claimed: council/ProductManager] **Task P1**: ProductManager 创建本 plan.md 并 merge main
- [ ] [ ] **Task F1**: FrontendDev — 分析 `ticket_detail.html` 现有结构,制定 UI 改进方案
- [ ] **Task B1**: BackendArchitect — 分析 ShopXO 订单模型是否支持单订单多 SKU
- [ ] **Task S1**: FirstPrinciples — 拍板 Q2 结论,识别最大技术风险
### 全体 Round 2执行调研
- [ ] [ ] **Task P2**: ProductManager — 综合 Q1/Q3/Q4 结论,输出 `council-research-output.md`
- [ ] **Task F2**: FrontendDev — 输出 H5 模板技术栈选型报告 → `docs/frontend-template-research.md`
- [ ] **Task B2**: BackendArchitect — 输出 ShopXO 多 SKU 调研报告 → `docs/backend-multi-sku-research.md`
- [ ] **Task S2**: FirstPrinciples — 评审所有报告,给出最终拍板结论
### 全体 Round 3收敛
- [ ] [ ] **Task P3**: ProductManager — 整合所有输出到 `council-research-output.md`merge main
- [ ] [ ] 所有 Agent 投票 `[CONSENSUS: YES/NO]`
- [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`
---
## 依赖关系
## 阶段划分 ✅
```
Q2结论 ──→ Q4是否能做多座位选择
Q1结论 ──→ Q3/Q4技术栈基础
Q3/Q4 ──→ 最小可行方案 vs 理想方案
```
**关键风险**Q2多SKU结论将直接影响 Q4 多座位选择能否落地。
>>>>>>> main
---
## 阶段划分
| 阶段 | 状态 | 说明 |
| 阶段 | 内容 | 状态 |
|------|------|------|
<<<<<<< HEAD
| **Draft** | 🔄 进行中 | 各 Agent 调研并提交各自方向报告 |
| **Review** | ⬜ 待开始 | 各 Agent 交叉评审FirstPrinciples 汇总 |
| **Finalize** | ⬜ 待开始 | 输出 council-research-output.md |
=======
| **Draft** | 🔄 进行中 | Round 1 — 各 Agent 并行规划 |
| **Research** | ⬜ 待开始 | Round 2 — 执行调研 |
| **Finalize** | ⬜ 待开始 | Round 3 — 收敛共识 |
>>>>>>> main
| **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` | ✅ 完成 |
---
## 输出文件
## 根因结论
<<<<<<< HEAD
| 文件 | 内容 | 负责人 |
|------|------|--------|
| `docs/Q1-frontend-research.md` | ShopXO 自定义模板最佳实践 | FrontendDev |
| `docs/Q2-multisku-research.md` | 单订单多 SKU 支持分析 | BackendArchitect |
| `docs/Q3-nocode-prompt-strategy.md` | 无代码构建提示词策略 | ProductManager |
| `docs/council-research-output.md` | 汇总报告(最终输出) | FirstPrinciples |
| 优先级 | 根因 | 文件:行号 |
|--------|------|-----------|
| **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 |
---
## 技术风险识别(初判)
## 关键文件
| 风险 | 方向 | 影响 | 应对 |
|------|------|------|------|
| ShopXO 不支持多 SKU 单订单 | Q2 | 高:多座位无法落地 | 备选:拆单或扩展订单模型 |
| uni-app 与 ShopXO H5 模板不兼容 | Q4 | 高:需二选一 | 最小方案:纯 H5理想方案uni-app |
| 无代码服务生成的代码无法集成 | Q3 | 中:增加后处理成本 | 限制 prompt 约束,或放弃无代码路线 |
| 酷炫 UI 需要 SSR/异步加载 | Q1 | 中ShopXO 模板引擎限制 | 使用前端 JS 框架渐进增强 |
| 文件 | 关注点 |
|------|--------|
| `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 行) |
---
## 共识收敛策略
## 修复方案
- **第 1 轮**(本轮):各 Agent 创建 plan 并 claim 任务
- **第 2 轮**:各 Agent 完成调研,提交报告到 docs/
- **第 3 轮**FirstPrinciples 汇总,如无法收敛则 FirstPrinciples 拍板
=======
| 文件 | Agent | 截止轮次 |
|------|-------|---------|
| `docs/council-research-output.md` | ProductManager | Round 3 |
| `docs/frontend-template-research.md` | FrontendDev | Round 2 |
| `docs/backend-multi-sku-research.md` | BackendArchitect | Round 2 |
| `docs/firstprinciples-verdict.md` | FirstPrinciples | Round 2 |
### 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 前增加模板存在性显式校验
---
### P2 Fix高优先级
1. SeatSkuService.php GetGoodsViewData — 遍历所有有效配置块,不只处理 `$vrGoodsConfig[0]`
2. 修改 DB 写回逻辑为写回 `validConfigs` 而非 `[$config]`
## 关键文件参考
- `docs/12_UNIAPP_FRONTEND_RESEARCH.md` — 现有 uni-app 调研(需更新)
- `docs/14_TEMPLATE_RENDER_INVESTIGATION.md` — 现有模板渲染调研
- `shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html` — 当前模板
- `docs/02_FRONTEND_CUSTOMIZATION.md` — 前端定制历史文档
>>>>>>> main
### P3 Fix中优先级
1. AdminGoodsSave.php — configs 为空时提示用户重新选择场馆

View File

@ -0,0 +1,232 @@
# 安全审计报告:幽灵 SpecGhost Spec安全问题评估
**审计人**: SecurityEngineer
**日期**: 2026-04-20
**审计对象**: 场馆硬删除后编辑商品的规格重复错误问题
**项目路径**: `/Users/bigemon/WorkSpace/vr-shopxo-plugin/`
---
## 一、审计范围
本次审计覆盖以下文件:
| 文件 | 关键行号 | 审计重点 |
|------|---------|---------|
| `shopxo/app/plugins/vr_ticket/hook/AdminGoodsSaveHandle.php` | 全文 | 保存钩子是否拒绝脏数据 |
| `shopxo/app/plugins/vr_ticket/service/SeatSkuService.php` | 全文 | BatchGenerate 安全校验、GetGoodsViewData fallback |
| `shopxo/app/plugins/vr_ticket/admin/Admin.php` | 858-912 | VenueDelete 硬删除逻辑 |
| `shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html` | 182-449 | 前端 fallback 安全风险 |
---
## 二、S1 — AdminGoodsSaveHandle.php 审计
### S1-Q1: 当 template_id 指向不存在的场馆时,是否拒绝保存?
**结论:行为正确,但错误信息不友好**
关键代码路径:
1. **保存阶段 1**(第 22-41 行,`plugins_service_goods_save_handle`
- 前端发送 `vr_goods_config_base64`(含 `template_id`、`selected_rooms`、`selected_sections`、`sessions`、`template_snapshot`
- 直接 base64 解码写入 `$params['data']['vr_goods_config']`
- **无任何校验** — 这是正确的,因为此时模板可能还未删除
2. **保存阶段 2**(第 55-182 行,`plugins_service_goods_save_thing_end`
- 第 77-90 行:遍历 configs尝试重建 `template_snapshot`
- **第 88-89 行**:模板不存在时执行 `continue`**跳过 snapshot 重建但不阻断流程**
- 第 158-172 行:对每个 `template_id > 0` 的 config 调用 `BatchGenerate`
3. **BatchGenerate 保护**SeatSkuService.php 第 51-57 行):
```php
$template = Db::name(self::table('seat_templates'))
->where('id', $seatTemplateId)->find();
if (empty($template)) {
return ['code' => -2, 'msg' => "座位模板 {$seatTemplateId} 不存在"];
}
```
**结论**:如果 `template_id` 仍存在于 `vr_goods_config` 中但模板已被硬删除,`BatchGenerate` 返回 `code: -2`,该错误被第 169-171 行捕获并向上游返回,**整个保存事务被阻断**。用户看到的错误是 "座位模板 N 不存在"。
**评估**:安全 — 保存被正确阻断。但错误信息不友好(应告知用户重新选择场馆),属于 P2。
### S1-Q2: 幽灵 spec 是否可被恶意注入到 vr_goods_config
**结论:不可注入,无漏洞**
分析:
- `vr_goods_config_base64` 中的字段:**由前端表单构造**,但不含 `spec_base_id_map`
- `spec_base_id_map` **仅存储在 `vr_seat_templates` 表中**Admin.php 第 177 行)
- AdminGoodsSaveHandle 的保存流程中,**不读取也不回写 `spec_base_id_map`**
- `template_snapshot` 在保存时由后端从 DB 重建(第 77-90 行),前端传来的值被覆盖
攻击路径分析:
1. 攻击者能否伪造 `vr_goods_config_base64` 注入恶意 `spec_base_id_map`?→ **不能**,该字段不在表单构造范围内,且若注入则与 `template_id` 关联的 DB 记录不匹配,`BatchGenerate` 失败
2. 攻击者能否通过 `template_snapshot` 注入 XSS**理论上可能**`template_snapshot.venue` 未做 HTML 转义但该字段仅在后端处理不渲染到前端ticket_detail.html 中 venue 数据来自 `$vr_seat_template` 而非 snapshot
3. 攻击者能否利用 `template_id` 复用已删除场馆的规格?→ **不能**`BatchGenerate` 会查 DB找不到模板则返回错误
**结论无安全漏洞NO VULNERABILITY**
### S1-Q3: spec_base_id 重复时是否有去重逻辑或安全阻断?
**结论有兜底阻断BatchGenerate 失败),但无专门去重逻辑**
- `BatchGenerate` 从 DB 读取当前模板的 `seat_map`,生成**新的**座位级 SKU
- 保存时会先清空现有规格数据(第 152-155 行):
```php
Db::name('GoodsSpecType')->where('goods_id', $goodsId)->delete();
Db::name('GoodsSpecBase')->where('goods_id', $goodsId)->delete();
Db::name('GoodsSpecValue')->where('goods_id', $goodsId)->delete();
```
- **先删后建**模式自然覆盖了旧的重复规格,不依赖去重
**结论:无 spec_base_id 重复安全问题
---
## 三、S2 — SeatSkuService.php 审计
### S2-Q1: GetGoodsViewData 在模板不存在时如何 fallback
**结论fallback 行为安全,但会修改数据库**
关键代码SeatSkuService.php 第 380-393 行):
```php
if (empty($seatTemplate)) {
$config['template_id'] = null;
$config['template_snapshot'] = null;
\think\facade\Db::name('Goods')->where('id', $goodsId)->update([
'vr_goods_config' => json_encode([$config], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]);
return [
'vr_seat_template' => null,
'goods_spec_data' => [],
'goods_config' => $config,
];
}
```
**安全分析**
- `vr_seat_template: null` — 前端收到的座位模板为空
- `goods_spec_data: []` — 场次列表为空
- **该方法会主动修改 DB**(将 `template_id` 置 null这是一个"自愈"行为
- 自愈行为本身**不引入安全漏洞**,但有副作用:编辑商品时,用户原本的场馆关联被静默清空
**结论fallback 逻辑本身安全,但会静默修改 DB 状态**
### S2-Q2: template_snapshot 是否可携带恶意 payload
**结论:理论风险低,实际不可利用**
- `template_snapshot` 在保存时由后端重建(第 139-142 行),前端传入值被覆盖
- `template_snapshot` 字段未在 ticket_detail.html 中直接渲染
- `template_snapshot` 存储在 `vr_goods_config` JSON 中无大小限制vr_goods_config 字段需确认 DB schema
**潜在风险**
- 如果 `vr_goods_config` 字段无大小限制,可存储超大 JSONDoS 风险)— 需 DB 层加限
- 如果未来代码变更直接渲染 `template_snapshot` 而不转义,可能 XSS — 当前代码无此路径
**结论:当前代码无实际可利用漏洞,建议在 DB 层对 `vr_goods_config` 加字段大小限制**
---
## 四、S3 — ShopXO 入口安全审计
### S3-Q1: ShopXO AdminGoodsSave.php 入口是否有参数校验?
**结论:入口层无专门校验,但 VR 插件有独立校验**
- `AdminGoodsSave.php`(文件不存在于项目根目录,可能位于 shopxo 源码中)作为 ShopXO 内核钩子入口
- VR 插件的商品保存通过插件钩子 `AdminGoodsSaveHandle::handle()` 处理
- 插件层面:校验逻辑在 `BatchGenerate` 中(模板存在性检查)
- **未发现**未授权保存、越权修改其他商品、参数注入等安全漏洞
**结论入口安全VR 插件有独立校验**
---
## 五、VenueDelete 硬删除逻辑审计
### 硬删除安全检查Admin.php 第 858-912 行)
关键代码:
```php
// 检查是否有关联商品(使用 is_delete_time 而不是 is_delete
$goods = \think\facade\Db::name('Goods')
->where('vr_goods_config', 'like', '%"template_id":' . $id . '%')
->where('is_delete_time', 0)
->find();
```
**安全分析**
- 硬删除**不检查商品是否有关联**,直接执行删除(第 888 行)
- 关联商品仍然持有旧的 `template_id`,但如前所述,下次保存会被 `BatchGenerate` 阻断
- SQL 注入风险:`$id` 为 `intval`,安全
- 审计日志已记录(第 889-895 行)
**结论:硬删除安全,不引入额外漏洞**
---
## 六、漏洞严重性评级
| ID | 问题 | 类别 | 严重性 | 说明 |
|----|------|------|--------|------|
| V-1 | 场馆硬删除后保存失败,错误信息不友好("座位模板 N 不存在" | 功能/体验 | **P2** | 用户无法理解需要重新选择场馆 |
| V-2 | GetGoodsViewData 会静默修改 DB将 template_id 置 null | 功能/行为 | **P2** | 编辑商品时场馆关联被静默清空 |
| V-3 | loadSoldSeats() 为空实现,前端无法标记已售座位 | 业务逻辑 | **P2** | 用户可选中已售座位(超卖风险) |
| V-4 | template_snapshot 字段无大小限制 | DoS 风险 | **P3** | 需 DB 层加字段限制 |
| V-5 | spec_base_id_map 在保存流程中不可达 | 无漏洞 | — | 该字段仅用于 Plan A 订单流程 |
**P1 发现0 个**
**P2 发现3 个**
**P3 发现1 个**
---
## 七、根因定性
**本次幽灵 spec 问题的根因是 P2功能缺陷不属于安全漏洞。**
具体机制:
1. 场馆 A 被硬删除,`vr_seat_templates` 表中无记录
2. 商品的 `vr_goods_config.template_id` 仍为 A 的 ID
3. `GetGoodsViewData` 在读取时将 `template_id` 置 null 并写回 DB自愈
4. 若用户在 `GetGoodsViewData` 执行前打开编辑页,前端收到 `template_id: null`,选单为空
5. 若 `vr_goods_config``template_id` 未被及时清理,下次保存时 `BatchGenerate` 返回错误阻断
**关键保护机制**`BatchGenerate` 是最后一道防线 — 只要 `template_id` 仍指向不存在的模板,保存就会被阻断,不会有脏数据写入规格表。
---
## 八、修复建议(按优先级)
### P2-1高优先级改善错误信息
**文件**: `shopxo/app/plugins/vr_ticket/service/SeatSkuService.php:55-57`
**修改**: 将错误信息改为用户可理解的形式,并引导重新选择场馆
### P2-2中优先级防止静默 DB 修改
**文件**: `shopxo/app/plugins/vr_ticket/service/SeatSkuService.php:383-388`
**修改**: GetGoodsViewData 不应主动修改 DB而应返回 flag 让调用方决定是否清理
### P2-3中优先级实现 loadSoldSeats
**文件**: `shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html:375-383`
**修改**: 实现从后端 API 加载已售座位数据
### P3-1低优先级DB 字段大小限制
**修改**: 为 `goods.vr_goods_config` 字段加 TEXT/MEDIUMTEXT 限制,防止超大 JSON 存储
---
## 九、审计结论
本次审计**未发现任何 P1 安全漏洞**。幽灵 spec 问题是由场馆硬删除引发的**功能缺陷**P2核心保护机制`BatchGenerate` 模板存在性检查)在场。关键安全属性:
- **无脏数据注入路径**`spec_base_id_map` 不可控,不在表单提交范围内
- **保存有保护**:模板不存在时保存被阻断
- **无 XSS/SQL 注入**:所有输入均有适当处理
- **权限控制依赖 ShopXO 内核**VR 插件不处理权限
建议优先处理 P2-1错误信息改善和 P2-3已售座位标记以提升用户体验和防止超卖。

View File

@ -1,7 +1,7 @@
# 幽灵 Spec 问题 — Council 调研汇总报告
> 日期2026-04-20 | AgentFrontendDev + BackendArchitect + SecurityEngineer
> 版本v2.1 | 基于 main 分支 `11fdf0309`
> 基于 main 分支 `f84f95b56`
---
@ -68,25 +68,40 @@ loadSoldSeats: function() {
---
### 2.2 BackendArchitect — 后端调研(`reviews/council-ghost-spec-BackendArchitect.md`
### 2.2 BackendArchitect — 后端调研(`reviews/BackendArchitect-on-Issue-13-debug.md`
#### 关键发现(逐行验证)
#### 关键发现
**根因 1Critical无效 config 块未被移除,脏数据写回 DB**
**Primary Bug — 99% 命中**
`AdminGoodsSaveHandle.php:83-90``continue` 跳过 snapshot 重建但不删除 config 块,第 148-150 行将脏 config 无条件写回 goods 表。
| 文件 | 行号 | 问题代码 |
|------|------|----------|
| `AdminGoodsSaveHandle.php` | **77** | `return in_array($r['id'], $config['selected_rooms'] ?? []);` |
**根因 2HighGetGoodsViewData 仅处理单模板模式,多模板时无效块不清理**
`$r`rooms 数组元素)缺少 `'id'` key 时,访问 `$r['id']` 直接抛出 `Undefined array key "id"`
`SeatSkuService.php:368` — 只取 `$vrGoodsConfig[0]`,多模板场景下其余配置块被完全忽略;第 386-388 行写回 DB 时只写 `[$config]` 单元素。
**对比SeatSkuService::BatchGenerate:100 已有正确防护**
```php
// ✅ 安全写法
$roomId = !empty($room['id']) ? $room['id'] : ('room_' . $rIdx);
```
`AdminGoodsSaveHandle:77` 没有这个防护。
**根因 3MediumBatchGenerate 对无效 template_id 返回 code=-2阻断保存**
**Secondary Bug — 模板不存在时 null 访问**
`AdminGoodsSaveHandle.php:164-170` — 无效 config 块的 `templateId` 仍为原值BatchGenerate 内部检测到模板不存在后返回错误码,阻断整个保存流程。
| 文件 | 行号 | 问题代码 |
|------|------|----------|
| `AdminGoodsSaveHandle.php` | **71** | `$seatMap = json_decode($template['seat_map'] ?? '{}', true);` |
**根因 4Medium前端过滤无法防御 DB 层污染**
`find()` 返回 null 后,`$template['seat_map']` 在 PHP 8.0+ 抛出 `TypeError`
`AdminGoodsSave.php:196-229` — 前端 JS 通过 `validTemplateIds.has(c.template_id)` 过滤无效块,但无法保证 DB 层 config 块被正确清理。
**Tertiary Bug — 类型不匹配静默失败**
| 文件 | 行号 | 问题代码 |
|------|------|----------|
| `AdminGoodsSaveHandle.php` | **77** | `in_array($r['id'], ...)` 类型不一致 |
`selected_rooms[]` 从前端传来是字符串(如 `"room_0"`),而 `$r['id']` 可能是整数。类型不匹配时 `in_array()` 永远返回 `false`,静默导致 `selectedRoomIds` 为空数组。
#### 后端根因
@ -94,10 +109,24 @@ loadSoldSeats: function() {
#### 后端修复建议(已合并)
1. `AdminGoodsSaveHandle.php:88``continue` 改为 `unset($configs[$i])`,第 145 行后加 `$configs = array_values($configs);`
2. `AdminGoodsSaveHandle.php:148-150` — 写回前加 `if (!empty($configs))`
3. `SeatSkuService.php:368` — 遍历所有配置块而非只处理第一个
4. `SeatSkuService.php:386-388` — 写回 validConfigs 而非 `[$config]`
```php
// AdminGoodsSaveHandle.php:83-90已修复
if ($templateId > 0) {
$template = Db::name('vr_seat_templates')->find($templateId);
if (empty($template)) {
continue; // ✅ 硬删除场景跳过
}
$seatMap = json_decode($template['seat_map'] ?? '{}', true);
// ...
}
// AdminGoodsSaveHandle.php:116-137已修复
array_filter($allRooms, function ($r) use ($selectedRooms) {
$rid = $r['id'] ?? ''; // ✅ P0 修复:空安全
// 尝试直接匹配 + 前缀匹配 + 索引回退
// ...
})
```
---
@ -106,7 +135,7 @@ loadSoldSeats: function() {
#### 审计报告来源
- `reviews/SecurityEngineer-AUDIT.md``AdminGoodsSaveHandle.php` 根因分析 + 修复建议
- `reviews/council-ghost-spec-BackendArchitect.md` — "幽灵 spec" 全链路根因分析4 个根因)
- `reviews/BackendArchitect-on-Issue-13-debug.md` — "Undefined array key 'id'" 根因分析
#### 审计结论来源SecurityEngineer-AUDIT.md
@ -186,7 +215,7 @@ f1173e3c8 docs: 补充硬删除修复记录 + Issue #13 关闭说明
| 报告 | 路径 |
|------|------|
| FrontendDev 前端调研 | `reviews/council-ghost-spec-FrontendDev.md` |
| BackendArchitect 后端调研 | `reviews/council-ghost-spec-BackendArchitect.md` |
| BackendArchitect 后端调研 | `reviews/BackendArchitect-on-Issue-13-debug.md` |
| SecurityEngineer 安全审计 | `reviews/SecurityEngineer-AUDIT.md` |
| BackendArchitect 幽灵 spec 调研 | `reviews/council-ghost-spec-BackendArchitect.md` |
| BackendArchitect Round 5 Review | `reviews/BackendArchitect-on-FrontendDev-P1.md` |
| 本汇总报告 | `reviews/council-ghost-spec-summary.md` |