汇率查询API,听上去就是一个数字的事儿。年初我接到一个跨境电商结算项目,需求方丢过来一句话:"你们接个汇率接口就行,实时换算,最好各大银行外汇牌价都带上,数据要全。"我一开始以为半天能搞定,结果调研完才发现,"实时换算"和"各大银行外汇牌价"这两件事凑在一起,根本不是大多数免费接口能覆盖的——市面上大部分汇率API只给一个中间价或者单一参考价,而真正做结算、对账、报价的系统,需要的是带银行标识、带价格类型、带时间戳的全量数据。
这篇文章就把我做的这个汇率查询API服务完整拆一遍:从数据模型设计、数据源采集策略、接口规范,到"实时"的真实含义、缓存与降级方案,再到上线之后踩过的坑。适合正在做金融数据服务、跨境电商后端、财务系统,或者单纯想把汇率数据服务做扎实的朋友参考。
1. 需求复盘:为什么"一个汇率接口"根本不够用
1.1 结算场景真正需要的不只是一个数字
当需求方说"汇率接口"时,通常脑子里想的是一个数字,比如"现在是1美元等于7.24人民币"。拿到这个数字,商品售价乘以它,订单金额就换算出来了。但这个数字到底是谁家的价格?是中间价还是银行卖出价?是现汇还是现钞?不同口径,结果能差出千分之几到百分之一以上。
举一个真实例子:同一时刻,中国银行的美元对人民币现汇卖出价可能是7.2458,而招商银行的现汇卖出价可能是7.2472,中间价可能只有7.2430。如果订单金额是100万美元,按中间价和按招行现汇卖出价结算,差了4200人民币。对于利润率本来就薄的跨境电商,这种差异不是可忽略的噪音,而是实打实的钱。
所以结算系统真正需要的不是"一个汇率",而是"一套完整、可追溯、口径明确的汇率数据":
- 能区分中间价和各类银行牌价;
- 能按银行维度查询和对比;
- 能明确这笔换算用的是哪个价格、哪个时点;
- 能回溯历史数据,方便对账和审计。
这些要求叠加起来,就不是调一个免费接口那么简单了。
1.2 "全量数据"拆开是四层能力
标题里"实时换算以及各大银行外汇牌价在内的全量数据",我把它拆成了四层:
- 基础汇率层:以中国外汇交易中心公布的中间价为准,作为全市场的锚。
- 银行牌价层:主流商业银行的现汇买入、现钞买入、现汇卖出、现钞卖出、折算价。
- 换算服务层:基于以上任意口径做实时换算,包括交叉盘。
- 历史数据层:按交易日和快照时间归档,支持区间查询。
这四层每一层单独拿出来都有现成的免费接口,但合在一起并保持口径一致和可用性,就需要自己动手搭了。后面所有章节都围绕这四层展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先把数据底盘搭对:快照表设计与五种价格口径
2.1 买入价、卖出价、折算价:五个字段的语义差别
银行外汇牌价页面上常见五个字段:现汇买入价、现钞买入价、现汇卖出价、现钞卖出价、折算价(各银行叫法略有差异,中行叫"中行折算价",有的银行叫"基准价")。它们业务语义完全不同:
- 现汇买入价:银行从客户手里买入外汇存款/汇款的价格,比如客户把境外汇来的美元卖给银行,按这个价。
- 现钞买入价:银行买入外币现金的价格,因为有现钞调运和保管成本,通常比现汇买入价低。
- 现汇卖出价:银行卖给客户外汇(用于电汇、转账)的价格。个人购汇、企业对外付汇看这个价。
- 现钞卖出价:银行卖外币现金给客户的价格。
- 折算价:银行内部折算参考,接近外汇交易中心中间价。
| 字段 | 方向 | 相对中间价 | 典型用途 |
|---|---|---|---|
| 现汇买入价 | 客户卖外汇给银行 | 低 | 收汇结汇 |
| 现钞买入价 | 客户卖外币现金给银行 | 最低 | 现金兑换 |
| 现汇卖出价 | 银行卖外汇给客户 | 高 | 购汇、付汇 |
| 现钞卖出价 | 银行卖外币现金给客户 | 最高 | 取外币现金 |
| 折算价 | 内部参考 | 接近中间价 | 折算、入账参考 |
如果只提供一个中间价,做"购汇"场景会低估成本,做"结汇"场景会高估收入。这也是我坚持在数据模型里把 rate_type 单独作为一个维度的原因。
2.2 为什么按快照存,而不是只存最新值
很多第一版实现会把汇率存在一张只有"最新值"的配置表里,每次采集覆盖。这在演示时没问题,但一遇到对账需求就废了:财务要的是"昨天下午3点这笔订单结算时用的牌价是多少",而不是"现在是多少"。
所以我设计成快照表,核心是下面这张表:
sql复制CREATE TABLE fx_rate_snapshot (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
bank_code VARCHAR(16) NOT NULL COMMENT '数据源代码:CFETS/BOC/ICBC/CCB/ABC/CMB...',
ccy_pair VARCHAR(10) NOT NULL COMMENT '货币对,统一为 USD/CNY 形式',
rate_type VARCHAR(12) NOT NULL COMMENT 'mid/buy_tt/buy_cash/sell_tt/sell_cash/convert',
bid_rate DECIMAL(18,6) DEFAULT NULL,
ask_rate DECIMAL(18,6) DEFAULT NULL,
mid_rate DECIMAL(18,6) DEFAULT NULL,
trade_date CHAR(10) NOT NULL COMMENT '交易日,北京时间',
snap_time CHAR(19) NOT NULL COMMENT '快照时间',
UNIQUE KEY uk_src_pair_type_time (bank_code, ccy_pair, rate_type, trade_date, snap_time),
KEY idx_pair_snap (ccy_pair, trade_date, snap_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
几个设计要点:
- 货币对统一成
USD/CNY形式,分母固定为CNY,避免方向混乱。所有采集和输出都按这个规则归一化,反向需求由API层处理。 bid_rate和ask_rate分开存储。买入价和卖出价天然是一对,放在一行方便比对、方便算点差。mid_rate用于基准价/折算价/中间价。- 把
trade_date和snap_time都独立出来,是因为同一交易日会有多次快照,而财务对账通常按交易日+时间点双重定位。 - 精度用 DECIMAL(18,6)。汇率小数位很多,尤其日元对人民币经常是0.0467这种,6位小数基本够用。
3. 数据源选型与采集策略:官方源、银行官网、第三方兜底
3.1 中间价:每个交易日只发布一次,别当实时汇率用
人民币中间价是中国外汇交易中心在每个交易日早上9:15发布的,是当天人民币兑主要货币的基准价格。它一天只发布一次,不存在"实时变动"。很多人把中间价当成实时汇率来用,这是概念错了。
我的采集策略:每个交易日9:16(留1分钟容错)定时拉取一次,并做二次校验——跟昨天的中间价对比,如果波动超过设定阈值(比如1%),就告警而不是直接入库,防止抓到半截数据或者解析错位。
3.2 银行牌价:各家官网结构不同,解析器要解耦
银行牌价的来源是各银行的官方网站外汇牌价栏目。中国银行、工商银行、建设银行、农业银行、交通银行、招商银行、浦发银行这些都有公开页面。工程上要给每个银行都写一套解析器,但页面结构各不相同,有的给JSON,有的给HTML表格,还有的给JS拼接的字符串。
我的做法是给每家银行写独立的 fetcher 类,统一封装成内部结构,内部结构跟前面那张快照表一一对应。这样哪家改版了,只需要改那家的解析器,不影响其他家。每家银行每小时检查一次上游字段结构是否完整,一旦发现解析出的字段缺失,立刻切换备用源并告警。
注意:这里说的都是各家银行对外公开的牌价数据,用途是展示和换算参考。采集频率要克制,别做高频轮询,更别绕人家的访问限制。被对方封IP是小事,给业务带来合规风险就得不偿失了。
3.3 采集频率、重试与故障兜底链路
采集频率不要拍脑袋,按数据本身的变动规律来定:
| 数据 | 频率 | 说明 |
|---|---|---|
| 中间价 | 交易日09:16一次 | 一天就一个数 |
| 银行牌价 | 交易时段内每60秒 | 银行牌价跟随市场波动,分钟级够了 |
| 历史归档 | 每日收盘后 | 生成当日最后一个快照归档 |
上线后一定会遇到"某家银行页面改版,采集连续失败"的情况。我的兜底链路是:主源失败 → 等下一个周期重试3次 → 切换备用第三方源(比如国内的数据服务平台,或者海外公开汇率源)→ 仍然失败则保留上一个快照,并在API响应里标记 is_stale: true。宁可给旧数据并明说,也不能给一个编造的新数据。
4. 接口设计:换算、牌价、历史数据怎么对外暴露
4.1 换算接口的 mode 设计:让调用方自己指定价格口径
实时换算是第一个要做的接口。但"实时换算"这四个字,在不同业务语境下含义不同。个人用户想看到的是中间价换算;企业财务想用的是某家银行某个牌价;订单结算想用的是"成交时刻的快照价"。所以在 convert 接口里,我设计了一个 mode 参数,让调用方明确指定价格口径:
code复制GET /api/v1/convert?from=USD&to=CNY&amount=100&bank=CMB&mode=sell_tt
参数含义:
bank:指定银行代码,不传默认用CFETS中间价。mode:mid(中间价)、buy_tt(现汇买入)、buy_cash(现钞买入)、sell_tt(现汇卖出)、sell_cash(现钞卖出)。
这个设计的直接好处是:前端一个下拉框就能让用户自由切换"我用哪个价格算",而后端逻辑完全一致,不用为每种场景写一套接口。响应示例:
json复制{
"code": 0,
"message": "success",
"data": {
"from": "USD",
"to": "CNY",
"amount": 100,
"converted": 724.58,
"rate": 7.2458,
"rate_type": "sell_tt",
"bank": "CMB",
"data_time": "2025-04-08 14:30:00",
"is_stale": false
},
"request_id": "req_8f8f0a2b"
}
注意 data_time 是汇率数据的快照时间,不是服务器当前时间。财务系统能通过这个字段确认他们用的价格对应哪个时点。request_id 是日志追踪用的,排查线上问题全靠它。
4.2 周边端点与统一响应、错误码规范
除了换算,我按 RESTful 规范补齐了下面几个端点:
