From b11830278cfc10b120062758bb81a703135a874f Mon Sep 17 00:00:00 2001 From: Script Generator Date: Mon, 29 Jun 2026 18:44:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E5=B0=8F=E5=AE=9D=E9=A2=84=E8=AD=A6):=20?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E8=BF=9C=E7=AB=AF=E9=A3=8E=E9=99=A9=E7=BC=93?= =?UTF-8?q?=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/lib/xiaobao-risk-cache.test.ts | 33 ++++++++++++++++++ apps/web/lib/xiaobao-risk-cache.ts | 20 +++++++++++ apps/web/stores/useXiaobaoRiskStore.ts | 46 +++++++++++++++++++------ 3 files changed, 88 insertions(+), 11 deletions(-) diff --git a/apps/web/lib/xiaobao-risk-cache.test.ts b/apps/web/lib/xiaobao-risk-cache.test.ts index 4fa5c22..a50ff65 100644 --- a/apps/web/lib/xiaobao-risk-cache.test.ts +++ b/apps/web/lib/xiaobao-risk-cache.test.ts @@ -3,6 +3,8 @@ import test from 'node:test'; import type { XiaobaoRiskSnapshot } from './xiaobao-risk-trend'; import { findCachedInsight, + mergeDailySnapshotCacheForSave, + mergeInsightCacheForSave, upsertDailySnapshot, upsertInsight, type XiaobaoRiskInsightCacheItem, @@ -68,3 +70,34 @@ test('upsertInsight replaces existing version signature pair', () => { assert.deepEqual(result, [replacement, otherSignature, otherVersion]); }); + +test('mergeDailySnapshotCacheForSave preserves remote rows and local-only rows', () => { + const remoteOnly: XiaobaoRiskSnapshot = { + versionId: 'remote-version', + date: '2026-06-29', + riskScore: 30, + riskLevel: 'attention', + openBugCount: 0, + failedTestCount: 0, + blockedCount: 0, + silentRiskCount: 0, + confidence: 90, + createdAt: '2026-06-29T08:00:00.000Z', + }; + const localOnly: XiaobaoRiskSnapshot = { ...remoteOnly, versionId: 'local-version', riskScore: 50 }; + const item: XiaobaoRiskSnapshot = { ...remoteOnly, versionId: 'current-version', riskScore: 70 }; + + const result = mergeDailySnapshotCacheForSave([localOnly], [remoteOnly], item); + + assert.deepEqual(result, [item, remoteOnly, localOnly]); +}); + +test('mergeInsightCacheForSave preserves remote rows and local-only rows', () => { + const remoteOnly: XiaobaoRiskInsightCacheItem = { ...insightA, versionId: 'remote-version' }; + const localOnly: XiaobaoRiskInsightCacheItem = { ...insightA, versionId: 'local-version' }; + const item: XiaobaoRiskInsightCacheItem = { ...insightA, versionId: 'current-version' }; + + const result = mergeInsightCacheForSave([localOnly], [remoteOnly], item); + + assert.deepEqual(result, [item, remoteOnly, localOnly]); +}); diff --git a/apps/web/lib/xiaobao-risk-cache.ts b/apps/web/lib/xiaobao-risk-cache.ts index 691ac44..98f7b9b 100644 --- a/apps/web/lib/xiaobao-risk-cache.ts +++ b/apps/web/lib/xiaobao-risk-cache.ts @@ -45,3 +45,23 @@ export function upsertDailySnapshot( ...rows.filter((row) => row.versionId !== item.versionId || row.date !== item.date), ]; } + +export function mergeDailySnapshotCacheForSave( + localRows: XiaobaoRiskSnapshot[], + remoteRows: XiaobaoRiskSnapshot[], + item: XiaobaoRiskSnapshot, +): XiaobaoRiskSnapshot[] { + const remoteKeys = new Set(remoteRows.map((row) => `${row.versionId}::${row.date}`)); + const localOnly = localRows.filter((row) => !remoteKeys.has(`${row.versionId}::${row.date}`)); + return upsertDailySnapshot([...remoteRows, ...localOnly], item); +} + +export function mergeInsightCacheForSave( + localRows: XiaobaoRiskInsightCacheItem[], + remoteRows: XiaobaoRiskInsightCacheItem[], + item: XiaobaoRiskInsightCacheItem, +): XiaobaoRiskInsightCacheItem[] { + const remoteKeys = new Set(remoteRows.map((row) => `${row.versionId}::${row.riskSignature}`)); + const localOnly = localRows.filter((row) => !remoteKeys.has(`${row.versionId}::${row.riskSignature}`)); + return upsertInsight([...remoteRows, ...localOnly], item); +} diff --git a/apps/web/stores/useXiaobaoRiskStore.ts b/apps/web/stores/useXiaobaoRiskStore.ts index 3878803..2428994 100644 --- a/apps/web/stores/useXiaobaoRiskStore.ts +++ b/apps/web/stores/useXiaobaoRiskStore.ts @@ -2,6 +2,8 @@ import { create } from 'zustand'; import { loadServerData, saveServerData } from '@/lib/server-data'; import { + mergeDailySnapshotCacheForSave, + mergeInsightCacheForSave, upsertDailySnapshot, upsertInsight, type XiaobaoRiskInsightCacheItem, @@ -11,45 +13,67 @@ import type { XiaobaoRiskSnapshot } from '@/lib/xiaobao-risk-trend'; interface XiaobaoRiskState { snapshots: XiaobaoRiskSnapshot[]; insights: XiaobaoRiskInsightCacheItem[]; + error?: string; fetchRiskData: () => Promise; saveSnapshot: (item: XiaobaoRiskSnapshot) => Promise; saveInsight: (item: XiaobaoRiskInsightCacheItem) => Promise; } -async function loadSnapshots(): Promise { +async function loadSnapshots(): Promise { try { const rows = await loadServerData('xiaobao-risk-snapshots'); return Array.isArray(rows) ? rows : []; } catch {} - return []; + return null; } -async function loadInsights(): Promise { +async function loadInsights(): Promise { try { const rows = await loadServerData('xiaobao-risk-insights'); return Array.isArray(rows) ? rows : []; } catch {} - return []; + return null; } export const useXiaobaoRiskStore = create((set, get) => ({ snapshots: [], insights: [], + error: undefined, fetchRiskData: async () => { const [snapshots, insights] = await Promise.all([loadSnapshots(), loadInsights()]); - set({ snapshots, insights }); + set({ + ...(snapshots ? { snapshots } : {}), + ...(insights ? { insights } : {}), + error: snapshots === null || insights === null ? '小宝预警缓存加载失败' : undefined, + }); }, saveSnapshot: async (item) => { - const snapshots = upsertDailySnapshot(get().snapshots, item); - set({ snapshots }); - await saveServerData('xiaobao-risk-snapshots', snapshots); + const optimistic = upsertDailySnapshot(get().snapshots, item); + set({ snapshots: optimistic, error: undefined }); + try { + const remote = await loadServerData('xiaobao-risk-snapshots'); + const snapshots = mergeDailySnapshotCacheForSave(optimistic, Array.isArray(remote) ? remote : [], item); + set({ snapshots, error: undefined }); + await saveServerData('xiaobao-risk-snapshots', snapshots); + } catch (error) { + set({ error: '小宝预警快照保存失败' }); + throw error; + } }, saveInsight: async (item) => { - const insights = upsertInsight(get().insights, item); - set({ insights }); - await saveServerData('xiaobao-risk-insights', insights); + const optimistic = upsertInsight(get().insights, item); + set({ insights: optimistic, error: undefined }); + try { + const remote = await loadServerData('xiaobao-risk-insights'); + const insights = mergeInsightCacheForSave(optimistic, Array.isArray(remote) ? remote : [], item); + set({ insights, error: undefined }); + await saveServerData('xiaobao-risk-insights', insights); + } catch (error) { + set({ error: '小宝预警解读保存失败' }); + throw error; + } }, }));