diff --git a/PROJECT_AUDIT_2026-03-03.md b/PROJECT_AUDIT_2026-03-03.md index 5b7fe0e..27dd9a0 100644 --- a/PROJECT_AUDIT_2026-03-03.md +++ b/PROJECT_AUDIT_2026-03-03.md @@ -119,7 +119,12 @@ - 改为服务端基于认证态校验,或强制 query/body 校验成员身份; - 前端补齐身份参数并确保不可伪造(推荐 token/cookie 方案)。 -### P2-2 “接受计划”前端逻辑乐观更新过早,且未检查 `res.ok` +### P2-2 “接受计划”前端逻辑乐观更新过早,且未检查 `res.ok`【已完成】 +- 修复状态:✅ 已完成(2026-03-03) +- 修复内容: + - `handleAcceptPlan` 改为“后端成功后再更新 `planAccepted/activeContract`”; + - 增加 `res.ok` 检查与失败错误提示; + - 防止后端失败时前端误显示“已接受”。 - 证据: - `src/hooks/useBlindboxPlan.ts:227-240`(先 `setPlanAccepted(true)`,请求后不判断 `res.ok`) - 影响: diff --git a/src/hooks/useBlindboxPlan.ts b/src/hooks/useBlindboxPlan.ts index 0793d40..b3a79bb 100644 --- a/src/hooks/useBlindboxPlan.ts +++ b/src/hooks/useBlindboxPlan.ts @@ -224,20 +224,30 @@ export function useBlindboxPlan( }, [profile, planDays, handlePlanDaysChange, toast]); const handleAcceptPlan = useCallback(async () => { - setPlanAccepted(true); - fireConfetti(); - if (planId && profile) { - try { - const res = await fetch("/api/blindbox/plan", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ planId, userId: profile.id, action: "accept" }), - }); - const data = await res.json(); - setActiveContract({ id: planId, days: planDays, endTime: data.endTime ?? null }); - } catch (e) { console.error("acceptPlan failed:", e); } + if (!planId || !profile) { + toast.show("计划信息不完整,请重新生成后再试"); + return; + } + + try { + const res = await fetch("/api/blindbox/plan", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ planId, userId: profile.id, action: "accept" }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + throw new Error(data.error || "接受契约失败"); + } + + setPlanAccepted(true); + setActiveContract({ id: planId, days: planDays, endTime: data.endTime ?? null }); + fireConfetti(); + toast.show("契约已接受!"); + } catch (e) { + console.error("acceptPlan failed:", e); + toast.show(e instanceof Error ? e.message : "接受契约失败"); } - toast.show("契约已接受!"); }, [planId, profile, planDays, fireConfetti, toast]); const handleRegenerate = useCallback(() => {