import axios from 'axios' // World Bank API 数据源 // 文档: https://datahelpdesk.worldbank.org/knowledgebase/articles/898581 // 优点: 官方、JSON、免 key、稳定, 中国数据滞后约 1 年 (这是全球宏观数据物理上限) // 接口: GET /v2/country/{code}/indicator/{id}?format=json&date={startYear}:{endYear}&per_page=100 const WB_BASE = 'https://api.worldbank.org/v2' const TIMEOUT = 15000 // 6 个核心宏观指标 // - 中国 GDP (现价美元) - 国民经济总规模 // - 中国 GDP 增速 - 增长动能 // - 中国 CPI 同比 - 通胀 // - 中国失业率 - 就业 // - 美国 GDP (现价美元) - 对标 // - 美国 CPI 同比 - 对标 export const WB_INDICATORS = [ { id: 'NY.GDP.MKTP.CD', name: 'china-gdp-usd', groupName: 'china-macro', title: '中国 GDP', unit: '万亿美元', scale: 1e-8, digits: 2 }, { id: 'NY.GDP.MKTP.KD.ZG',name: 'china-gdp-growth', groupName: 'china-macro', title: '中国 GDP 增速', unit: '%', scale: 1, digits: 2 }, { id: 'FP.CPI.TOTL.ZG', name: 'china-cpi', groupName: 'china-macro', title: '中国 CPI', unit: '%', scale: 1, digits: 2 }, { id: 'SL.UEM.TOTL.ZS', name: 'china-unemploy', groupName: 'china-macro', title: '中国失业率', unit: '%', scale: 1, digits: 2 }, { id: 'NY.GDP.MKTP.CD', name: 'usa-gdp-usd', groupName: 'usa-macro', title: '美国 GDP', unit: '万亿美元', scale: 1e-8, digits: 2 }, { id: 'FP.CPI.TOTL.ZG', name: 'usa-cpi', groupName: 'usa-macro', title: '美国 CPI', unit: '%', scale: 1, digits: 2 }, ] // World Bank 中国是 CHN, 美国是 USA function countryCodeOf(name) { if (name.startsWith('china-')) return 'CHN' if (name.startsWith('usa-')) return 'USA' throw new Error('Unknown country for ' + name) } // 拉单个指标从 startYear 到当前年 // 返回 { name, groupName, title, unit, rows: [{ year, value }] } // 注意: WB 对连续请求会限流返回 400 Invalid value, 加 3 次退避重试 export async function fetchIndicator(meta, startYear = 2015) { const country = countryCodeOf(meta.name) const currentYear = new Date().getFullYear() const url = `${WB_BASE}/country/${country}/indicator/${meta.id}?format=json&date=${startYear}:${currentYear}&per_page=200` let resp const delays = [0, 1500, 4000] for (let attempt = 0; attempt < delays.length; attempt++) { if (delays[attempt]) await new Promise((r) => setTimeout(r, delays[attempt])) try { resp = await axios.get(url, { timeout: TIMEOUT, headers: { 'User-Agent': 'iboard-refresh/1.0' } }) break } catch (e) { if (attempt === delays.length - 1) throw e console.log(`[WB] ${meta.name} 请求失败, ${delays[attempt + 1] / 1000}s 后重试 (${e.message})`) } } // WB 返回: [{page,pages,per_page,total,...}, [{indicator, country, value, date}, ...]] const data = resp.data if (!Array.isArray(data) || data.length < 2 || !Array.isArray(data[1])) { throw new Error('World Bank 返回结构异常: ' + JSON.stringify(data).slice(0, 200)) } const points = data[1] .filter((p) => p.value !== null && p.value !== undefined) .map((p) => ({ year: String(p.date), value: Number((Number(p.value) * meta.scale).toFixed(meta.digits)), })) .sort((a, b) => Number(a.year) - Number(b.year)) return { name: meta.name, groupName: meta.groupName, data: { unit: meta.unit, title: meta.title, source: 'World Bank API', rows: points, }, } } export const META = { name: 'wb-indicators', description: 'World Bank 官方 API 宏观数据', docs: 'https://datahelpdesk.worldbank.org/knowledgebase/articles/898581', } // 拉全部 (返回符合 fetcher 契约的 [{name, groupName, data}, ...]) // 错误隔离: 单个指标失败 -> 跳过 + 记日志, 不影响其他 // opts.skipNames: Set — DB 已存在的指标名, 跳过不拉 (WB 免费但也要省请求) export async function fetchAll(opts = {}) { const skip = opts.skipNames || new Set() const startYear = 2015 const results = [] for (const m of WB_INDICATORS) { if (skip.has(m.name)) { console.log(`[WB] ${m.name} 跳过 (DB 已存在)`) continue } try { const r = await fetchIndicator(m, startYear) console.log(`[WB] ${r.name} OK, ${r.data.rows.length} rows (${r.data.rows[0]?.year}..${r.data.rows.at(-1)?.year})`) results.push(r) } catch (e) { console.error(`[WB] ${m.name} FAIL: ${e.message}`) } } // 全部被 DB 优先跳过时, 返回显式标记而不是空数组 (避免被误判为失败) if (results.length === 0 && WB_INDICATORS.every((m) => skip.has(m.name))) { return { skipped: true, reason: 'DB 已存在全部指标' } } return results }