多币种独立站最容易亏钱的地方不是支付通道费,是汇率。商品页按当前汇率折出 39.99 美元,客户下单三天后发货,财务按发货当天的汇率把货款折回人民币核成本,两边一对,毛利差了近两个点,谁也说不清差在哪个环节。问题通常不是汇率源不准,而是同一个订单在系统里用了不止一个汇率。
下面按"取价 → 锁价 → 结算 → 退款"的顺序,把汇率快照该落在哪张表、哪个字段讲清楚。报文和成功率数据都来自实际调用与后台统计。
一、三个时点,三种语义
报价时点:列表页、详情页展示价格。这时用实时汇率没问题,因为价格只是参考值,客户不会拿它当合同。
下单时点:订单成立那一刻。必须生成一条不可变的汇率快照,订单引用它的主键,之后任何环节都不得再取"最新汇率"。
结算时点:支付网关实际入账、采购实际付款。这时只记录真实发生的金额和币种,不再做任何汇率换算。
把三个时点混在一起的典型症状是:订单表里存了一个 rate 字段,退款时有人按当日汇率改它,运营手工调价时顺手把它也改了。存"值"必然被改,所以订单表只存 rate_id------这是本文最重要的一条设计约束,比选哪个汇率源重要得多。
二、商品侧取价:先把人民币口径固定
汇率再准,如果人民币成本本身就是错的,后面对什么都没意义。1688 商品详情的取价请求如下:
GET https://api-gw.onebound.cn/1688/item_get/?key=<key>&secret=<secret>
&num_iid=685532109876&cache=no&result_type=json&lang=cn
公共参数四件套在每个接口上语义一致,cache 建议在核价场景一律传 no:
| 参数 | 必填 | 说明 |
|---|---|---|
| key / secret | 是 | 控制台分配的调用凭证 |
| api_name | 否 | 部分网关按 api_name 路由,与路径重复时以路径为准 |
| cache | 否 | yes 走网关侧快照,no 强制回源 |
| result_type | 否 | json / jsonu(转义 unicode)/ xml |
| lang | 否 | cn / en,影响文案类字段,不影响数值 |
返回里与报价相关的只有几个字段:
json
{
"item": {
"num_iid": "685532109876",
"title": "304不锈钢保温杯 500ml 双层真空",
"price": "12.50-18.00",
"orginal_price": "26.00",
"min_order_quantity": "2",
"price_range": "2-99:18.00;100-499:15.00;500-:12.50",
"skus": [
{"sku_id": "5xxx1", "properties": "容量:500ml;颜色:哑黑", "price": "18.00", "quantity": "8642"}
]
},
"secache": "...",
"error_code": "0000"
}
三个口径必须注意。price 是区间字符串不是数字,直接 parseFloat 会得到 12.5,把小批量客户的成本算低三成;orginal_price 的拼写沿用了上游命名,写代码时别手改成 original,字段名对不上会静默取到 null,而 null 参与运算在多数语言里不会报错,只会让毛利变成 NaN;min_order_quantity 与 price_range 决定真实单价,报价公式里必须先按起订量落档,再乘汇率。
三、汇率快照表
sql
CREATE TABLE fx_rate_snapshot (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
base_currency CHAR(3) NOT NULL,
quote_currency CHAR(3) NOT NULL,
rate DECIMAL(18,8) NOT NULL,
source VARCHAR(24) NOT NULL,
effective_minute DATETIME NOT NULL,
captured_at DATETIME NOT NULL,
effective_at DATETIME NOT NULL,
UNIQUE KEY uk_rate (base_currency, quote_currency, source, effective_minute)
);
三条要点。rate 用 DECIMAL(18,8) 不用 DOUBLE,0.0001 的误差在十万美元订单上是十美元,而浮点误差会随乘法放大。唯一键包含 effective_minute,同一分钟内所有订单复用同一条快照------如果每笔订单都实时抓一次,同一天两笔订单的汇率会差在小数点后四位,财务对账时会先怀疑数据错。captured_at 和 effective_at 要分开存:前者是抓取时刻,后者是汇率源声明的生效时刻,跨境结算里"周五收盘价用到周一"是常态,两个字段差三天完全正常。
字段口径这一块我是对着控制台的返回示例逐条核的,测试用的 key 在万邦开放平台的控制台申请,入口放这里 https://console.open.onebound.cn/console/?i=article2026-10-17-1 ,需要的自取。
四、订单绑定与退款回退
python
def create_order(item, qty, currency, db):
snap = db.fx.pick_latest("CNY", currency) # 取最近一条,缺则抛错
unit_cny = decimal.Decimal(item["price_range"].split(";")[-1].split(":")[1])
goods_cny = unit_cny * qty
return db.order.insert({
"goods_amount_cny": goods_cny,
"currency": currency,
"rate_id": snap["id"], # 只存 id,不存 rate
"amount_foreign": (goods_cny * snap["rate"]).quantize(
decimal.Decimal("0.01"), rounding=decimal.ROUND_HALF_UP)
})
def refund(order, db):
snap = db.fx.get(order["rate_id"]) # 原单快照,不是当日汇率
return (order["amount_foreign"] / snap["rate"]).quantize(
decimal.Decimal("0.01"), rounding=decimal.ROUND_HALF_UP)
退款必须按原单 rate_id 折算。若按当日汇率退,等于客户在你这里做了一笔无风险的外汇买卖:汇率下跌时退款更划算,汇率上涨时他申请退款再重新下单。这种情况真实业务里不常见,但一旦碰上大额订单,损失是直接的,而且客服很难跟客户解释"为什么退款金额和付款金额不一样"。
五、把汇兑差异做成一张表
汇兑损益 = 实际入账人民币 − 订单外币金额 ÷ 锁定汇率。按月、按币种汇总,超过 0.3% GMV 就说明快照采集频率太低或者某个币种用了错误的源。这张表的价值不在财务口径,而在工程侧:它能反过来告诉你哪条链路在偷偷用实时汇率。
六、小数位与展示币种
| 币种 | 小数位 | 说明 |
|---|---|---|
| JPY / KRW / IDR / VND | 0 | 无小数,向上取整到整数 |
| TWD | 0 | 台币报价习惯取整 |
| USD / EUR / GBP / AUD / CAD / SGD / MYR / THB / PHP | 2 | 标准两位 |
舍入统一用 ROUND_HALF_UP,别用 Python 内置 round()------它是银行家舍入,round(2.5) 得 2,round(3.5) 得 4,报价时会出现"同一件商品买两件比买一件单价更低"的诡异现象。
另外要把 display_currency 和 settle_currency 分开。面向日本站展示 JPY,但支付通道只支持 USD 结算,中间会多出一次折算,这次折算同样要落快照,否则又回到"一个订单两个汇率"。
七、监控三个指标
快照缺失率(下单时取不到快照直接拒绝,不要降级成实时汇率)、单小时汇率跳变超过 2% 的告警(区分真实行情和源站异常)、订单 rate_id 空值率(正常应恒为 0)。商品侧本身是稳定的,1688 的 item_get / item_get_app / item_get_pro / custom 后台统计都是 99%,cat_get 97%,item_fee 96%,item_search 53%。也就是说取价链路的抖动基本来自外部汇率源和支付通道,把监控盯在这两处比盯着商品接口有效。
顺带记一下计费口径,它决定重试策略:0000(成功)与 2000(无结果)计费,4000(参数错)、4001(凭证错)、4002(并发超限)、4017 不计费。核价失败时先看错误码再决定要不要重试------4000 类重试一万次也不会成功,只会把队列堵住,而 4002 需要的是退避而不是加机器。
同日还写了一篇网关 cache 参数与多级缓存的,做成本优化时可以接着看。