diff --git a/apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.ts b/apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.ts index 30dd94b..b6ea601 100644 --- a/apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.ts +++ b/apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.ts @@ -244,10 +244,22 @@ Page({ _longPressed: false, _animTimer: null as ReturnType | null, + // CHANGE 2026-05-05 | F1-5b MP-4: 补 id 边界保护, + // 防止 options.id 缺失或为字符串 'undefined' 时调用 /api/xcx/coaches/undefined 触发 422 onLoad(options: { id?: string }) { - const id = options?.id || '' - this.setData({ coachId: id }) - this.loadData(id) + const raw = options?.id + const idStr = (raw && raw !== 'undefined') ? String(raw) : '' + if (!idStr || !/^\d+$/.test(idStr)) { + wx.showToast({ title: '缺少助教标识', icon: 'none' }) + setTimeout(() => { + wx.navigateBack({ + fail: () => wx.switchTab({ url: '/pages/board-finance/board-finance' }), + }) + }, 1000) + return + } + this.setData({ coachId: idStr }) + this.loadData(idStr) }, onHide() { diff --git a/docs/audit/changes/2026-05-05__wave1_f1_5b_mp4_coach_detail_id_guard.md b/docs/audit/changes/2026-05-05__wave1_f1_5b_mp4_coach_detail_id_guard.md new file mode 100644 index 0000000..69abb0c --- /dev/null +++ b/docs/audit/changes/2026-05-05__wave1_f1_5b_mp4_coach_detail_id_guard.md @@ -0,0 +1,90 @@ +# 2026-05-05 · F1-5b MP-4 coach-detail id 边界保护 + +> Wave 1 / F1-5b Wave B 第 4 项任务(详见 `docs/_overview/wave1-findings/F1-5b-tasks.md` §4.2 顺序 16) +> +> 工作量 S / ~1h(实际 ~ 30min),按 §3 五步流程完成(4b 跳过 — sandbox 无关)。 + +## 背景 + +Wave 1 走查发现 `pages/coach-detail` 在某种入口下 `data.coachId` 为 undefined / 空字符串,导致后端 `/api/xcx/coaches/undefined` 请求 422 失败,体现为助教详情页加载失败。 + +后端日志(本会话期间观察)出现过:`GET /api/xcx/coaches/undefined HTTP/1.1 422 Unprocessable Content`。 + +## 改动清单 + +### Step 3 实施(Step 1 调研已含足够上下文,Step 2 跳过 — 小程序无 unit test) + +[apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.ts:247-251](apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.ts#L247-L251): + +**改前**(无任何 guard): +```typescript +onLoad(options: { id?: string }) { + const id = options?.id || '' + this.setData({ coachId: id }) + this.loadData(id) +} +``` + +**改后**(参考 coach-service-records.ts 同款 guard 模式): +```typescript +onLoad(options: { id?: string }) { + const raw = options?.id + const idStr = (raw && raw !== 'undefined') ? String(raw) : '' + if (!idStr || !/^\d+$/.test(idStr)) { + wx.showToast({ title: '缺少助教标识', icon: 'none' }) + setTimeout(() => { + wx.navigateBack({ + fail: () => wx.switchTab({ url: '/pages/board-finance/board-finance' }), + }) + }, 1000) + return + } + this.setData({ coachId: idStr }) + this.loadData(idStr) +} +``` + +### Step 4 验证 + +由于 MP-4 §3.3 标"sandbox 无关",仅做 4a: + +| 入口 | 行为 | 结果 | +|------|------|------| +| **缺参**:`/pages/coach-detail/coach-detail`(无 query) | guard 触发 → toast"缺少助教标识"→ 1s 后 navigateBack(失败时 switchTab board-finance) | 当前 route 退回 board-finance,**不触发后端 422** ✓ | +| **字面 'undefined'**:`?id=undefined`(模拟 dataset 字符串污染) | 同上,被 `raw !== 'undefined'` 判断拦截 | guard 触发 ✓ | +| **正常**:`?id=3148987180059141` | 通过 guard → setData coachId → loadData → pageState=normal | 页面成功加载 ✓ | + +**走查方式**:weixin-devtools-mcp 实地 navigate_to(无 params 缺参 / 含 params 正常) + evaluate getCurrentPages()[-1].data 取 route + coachId + pageState。 + +### Step 5 审计 + +本文件。 + +## 影响范围 + +| 端 | 影响 | 验证 | +|----|------|------| +| 小程序 coach-detail | onLoad 加 guard,无效 id 优雅降级(toast + 退回) | weixin-devtools-mcp PASS | +| 后端 | 无影响(以前可能收到 /api/xcx/coaches/undefined 422,现在源头拦截不再发出) | 日志侧观察后续无相关 422 即可 | +| 其他端 | 无影响 | — | + +## 测试 + +小程序无 unit test 框架,以 weixin-devtools-mcp + 后端日志反馈作为验证主线。 + +## 风险与未覆盖 + +- **上游入口 board-coach `dataset.id` 来源**:理论上 board-coach API 返回的 coach.id 不该为 null,但 setData 时机异常时仍可能传 undefined 给 dataset。本任务仅修 coach-detail 兜底,根因如出现在 board-coach 数据流,后续可单独调研(本任务范围外)。 +- **同类页面 audit**:`pages/customer-detail` / `pages/customer-records` 等也接受 query id,可参考本 guard 模式补强,留待 Wave C 评估必要性。 + +## 回滚策略 + +```bash +git revert +``` + +回滚后 onLoad 恢复无 guard 状态,缺参时仍会触发后端 422。 + +## Co-Authored-By + +Claude Opus 4.7 (1M context)