fix: avoid repeated daily starlanguage refresh
This commit is contained in:
+16
-1
@@ -184,7 +184,7 @@
|
||||
- 防复发:咨询路由测试锁定“有效填报分钟无需授权即可使用”;页面契约禁止重新引入生时校正 toast 或阻断式选择。
|
||||
- 相关记录:BUG-003、BUG-004
|
||||
- 复发自:无
|
||||
- 修复版本:待提交(本地可测)
|
||||
- 修复版本:本次 staging 修复提交(精确 SHA 以远端分支与 staging 验收结果为准)
|
||||
|
||||
## BUG-010 | 浏览器直连 Supabase 导致自托管 PostgreSQL staging 误报未配置
|
||||
|
||||
@@ -3378,3 +3378,18 @@
|
||||
- 防复发:缺少准确出生分钟只限制分钟敏感的个性化结论,不得把用户入口改写成占星教学。首页卡片和 Agent 推荐问题必须直接表达用户要解决的事;Prompt 约束之外必须保留服务端输出校验和版本化缓存失效。
|
||||
- 相关记录:BUG-194、BUG-197、BUG-198
|
||||
- 修复版本:本次 staging 修复提交(精确 SHA 以远端分支与 staging health 验收结果为准)
|
||||
|
||||
## BUG-201 | 咨询完成后的账户刷新重复请求每日星语接口
|
||||
|
||||
- 状态:resolved(staging 修复候选,待部署验收)
|
||||
- 首次发现:2026-08-15
|
||||
- 最近更新:2026-08-15
|
||||
- 影响面:首页每日星语卡片、咨询完成后的账户与积分刷新、`POST /api/daily-starlanguage`。
|
||||
- 用户现象:每完成一次普通咨询,浏览器都会再次请求每日星语接口;即使账户返回的标准化出生资料没有任何变化,也会重复生成同一张每日卡片。
|
||||
- 触发条件:咨询流成功完成后调用 `refreshAccount()`;账户接口返回与当前状态值完全相同但引用不同的 Profile 对象。
|
||||
- 根因:`readProfile()` 每次都会创建新的标准化对象,`refreshAccount()` 又无条件用该对象替换 Profile state;每日星语 effect 需要跟踪完整 Profile,因此依赖对象引用并在引用变化后重新执行。问题不在咨询结算,也不能通过移除账户刷新或缩减 Profile 依赖来规避。
|
||||
- 修复:保留咨询完成后的账户与积分刷新;新增浅等值引用保持 helper。账户刷新得到的新 Profile 与当前 Profile 所有标准化字段等值时继续使用当前引用,只有真实字段变化时才替换 state,从而避免无意义地重跑每日星语及其他 Profile 对象 effect。
|
||||
- 验证:先添加回归测试并确认因 helper 尚不存在而失败;修复后 Profile 引用行为与 `refreshAccount()` 集成测试 3/3 通过。相关 account、consultation entrypoint、starter questions 聚焦测试合计 53/53 通过;目标 ESLint 与 TypeScript `--noEmit` 通过;`git diff --check` 按本次本地验收执行。
|
||||
- 防复发:服务器资料刷新不得把“值相同”转化为无意义的状态引用变化;依赖完整 Profile 的 effect 必须在真实资料变化时执行,不能为消除重复请求而遗漏依赖字段。
|
||||
- 相关记录:BUG-200
|
||||
- 修复版本:待提交(本地可测)
|
||||
|
||||
@@ -102,6 +102,7 @@ import {
|
||||
requestOnboardingWithRecovery,
|
||||
} from "@/lib/onboarding-client";
|
||||
import { protectOnboardingPhrases } from "@/lib/onboarding-copy";
|
||||
import { preserveShallowEqual } from "@/lib/preserve-shallow-equal";
|
||||
import {
|
||||
SessionModelPersistenceQueue,
|
||||
persistSessionModelSelection,
|
||||
@@ -1797,7 +1798,7 @@ export default function Home() {
|
||||
const latest = await fetchAccount();
|
||||
if (!accountRefreshGuard.current.isCurrent(requestIdentity)) return;
|
||||
const nextProfile = readProfile(latest.profile);
|
||||
setProfile(nextProfile);
|
||||
setProfile((current) => preserveShallowEqual(current, nextProfile));
|
||||
setAccount(latest);
|
||||
setAccountError("");
|
||||
} catch (caught) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export function preserveShallowEqual<T extends object>(current: T, next: T): T {
|
||||
if (Object.is(current, next)) return current;
|
||||
|
||||
const currentRecord = current as Record<string, unknown>;
|
||||
const nextRecord = next as Record<string, unknown>;
|
||||
const currentKeys = Object.keys(currentRecord);
|
||||
const nextKeys = Object.keys(nextRecord);
|
||||
|
||||
if (currentKeys.length !== nextKeys.length) return next;
|
||||
|
||||
for (const key of currentKeys) {
|
||||
if (!Object.hasOwn(nextRecord, key) || !Object.is(currentRecord[key], nextRecord[key])) {
|
||||
return next;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import { preserveShallowEqual } from "../src/lib/preserve-shallow-equal.ts";
|
||||
|
||||
const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
|
||||
test("preserves the current profile reference when refreshed values are unchanged", () => {
|
||||
const current = {
|
||||
name: "测试用户",
|
||||
date: "2000-01-01",
|
||||
time: "",
|
||||
birthTimeSource: "period_only",
|
||||
birthTimePeriod: "evening",
|
||||
timezoneId: "Asia/Shanghai",
|
||||
latitude: 31.23,
|
||||
longitude: 121.47,
|
||||
timezoneOffset: null,
|
||||
};
|
||||
const refreshed = { ...current };
|
||||
|
||||
assert.notEqual(refreshed, current);
|
||||
assert.equal(preserveShallowEqual(current, refreshed), current);
|
||||
});
|
||||
|
||||
test("uses the refreshed profile reference when a value changed", () => {
|
||||
const current = {
|
||||
name: "测试用户",
|
||||
birthTimePeriod: "evening",
|
||||
timezoneId: "Asia/Shanghai",
|
||||
};
|
||||
const refreshed = {
|
||||
...current,
|
||||
birthTimePeriod: "morning",
|
||||
};
|
||||
|
||||
assert.equal(preserveShallowEqual(current, refreshed), refreshed);
|
||||
});
|
||||
|
||||
test("account refresh preserves an equivalent normalized profile instead of replacing it", () => {
|
||||
const refreshAccountStart = pageSource.indexOf("async function refreshAccount()");
|
||||
const refreshAccountSource = pageSource.slice(
|
||||
refreshAccountStart,
|
||||
pageSource.indexOf("function updateSession", refreshAccountStart),
|
||||
);
|
||||
|
||||
assert.match(
|
||||
refreshAccountSource,
|
||||
/setProfile\(\(current\) => preserveShallowEqual\(current, nextProfile\)\)/,
|
||||
);
|
||||
assert.doesNotMatch(refreshAccountSource, /setProfile\(nextProfile\)/);
|
||||
});
|
||||
Reference in New Issue
Block a user