记录下海外第三方支付开发的一些关键点, 常见的海外第三方开发推荐按照以下方式处理.
时间戳记录
统一采用 UTC 的时间戳处理, 避免采用服务器地区时间戳导致的跨时区异常, 日常使用的时间戳获取:
- Java:
System.currentTimeMillis() - Python:
round(time.time() * 1000) - .Net:
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - PHP:
round(microtime(true) * 1000)
并且推荐时间戳记录记录在数据库当中采用毫秒级, 可以精确到具体高精度时间.
唯一订单号
这里推荐数据库的订单号类型为 varchar(32) 或者 varchar(64) 且可以设置为数据库表主键, 具体生成时间相关格式订单如下:
/**
* PHP生成和时间关联尽可能防止碰撞的订单号
* @return string
*/
function order(): string {
$microtime = microtime(true);
$millisecond = round($microtime * 1000); // 获取毫秒
$microseconds = $microtime - intval($microtime); // 获取微秒段
$pos1 = sprintf("%04d", $microseconds * 10000); // 提取4位毫秒
$pos2 = sprintf("%04d", mt_rand(1, 9999)); // 随机提取 0001-9999
$pos3 = sprintf("%04d", mt_rand(1, 9999));// 随机提取 0001-9999
$pos4 = sprintf("%04d", mt_rand(1, 9999));// 随机提取 0001-9999
$pos5 = sprintf("%02d", mt_rand(1, 99));// 随机提取 01-99
return date('YmdHis', intval($microtime)) . $pos1 . $pos2 . $pos3 . $pos4 . $pos5;
}
// 生成的格式 20250810081008593060373904554178
实际正式环境下生成的订单ID碰撞几率特别小, 并且因为提取的是 YmdHis + microseconds,
数据插入的顺序也是比较符合递增数据的日常直觉.
可以避免被嗅探到具体订单量, 如果采用递增 order_id, 可能会被接口抓取到真实交易量;
比如接口早上抓起递增订单 order_id=1001, 晚上再抓取到 order_id=1005 就可以知道当天交易数量.
结算地区与货币
需要注意面向海外的订单货币和国家地区是不一致的, 所以需要记录订单发起的结算地区和结算货币:
order_region: 发起订单结算地区,US|HK|CN等order_currency: 发起订单结算货币, 如USD|HKD|CNY等settlement_region: 支付完成第三方通知结算的地区settlement_currency: 支付完成第三方通知结算的货币
# 基础的支付信息, 支持跨境支付
create table if not exists order_info
(
order_id varchar(64) not null comment '订单标识',
# 订单支付信息
order_region varchar(3) not null comment '订单发起支付的地区, 如 CN|US|HK 等',
order_currency varchar(3) not null comment '订单发起支付的货币, 如 CNY|USD|HKD 等',
# 订单结算信息
settlement_region varchar(3) not null default '' comment '订单结算支付的地区, 如 CN|US 等',
settlement_currency varchar(3) not null default '' comment '订单结算支付的货币, 如 CNY|USD 等',
# 其他略
primary key (order_id)
) comment '支付订单信息表'
engine = InnoDB
charset = utf8mb4
collate = utf8mb4_unicode_ci;
如果单纯某个地区就不需要记录这些字段, 如果全球化处理就需要记录管理这方面的数据.
最小货币单位
国内对于货币处理一般是 1元人民币 = 100分, 在数据库之中记录的是以分为单位做处理;
而在海外也是一样处理, 但是有比较特殊的细微之处, 那就是最小货币单位.
这里整理海外支付的最小单位取值, 推荐做海外支付项目的时候采用:
| 货币代码 | 最小单位(小数点后位数) | 金额中的值示例 |
|---|---|---|
| AUD | 分(2) | 1.00 AUD 需设置为 “value:100” |
| BDT | 分(2) | 1.00 BDT 需设置为 “value:100” |
| BRL | 分(2) | 1.00 BRL 需设置为 “value:100” |
| CAD | 分(2) | 1.00 CAD 需设置为 “value:100” |
| CLP | 分(0) | 1 CLP 需设置为 “value:1” |
| CNY | 分(2) | 1.00 CNY 需设置为 “value:100” |
| EUR | 分(2) | 1.00 EUR 需设置为 “value:100” |
| GBP | 分(2) | 1.00 GBP 需设置为 “value:100” |
| HKD | 分(2) | 1.00 HKD 需设置为 “value:100” |
| IDR | 美分(2) | 1.00 IDR 需设置为 “value:100” |
| JPY | 元(0) | 1 JPY 需设置为 “value:1” |
| KRW | 元(0) | 1 KRW 需设置为 “value:1” |
| MXN | 分(2) | 1.00 MXN 需设置为 “value:100” |
| MYR | 分(2) | 1.00 MYR 需设置为 “value:100” |
| NZD | 分(2) | 1.00 NZD 需设置为 “value:100” |
| PEN | 分(2) | 1.00 PEN 需设置为 “value:100” |
| PHP | 美分(2) | 1.00 PHP 需设置为 “value:100” |
| PKR | 分(2) | 1.00 PKR 需设置为 “value:100” |
| PLN | 分(2) | 1.00 PLN 需设置为 “value:100” |
| SGD | 分(2) | 1.00 SGD 需设置为 “value:100” |
| THB | 分(2) | 1.00 THB 需设置为 “value:100” |
| TWD | 分(2) | 1.00 TWD 需设置为 “value:100” |
| USD | 美分(2) | 1.00 USD 需设置为 “value:100” |
| VND | 分(0) | 1 VND 需设置为 “value:1” |
这里可以看到比较特殊的 JPY|KRW 采用的最小货币单位还是 日元|韩元, 数据库取值的是 1:1 结算;
在数据库当中保存也需要处理下, 不像之前单一地区那样仅仅记录以 分 为单位, 而是需要扩展成更加精细的格式:
# 基础的支付信息, 支持跨境支付
create table if not exists order_info
(
order_id varchar(64) not null comment '订单标识',
# 订单支付信息
order_region varchar(3) not null comment '订单发起支付的地区, 如 CN|US|HK 等',
order_currency varchar(3) not null comment '订单发起支付的货币, 如 CNY|USD|HKD 等',
order_unit tinyint unsigned not null default '2' comment '订单发起的最小单位, 即小数点后位数',
order_amount bigint unsigned not null comment '订单发起金额, 需要采用最小货币单位',
# 订单结算信息
settlement_region varchar(3) not null default '' comment '订单结算支付的地区, 如 CN|US 等',
settlement_currency varchar(3) not null default '' comment '订单结算支付的货币, 如 CNY|USD 等',
settlement_unit tinyint unsigned not null default '2' comment '订单结算货币的最小单位, 即小数点后位数',
settlement_amount bigint unsigned not null comment '订单结算金额, 需要采用最小货币单位',
# 其他略
primary key (order_id)
) comment '支付订单信息表'
engine = InnoDB
charset = utf8mb4
collate = utf8mb4_unicode_ci;
# 假设目前用户支付 100 日元, 获取用日元最小货币金额
# 计算公式为 最小货币数值 = 结算金额 * (10 ^ 最小单位)
SELECT (settlement_amount * POW(10, settlement_unit)) as amount
FROM order_info
WHERE settlement_currency = 'JPY'
# 如果先直接转化为元单位, 比如人民币的 分 -> 元 方式就可以按照下面处理
SELECT (settlement_amount / POW(10, settlement_unit)) as amount
FROM order_info
WHERE settlement_currency = 'CNY'
- 最小货币计算公式:
amount * (10 ^ 最小单位) = 最小货币数值 - 最小货币转元单位:
amount / (10 ^ 最小单位) = 货币标准数值
之所以会有最小货币单位概念, 那是因为在某些国家地区的最小金额并不支持 0.5 的单位,
比如 日元|韩元 这种是没有分概念的, 0.5日元|韩元 在第三方支付系统当中是不允许的.
多机部署
如果同个第三方支付聚合大量商户, 比如在海外阿里支付的 AtomPay 之中创建多个商户账号用于支付的情况,
一定要做好多台服务器请求隔离防止因为某个支付商户问题导致服务器被 '连坐'.
外国的第三方支付商户出问题有的会导致服务器IP所有商户连带被封, 所以部署支付服务器的时候不要怕投入成本.
哪怕是 1CPU + 1G内存 的分别搭建同家第三方支付商户SDK服务, 也不要把相同第三方SDK服务部署在同个高性能服务器.
这是个血泪教训, 如果某个第三方支付SDK查的严, 商户账号违规连坐就需要外国服务商上扯皮, 时区+沟通的成本远远大于云服务多部署台服务器
比较合理的情况聚合支付商的架构部署如下, 这种架构最为稳妥和安全:
接入第三方支付A商户服务器 -----> 服务器A[172.1.1.1] -------
|
接入第三方支付B商户服务器 -----> 服务器B[172.1.1.2] ---------- 云数据库[内网地址访问入库]
|
接入第三方支付C商户服务器 -----> 服务器C[172.1.1.3] -------
同理还有域名方面也要单独隔离处理, 别用二级域名而是直接完全全新服务器设置解析
如果真的要做聚合支付的功能, 这方面服务器成本投入一定不能少.
信用卡支付
这是海外做信用卡支付更大的坑, 需要注意海外信用卡支付有以下特征:
- 结算周期长(按月结算)
- 跨境风控(洗钱)
- 冻结挂失(直接拒绝)
- 过期失效(换卡周期)
哪怕是国外信用卡采用 3DS(3-D Secure), 这是为防范网络欺诈、实现责任转移而设计的身份验证协议,
用于对持卡人支付做安全责任转移; 当时实际上这种情况还是会出现订单异常问题:
- 部分地方银行支持版本不一样(信用卡银行仅支持
3DS1.0, 最新版本2.0+不兼容) - 欧美部分国家地区因为隐私问题会被限制不允许启用(3DS验证会涉及用户设备和地理信息上传验证)
所以有时候海外信用卡支付对账异常的情况, 可能和作为开发人员的服务端是没有关系的, 完全就是因为信用卡本身就是很特殊支付方式.