From 753210747a1205d2b3a100dc6e59ceedf4d93653 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 22 Jul 2026 12:10:56 +0800 Subject: [PATCH] fix: persist reported birth-time status --- docs/BUG_HISTORY.md | 16 ++++++++++++++++ frontend/src/app/api/account/route.ts | 9 ++++++++- frontend/src/lib/account-profile-patch.ts | 4 ---- frontend/tests/account-api.test.ts | 13 +++++++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index aaedfb88..69257fe0 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -313,3 +313,19 @@ - 相关记录:BUG-008、BUG-009、BUG-016、ERR-085、ERR-091 - 复发自:无 - 修复版本:待提交(生产 smoke 待当前精确 SHA 发布后补齐) + +## BUG-018 | 首次保存未确认出生时间没有写入档案状态 + +- 状态:resolved +- 首次发现:2026-07-22 +- 最近更新:2026-07-22 +- 影响面:账户出生资料保存、未确认星盘咨询、生时校正后继续原问题 +- 用户现象:已经保存“大概时间”等出生资料,但继续咨询时仍提示出生时间状态发生变化,接口返回 409。 +- 触发条件:档案没有旧的 active minute、legacy minute、candidate 或 case pointer,第一次经账户接口保存未确认出生时间声明。 +- 根因:账户写入只在清除旧候选结果时补写 `birth_time_status=reported`;干净档案和首次 upsert 被提前跳过,留下完整声明但空状态,随后被服务端星盘真值校验拒绝。 +- 修复:所有未确认声明变更都原子写入 `reported` 并清空不可沿用的应用结果;首次创建档案时也写入同一状态;确认分钟仍禁止被普通资料编辑覆盖。 +- 验证:`frontend/tests/account-api.test.ts` 覆盖空状态档案与首次档案写入;生产使用认证账户重新保存合法声明后复跑范围终态 handoff、模型咨询、计费和会话删除。 +- 防复发:出生声明与 `birth_time_status` 必须由同一次服务端写入建立,不允许依赖后续校正流程补齐状态。 +- 相关记录:BUG-009、BUG-017 +- 复发自:无 +- 修复版本:待提交 diff --git a/frontend/src/app/api/account/route.ts b/frontend/src/app/api/account/route.ts index d19598af..e7cb29cf 100644 --- a/frontend/src/app/api/account/route.ts +++ b/frontend/src/app/api/account/route.ts @@ -132,7 +132,14 @@ export async function PATCH(request: Request) { } const applicationPatch = currentProfile ? resolveAccountBirthTimeApplicationPatch(currentProfile, payload) - : {}; + : payload.birth_time_source + ? { + active_birth_time: null, + birth_time: null, + birth_time_status: "reported" as const, + rectification_case_id: null, + } + : {}; const baseProfile = { id: userId, ...(payload.name !== undefined ? { name: payload.name } : {}), diff --git a/frontend/src/lib/account-profile-patch.ts b/frontend/src/lib/account-profile-patch.ts index 16b8eadb..4d9440df 100644 --- a/frontend/src/lib/account-profile-patch.ts +++ b/frontend/src/lib/account-profile-patch.ts @@ -217,10 +217,6 @@ export function resolveAccountBirthTimeApplicationPatch( const confirmed = current.birth_time_status === "confirmed" || (current.birth_time_status === null && isBirthClockTime(current.birth_time ?? "")); if (confirmed) return {}; - if (!current.active_birth_time - && !current.birth_time - && current.birth_time_status !== "candidate" - && !current.rectification_case_id) return {}; return { active_birth_time: null, birth_time: null, diff --git a/frontend/tests/account-api.test.ts b/frontend/tests/account-api.test.ts index d1043aaf..a2b7b963 100644 --- a/frontend/tests/account-api.test.ts +++ b/frontend/tests/account-api.test.ts @@ -324,11 +324,24 @@ test("ordinary declaration edits clear stale candidate application but never ove birth_time_status: "reported", rectification_case_id: null, }); + assert.deepEqual(resolveAccountBirthTimeApplicationPatch({ + ...candidate, + active_birth_time: null, + birth_time: null, + birth_time_status: null, + rectification_case_id: null, + }, edited), { + active_birth_time: null, + birth_time: null, + birth_time_status: "reported", + rectification_case_id: null, + }); }); test("account PATCH uses the shared validator and never writes client birth_time over account truth", () => { assert.match(source, /accountProfilePatchSchema\.safeParse/); assert.match(source, /resolveAccountBirthTimeApplicationPatch/); + assert.match(source, /payload\.birth_time_source[\s\S]*birth_time_status: "reported"/); assert.doesNotMatch(source, /birth_time:\s*nullableString\(payload\.birth_time\)/); assert.match(source, /invalidatesUnconfirmedApplication/); assert.match(patchSource, /"birth_time_status"/);