修复数据链路 P0 瓶颈:缓存炸弹、Context 重渲染、LGBM 循环、TTL 不匹配
P0-1 sessionStorage 限制: - writeCityDetailCacheBundle 只保留最近 3 个城市的详情 - 避免 3-10MB JSON 序列化阻塞主线程 P0-2 Context 拆分: - 新增 CityDetailsContext 独立管理 cityDetailsByName 变更 - 新增 useCityDetails hook,只订阅详情的组件不再因 cities/proAccess 变化重渲染 - DashboardStoreContext 保持不变,向后兼容 P0-3 扫描终端 TTL: - SCAN_TERMINAL_PAYLOAD_TTL_SEC 30s → 120s - 匹配 ThreadPoolExecutor(4) x 60 城的实际重算耗时 P0-4 LGBM 循环依赖: - LGBM 预测值不再作为 DEB 输入参与权重计算 - 保留为独立参考字段 lgbm.prediction 输出给前端展示 - 消除 DEB → LGBM 训练 → DEB 的循环 新增 docs/data-architecture-review.md 完整数据链路审查报告 Tested: npx tsc --noEmit, python -m ruff check .
This commit is contained in:
@@ -73,6 +73,12 @@ interface DashboardStoreValue extends DashboardState {
|
||||
}
|
||||
|
||||
const DashboardStoreContext = createContext<DashboardStoreValue | null>(null);
|
||||
const CityDetailsContext = createContext<{
|
||||
cityDetailsByName: Record<string, CityDetail>;
|
||||
cityDetailMetaByName: Record<string, { cachedAt: number; revision: string }>;
|
||||
citySummariesByName: Record<string, CitySummary>;
|
||||
loadingState: LoadingState;
|
||||
} | null>(null);
|
||||
|
||||
function getInitialLoadingState(): LoadingState {
|
||||
return {
|
||||
@@ -1520,9 +1526,16 @@ export function DashboardStoreProvider({
|
||||
],
|
||||
);
|
||||
|
||||
const cityDetailsValue = useMemo(
|
||||
() => ({ cityDetailsByName, cityDetailMetaByName, citySummariesByName, loadingState }),
|
||||
[cityDetailsByName, cityDetailMetaByName, citySummariesByName, loadingState],
|
||||
);
|
||||
|
||||
return (
|
||||
<DashboardStoreContext.Provider value={value}>
|
||||
{children}
|
||||
<CityDetailsContext.Provider value={cityDetailsValue}>
|
||||
{children}
|
||||
</CityDetailsContext.Provider>
|
||||
</DashboardStoreContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1537,6 +1550,16 @@ export function useDashboardStore() {
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useCityDetails() {
|
||||
const context = useContext(CityDetailsContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useCityDetails must be used within DashboardStoreProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useCityData(name?: string | null) {
|
||||
const store = useDashboardStore();
|
||||
const key = name || store.selectedCity;
|
||||
|
||||
@@ -542,15 +542,19 @@ export const dashboardClient = {
|
||||
meta: Record<string, CityCacheMeta>,
|
||||
) {
|
||||
if (!isClient()) return;
|
||||
// Keep only the 3 most-recently-accessed cities to prevent sessionStorage bloat
|
||||
const MAX_CACHED_CITIES = 3;
|
||||
const allEntries = Object.entries(details).map(([cityName, detail]) => ({
|
||||
cityName,
|
||||
cachedAt: meta[cityName]?.cachedAt || 0,
|
||||
detail,
|
||||
revision: meta[cityName]?.revision || getCityRevision(detail),
|
||||
}));
|
||||
const topEntries = allEntries
|
||||
.sort((a, b) => b.cachedAt - a.cachedAt)
|
||||
.slice(0, MAX_CACHED_CITIES);
|
||||
const entries = Object.fromEntries(
|
||||
Object.entries(details).map(([cityName, detail]) => [
|
||||
cityName,
|
||||
{
|
||||
cachedAt: meta[cityName]?.cachedAt || Date.now(),
|
||||
detail,
|
||||
revision: meta[cityName]?.revision || getCityRevision(detail),
|
||||
},
|
||||
]),
|
||||
topEntries.map((e) => [e.cityName, { cachedAt: e.cachedAt, detail: e.detail, revision: e.revision }]),
|
||||
);
|
||||
window.sessionStorage.setItem(
|
||||
CACHE_KEY,
|
||||
|
||||
Reference in New Issue
Block a user