diff --git a/plan.md b/plan.md index 44ef2cf..8ea9abf 100644 --- a/plan.md +++ b/plan.md @@ -11,11 +11,11 @@ | Task | 内容 | 状态 | |------|------|------| -| B1 | 分析 GoodsCartService::Save 的真实 API 契约(期望参数格式) | [Claimed: council/BackendArchitect] | -| B2 | 验证 ticket_detail.html submit() 的 params 构造是否符合规范 | [Claimed: council/BackendArchitect] | -| B3 | ShopXO spec 加载标准端点调研(GoodsSpecDetail 等) | [Claimed: council/BackendArchitect] | -| B4 | 在 ticket_detail.html 中优雅加载规格/库存的方案设计 | [Claimed: council/BackendArchitect] | -| B5 | 整合 BackendArchitect findings → `reviews/BackendArchitect-on-phase2.md` | Pending | +| B1 | 分析 GoodsCartService::Save 的真实 API 契约(期望参数格式) | [Done: council/BackendArchitect] | +| B2 | 验证 ticket_detail.html submit() 的 params 构造是否符合规范 | [Done: council/BackendArchitect] | +| B3 | ShopXO spec 加载标准端点调研(GoodsSpecDetail 等) | [Done: council/BackendArchitect] | +| B4 | 在 ticket_detail.html 中优雅加载规格/库存的方案设计 | [Done: council/BackendArchitect] | +| B5 | 整合 BackendArchitect findings → `reviews/BackendArchitect-on-phase2.md` | [Done: council/BackendArchitect] | ### FrontendDev diff --git a/reviews/BackendArchitect-on-phase2.md b/reviews/BackendArchitect-on-phase2.md new file mode 100644 index 0000000..8938c29 --- /dev/null +++ b/reviews/BackendArchitect-on-phase2.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` +- 应该用 `