From 864b1fa1f92e1d837488f502a5bb9bca08a17dd0 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sat, 23 May 2026 21:18:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=89=8D=E7=AB=AF=E9=95=BF?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E6=8C=82=E6=9C=BA=E5=86=85=E5=AD=98=E7=B4=AF?= =?UTF-8?q?=E7=A7=AF=EF=BC=9A=E5=9F=8E=E5=B8=82=E7=BC=93=E5=AD=98=20LRU=20?= =?UTF-8?q?=E9=80=90=E5=87=BA=20+=20AI=20=E9=A2=84=E6=B5=8B=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E4=B8=8A=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Constraint: cityDetailsByName/citySummariesByName/cityDetailMetaByName 只增不减,52 城全量可达 ~5MB+ Constraint: aiCityForecastStateCache 无上限,随城市×日期组合膨胀 Tested: tsc --noEmit OK, ruff OK, pytest 184 passed --- .../ai-city-forecast-stream-state.ts | 11 +++++ frontend/hooks/useDashboardStore.tsx | 41 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/frontend/components/dashboard/scan-terminal/ai-city-forecast-stream-state.ts b/frontend/components/dashboard/scan-terminal/ai-city-forecast-stream-state.ts index b3721e7e..52408696 100644 --- a/frontend/components/dashboard/scan-terminal/ai-city-forecast-stream-state.ts +++ b/frontend/components/dashboard/scan-terminal/ai-city-forecast-stream-state.ts @@ -20,6 +20,16 @@ const aiCityForecastStateCache = new Map< string, { state: AiCityForecastState; updatedAt: number } >(); +const MAX_AI_FORECAST_CACHE_SIZE = 40; + +function trimAiForecastCache() { + if (aiCityForecastStateCache.size <= MAX_AI_FORECAST_CACHE_SIZE) return; + const excess = aiCityForecastStateCache.size - MAX_AI_FORECAST_CACHE_SIZE; + const keys = Array.from(aiCityForecastStateCache.keys()); + for (let i = 0; i < excess && i < keys.length; i++) { + aiCityForecastStateCache.delete(keys[i]); + } +} function isHkoObservationCity(detail?: CityDetail | null) { const source = String( @@ -100,6 +110,7 @@ export function writeCachedAiForecastState( state, updatedAt: Date.now(), }); + trimAiForecastCache(); } export function readReadyCachedAiForecastState(cacheKey: string, refreshToken: number) { diff --git a/frontend/hooks/useDashboardStore.tsx b/frontend/hooks/useDashboardStore.tsx index a389be9b..aa6e32c7 100644 --- a/frontend/hooks/useDashboardStore.tsx +++ b/frontend/hooks/useDashboardStore.tsx @@ -659,6 +659,42 @@ export function DashboardStoreProvider({ const [cityDetailMetaByName, setCityDetailMetaByName] = useState< Record >({}); + const MAX_CACHED_CITIES = 20; + const cityLruRef = useRef([]); + + const touchCityLru = (cityName: string) => { + cityLruRef.current = [ + ...cityLruRef.current.filter((c) => c !== cityName), + cityName, + ]; + }; + + // Evict LRU cities when cache exceeds limit + useEffect(() => { + const detailsCount = Object.keys(cityDetailsByName).length; + if (detailsCount <= MAX_CACHED_CITIES) return; + const lru = cityLruRef.current; + const excess = detailsCount - MAX_CACHED_CITIES; + const toEvict = lru.slice(0, excess); + if (!toEvict.length) return; + cityLruRef.current = lru.slice(excess); + setCityDetailsByName((current) => { + const next = { ...current }; + toEvict.forEach((c) => delete next[c]); + return next; + }); + setCitySummariesByName((current) => { + const next = { ...current }; + toEvict.forEach((c) => delete next[c]); + return next; + }); + setCityDetailMetaByName((current) => { + const next = { ...current }; + toEvict.forEach((c) => delete next[c]); + return next; + }); + }, [cityDetailsByName]); + const [selectedCity, setSelectedCity] = useState(null); const [isPanelOpen, setIsPanelOpen] = useState(false); const [selectedForecastDate, setSelectedForecastDate] = useState< @@ -766,6 +802,7 @@ export function DashboardStoreProvider({ const preloadCityFromRow = (row: { city?: string | null; city_display_name?: string | null; display_name?: string | null; [key: string]: unknown }) => { const cityName = (row.city || row.city_display_name || row.display_name || "").trim(); if (!cityName || findCachedCityDetail(cityDetailsByName, cityName)) return; + touchCityLru(cityName); // Pre-populate cache from scan terminal row so detail panel shows data immediately const now = Date.now(); const currentTemp = Number(row.current_temp ?? row.current_max_so_far); @@ -837,6 +874,7 @@ export function DashboardStoreProvider({ force = false, depth: CityDetailDepth = "panel", ) => { + touchCityLru(cityName); const cached = findCachedCityDetail(cityDetailsByName, cityName); const cachedMeta = cityDetailMetaByName[cityName]; const marketTargetDate = @@ -1146,6 +1184,7 @@ export function DashboardStoreProvider({ }, []); const ensureCitySummary = async (cityName: string, force = false) => { + touchCityLru(cityName); const existing = citySummariesRef.current[cityName]; if (!force && existing) { return existing; @@ -1346,7 +1385,9 @@ export function DashboardStoreProvider({ const refreshAll = async () => { dashboardClient.clearCityDetailCache(); setCityDetailsByName({}); + setCitySummariesByName({}); setCityDetailMetaByName({}); + cityLruRef.current = []; if (!citiesRef.current.length) { await loadCities(); }