Compare commits
4 Commits
main
...
council/Fi
| Author | SHA1 | Date |
|---|---|---|
|
|
875b7c1848 | |
|
|
a6cb084ca2 | |
|
|
aaa3d1a7ef | |
|
|
48cd9d4c6b |
132
plan.md
132
plan.md
|
|
@ -1,72 +1,51 @@
|
||||||
# Plan — ShopXO 酷炫前端模板调研
|
# Plan — VR 演唱会票务小程序 Phase 2 技术评估
|
||||||
|
|
||||||
> 版本:v1.0 | 日期:2026-04-20 | Agent:council/FirstPrinciples + council/FrontendDev + council/BackendArchitect + council/ProductManager
|
> 版本:v1.0 | 日期:2026-04-21 | Agent:council/FirstPrinciples + council/BackendArchitect + council/FrontendDev
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 任务概述
|
## 任务概述
|
||||||
|
|
||||||
vr-shopxo-plugin 项目 Phase 0/1/2 后台开发已完成,现需调研票务商品详情页(`ticket_detail.html`)的酷炫前端模板实现方案。
|
评估 Phase 2 已完成的 4 个已知问题(购物车提交格式、舞台缩放、spec 加载、商品详情),识别根因,给出修复方案。
|
||||||
|
|
||||||
**4个调研方向**:
|
|
||||||
- Q1:ShopXO 自定义模板最佳实践
|
|
||||||
- Q2:单订单多 SKU 支持(多座位选择前提)
|
|
||||||
- Q3:第三方无代码构建服务提示词策略
|
|
||||||
- Q4:uni-app 兼容性技术栈选型
|
|
||||||
|
|
||||||
**输出文件**:`docs/council-research-output.md`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 依赖关系分析
|
## 问题清单与分工
|
||||||
|
|
||||||
```
|
### P0 — 购物车提交格式错误
|
||||||
Q2(多SKU) ──→ Q4(uni-app选型)的下单流程基础
|
|
||||||
Q3(无代码) ──→ 依赖 Q1 的 ShopXO 模板约束
|
|
||||||
Q1(最佳实践) ──→ 基础,供 Q3/Q4 引用
|
|
||||||
```
|
|
||||||
|
|
||||||
**结论**:Q1 + Q2 可并行调研;Q3 依赖 Q1;Q4 依赖 Q2。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Agent 任务分工
|
|
||||||
|
|
||||||
### 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 兼容性保障方案
|
|
||||||
|
|
||||||
### Q2 — 单订单多 SKU 支持
|
|
||||||
**负责人**:council/BackendArchitect
|
**负责人**:council/BackendArchitect
|
||||||
**任务清单**:
|
**任务清单**:
|
||||||
- [ ] [Claimed: council/BackendArchitect] **Task Q2-1**: 研究 ShopXO 标准订单模型是否支持单订单多 SKU 行项目
|
- [ ] [Claimed: council/BackendArchitect] **Task B-P0-1**: 逆向 GoodsCartService::Save,提取真实 API 契约(参数名、格式、类型)
|
||||||
- [ ] [ ] [Claimed: council/BackendArchitect] **Task Q2-2**: 分析多 SKU 下单流程触发条件
|
- [ ] [ ] [Claimed: council/BackendArchitect] **Task B-P0-2**: 对比 ticket_detail.html submit() 当前构造的 params,指出具体差异
|
||||||
- [ ] [ ] [Claimed: council/BackendArchitect] **Task Q2-3**: 若不支持,给出最小改动方案
|
- [ ] [ ] [Claimed: council/BackendArchitect] **Task B-P0-3**: 确认 spec_base_id_map 的语义和作用
|
||||||
|
- [ ] [ ] [Claimed: council/BackendArchitect] **Task B-P0-4**: spec 加载标准端点分析(GoodsSpecDetail 或其他)
|
||||||
|
|
||||||
### Q3 — 第三方无代码构建提示词策略
|
### P1 — 舞台缩放不跟随
|
||||||
**负责人**:council/ProductManager
|
**负责人**:council/FrontendDev
|
||||||
**任务清单**:
|
**任务清单**:
|
||||||
- [ ] [Claimed: council/ProductManager] **Task Q3-1**: 调研 Google App Build 等无代码服务的能力边界
|
- [ ] [Claimed: council/FrontendDev] **Task F-P1-1**: 读取 ticket_detail.html 中 .vr-stage 和 .vr-seat-rows 的 DOM 关系
|
||||||
- [ ] [ ] [Claimed: council/ProductManager] **Task Q3-2**: 设计 ShopXO 模板约束的 prompt 工程策略
|
- [ ] [ ] [Claimed: council/FrontendDev] **Task F-P1-2**: 评估「舞台进入 .vr-seat-rows」vs「共享 CSS 缩放变量」两个方案
|
||||||
- [ ] [ ] [Claimed: council/ProductManager] **Task Q3-3**: 生成代码后处理(集成到 ShopXO 的步骤清单)
|
- [ ] [ ] [Claimed: council/FrontendDev] **Task F-P1-3**: 给出推荐修复方案
|
||||||
|
|
||||||
### Q4 — uni-app 兼容性技术栈选型
|
### P1 — spec 加载机制回滚问题
|
||||||
**负责人**:council/FirstPrinciples(由 BackendArchitect + FrontendDev 提供输入后 FirstPrinciples 汇总)
|
**负责人**:council/BackendArchitect
|
||||||
**前置条件**:Task Q2-3 完成
|
|
||||||
**任务清单**:
|
**任务清单**:
|
||||||
- [ ] [Claimed: council/FirstPrinciples] **Task Q4-1**: "一套代码双端"方案评估(H5 + 微信小程序)
|
- [ ] [Claimed: council/BackendArchitect] **Task B-P1-1**: 梳理 ShopXO spec 加载的完整调用链(从商品详情页到 GoodsSpecDetail)
|
||||||
- [ ] [ ] [Claimed: council/FirstPrinciples] **Task Q4-2**: ShopXO H5 模板与 uni-app 项目桥接方案
|
- [ ] [ ] [Claimed: council/BackendArchitect] **Task B-P1-2**: 判断 ticket_detail.html 能否接入原生 spec 加载,或需要自定义接口
|
||||||
|
|
||||||
### FirstPrinciples 最终拍板
|
### P2 — 商品详情/图片加载
|
||||||
|
**负责人**:council/FrontendDev
|
||||||
|
**任务清单**:
|
||||||
|
- [ ] [ ] [Claimed: council/FrontendDev] **Task F-P2-1**: 读取 ticket_detail.html 当前 goods_detail/content 渲染状态
|
||||||
|
- [ ] [ ] [Claimed: council/FrontendDev] **Task F-P2-2**: 对比 ShopXO 标准商品详情页的渲染方式
|
||||||
|
|
||||||
|
### FirstPrinciples 核心分析
|
||||||
**负责人**:council/FirstPrinciples
|
**负责人**:council/FirstPrinciples
|
||||||
**任务清单**:
|
**任务清单**:
|
||||||
- [ ] [Claimed: council/FirstPrinciples] **Task FP-1**: 汇总 Q1-Q4 输出,写入 `docs/council-research-output.md`
|
- [x] [Done: council/FirstPrinciples] **Task FP-1**: 多座位串行提交 — API 设计正交性分析
|
||||||
- [ ] [ ] [Claimed: council/FirstPrinciples] **Task FP-2**: 明确优先级、依赖关系、技术风险
|
- [x] [Done: council/FirstPrinciples] **Task FP-2**: spec_base_id_map 复杂度质疑:是否存在更简单方案?
|
||||||
- [ ] [ ] [Claimed: council/FirstPrinciples] **Task FP-3**: 给出"最小可行方案 vs 理想方案"对比
|
- [x] [Done: council/FirstPrinciples] **Task FP-3**: 选座 → 购物车流程是否必要?直购是否更合适?
|
||||||
|
- [x] [Done: council/FirstPrinciples] **Task FP-4**: 识别被忽略的关键目标(为什么需要 spec?为什么需要库存?)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -74,36 +53,49 @@ Q1(最佳实践) ──→ 基础,供 Q3/Q4 引用
|
||||||
|
|
||||||
| 阶段 | 状态 | 说明 |
|
| 阶段 | 状态 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| **Draft** | 🔄 进行中 | 各 Agent 调研并提交各自方向报告 |
|
| **Draft** | ✅ 完成 | 各 Agent 读取核心文件,提交 findings |
|
||||||
| **Review** | ⬜ 待开始 | 各 Agent 交叉评审,FirstPrinciples 汇总 |
|
| **Review** | ✅ 完成 | FirstPrinciples 完成独立 review |
|
||||||
| **Finalize** | ⬜ 待开始 | 输出 council-research-output.md |
|
| **Finalize** | ✅ 完成 | BackendArchitect + FrontendDev 合并评估报告已产出 |
|
||||||
|
|
||||||
|
## 共识状态
|
||||||
|
|
||||||
|
- BackendArchitect: `[APPROVE]` — merged report produced
|
||||||
|
- FrontendDev: `[APPROVE]` — merged report produced, Issue 2 zoom fix proposed
|
||||||
|
- FirstPrinciples: `[APPROVE]` — final consolidated report at `reviews/council-phase2-assessment.md`
|
||||||
|
|
||||||
|
## 新增发现(P2 潜在)
|
||||||
|
|
||||||
|
- **GetGoodsViewData multi-session bug**: `validConfigs[0]` 只返回第一个场次,多场次商品会丢失其余场次
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 输出文件
|
## 输出文件
|
||||||
|
|
||||||
| 文件 | 内容 | 负责人 |
|
| 文件 | 内容 | 状态 |
|
||||||
|------|------|--------|
|
|------|------|------|
|
||||||
| `docs/Q1-frontend-research.md` | ShopXO 自定义模板最佳实践 | FrontendDev |
|
| `reviews/council-phase2-assessment.md` | 合并评估报告(最终输出) | ✅ Done |
|
||||||
| `docs/Q2-multisku-research.md` | 单订单多 SKU 支持分析 | BackendArchitect |
|
| `reviews/FirstPrinciples-on-council-assessment.md` | FirstPrinciples review | ✅ Done |
|
||||||
| `docs/Q3-nocode-prompt-strategy.md` | 无代码构建提示词策略 | ProductManager |
|
| `reviews/FirstPrinciples-on-phase2-assessment.md` | 第一性原则分析报告 | ✅ Done |
|
||||||
| `docs/council-research-output.md` | 汇总报告(最终输出) | FirstPrinciples |
|
|
||||||
|
**Phase 2 评估完成,所有文件已合并至 `reviews/council-phase2-assessment.md`**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 技术风险识别(初判)
|
## 依赖关系
|
||||||
|
|
||||||
| 风险 | 方向 | 影响 | 应对 |
|
```
|
||||||
|------|------|------|------|
|
Task B-P0-1 → B-P0-2 → B-P0-3(串行,BackendArchitect)
|
||||||
| ShopXO 不支持多 SKU 单订单 | Q2 | 高:多座位无法落地 | 备选:拆单或扩展订单模型 |
|
Task B-P1-1 → B-P1-2(串行,BackendArchitect)
|
||||||
| uni-app 与 ShopXO H5 模板不兼容 | Q4 | 高:需二选一 | 最小方案:纯 H5;理想方案:uni-app |
|
Task F-P1-1 → F-P1-2 → F-P1-3(串行,FrontendDev)
|
||||||
| 无代码服务生成的代码无法集成 | Q3 | 中:增加后处理成本 | 限制 prompt 约束,或放弃无代码路线 |
|
Task F-P2-1 → F-P2-2(串行,FrontendDev)
|
||||||
| 酷炫 UI 需要 SSR/异步加载 | Q1 | 中:ShopXO 模板引擎限制 | 使用前端 JS 框架渐进增强 |
|
Task FP-1~4 → 等待 B/F 报告后执行(并行但依赖输入)
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 共识收敛策略
|
## 关键问题提醒(FirstPrinciples 视角)
|
||||||
|
|
||||||
- **第 1 轮**(本轮):各 Agent 创建 plan 并 claim 任务
|
1. **购物车是否必要?** VR 演唱会票务是单场次、强时效性商品,购物车流程是否增加了不必要的复杂度?
|
||||||
- **第 2 轮**:各 Agent 完成调研,提交报告到 docs/
|
2. **spec_base_id_map 的隐式假设**:为什么一个座位选择需要映射到 spec?这个设计是否源于对 ShopXO 架构的路径依赖?
|
||||||
- **第 3 轮**:FirstPrinciples 汇总,如无法收敛则 FirstPrinciples 拍板
|
3. **缩放是技术问题还是设计问题?** 如果舞台是「视觉引导」而非「交互元素」,缩放需求本身是否合理?
|
||||||
|
4. **已售座位展示的优先级**:是否真的是 P0?如果座位图本身就是展示性的,已售状态是否可以通过其他方式(如下单时返回错误)处理?
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
# FirstPrinciples Review — council-phase2-assessment.md
|
||||||
|
|
||||||
|
**Reviewer**: council/FirstPrinciples
|
||||||
|
**Date**: 2026-04-21
|
||||||
|
**Document reviewed**: `reviews/council-phase2-assessment.md` (BackendArchitect + FrontendDev joint output)
|
||||||
|
**Verdict**: `[APPROVE]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Review Summary
|
||||||
|
|
||||||
|
The merged assessment accurately captures the root causes and recommended fixes. My review confirms alignment with my independent analysis and adds supplementary first-principles commentary.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What the Assessment Gets Right
|
||||||
|
|
||||||
|
### P0 — submit() GET vs POST
|
||||||
|
|
||||||
|
Correctly identified that `location.href` produces a GET request that never calls `BuyDataStorage()`. The field name mismatch (`goods_params` vs `goods_data`) is also correctly flagged.
|
||||||
|
|
||||||
|
The recommended fix (hidden form POST with `goods_data`) is minimal and correct.
|
||||||
|
|
||||||
|
### P1 — Zoom container
|
||||||
|
|
||||||
|
The `.vr-zoom-container` wrapper solution is the right approach. Shared `transform-origin: center top` ensures both stage and seats scale from the same anchor.
|
||||||
|
|
||||||
|
### P1 — spec loading
|
||||||
|
|
||||||
|
Correctly identifies that ShopXO's spec matching is `type:value` string-based, not ID-based. The recommendation to use a plugin-specific API endpoint for sold-seats is architecturally sound.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FirstPrinciples Supplementary Commentary
|
||||||
|
|
||||||
|
### A note on spec_base_id_map complexity
|
||||||
|
|
||||||
|
The assessment recommends keeping the map as-is. From a first-principles view, this is a pragmatic choice (performance optimization) but should be documented as such. Future maintainers should know:
|
||||||
|
- `spec_base_id_map` is a **performance cache**, not a business invariant
|
||||||
|
- If `BatchGenerate` reruns and clears the map, the frontend will have stale data
|
||||||
|
- The authoritative source of truth is `GoodsSpecBase` + `GoodsSpecValue`, not the JSON map
|
||||||
|
|
||||||
|
### Shopping cart is not the problem
|
||||||
|
|
||||||
|
The assessment frames Issue 1 as a "cart format" issue. From first-principles, the more accurate framing is: **the Buy pathway is correct; only the transport mechanism is wrong**. The code already bypasses cart (goes directly to `Buy::Index()`). The fix should be framed as "fixing the Buy transport" not "fixing cart format."
|
||||||
|
|
||||||
|
### The real P0 no one mentioned
|
||||||
|
|
||||||
|
`onOrderPaid()` is the seat-uniqueness authority. No assessment document addresses whether this hook is correctly implemented. If it isn't, no amount of frontend work matters — duplicate sales can still occur. This should be verified before any release.
|
||||||
|
|
||||||
|
### New finding: GetGoodsViewData() returns only first session
|
||||||
|
|
||||||
|
Confirmed from `SeatSkuService.php`: `validConfigs[0]` means multi-session products show only the first session. This is a latent bug that will surface when multiple shows are configured. Not in the original 4-issue list — flagging as **P2 (latent)**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Additions to council-phase2-assessment.md
|
||||||
|
|
||||||
|
1. **Framing correction**: Issue 1 is "Buy transport broken" not "cart format error"
|
||||||
|
2. **onOrderPaid audit**: Add as prerequisite before P0 fix deployment
|
||||||
|
3. **GetGoodsViewData multi-session bug**: Add to issue list as P2 latent
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vote
|
||||||
|
|
||||||
|
`[APPROVE]` — Assessment quality meets standard. Ready for implementation phase.
|
||||||
|
|
@ -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 系统,不需要引入实时已售座位更新。
|
||||||
|
|
@ -0,0 +1,259 @@
|
||||||
|
# VR 演唱会票务小程序 Phase 2 技术评估报告
|
||||||
|
|
||||||
|
> 日期:2026-04-21
|
||||||
|
> 协作产出:BackendArchitect、FrontendDev、FirstPrinciples
|
||||||
|
> 源码依据:BuyService.php、GoodsCartService.php、SeatSkuService.php、ticket_detail.html
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 执行摘要
|
||||||
|
|
||||||
|
Phase 2 完成了对 4 个已知问题的根因分析,识别了 1 个未被提及的潜在 Bug(多场次),并给出了分优先级的修复方案。
|
||||||
|
|
||||||
|
**结论**:所有 4 个问题均为可修复的技术问题,无架构级障碍。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题总览
|
||||||
|
|
||||||
|
| # | 问题 | 优先级 | 根因分类 |
|
||||||
|
|---|------|--------|---------|
|
||||||
|
| 1 | 购买提交流程失效 | **P0** | GET→POST 机制错误 + 参数契约不匹配 |
|
||||||
|
| 2 | 缩放时舞台不跟随 | **P1** | DOM 结构导致 transform 不共享 |
|
||||||
|
| 3 | spec 加载机制回滚 | **P1** | ShopXO spec 匹配机制不兼容 + API 端点缺失 |
|
||||||
|
| 4 | 商品详情/图片加载 | **P2** | 模板未引入 ShopXO 内容渲染组件 |
|
||||||
|
|
||||||
|
**新增发现(潜在 Bug)**:
|
||||||
|
| # | 问题 | 优先级 |
|
||||||
|
|---|------|--------|
|
||||||
|
| 5 | GetGoodsViewData() 只返回第一个场次 | **P2 潜在** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 1(P0):购买提交流程失效
|
||||||
|
|
||||||
|
### 根因分析(三层叠加)
|
||||||
|
|
||||||
|
**第一层(致命)**:`location.href` 产生 GET 请求,但 `Buy::Index()` 只在 `data_post` 为真时调用 `BuyDataStorage()`。
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Buy.php:56-62
|
||||||
|
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 表
|
||||||
|
|
||||||
|
### 推荐修复
|
||||||
|
|
||||||
|
**前端(FrontendDev)**:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 隐藏表单 POST(最小化变更,复用 Buy 链路)
|
||||||
|
submit: function() {
|
||||||
|
var goodsDataList = this.selectedSeats.map(function(seat, i) {
|
||||||
|
var specBaseId = self.specBaseIdMap[seat.seatKey] || self.sessionSpecId;
|
||||||
|
return {
|
||||||
|
goods_id: self.goodsId,
|
||||||
|
spec: [{type: '座位', value: seat.seatKey}], // ShopXO 规格格式
|
||||||
|
stock: 1
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = Base64.encode(JSON.stringify(goodsDataList));
|
||||||
|
form.appendChild(input);
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关于 extension_data(观演人信息)**:ShopXO 原生不支持 `extension_data`(OrderDetail 表无此字段)。推荐方案:新增 `vr_order_attendee` 表,在 `BuyService::OrderInsert()` 后存储观演人信息。
|
||||||
|
|
||||||
|
### 关键提醒(FirstPrinciples)
|
||||||
|
|
||||||
|
> Issue 1 的准确描述是「Buy 传输机制损坏」,而非「购物车格式错误」。
|
||||||
|
>
|
||||||
|
> 当前代码实际上绕过了购物车,直接进入 Buy 链路。这说明直觉上的「绕过购物车」需求并不存在——只是 `submit()` 的传递方式错了。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 2(P1):缩放时舞台不跟随
|
||||||
|
|
||||||
|
### 根因分析
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="vr-seat-map-wrapper">
|
||||||
|
<div class="vr-stage">舞 台</div> <!-- 平级 sibling -->
|
||||||
|
<div class="vr-seat-rows" id="seatRows"></div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
CSS `transform: scale()` 只作用于应用元素的子树。舞台和座位行是平级元素,缩放座位时舞台不动。
|
||||||
|
|
||||||
|
### 推荐修复
|
||||||
|
|
||||||
|
**方案:将舞台和座位包裹在同一 zoom 容器内**(FrontendDev 实施)
|
||||||
|
|
||||||
|
```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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:舞台的 `border-radius` 在缩放后可能变形,需在 zoom 场景下单独调整。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 3(P1):spec 加载机制回滚
|
||||||
|
|
||||||
|
### 根因分析
|
||||||
|
|
||||||
|
**问题 A**:`loadSoldSeats()` 是空 stub,无任何网络请求。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
loadSoldSeats: function() {
|
||||||
|
// TODO: 从后端加载已售座位
|
||||||
|
// $.get(...); // 空,无任何调用
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题 B**:ShopXO 的 `GoodsSpecDetail` 通过 `spec.value` 字符串匹配规格,而非直接接受 `spec_base_id`。
|
||||||
|
|
||||||
|
```php
|
||||||
|
// GoodsService.php:2749-2757
|
||||||
|
$spec = array_column($params['spec'], 'value');
|
||||||
|
$where['value'] = $spec;
|
||||||
|
$ids = Db::name('GoodsSpecValue')->where($where)->column('goods_spec_base_id');
|
||||||
|
```
|
||||||
|
|
||||||
|
VR 票务场景下,每个座位对应独立的 `GoodsSpecBase` 记录(inventory=1)。ShopXO 标准流程需要生成 `GoodsSpecValue` 记录(type='座位', value='seatKey')。
|
||||||
|
|
||||||
|
### 推荐修复
|
||||||
|
|
||||||
|
**后端(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"]}}
|
||||||
|
```
|
||||||
|
|
||||||
|
前端在选中场次后调用此接口,标记 `.sold` class。
|
||||||
|
|
||||||
|
**关于 spec 加载**:ShopXO 的 spec 数据通过 `GetGoodsViewData()` 在模板渲染时注入前端。如果已生成了 `GoodsSpecBase` 记录,最直接的方式是维护座位→规格映射,并提供独立的已售座位查询 API,而不是依赖 ShopXO 的规格匹配流程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 4(P2):商品详情/图片加载
|
||||||
|
|
||||||
|
### 现状
|
||||||
|
|
||||||
|
- **商品内容**(`$goods['content']`):✅ 正常渲染
|
||||||
|
- **商品相册**(`$goods['images']`):⚠️ 数据存在但未使用
|
||||||
|
- `renderSessions()` 依赖 `goods_spec_data`,不含 `images`
|
||||||
|
- `.vr-goods-photos` 已定义样式但从未被调用
|
||||||
|
- **`.goods-detail-content` CSS**:⚠️ 缺失
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
如需展示商品图片,在模板中添加图片渲染逻辑。如票务详情页不需要 ShopXO 商品内容区,可降级为「确认不需要」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 5(P2 潜在):GetGoodsViewData 只返回第一个场次
|
||||||
|
|
||||||
|
### 根因分析
|
||||||
|
|
||||||
|
```php
|
||||||
|
// SeatSkuService.php:BatchGenerate()
|
||||||
|
return array_merge($data, [
|
||||||
|
'goods_spec_data' => $validConfigs[0], // ← 只取第一个配置
|
||||||
|
'vr_seat_template' => $template,
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
`validConfigs[0]` 意味着多场次商品只返回第一个场次的座位模板和规格数据。当一个商品配置了多场演唱会时,其余场次对用户不可见。
|
||||||
|
|
||||||
|
### 修复方向
|
||||||
|
|
||||||
|
修改 `BatchGenerate()` 返回格式,将 `goods_spec_data` 改为数组(`$validConfigs`),前端根据选中场次索引读取对应数据。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第一性原则视角的关键提醒
|
||||||
|
|
||||||
|
1. **P0 的真正来源**:`submit()` 用 GET 做 POST 的事。Buy 链路本身可用,不需要重构 spec 系统。
|
||||||
|
|
||||||
|
2. **spec_base_id_map 是性能缓存**:不是业务必需。如果 `onOrderPaid` 能通过 seatKey 查询到 spec_base_id,map 可以去掉。保留它是合理的优化,但需要确保同步机制。
|
||||||
|
|
||||||
|
3. **购物车对票务无价值**:当前实现已绕过购物车进入 Buy 链路。选座的实时性决定了购物车会增加超卖风险,快速购买是正确方向。
|
||||||
|
|
||||||
|
4. **已售座位展示是 P1,不是 P0**:真正的 P0 是 `onOrderPaid` 防双售。前端是否实时显示已售状态,是体验优化,不是业务正确性的根本。
|
||||||
|
|
||||||
|
5. **`onOrderPaid` 是座位唯一性权威**(未审计):在 P0 修复部署前,必须验证此 Hook 是否正确实现了座位锁定逻辑。若未实现,任何前端修复都无法防止重复销售。
|
||||||
|
|
||||||
|
6. **最小修复范围**:只需修复 `submit()` 的传递方式(隐藏表单 POST)。不需要重构 spec 系统,不需要引入实时已售座位更新(除非 spec 加载方案已实施)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 修复优先级与分工
|
||||||
|
|
||||||
|
| 优先级 | 问题 | 负责 | 修复说明 |
|
||||||
|
|--------|------|------|---------|
|
||||||
|
| P0 | Issue 1 submit() | FrontendDev | 改用隐藏表单 POST,复用 Buy 链路 |
|
||||||
|
| P1 | Issue 2 舞台缩放 | FrontendDev | 新增 zoom wrapper 容器 |
|
||||||
|
| P1 | Issue 3 spec 加载 | BackendArchitect | 新增插件 sold_seats API 端点 |
|
||||||
|
| P2 | Issue 4 商品详情 | FrontendDev | 确认是否需要,补充 CSS |
|
||||||
|
| P2 | Issue 5 多场次 | BackendArchitect | BatchGenerate 返回数组格式 |
|
||||||
|
| FP | extension_data | BackendArchitect | 新增 vr_order_attendee 表 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录: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) |
|
||||||
|
| `SeatSkuService.php` | BatchGenerate | 返回 validConfigs[0] — 多场次 Bug |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*VR 演唱会票务小程序 Phase 2 技术评估 — Council 协作完成*
|
||||||
Loading…
Reference in New Issue