Compare commits
7 Commits
main
...
council/Ba
| Author | SHA1 | Date |
|---|---|---|
|
|
58b0d0f8fd | |
|
|
7234660efe | |
|
|
946d53e6a6 | |
|
|
d7fca62d14 | |
|
|
470ffdeec0 | |
|
|
e5736338bd | |
|
|
bed933e8df |
|
|
@ -0,0 +1,179 @@
|
|||
# Council 评估报告 — BackendArchitect(Round 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 调用(可接受)
|
||||
|
||||
### 方案 C:Phase 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 变通同时推进。
|
||||
- **D(Phase 4 优先)**:不建议。Phase 4 是锦上添花,不是票务购买的基础设施。
|
||||
|
||||
---
|
||||
|
||||
*报告人:BackendArchitect | 2026-05-26 | Round 4*
|
||||
149
plan.md
149
plan.md
|
|
@ -1,109 +1,96 @@
|
|||
# Plan — 调研「场馆删除后编辑商品出现规格重复错误」问题
|
||||
# Plan — VR 演唱会票务小程序 Round 4 执行
|
||||
|
||||
> 版本:v1.3 | 日期:2026-04-20 | Agent:council/FrontendDev + council/SecurityEngineer + council/BackendArchitect
|
||||
> 版本:v4.0 | 日期:2026-05-26 | Agent:council/BackendArchitect
|
||||
> 任务:Round 4 现场核查 — 修正误判,投票 A
|
||||
|
||||
---
|
||||
|
||||
## BackendArchitect(Task 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-7(FrontendDev)+ 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` | GetGoodsViewData:P2 根因,多模板处理缺陷 |
|
||||
| `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#五投票
|
||||
|
|
@ -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. 直接调用插件 API(AJAX) | 低 | 高 | 需新增插件端点 `/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 1(P0)— 购物车提交格式错误
|
||||
|
||||
**根因**:submit() 把 `goods_params` 拼到 URL(GET),但 `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 2(P1)— 缩放时舞台不跟随
|
||||
|
||||
**根因**:`.vr-stage` 在 `.vr-seat-rows` 容器外,CSS `transform: scale()` 只作用于容器内子元素。
|
||||
|
||||
**修复方案(前端)**:将 `.vr-stage` 移入 `.vr-seat-rows` 容器,或创建共享的 zoom wrapper(详见 FrontendDev findings)。
|
||||
|
||||
### Issue 3(P1)— spec 加载问题
|
||||
|
||||
**根因**:ShopXO 的规格匹配通过 `spec.value` 字符串匹配,而非直接接受 `spec_base_id`。VR 票务场景下,每个座位对应独立的 `GoodsSpecBase`,ShopXO 标准流程需要为每个座位生成 ShopXO 规格记录。
|
||||
|
||||
**修复方案**:插件需要维护座位→规格映射,并在选中场次后通过 AJAX 加载已售座位数据(新增插件 API)。
|
||||
|
||||
### Issue 4(P2)— 商品详情/图片加载
|
||||
|
||||
**根因**: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 完成*
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
# Council Phase 2 技术评估报告
|
||||
|
||||
> 协作产出 | 日期:2026-04-21
|
||||
> 参与 Agent:BackendArchitect、FrontendDev、FirstPrinciples
|
||||
|
||||
---
|
||||
|
||||
## 问题总览
|
||||
|
||||
| # | 问题 | 优先级 | 根因分类 |
|
||||
|---|------|--------|---------|
|
||||
| 1 | 购物车提交格式错误 | P0 | API 传递方式 + 参数契约不匹配 |
|
||||
| 2 | 缩放时舞台元素不跟随 | P1 | DOM 结构导致 CSS transform 不共享 |
|
||||
| 3 | spec 加载问题(已回滚) | P1 | ShopXO 规格匹配机制 + API 链路不明确 |
|
||||
| 4 | 商品详情/图片加载 | P2 | 模板未引入 ShopXO 商品内容组件 |
|
||||
|
||||
---
|
||||
|
||||
## Issue 1(P0):购物车提交格式错误
|
||||
|
||||
### 根因分析(三层)
|
||||
|
||||
**第一层(致命)**:`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 2(P1):缩放时舞台元素不跟随
|
||||
|
||||
### 根因分析
|
||||
|
||||
```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 3(P1):spec 加载问题
|
||||
|
||||
### 根因分析
|
||||
|
||||
**问题 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 4(P2):商品详情/图片加载
|
||||
|
||||
### 根因分析
|
||||
|
||||
`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_id,map 可以去掉。保留它是合理的优化,但需要确保同步机制。
|
||||
|
||||
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 协作完成*
|
||||
Loading…
Reference in New Issue