Compare commits
3 Commits
dbd62f5658
...
aaa3d1a7ef
| Author | SHA1 | Date |
|---|---|---|
|
|
aaa3d1a7ef | |
|
|
48cd9d4c6b | |
|
|
919c5cfd4e |
123
plan.md
123
plan.md
|
|
@ -1,91 +1,88 @@
|
|||
# Plan — 调研「场馆删除后编辑商品出现规格重复错误」问题
|
||||
# Plan — VR 演唱会票务小程序 Phase 2 技术评估
|
||||
|
||||
> 版本:v1.3 | 日期:2026-04-20 | Agent:council/FrontendDev + council/SecurityEngineer + council/BackendArchitect
|
||||
> 版本:v1.0 | 日期:2026-04-21 | Agent:council/FirstPrinciples + council/BackendArchitect + council/FrontendDev
|
||||
|
||||
---
|
||||
|
||||
## 任务概述
|
||||
|
||||
当票务商品关联的场馆模板被硬删除后,编辑商品时出现「规格不允许重复」错误。
|
||||
|
||||
**根因调查分工**:
|
||||
- FrontendDev:前端规格项构建与 fallback 行为
|
||||
- BackendArchitect:后端规格去重逻辑、`spec_base_id_map` 解析
|
||||
- SecurityEngineer:安全风险评估(P1 vs P2)
|
||||
评估 Phase 2 已完成的 4 个已知问题(购物车提交格式、舞台缩放、spec 加载、商品详情),识别根因,给出修复方案。
|
||||
|
||||
---
|
||||
|
||||
## FrontendDev 任务清单
|
||||
## 问题清单与分工
|
||||
|
||||
- [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`
|
||||
### P0 — 购物车提交格式错误
|
||||
**负责人**:council/BackendArchitect
|
||||
**任务清单**:
|
||||
- [ ] [Claimed: council/BackendArchitect] **Task B-P0-1**: 逆向 GoodsCartService::Save,提取真实 API 契约(参数名、格式、类型)
|
||||
- [ ] [ ] [Claimed: council/BackendArchitect] **Task B-P0-2**: 对比 ticket_detail.html submit() 当前构造的 params,指出具体差异
|
||||
- [ ] [ ] [Claimed: council/BackendArchitect] **Task B-P0-3**: 确认 spec_base_id_map 的语义和作用
|
||||
- [ ] [ ] [Claimed: council/BackendArchitect] **Task B-P0-4**: spec 加载标准端点分析(GoodsSpecDetail 或其他)
|
||||
|
||||
---
|
||||
### P1 — 舞台缩放不跟随
|
||||
**负责人**:council/FrontendDev
|
||||
**任务清单**:
|
||||
- [ ] [Claimed: council/FrontendDev] **Task F-P1-1**: 读取 ticket_detail.html 中 .vr-stage 和 .vr-seat-rows 的 DOM 关系
|
||||
- [ ] [ ] [Claimed: council/FrontendDev] **Task F-P1-2**: 评估「舞台进入 .vr-seat-rows」vs「共享 CSS 缩放变量」两个方案
|
||||
- [ ] [ ] [Claimed: council/FrontendDev] **Task F-P1-3**: 给出推荐修复方案
|
||||
|
||||
## SecurityEngineer 任务清单
|
||||
### P1 — spec 加载机制回滚问题
|
||||
**负责人**:council/BackendArchitect
|
||||
**任务清单**:
|
||||
- [ ] [Claimed: council/BackendArchitect] **Task B-P1-1**: 梳理 ShopXO spec 加载的完整调用链(从商品详情页到 GoodsSpecDetail)
|
||||
- [ ] [ ] [Claimed: council/BackendArchitect] **Task B-P1-2**: 判断 ticket_detail.html 能否接入原生 spec 加载,或需要自定义接口
|
||||
|
||||
- [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`
|
||||
### P2 — 商品详情/图片加载
|
||||
**负责人**:council/FrontendDev
|
||||
**任务清单**:
|
||||
- [ ] [ ] [Claimed: council/FrontendDev] **Task F-P2-1**: 读取 ticket_detail.html 当前 goods_detail/content 渲染状态
|
||||
- [ ] [ ] [Claimed: council/FrontendDev] **Task F-P2-2**: 对比 ShopXO 标准商品详情页的渲染方式
|
||||
|
||||
---
|
||||
|
||||
## BackendArchitect 任务清单
|
||||
|
||||
- [x] [Done: council/BackendArchitect] **Task B1**: 读取 AdminGoodsSaveHandle.php,找出 `vr_goods_config` 的读取和解析逻辑
|
||||
- [x] [Done: council/BackendArchitect] **Task B2**: 找出 `spec_base_id_map` 如何被转换成规格项
|
||||
- [x] [Done: council/BackendArchitect] **Task B3**: 当 `template_id` 指向不存在的场馆时,SeatSkuService.php 的 GetGoodsViewData 如何 fallback?
|
||||
- [x] [Done: council/BackendArchitect] **Task B4**: 幽灵 spec 是在哪个环节产生的?是否在保存时过滤?
|
||||
- [x] [Done: council/BackendArchitect] **Task B5**: 商品保存时规格去重逻辑在哪里?`vr_goods_config` 中若有多个规格项的 `spec_base_id` 相同会怎样?
|
||||
- [x] [Done: council/BackendArchitect] **Task B6**: 给出根因分析(含具体行号)和修复方案
|
||||
- [x] [Done: council/BackendArchitect] **Task B7**: 将调研报告写入 `reviews/council-ghost-spec-BackendArchitect.md`
|
||||
### FirstPrinciples 核心分析
|
||||
**负责人**:council/FirstPrinciples
|
||||
**任务清单**:
|
||||
- [x] [Done: council/FirstPrinciples] **Task FP-1**: 多座位串行提交 — API 设计正交性分析
|
||||
- [x] [Done: council/FirstPrinciples] **Task FP-2**: spec_base_id_map 复杂度质疑:是否存在更简单方案?
|
||||
- [x] [Done: council/FirstPrinciples] **Task FP-3**: 选座 → 购物车流程是否必要?直购是否更合适?
|
||||
- [x] [Done: council/FirstPrinciples] **Task FP-4**: 识别被忽略的关键目标(为什么需要 spec?为什么需要库存?)
|
||||
|
||||
---
|
||||
|
||||
## 阶段划分
|
||||
|
||||
| 阶段 | 状态 |
|
||||
|------|------|
|
||||
| **Draft** | ✅ 完成(所有 Agent 完成文件读取和分析)|
|
||||
| **Review** | ✅ 完成(各 Agent 已提交调研报告)|
|
||||
| **Finalize** | ✅ 完成(summary.md 写入,含 BackendArchitect 最终报告)|
|
||||
|
||||
---
|
||||
|
||||
## 关键文件(必须全部检查)
|
||||
|
||||
| 文件 | 关注点 |
|
||||
|------|--------|
|
||||
| `shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html` | 前端规格项构建、template_snapshot fallback |
|
||||
| `shopxo/app/plugins/vr_ticket/service/SeatSkuService.php` | GetGoodsViewData,模板不存在时的 fallback |
|
||||
| `shopxo/app/plugins/vr_ticket/hook/AdminGoodsSaveHandle.php` | 商品保存钩子,vr_goods_config 处理 |
|
||||
| `shopxo/app/plugins/vr_ticket/admin/Admin.php` | VenueDelete 硬删除逻辑 |
|
||||
| `shopxo/app/admin/hook/AdminGoodsSave.php` | ShopXO 商品保存钩子入口 |
|
||||
| 阶段 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| **Draft** | ✅ 完成 | 各 Agent 读取核心文件,提交 findings |
|
||||
| **Review** | 🔄 进行中 | FirstPrinciples 汇总所有 findings |
|
||||
| **Finalize** | ⬜ 待开始 | 输出 `reviews/council-phase2-assessment.md` |
|
||||
|
||||
---
|
||||
|
||||
## 输出文件
|
||||
|
||||
| 文件 | Agent | 状态 |
|
||||
|------|-------|------|
|
||||
| `reviews/council-ghost-spec-FrontendDev.md` | FrontendDev | ✅ |
|
||||
| `reviews/SecurityEngineer-GHOST_SPEC_SECURITY.md` | SecurityEngineer | ✅ |
|
||||
| `reviews/council-ghost-spec-BackendArchitect.md` | BackendArchitect | ✅ |
|
||||
| `reviews/council-ghost-spec-summary.md` | SecurityEngineer | ✅ (v2.1 — 含 BackendArchitect 报告) |
|
||||
| 文件 | 内容 | 负责人 |
|
||||
|------|------|--------|
|
||||
| `reviews/FirstPrinciples-on-phase2-assessment.md` | 第一性原则分析报告 | FirstPrinciples |
|
||||
| `reviews/council-phase2-assessment.md` | 合并评估报告(最终输出) | FirstPrinciples |
|
||||
|
||||
---
|
||||
|
||||
## 根因结论(BackendArchitect 验证)
|
||||
## 依赖关系
|
||||
|
||||
| 优先级 | 根因 | 文件:行号 |
|
||||
|--------|------|-----------|
|
||||
| **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 |
|
||||
```
|
||||
Task B-P0-1 → B-P0-2 → B-P0-3(串行,BackendArchitect)
|
||||
Task B-P1-1 → B-P1-2(串行,BackendArchitect)
|
||||
Task F-P1-1 → F-P1-2 → F-P1-3(串行,FrontendDev)
|
||||
Task F-P2-1 → F-P2-2(串行,FrontendDev)
|
||||
Task FP-1~4 → 等待 B/F 报告后执行(并行但依赖输入)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键问题提醒(FirstPrinciples 视角)
|
||||
|
||||
1. **购物车是否必要?** VR 演唱会票务是单场次、强时效性商品,购物车流程是否增加了不必要的复杂度?
|
||||
2. **spec_base_id_map 的隐式假设**:为什么一个座位选择需要映射到 spec?这个设计是否源于对 ShopXO 架构的路径依赖?
|
||||
3. **缩放是技术问题还是设计问题?** 如果舞台是「视觉引导」而非「交互元素」,缩放需求本身是否合理?
|
||||
4. **已售座位展示的优先级**:是否真的是 P0?如果座位图本身就是展示性的,已售状态是否可以通过其他方式(如下单时返回错误)处理?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,203 @@
|
|||
# FirstPrinciples — Phase 2 Technical Assessment
|
||||
|
||||
**Agent**: council/FirstPrinciples
|
||||
**Date**: 2026-04-21
|
||||
**Files analyzed**:
|
||||
- `shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html`
|
||||
- `shopxo/app/plugins/vr_ticket/service/SeatSkuService.php`
|
||||
- `shopxo/app/service/GoodsCartService.php`
|
||||
- `shopxo/app/service/BuyService.php`
|
||||
- `shopxo/app/index/controller/Buy.php`
|
||||
|
||||
---
|
||||
|
||||
## FP-1: 多座位串行提交 — API 设计正交性分析
|
||||
|
||||
### 根因:URL 重定向完全绕过了 ShopXO 的下单流程
|
||||
|
||||
**当前 submit() 行为**(ticket_detail.html:439-442):
|
||||
```javascript
|
||||
var checkoutUrl = this.requestUrl + '?s=index/buy/index' +
|
||||
'&goods_params=' + encodeURIComponent(goodsParams);
|
||||
location.href = checkoutUrl;
|
||||
```
|
||||
|
||||
**ShopXO Buy::Index() 实际逻辑**(Buy.php:56-74):
|
||||
```php
|
||||
public function Index()
|
||||
{
|
||||
if($this->data_post) {
|
||||
// POST 时:存储到 session,然后 redirect
|
||||
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']);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**断裂点**:
|
||||
1. `location.href` 产生 **GET 请求**,所以 `$this->data_post` 为空
|
||||
2. `BuyDataStorage()` 从未被调用,session 中没有任何数据
|
||||
3. `BuyDataRead()` 返回空,订单确认页显示"商品数据为空"错误
|
||||
4. URL 中的 `goods_params` **从未被读取**
|
||||
|
||||
**另外**:`submit()` 发送的是 `goods_params`,但 `BuyDataStorage` / `BuyGoods` 期望的是 `goods_data`。参数名不匹配。
|
||||
|
||||
### API 设计正交性评估
|
||||
|
||||
| 设计决策 | 评估 | 问题 |
|
||||
|---------|------|------|
|
||||
| 多座位用 `goods_params` 数组 | ⚠️ 可行 | ShopXO BuyGoods 支持 goods_data 数组 |
|
||||
| URL 传递购物数据 | ❌ 违反关注点分离 | URL 是导航用的,不是数据传递通道 |
|
||||
| session 存储购买意图 | ✅ 正确 | 但 submit() 没有写入 session |
|
||||
| redirect 后自读取 | ✅ 正确 | 但 redirect 需要 POST,submit() 用了 GET |
|
||||
|
||||
**结论**:当前实现是一个「想用 GET 做 POST 的事」的混合方案。两步正确做法:
|
||||
- **方案 A(表单 POST)**:创建隐藏 form,POST `goods_data` 到 `Buy::Index()`
|
||||
- **方案 B(直接 API)**:POST JSON 到 `plugins/vr_ticket/index/buy` 自定义端点,自行调用 `BuyDataStorage`
|
||||
|
||||
---
|
||||
|
||||
## FP-2: spec_base_id_map 复杂度质疑
|
||||
|
||||
### 为什么要 map?
|
||||
|
||||
`spec_base_id_map` 的语义是:`{seatKey: specBaseId}` — 把前端座位标识映射到 ShopXO 的 `goods_spec_base.id`。
|
||||
|
||||
**问题:这个映射层是必要的吗?**
|
||||
|
||||
有两种消费方需要 spec_base_id:
|
||||
|
||||
1. **前端 submit()** — 把 spec_base_id 发给 BuyService,用于锁定库存
|
||||
2. **后端 onOrderPaid()** — 验证座位是否被重复销售
|
||||
|
||||
**当前设计**:
|
||||
```
|
||||
SeatSkuService::BatchGenerate → 生成 GoodsSpecBase 行 →
|
||||
写入 seatTemplate.spec_base_id_map →
|
||||
前端读取 → submit() 使用
|
||||
```
|
||||
|
||||
**替代方案**(不需要 map):
|
||||
```
|
||||
前端:只传 {goods_id, seatKey}
|
||||
后端 onOrderPaid():按 seatKey 在 GoodsSpecValue 中查找对应的 spec_base_id
|
||||
```
|
||||
|
||||
即:`spec_base_id` 是可以通过查询得到的,不需要提前存储在 map 中。
|
||||
|
||||
**spec_base_id_map 的额外成本**:
|
||||
- 存储冗余(每个座位行在模板表 + GoodsSpecBase 表中都有记录)
|
||||
- 同步风险:BatchGenerate 重新运行时,如果模板中 spec_base_id_map 被清空,前端拿到的是过时的 map
|
||||
- 复杂度:spec_base_id_map 的 key 格式(`A_1`)需要与前端 seatKey 格式严格一致
|
||||
|
||||
**spec_base_id_map 的合理存在理由**:
|
||||
- 如果 `onOrderPaid()` 的 seatKey → spec_base_id 查找太慢(数千座位时 JOIN 查询),缓存 map 是合理的性能优化
|
||||
- 但当前实现中,spec_base_id_map 的正确性完全依赖 BatchGenerate 没有失败
|
||||
|
||||
**结论**:spec_base_id_map 是一个**性能缓存**,不是业务必需的。如果 spec 数量少(<1000),直接 JOIN 查询更简单正确。如果数量大(>5000),才值得维护这个 map。
|
||||
|
||||
---
|
||||
|
||||
## FP-3: 选座 → 购物车流程是否必要?
|
||||
|
||||
### 问题重构
|
||||
|
||||
VR 演唱会票务是「强时效性单场次商品」:
|
||||
- 用户选座 → 立即下单
|
||||
- 不需要跨 session 持久化(今天选座,明天买)
|
||||
- 不需要多件合并购买(演唱会票几乎不存在"加购"场景)
|
||||
- 不需要 wishlist / 价格比较 / 购物车管理
|
||||
|
||||
**ShopXO 购物车的核心价值**(对标准电商):
|
||||
1. 跨页面收集购买意向
|
||||
2. 合并结算多店铺/多商品
|
||||
3. 未登录时暂存选购
|
||||
|
||||
**VR 票务场景下,这些价值全部为零。**
|
||||
|
||||
### 购物车流程的额外成本
|
||||
|
||||
| 成本项 | 影响 |
|
||||
|--------|------|
|
||||
| 座位库存锁 | 需要考虑购物车超时释放 |
|
||||
| 购物车页面 UI | 与票务流程无关 |
|
||||
| 多座位串行提交逻辑 | 增加 submit() 复杂度 |
|
||||
| 观演人信息持久化 | 隐私风险(暂存他人信息) |
|
||||
|
||||
### 直购方案的优点
|
||||
|
||||
如果绕过购物车,直接进入订单确认页:
|
||||
- 消除购物车超时/锁座问题
|
||||
- 减少 1 个跳转步骤(选座 → 订单确认 vs 选座 → 购物车 → 订单确认)
|
||||
- 观演人信息只存在表单中,不落持久化存储
|
||||
|
||||
**但注意**:ShopXO 的 `Buy::Index()` + `BuyService::BuyGoods()` 流程仍然可用,只是应该直接 POST到这个链路,而不是绕弯子。
|
||||
|
||||
**结论**:从第一性原则看,票务场景不需要购物车。直接进入订单确认页(Buy 链路)更简洁。但当前实现**已经在用 Buy 链路**(不是 Cart 链路),只是 submit() 的传递方式错了。修复 submit() 后,这个问题就不存在了。
|
||||
|
||||
---
|
||||
|
||||
## FP-4: 被忽略的关键目标
|
||||
|
||||
### 为什么需要 spec?为什么需要库存?
|
||||
|
||||
**当前的隐式假设**:
|
||||
1. 每个座位 = 一个 ShopXO spec_base 行(inventory = 1)
|
||||
2. 用户下单时,通过 spec_base_id 锁定库存
|
||||
|
||||
**更深层的问题**:
|
||||
|
||||
**问题 A:库存一致性的真正来源是什么?**
|
||||
|
||||
ShopXO 的 spec_base.inventory 由谁维护?
|
||||
- `SeatSkuService::BatchGenerate` 写入 `inventory = 1`
|
||||
- `SeatSkuService::refreshGoodsBase` 更新总库存
|
||||
- **但:用户下单后,ShopXO 是否会原子性地将 spec_base.inventory 减 1?**
|
||||
|
||||
如果不会,则 inventory 只是「参考值」,真实库存安全需要靠业务层(onOrderPaid)保证。这意味着 spec_base.inventory 只是一个「建议库存」,而不是「锁定库存」。
|
||||
|
||||
**如果 onOrderPaid 才是真正的库存权威**,那么前端实时显示「已售座位」的价值就降低了——座位只在付款成功后才真正被占用。
|
||||
|
||||
**问题 B:已售座位显示的用户体验价值**
|
||||
|
||||
`loadSoldSeats()` 当前是 TODO stub。如果不显示已售座位:
|
||||
- 用户可能选了 5 个座位,提交时才发现有 1 个已售
|
||||
- 体验是「提交失败」而不是「选座时就知道」
|
||||
|
||||
**但**:如果下单流程足够快(5 秒内完成支付),用户在支付前选到已售座位的概率极低。「提交时返回错误」是可接受的降级体验。
|
||||
|
||||
**真正的 P0 是什么?**
|
||||
|
||||
无论是否显示已售座位,**后端必须在 onOrderPaid 层面防止双售**。这是业务正确性的根本。前端是否实时显示已售状态,是 P1 优化。
|
||||
|
||||
**问题 C:多场次场景**
|
||||
|
||||
当前实现中,场次用 `goods_spec_data` 展示。但 `GetGoodsViewData()` 只返回第一个配置的场次(取 `validConfigs[0]`)。如果一个商品有多个场次配置,只显示第一个——这是 bug,不是设计。
|
||||
|
||||
**问题 D:为什么用 ShopXO 的 spec 系统?**
|
||||
|
||||
核心问题被掩盖在「我们必须用 ShopXO」的前提下了。真正的选择是:
|
||||
- **方案 1(当前)**:把座位映射到 ShopXO spec_base,每个座位一行
|
||||
- **方案 2**:ShopXO 商品只表示「演出票」这个品类,座位管理完全在 vr_ticket 插件自己的表中,ShopXO order_goods 中的数量=座位数,不区分具体座位
|
||||
|
||||
方案 2 避免了 spec_base_id_map 的复杂性,座位验证全在 onOrderPaid 中完成。代价是需要自行维护座位状态表。
|
||||
|
||||
---
|
||||
|
||||
## 汇总:第一性原则视角的关键提醒
|
||||
|
||||
1. **submit() 的 URL 重定向是根本性 bug**(P0),需要改成表单 POST 或直接 API 调用。修复后 Buy 链路本身是可用的。
|
||||
|
||||
2. **spec_base_id_map 是路径依赖**,不是必需的设计。如果 onOrderPaid 能通过 seatKey 查询到 spec_base_id,则 map 可以去掉。保留它是合理的性能优化,但需要确保同步机制。
|
||||
|
||||
3. **购物车对票务无价值**,但当前实现已经在用 Buy 链路,不是 Cart 链路——说明直觉上的「绕过购物车」需求其实不存在,只是 submit() 的传递方式错了。
|
||||
|
||||
4. **已售座位展示是 P1,不是 P0**。真正的 P0 是 onOrderPaid 防双售——无论前端是否显示已售,后端必须在付款时保证座位唯一性。
|
||||
|
||||
5. **GetGoodsViewData() 只返回第一个配置的场次**——这是一个潜在的 bug,影响多场次商品。
|
||||
|
||||
6. **最小修复范围**:只需修复 submit() 的传递方式(表单 POST),不需要重构 spec 系统,不需要引入实时已售座位更新。
|
||||
Loading…
Reference in New Issue