Guide/货币转换器架构

货币转换器应用如何保持汇率更新?

货币转换器会保存完整的汇率观测值,按受控周期刷新,并在请求失败或市场关闭时保留数据真实的观测时间。

ERexchangerate.dev·Aug 23, 2026·阅读约8分钟

可靠的转换器不会在每次页面渲染时重新请求一个数字。它会把汇率与 sourcemarket_sessiondata_updated_at 一起保存,在短时间内复用,并且只在刷新成功后替换。响应时间表示API何时回答,观测时间表示汇率本身何时变化。

Key points
缓存完整观测值,包括 ratessourcesmarket_sessiondata_updated_at
使用 data_updated_at 判断数据年龄;timestamp 是响应构建时间。
活跃汇率可在交易周内日内更新,日参考汇率按自身发布时间更新。
周末或市场关闭时,保留最后有效观测值和原始时间。
刷新失败时明确显示最后有效值;若没有有效值则返回错误。

把整个响应视为一次观测

脱离元数据的汇率含义不完整:它可能是日内现货、每日参考值或推导交叉汇率。因此应保存完整对象,而不是只取出 rates.EURAPI参考定义了响应字段,方法说明解释了来源和市场状态标签的生成方式。

json · abbreviated latest responsecopy
{
  "base": "USD",
  "source": "live",
  "market_session": "open",
  "timestamp": "2026-08-23T09:42:06Z",
  "data_updated_at": "2026-08-23T09:41:00Z",
  "rates": { "EUR": 0.86124, "GBP": 0.74281 },
  "sources": { "EUR": "live", "GBP": "live" },
  "derived_symbols": []
}

source 汇总多币种响应中最不及时的数据类别。单个币种应读取 sourcesderived_symbols 标记通过其他币种路径计算出的汇率。

两个时间,两种含义
timestamp 是API组装响应的时间,data_updated_at 是底层观测值写入的时间。展示更新时间和判断新鲜度时应使用后者。

先定义刷新政策,再写定时器

不存在适合所有产品的间隔。商店估算价格可共享几分钟缓存,运营面板可在页面可见时刷新,日报可在所需定盘价发布后执行一次。每次渲染都请求不仅浪费调用,也可能得到同一个观测值。

用途应用政策界面显示
商店估算共享缓存;每几分钟刷新币种、指示性说明、观测时间
运营面板短缓存;可见时刷新汇率、来源、市场状态、时间
日报定盘后定时获取一次汇率与存储的观测时间
结算金额不使用指示性转换汇率支付或交易服务结果

market_session 的值为 openweekendinterbank_closed。活跃货币可在交易周内变化,每日参考序列则按自己的发布计划更新。

在服务器端建立小型缓存

先对币种排序以生成稳定键,只缓存成功响应,并避免并发刷新重复发出。带密钥的请求必须留在服务器端。

typescript · latest-rates.tscopy
type RateSource = "live" | "ecb_daily" | "fred_daily";

type LatestRates = {
  base: string;
  source: RateSource;
  sources: Record<string, RateSource>;
  market_session: "open" | "weekend" | "interbank_closed";
  timestamp: string;
  data_updated_at: string;
  rates: Record<string, number>;
  derived_symbols: string[];
};

type CacheEntry = {
  value: LatestRates;
  fetchedAt: number;
};

const cache = new Map<string, CacheEntry>();
const pending = new Map<string, Promise<LatestRates>>();

function cacheKey(base: string, symbols: string[]): string {
  return `${base}:${[...symbols].sort().join(",")}`;
}

async function fetchLatest(base: string, symbols: string[]): Promise<LatestRates> {
  const url = new URL(`https://api.exchangerate.dev/v1/latest/${base}`);
  url.searchParams.set("symbols", [...symbols].sort().join(","));

  const response = await fetch(url, {
    headers: process.env.EXCHANGERATE_API_KEY
      ? { Authorization: `Bearer ${process.env.EXCHANGERATE_API_KEY}` }
      : {},
    signal: AbortSignal.timeout(8000),
  });

  if (!response.ok) {
    throw new Error(`FX refresh failed: ${response.status}`);
  }

  return response.json() as Promise<LatestRates>;
}

export async function getLatestRates(
  base: string,
  symbols: string[],
  maxAgeMs = 60_000,
): Promise<LatestRates> {
  const key = cacheKey(base, symbols);
  const existing = cache.get(key);

  if (existing && Date.now() - existing.fetchedAt < maxAgeMs) {
    return existing.value;
  }

  const inFlight = pending.get(key);
  if (inFlight) return inFlight;

  const refresh = fetchLatest(base, symbols)
    .then((value) => {
      cache.set(key, { value, fetchedAt: Date.now() });
      return value;
    })
    .finally(() => pending.delete(key));

  pending.set(key, refresh);
  return refresh;
}

多实例部署应使用共享存储或框架请求缓存。原则不变:数值和观测元数据一起保存,只在成功后替换。

刷新错误不是新汇率

超时、429或上游错误并不代表市场有新观测。若产品允许使用最后有效值,应保留原始 data_updated_at 并显示回退状态;没有旧数据时直接报错。429应遵守 Retry-After,临时错误采用带随机抖动的指数退避,401则需要修正配置。

typescript · explicit stale fallbackcopy
type ConverterResult =
  | { status: "fresh"; data: LatestRates }
  | { status: "last_known_good"; data: LatestRates; reason: string };

async function getConverterRates(
  base: string,
  symbols: string[],
): Promise<ConverterResult> {
  const key = cacheKey(base, symbols);

  try {
    return { status: "fresh", data: await getLatestRates(base, symbols) };
  } catch (error) {
    const previous = cache.get(key);
    if (!previous) throw error;

    return {
      status: "last_known_good",
      data: previous.value,
      reason: error instanceof Error ? error.message : "Refresh failed",
    };
  }
}

对于429响应,应在存在时遵守 Retry-After,并避免所有实例同时重试。临时故障可使用带随机抖动的指数退避。认证错误需要修正配置,而不是反复重试。

市场关闭是正常状态

周末响应的 market_session: weekend 可以合理地携带交易周最后一次观测。不要用新HTTP响应的时间覆盖观测时间,也不要制造周末报价。

市场关闭时,转换器可以延长应用缓存时间,但交易周恢复后仍应刷新。应根据返回的市场状态和产品要求作出决定,不要假设所有货币都遵循同一个时间表。

换算后只舍入一次

以USD为基础时,25 USD换算EUR为 25 × rates.EUR。保留汇率精度,只按目标币种规则舍入最终金额。

指示性,不可执行
转换汇率适合展示、估算和分析。实际扣款金额应由支付或交易服务给出,并包含点差、费用、舍入与执行时间。

上线检查清单

  • 只请求界面需要的币种。
  • 用基础币种与排序后的符号生成缓存键。
  • 只缓存完整成功响应,不缓存HTTP错误。
  • 合并并发刷新,避免缓存击穿。
  • 需要显示数据年龄时使用 data_updated_at
  • 保留 sourcesmarket_sessionderived_symbols
  • API密钥仅放在服务器端。
  • 使用最后有效值时保留原始时间并明确标记。
ER
exchangerate.dev
面向外汇数据开发者的架构与集成指南。

继续阅读

Reference读取source与market_session阅读 Tutorial在Next.js与TypeScript中使用汇率阅读 Guide指示性汇率与可执行汇率阅读
更多比较Fixer vs exchangerate.devOpen Exchange Rates vs exchangerate.devCurrencylayer vs exchangerate.dev
学习Reading source and market_session in your pipelineIndicative vs executable FX rates: what a rates API actually gives youECB reference rates, explained
实时汇率EUR/USDGBP/USDUSD/JPY