// 社交 BI v2 的数据层:负责 master/overview/funnel/kpi 接口的拉取与派生行, // 视图不直接调 API,只消费这里的产物(原始 snake_case 指标行 + 维度字段)。 import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { fetchSocialBiFunnel, fetchSocialBiKpi, fetchSocialBiMaster, fetchSocialBiOverview } from "../api.js"; import { DEFAULT_TIME_ZONE, rangeEndMs, rangeStartMs } from "../utils/time.js"; import { ALL, appSelectionMatches, regionSelectionMatches, resolveDateRange } from "./state.js"; export const SOCIAL_BI_TZ = DEFAULT_TIME_ZONE; export const SOCIAL_BI_FUNNEL_APP_CODES = ["lalu", "huwaa", "fami"]; export function useSocialBiData(filters) { const [master, setMaster] = useState(null); // 数据请求等 master 结果落地后再发:否则会先用兜底 App 列表打一轮,master 到达后立刻重打一轮(整轮浪费)。 const [isMasterSettled, setIsMasterSettled] = useState(false); const [overview, setOverview] = useState(null); const [funnel, setFunnel] = useState(null); const [kpi, setKpi] = useState(null); const [isLoading, setIsLoading] = useState(true); const [errors, setErrors] = useState([]); const [refreshToken, setRefreshToken] = useState(0); const requestSeq = useRef(0); useEffect(() => { let ignore = false; fetchSocialBiMaster() .then((data) => { if (!ignore && data) { setMaster(data); } }) .catch(() => { if (!ignore) { setMaster(null); } }) .finally(() => { if (!ignore) { setIsMasterSettled(true); } }); return () => { ignore = true; }; }, []); // range 只依赖日期相关筛选:切换粒度/区域/App 多选不应触发接口重拉(那些是纯前端派生)。 const { customEnd, customStart, preset } = filters; const range = useMemo( () => resolveDateRange({ customEnd, customStart, preset }, SOCIAL_BI_TZ), [customEnd, customStart, preset] ); const appCodes = useMemo(() => { const available = (master?.apps || []).map((app) => app.app_code); if (!filters.apps.length || filters.apps.includes(ALL)) { return available; } return filters.apps.filter((appCode) => !available.length || available.includes(appCode)); }, [filters.apps, master]); const funnelAppCodes = useMemo(() => { const available = (master?.apps || []).map((app) => app.app_code).filter((appCode) => SOCIAL_BI_FUNNEL_APP_CODES.includes(appCode)); if (!filters.apps.length || filters.apps.includes(ALL)) { return available; } const selected = filters.apps.filter((appCode) => available.includes(appCode)); return selected.length ? selected : available; }, [filters.apps, master]); useEffect(() => { if (!isMasterSettled) { return; } const sequence = ++requestSeq.current; const startMs = rangeStartMs(range, SOCIAL_BI_TZ); const endMs = rangeEndMs(range, SOCIAL_BI_TZ); setIsLoading(true); Promise.allSettled([ fetchSocialBiOverview({ appCodes, endMs, startMs, statTz: SOCIAL_BI_TZ }), fetchSocialBiFunnel({ appCodes: funnelAppCodes, endMs, startMs, statTz: SOCIAL_BI_TZ }), fetchSocialBiKpi({ appCodes, endMs, startMs, statTz: SOCIAL_BI_TZ }) ]).then(([overviewResult, funnelResult, kpiResult]) => { if (sequence !== requestSeq.current) { return; } const nextErrors = []; if (overviewResult.status === "fulfilled") { setOverview(overviewResult.value); (overviewResult.value?.apps || []).forEach((app) => { if (app.error) { nextErrors.push(`${app.app_name || app.app_code}: ${app.error}`); } }); } else { setOverview(null); nextErrors.push(`统计数据加载失败: ${overviewResult.reason?.message || "未知错误"}`); } if (funnelResult.status === "fulfilled") { setFunnel(funnelResult.value); (funnelResult.value?.apps || []).forEach((app) => { if (app.error) { nextErrors.push(`${app.app_name || app.app_code}: ${app.error}`); } }); } else { setFunnel(null); nextErrors.push(`埋点漏斗加载失败: ${funnelResult.reason?.message || "未知错误"}`); } if (kpiResult.status === "fulfilled") { setKpi(kpiResult.value); Object.entries(kpiResult.value?.app_errors || {}).forEach(([appCode, message]) => { nextErrors.push(`${appCode}: ${message}`); }); } else { setKpi(null); nextErrors.push(`运营充值数据加载失败: ${kpiResult.reason?.message || "未知错误"}`); } setErrors(nextErrors); setIsLoading(false); }); }, [appCodes, funnelAppCodes, isMasterSettled, range, refreshToken]); const refresh = useCallback(() => { setRefreshToken((current) => current + 1); }, []); const derived = useMemo(() => deriveRows(overview, filters, master), [filters, master, overview]); return { appCodes, derived, errors, funnel, funnelAppCodes, isLoading, kpi, master, overview, range, refresh }; } // deriveRows 输出四类行,全部带维度字段 + 原始指标字段: // appTotals/appDaily(App 粒度)、regionTotals/regionDaily(大区粒度,已按顶栏区域选择过滤)、 // countryTotals(国家粒度,已按国家→区域目录归一 region_id 并跟随区域筛选)。restricted=true 表示该 App 已被数据范围裁剪。 function deriveRows(overview, filters, master) { const appTotals = []; const appDaily = []; const regionTotals = []; const regionDaily = []; const countryTotals = []; const appErrors = []; const regionByCountry = buildRegionByCountryIndex(master); (overview?.apps || []).forEach((app) => { if (!appSelectionMatches(filters.apps, app.app_code)) { return; } const context = { app_code: app.app_code, app_name: app.app_name || app.app_code, kind: app.kind, restricted: Boolean(app.restricted) }; if (app.error) { appErrors.push({ ...context, error: app.error }); return; } appTotals.push({ ...context, ...(app.total || {}) }); (app.daily_series || []).forEach((row) => { appDaily.push({ ...context, ...row, stat_day: row.stat_day || row.label }); }); (app.region_breakdown || []).forEach((row) => { if (!regionSelectionMatches(filters.regions, app.app_code, Number(row.region_id ?? 0))) { return; } regionTotals.push({ ...context, ...row, region_id: Number(row.region_id ?? 0), region_name: row.region_name || row.region_code || `区域 ${row.region_id}` }); }); (app.daily_region_breakdown || []).forEach((row) => { if (!regionSelectionMatches(filters.regions, app.app_code, Number(row.region_id ?? 0))) { return; } regionDaily.push({ ...context, ...row, region_id: Number(row.region_id ?? 0), region_name: row.region_name || row.region_code || `区域 ${row.region_id}`, stat_day: row.stat_day || row.label }); }); (app.country_breakdown || []).forEach((row) => { const countryCode = row.country_code || String(row.country_id || ""); // legacy 国家行不带 region_id,用区域目录(countries 列表)归一,保证下钻与区域筛选口径一致。 let regionId = Number(row.region_id ?? 0); let regionName = row.region_name || ""; if (!regionId) { const region = regionByCountry.get(`${app.app_code}:${String(countryCode).toUpperCase()}`); if (region) { regionId = region.region_id; regionName = region.region_name || region.region_code || regionName; } } if (!regionSelectionMatches(filters.regions, app.app_code, regionId)) { return; } countryTotals.push({ ...context, ...row, country_code: countryCode, country_name: row.country_name || row.country || row.country_code || `国家 ${row.country_id}`, region_id: regionId, region_name: regionName }); }); }); // 顶栏选了区域时,App 合计/日行切换成所选区域行的聚合视角由各视图用 regionScoped 判断。 const regionScoped = Boolean(filters.regions.length && !filters.regions.includes(ALL)); return { appDaily, appErrors, appTotals, countryTotals, regionDaily, regionScoped, regionTotals }; } function buildRegionByCountryIndex(master) { const index = new Map(); (master?.regions || []).forEach((region) => { (region.countries || []).forEach((countryCode) => { const key = `${region.app_code}:${String(countryCode).toUpperCase()}`; if (!index.has(key)) { index.set(key, region); } }); }); return index; }