微信/支付宝:两大巨头的对接差异
同样的"扫码支付",背后却是两套完全不同的世界观
写在前面
做过支付对接的同学都知道,微信和支付宝虽然都是"扫一扫就付钱",但当你真正开始对接时,会发现这两个平台的设计理念差异巨大。
就像同样是"盖房子",一个是精装修交付(微信),一个是毛坯房自己装(支付宝)。各有优劣,也各有坑点。
今天我们来聊聊这两大支付平台的核心差异,帮你在对接时少踩几个坑 🐛
一、接入流程:两种风格的开局体验
微信支付:严格的"入职流程"
微信支付的接入,就像大厂入职——审核严格、流程规范、资料要全。
- 营业执照(必须是最新年检的)
- 组织机构代码证(或统一社会信用代码证)
- 法人身份证正反面
- 对公账户(还要打款验证,金额几分钱)
- 经营场景说明(详细描述你的业务)
- 有时候还需要 ICP 备案截图
审核周期通常 1-5 个工作日,期间还可能被打回补充材料。常见被拒原因:
- 经营范围不包含相关业务
- 网站内容与申请描述不符
- 资质照片不清晰
- 法人信息不一致
- 商户号(mch_id)
- API 密钥(用于签名)
- API 证书(用于敏感操作)
- 各种支付权限(根据申请的类目)
支付宝:灵活的"快车道"
支付宝的接入相对灵活,支持多种商户类型。
- 可以用"支付宝个人支付"快速接入
- 门槛低,但功能有限
- 适合小额、非商业场景
- 走标准流程,但也比微信宽松
- 资质要求会根据业务场景调整
- 小额高频的场景,门槛相对低一些
- 即时到账 - T+0 结算,适合高频小额
- 担保交易 - 有确认收货流程,适合电商
- 双功能支付 - 用户可选即时到账或担保
| 维度 | 微信支付 | 支付宝 |
|---|---|---|
| 审核严格度 | 高,资料要求齐全 | 中等,根据场景调整 |
| 审核周期 | 1-5 个工作日 | 1-3 个工作日 |
| 个人接入 | 不支持 | 支持(功能受限) |
| 资质要求 | 营业执照必须 | 部分场景可灵活 |
| 开通速度 | 慢但规范 | 快但需自己把握 |
[配图建议:对比图 - 左边是微信的"严格审核"流程图,右边是支付宝的"灵活接入"流程图]
二、文档风格:说明书 vs 教科书
微信文档:精简的说明书
微信的文档像产品说明书——够用,但不多。
- 讲清楚了"怎么调用"
- 但"为什么这样设计"讲得少
- 示例代码够用,但不够丰富
- 错误码说明简略,需要自己摸索
// 微信的错误码说明
错误码:SYSTEMERROR
描述:系统错误
解决方案:请使用相同参数稍后重新调用
// 程序员:...所以具体是什么错误?
支付宝文档:详尽的教科书
支付宝的文档更像教科书——全面、系统、有理论深度。
- 不仅有接口文档,还有业务场景说明
- 风控策略、安全机制讲得比较透
- 示例代码丰富,Java/PHP/Python/.NET/Node.js 都有
- 有完整的"开放平台",包含最佳实践
// 支付宝的错误码说明
错误码:ACQ.TRADE_NOT_EXIST
描述:交易不存在
解决方案:
1. 检查商户订单号是否正确
2. 确认该订单是否已经发起支付
3. 如果使用外部商户号,请确认映射关系
4. 联系技术支持提供完整的请求参数和响应
| 维度 | 微信支付 | 支付宝 |
|---|---|---|
| 文档完整度 | 中等 | 高 |
| 代码示例 | 基础 | 丰富 |
| 错误码说明 | 简略 | 详细 |
| 搜索体验 | 一般 | 较好 |
| 最佳实践 | 少 | 多 |
| 更新及时性 | 较好 | 一般 |
三、微信支付的"四大金刚"
微信支付提供了多种支付方式,每种都有特定的使用场景:
JSAPI 支付:公众号里的"原住民"
- 用户在微信内打开 H5 页面
- 通过 OAuth2.0 获取用户的 openid
- 调用微信 JS-SDK 的
chooseWXPay接口 - 唤起微信支付界面
- 用户完成支付后,回到原页面
// 前端调用微信支付
function onBridgeReady() {
WeixinJSBridge.invoke('getBrandWCPayRequest', {
"appId": "wx2421b700c0", // 公众号名称
"timeStamp": "1395712654", // 时间戳
"nonceStr": "e61463f8efa", // 随机串
"package": "prepay_id=wx212", // 预支付交易会话标识
"signType": "MD5", // 签名方式
"paySign": "70EA570631E4" // 签名
}, function(res) {
if (res.err_msg == "get_brand_wcpay_request:ok") {
// 支付成功
}
});
}
- 需要获取用户的 openid(这是最大的门槛)
- 只能在微信环境内使用
- 体验最流畅,不需要跳转
- 需要配置支付授权目录
- 同一个用户,不同公众号的 openid 不同
- 需要统一用户身份?用开放平台的 unionid
- 获取 openid 需要网页授权,有用户同意流程
H5 支付:跳出微信的"自由人"
- 商户后台调用统一下单接口,获取支付链接
- 前端跳转到微信提供的中间页
- 中间页唤起微信 App
- 用户在微信内完成支付
- 支付完成后跳回商户页面(需设置 referer)
{
"type": "WAP",
"wap_url": "https://www.example.com", // WAP网站URL
"wap_name": "某某游戏" // WAP网站名称
}
- 需要跳转到微信 App 完成
- 适合非微信环境
- 有一定的风控要求(需要设置正确的 referer)
- 在微信内打开会被拦截(微信内应该用 JSAPI)
- "商家参数格式有误" - referer 设置问题
- "网络环境未能通过安全验证" - IP 白名单问题
- 在微信内被拦截 - 应该用 JSAPI 而不是 H5
小程序支付:生态内的"特权阶级"
- 小程序调用
wx.login()获取 code - 后端用 code 换取 openid 和 session_key
- 后端调用统一下单接口,获取 prepay_id
- 前端调用
wx.requestPayment()唤起支付
// 小程序端
wx.requestPayment({
timeStamp: '', // 时间戳
nonceStr: '', // 随机字符串
package: '', // 统一下单接口返回的 prepay_id 参数值
signType: 'MD5', // 签名算法
paySign: '', // 签名
success(res) {
// 支付成功
},
fail(res) {
// 支付失败
}
})
- 体验最好,完全无感
- 需要小程序和支付商户号绑定
- 只能在小程序内使用
- openid 获取更简单(通过 wx.login)
- 小程序和商户号需要同主体,或者有关联关系
- 需要在商户平台进行绑定操作
- 一个商户号可以绑定多个小程序
APP 支付:独立的"正规军"
- 后端调用统一下单接口,获取预支付交易会话标识
- App 调用微信 SDK,传入参数
- SDK 唤起微信 App
- 用户完成支付后,返回商户 App
// iOS 调用微信支付
PayReq *request = [[[PayReq alloc] init] autorelease];
request.partnerId = @"10000100";
request.prepayId = @"wx2016051";
request.nonceStr = @"a32ef42";
request.timeStamp = 1493852427;
request.package = @"Sign=WXPay";
request.sign = @"C380BEC2BFD727A4B6845133519F3AD6";
[WXApi sendReq:request];
// Android 调用微信支付
IWXAPI api = WXAPIFactory.createWXAPI(this, null);
api.registerApp(Constants.APP_ID);
PayReq request = new PayReq();
request.appId = Constants.APP_ID;
request.partnerId = "10000100";
request.prepayId = "wx2016051";
request.nonceStr = "a32ef42";
request.timeStamp = "1493852427";
request.packageValue = "Sign=WXPay";
request.sign = "C380BEC2BFD727A4B6845133519F3AD6";
api.sendReq(request);
- 需要在微信开放平台注册 App
- 用户需要安装微信客户端
- 体验相对流畅
- 需要处理微信未安装的情况
- 在微信开放平台创建移动应用
- 提交应用审核(需要软著等资质)
- 审核通过后获得 AppID
- 还需要在商户平台关联 AppID
| 支付方式 | 使用场景 | openid要求 | 跳转 | 体验评分 |
|---|---|---|---|---|
| JSAPI | 微信内H5 | 必须 | 无 | ⭐⭐⭐⭐⭐ |
| H5 | 微信外 | 不需要 | 有 | ⭐⭐⭐ |
| 小程序 | 小程序内 | 必须 | 无 | ⭐⭐⭐⭐⭐ |
| APP | 独立App | 不需要 | 有 | ⭐⭐⭐⭐ |
[配图建议:四种支付方式的场景对比图,标注各自的使用环境]
四、支付宝的"三剑客"
支付宝的支付方式也有自己的特色:
手机网站支付:Web 时代的经典
- 商户后端调用
alipay.trade.wap.pay接口 - 获取支付表单(HTML form)
- 前端自动提交表单,跳转到支付宝
- 用户完成支付后,跳回商户页面
// Java 示例
AlipayClient alipayClient = new DefaultAlipayClient(
"https://openapi.alipay.com/gateway.do",
APP_ID,
APP_PRIVATE_KEY,
"json",
"UTF-8",
ALIPAY_PUBLIC_KEY,
"RSA2"
);
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
request.setReturnUrl("https://www.example.com/return");
request.setNotifyUrl("https://www.example.com/notify");
request.setBizContent("{" +
"\"out_trade_no\":\"20231001001\"," +
"\"total_amount\":88.88," +
"\"subject\":\"游戏充值\"," +
"\"product_code\":\"QUICK_WAP_WAY\"" +
"}");
String form = alipayClient.pageExecute(request).getBody();
// 返回 form 给前端,前端自动提交
- 支持在手机浏览器中唤起支付宝 App
- 也可以在支付宝内完成支付
- 跨平台兼容性好
- 适合 PC 端用户(扫码支付)
APP 支付:移动优先的设计
- 后端调用
alipay.trade.app.pay接口 - 获取支付订单字符串
- App 调用支付宝 SDK,传入订单字符串
- SDK 唤起支付宝 App 或内嵌 H5
// iOS 调用支付宝支付
NSString *orderString = @"app_id=2021001&biz_content=...";
[[AlipaySDK defaultService] payOrder:orderString
fromScheme:@"yourappscheme"
callback:^(NSDictionary *resultDict) {
// 处理支付结果
}];
// Android 调用支付宝支付
String orderString = "app_id=2021001&biz_content=...";
PayTask alipay = new PayTask(MainActivity.this);
Map<String, String> result = alipay.payV2(orderString, true);
// 处理支付结果
- 需要集成支付宝 SDK
- 支持唤起支付宝 App 或内嵌 H5
- 用户体验取决于集成方式
- 支付宝 App 未安装时会降级到 H5
当面付:线下场景的利器
- 付款码支付: 用户出示付款码,商家扫码收款
- 扫码支付: 商家生成二维码,用户扫码付款
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
request.setNotifyUrl("https://www.example.com/notify");
request.setBizContent("{" +
"\"out_trade_no\":\"20231001001\"," +
"\"total_amount\":88.88," +
"\"subject\":\"游戏充值\"" +
"}");
AlipayTradePrecreateResponse response = alipayClient.execute(request);
String qrCode = response.getQrCode();
// 将 qrCode 生成二维码图片
- 适合线下场景
- 也可以用于 PC 端扫码支付
- 二维码有效期较长(可配置)
- 支持轮询查询支付状态
生活号支付:生态内的"私域流量"
- 类似微信公众号
- 可以沉淀用户关系
- 适合长期运营
- 支持消息推送
| 支付方式 | 使用场景 | SDK要求 | 跳转 | 体验评分 |
|---|---|---|---|---|
| 手机网站支付 | H5/PC | 不需要 | 有 | ⭐⭐⭐⭐ |
| APP支付 | 独立App | 需要 | 有 | ⭐⭐⭐⭐ |
| 当面付 | 线下/PC | 不需要 | 无 | ⭐⭐⭐⭐ |
| 生活号支付 | 生活号内 | 不需要 | 无 | ⭐⭐⭐⭐⭐ |
五、签名机制:MD5 vs RSA 的哲学差异
这是两个平台最核心的技术差异之一,也是新手最容易踩坑的地方。
微信:MD5/HMAC-SHA256 签名——简单直接
1. 将所有非空参数按字典序排序
2. 用 & 连接成 key=value 格式
3. 最后拼接 &key=商户密钥
4. 对整个字符串做 MD5 运算
5. 转换为大写
原始参数:
appid=wxd930ea5705
mch_id=10000100
device_info=1000
body=test
nonce_str=ibuaiVcKdpRxkhJA
步骤1:字典序排序后拼接
stringA = "appid=wxd930ea5705&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA"
步骤2:拼接密钥
stringSignTemp = stringA + "&key=192006250b4c09247ec02e" // key 为商户密钥
步骤3:MD5 运算并转大写
sign = MD5(stringSignTemp).toUpperCase() = "9A0A8659F005D6984697E2CA0A9CF3B7"
function MakeSign($params, $key) {
// 1. 按字典序排序
ksort($params);
// 2. 拼接字符串
$string = '';
foreach ($params as $k => $v) {
if ($k != 'sign' && $v != '' && !is_array($v)) {
$string .= $k . '=' . $v . '&';
}
}
$string = trim($string, '&');
// 3. 拼接密钥
$string = $string . '&key=' . $key;
// 4. MD5 运算
$string = md5($string);
// 5. 转大写
$result = strtoupper($string);
return $result;
}
// 步骤3 改为
$string = hash_hmac('sha256', $string, $key);
- 实现简单,几行代码搞定
- 计算快速,性能好
- 安全性依赖密钥保管
- 密钥泄露后风险较大
支付宝:RSA2 签名——非对称的艺术
1. 商户生成 RSA 密钥对(公钥+私钥)
2. 把公钥上传到支付宝
3. 支付宝生成支付宝公钥(不同于商户公钥)
4. 商户用私钥对请求参数签名
5. 支付宝用商户公钥验证签名
6. 支付宝用支付宝私钥对响应签名
7. 商户用支付宝公钥验证响应
# 使用 OpenSSL 生成 RSA 密钥对
openssl genrsa -out app_private_key.pem 2048
openssl rsa -in app_private_key.pem -pubout -out app_public_key.pem
# 查看私钥
cat app_private_key.pem
# 查看公钥
cat app_public_key.pem
// 构造待签名字符串
String content = getSignContent(params);
// 使用私钥签名
PrivateKey privateKey = getPrivateKeyFromPem(privateKeyStr);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(content.getBytes("UTF-8"));
byte[] signBytes = signature.sign();
// Base64 编码
String sign = Base64.getEncoder().encodeToString(signBytes);
// 使用支付宝公钥验证签名
PublicKey publicKey = getPublicKeyFromPem(alipayPublicKeyStr);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(content.getBytes("UTF-8"));
boolean verified = signature.verify(Base64.getDecoder().decode(sign));
- 安全性更高,私钥不需要传输
- 实现复杂,需要理解非对称加密
- 密钥管理要求严格
- 签名计算耗时更长
两种签名机制对比
| 维度 | 微信 MD5/HMAC-SHA256 | 支付宝 RSA2 |
|---|---|---|
| 加密类型 | 对称加密 | 非对称加密 |
| 密钥管理 | 双方共享密钥 | 公私钥分离 |
| 实现复杂度 | 简单 | 复杂 |
| 安全级别 | 中等 | 高 |
| 性能 | 快 | 较慢 |
| 密钥泄露风险 | 高 | 低(私钥不传输) |
| 适用场景 | 内部系统、可信环境 | 开放平台、高安全要求 |
- 微信是对称加密,双方共享密钥,密钥泄露风险较大
- 支付宝是非对称加密,公私钥分离,安全性更高
- 微信签名计算快,适合高并发场景
- 支付宝签名更安全,适合开放平台
- 微信:密钥要严格保密,定期更换
- 支付宝:私钥绝对不能泄露,公钥可以公开
- 两者都需要验证回调签名,防止伪造请求
- 建议使用官方 SDK,避免自己实现签名算法
[配图建议:签名机制对比图,左边是 MD5 的对称加密流程,右边是 RSA 的非对称加密流程]
六、回调处理:通知的"性格差异"
支付完成后,平台会通知你的服务器。这块的设计也很有意思。
微信回调:话痨型通知
15s, 15s, 30s, 3m, 10m, 20m, 30m, 30m, 30m, 60m, 3h, 3h, 3h, 6h, 6h
总共通知 15 次,持续约 24 小时。
<xml>
<appid>wx2421b700c0</appid>
<attach>支付测试</attach>
<bank_type>CFT</bank_type>
<fee_type>CNY</fee_type>
<is_subscribe>N</is_subscribe>
<mch_id>10000100</mch_id>
<nonce_str>5d2b6c2a8db53831f7eda20af46e531c</nonce_str>
<openid>oUpF8uMEb4qRXf22hE3X68TekukE</openid>
<out_trade_no>1409811653</out_trade_no>
<result_code>SUCCESS</result_code>
<return_code>SUCCESS</return_code>
<sign>594B6D97F089D24D683F600E71E9</sign>
<time_end>20140903131540</time_end>
<total_fee>1</total_fee>
<trade_type>JSAPI</trade_type>
<transaction_id>1004400740201409030005092168</transaction_id>
</xml>
<!-- 成功响应 -->
<xml>
<return_code>SUCCESS</return_code>
<return_msg>OK</return_msg>
</xml>
<!-- 失败响应(会继续重试) -->
<xml>
<return_code>FAIL</return_code>
<return_msg>签名失败</return_msg>
</xml>
支付宝回调:稳重派通知
0s, 2m, 10m, 10m, 1h, 2h, 6h, 15h
最多通知 8 次,持续约 25 小时。
gmt_create=2023-10-01+12%3A00%3A00
&charset=UTF-8
&gmt_payment=2023-10-01+12%3A00%3A05
¬ify_time=2023-10-01+12%3A00%3A10
&subject=游戏充值
&sign_type=RSA2
&buyer_id=2088101117955611
&out_trade_no=20231001001
&total_amount=88.88
&trade_status=TRADE_SUCCESS
&trade_no=202310012200140895...
&...
&sign=xxxxx
// 成功响应
success
// 失败响应(会继续重试)
failure
回调处理最佳实践
// 微信验签
String sign = params.get("sign");
params.remove("sign");
String calculatedSign = MD5Sign(params, apiKey);
if (!sign.equals(calculatedSign)) {
log.error("微信回调签名验证失败");
return "FAIL";
}
// 支付宝验签
boolean verified = AlipaySignature.rsaCheckV1(
params, alipayPublicKey, charset, signType
);
if (!verified) {
log.error("支付宝回调签名验证失败");
return "failure";
}
// 使用数据库唯一索引或分布式锁
String orderId = params.get("out_trade_no");
String lockKey = "pay:notify:" + orderId;
boolean locked = redisLock.tryLock(lockKey, 30);
if (!locked) {
log.info("订单正在处理中: {}", orderId);
return "success"; // 返回成功,避免重复通知
}
try {
// 检查订单状态
Order order = orderDao.findByOrderId(orderId);
if (order.getStatus() == PAID) {
log.info("订单已处理: {}", orderId);
return "success";
}
// 处理支付成功逻辑
orderService.paySuccess(orderId, ...);
} finally {
redisLock.unlock(lockKey);
}
return "success";
// 记录原始回调数据
PayNotifyLog log = new PayNotifyLog();
log.setOrderId(orderId);
log.setNotifyData(JSON.toJSONString(params));
log.setNotifyTime(new Date());
log.setChannel("wechat"); // 或 "alipay"
payNotifyLogDao.save(log);
| 维度 | 微信支付 | 支付宝 |
|---|---|---|
| 通知次数 | 15 次 | 8 次 |
| 持续时间 | ~24 小时 | ~25 小时 |
| 数据格式 | XML | 表单 |
| 成功响应 | XML 格式 | 纯文本 "success" |
| 重试策略 | 指数退避 | 相对固定间隔 |
- 都需要验证签名,防止伪造
- 都要求幂等处理,防止重复
- 都建议记录日志,便于排查
- 都需要在 5 秒内响应
七、API 详细对比
统一下单接口
| 参数 | 必填 | 说明 |
|---|---|---|
| appid | 是 | 公众号/小程序/AppID |
| mch_id | 是 | 商户号 |
| nonce_str | 是 | 随机字符串 |
| sign | 是 | 签名 |
| body | 是 | 商品描述 |
| out_trade_no | 是 | 商户订单号 |
| total_fee | 是 | 金额(分) |
| spbill_create_ip | 是 | 终端IP |
| notify_url | 是 | 回调地址 |
| trade_type | 是 | JSAPI/H5/NATIVE/APP |
| openid | 否 | 用户标识(JSAPI必填) |
| 参数 | 必填 | 说明 |
|---|---|---|
| app_id | 是 | 应用ID |
| method | 是 | 接口名称 |
| charset | 是 | 编码格式 |
| sign_type | 是 | 签名类型 |
| sign | 是 | 签名 |
| timestamp | 是 | 时间戳 |
| version | 是 | 版本 |
| biz_content | 是 | 业务参数(JSON) |
{
"out_trade_no": "20231001001",
"total_amount": "88.88",
"subject": "游戏充值",
"product_code": "QUICK_WAP_WAY",
"body": "游戏道具-钻石100"
}
订单查询接口
| 参数 | 必填 | 说明 |
|---|---|---|
| appid | 是 | 公众号ID |
| mch_id | 是 | 商户号 |
| nonce_str | 是 | 随机字符串 |
| sign | 是 | 签名 |
| out_trade_no | 二选一 | 商户订单号 |
| transaction_id | 二选一 | 微信订单号 |
| 参数 | 必填 | 说明 |
|---|---|---|
| app_id | 是 | 应用ID |
| method | 是 | alipay.trade.query |
| ... | ... | 公共参数 |
| biz_content | 是 | {"out_trade_no":"xxx"} |
退款接口
| 参数 | 必填 | 说明 |
|---|---|---|
| appid | 是 | 公众号ID |
| mch_id | 是 | 商户号 |
| nonce_str | 是 | 随机字符串 |
| sign | 是 | 签名 |
| out_trade_no | 二选一 | 商户订单号 |
| transaction_id | 二选一 | 微信订单号 |
| out_refund_no | 是 | 商户退款单号 |
| total_fee | 是 | 原订单金额(分) |
| refund_fee | 是 | 退款金额(分) |
| refund_desc | 否 | 退款原因 |
| 参数 | 必填 | 说明 |
|---|---|---|
| app_id | 是 | 应用ID |
| method | 是 | alipay.trade.refund |
| ... | ... | 公共参数 |
| biz_content | 是 | 见下表 |
{
"out_trade_no": "20231001001",
"refund_amount": "88.88",
"refund_reason": "用户申请退款",
"out_request_no": "REF20231001001"
}
API 对比总结
| 维度 | 微信支付 | 支付宝 |
|---|---|---|
| 数据格式 | XML | JSON(新)/ 表单(旧) |
| 签名算法 | MD5/HMAC-SHA256 | RSA2 |
| 证书要求 | 部分接口需要 | 不需要 |
| 沙箱环境 | 有 | 有 |
| API 版本 | V2/V3 | 无版本区分 |
| 错误码 | 数字 | 字符串 |
八、实际对接案例
案例1:小游戏充值
- 使用微信小程序支付
- 通过
wx.login()获取 code,后端换取 openid - 后端调用统一下单接口,获取 prepay_id
- 前端调用
wx.requestPayment()完成支付
问题:调用 wx.login() 后,后端换取 openid 时报错
原因:小程序的 appid 和 secret 配置错误
解决:检查小程序后台配置,确保 appid 和 secret 正确
问题:前端调用支付时提示"签名错误"
原因:后端生成签名时参数名错误(appId 应该是 appId 不是 appid)
解决:严格按照文档参数名,注意大小写
问题:用户支付成功,但道具发放了两次
原因:回调处理没有做幂等
解决:在发放道具前检查订单状态,使用数据库唯一索引
// 小程序端
const pay = async (productId) => {
// 1. 登录获取 code
const { code } = await wx.login();
// 2. 请求后端创建订单
const res = await request({
url: '/api/pay/create',
method: 'POST',
data: { code, productId }
});
// 3. 调用支付
await wx.requestPayment({
timeStamp: res.timeStamp,
nonceStr: res.nonceStr,
package: res.package,
signType: res.signType,
paySign: res.paySign
});
// 4. 支付成功
wx.showToast({ title: '支付成功' });
};
案例2:独立游戏App
- 同时接入微信支付和支付宝
- 后端统一封装支付接口,屏蔽差异
- 根据用户选择调用对应支付方式
┌─────────────────────────────────────┐
│ 游戏客户端 │
│ ┌──────────┐ ┌──────────┐ │
│ │ 微信支付SDK │ │支付宝SDK │ │
│ └─────┬────┘ └────┬─────┘ │
└────────┼──────────────┼─────────────┘
│ │
▼ ▼
┌─────────────────────────────────────┐
│ 统一支付网关 │
│ ┌─────────────────────────────┐ │
│ │ PayService │ │
│ │ - createOrder() │ │
│ │ - queryOrder() │ │
│ │ - refund() │ │
│ │ - handleNotify() │ │
│ └─────────────────────────────┘ │
│ ┌─────────┐ ┌───────────┐ │
│ │WechatPay│ │AlipayService│ │
│ └─────────┘ └───────────┘ │
└─────────────────────────────────────┘
// 支付服务接口
public interface PayService {
// 创建订单
PayResult createOrder(PayRequest request);
// 查询订单
OrderResult queryOrder(String orderId);
// 退款
RefundResult refund(RefundRequest request);
// 处理回调
String handleNotify(String channel, Map<String, String> params);
}
// 支付请求(统一格式)
public class PayRequest {
private String channel; // wechat/alipay
private String orderId; // 商户订单号
private BigDecimal amount; // 金额
private String subject; // 商品名称
private String body; // 商品描述
private String userId; // 用户ID
private String extra; // 扩展信息
}
问题:微信支付时提示"商户号未绑定AppID"
原因:AppID 需要在商户平台单独绑定
解决:登录商户平台 → 产品中心 → AppID账号管理 → 关联AppID
问题:签名验证一直失败
原因:使用了错误的公钥(应该用支付宝公钥,不是应用公钥)
解决:在支付宝开放平台获取"支付宝公钥",不是自己生成的应用公钥
问题:App 审核被拒,因为虚拟商品必须用 IAP
原因:iOS 规定虚拟商品必须走苹果内购
解决:虚拟商品(钻石、道具)用 IAP,实体商品可以用第三方支付
- 微信支付占比:约 65%
- 支付宝占比:约 35%
- 支付成功率提升:15%(原来只有微信)
- 用户投诉下降:80%(用户可以选择习惯的方式)
案例3:PC 网页游戏
- 微信:使用 Native 支付(扫码支付)
- 支付宝:使用手机网站支付(扫码或账号登录)
// 生成支付二维码
public String createQRCode(String orderId, BigDecimal amount) {
// 1. 调用统一下单接口
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
.outTradeNo(orderId)
.totalFee(amount.multiply(new BigDecimal(100)).intValue()) // 元转分
.body("游戏充值")
.tradeType(TradeType.NATIVE) // 扫码支付
.notifyUrl(notifyUrl)
.build();
WxPayUnifiedOrderResult result = wxPayService.unifiedOrder(request);
// 2. 返回二维码链接
return result.getCodeURL();
}
// 前端生成二维码
// 使用 qrcode.js 或类似库将 codeURL 转成二维码图片
// 生成支付二维码
public String createQRCode(String orderId, BigDecimal amount) {
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
request.setNotifyUrl(notifyUrl);
request.setBizContent("{" +
"\"out_trade_no\":\"" + orderId + "\"," +
"\"total_amount\":\"" + amount + "\"," +
"\"subject\":\"游戏充值\"" +
"}");
AlipayTradePrecreateResponse response = alipayClient.execute(request);
return response.getQrCode();
}
// 前端轮询
let pollTimer = null;
let pollCount = 0;
const MAX_POLL = 60; // 最多轮询60次(5分钟)
function startPolling(orderId) {
pollTimer = setInterval(async () => {
pollCount++;
const res = await fetch(`/api/pay/query?orderId=${orderId}`);
const data = await res.json();
if (data.status === 'PAID') {
clearInterval(pollTimer);
showSuccess();
} else if (pollCount >= MAX_POLL) {
clearInterval(pollTimer);
showTimeout();
}
}, 5000); // 每5秒查询一次
}
// 用户扫码后,前端轮询检测支付状态
问题:用户扫码时提示"二维码已过期"
原因:微信扫码支付二维码有效期 2 小时
解决:设置定时器,1.5 小时后刷新二维码,或提示用户刷新页面
问题:1.10 元的订单,实际扣款 1.1 元
原因:后端使用了 float 类型,精度丢失
解决:使用 BigDecimal,微信支付用整数分,支付宝用字符串
问题:回调通知无法接收
原因:测试环境和生产环境域名不同
解决:使用内网穿透工具(如 ngrok)测试,或部署到测试服务器
九、游戏行业的选型建议
说了这么多差异,作为游戏平台,该怎么选?
选择微信支付的场景
✅ 社交属性强的游戏
- 有微信登录的游戏
- 需要分享、裂变
- 用户主要在微信生态
- 典型案例:休闲小游戏、社交游戏
✅ 小游戏
- 微信小游戏只能用微信支付(苹果要求)
- 小程序游戏同理
- 无需额外选择
✅ H5 游戏,主要流量来自微信
- 微信内打开的 H5
- JSAPI 支付体验最好
- 无缝体验,用户感知低
✅ 国内用户为主的游戏
- 微信支付在国内覆盖率接近 100%
- 用户习惯已养成
- 无需教育成本
选择支付宝的场景
✅ 独立 App 游戏
- 有独立客户端
- 用户不一定用微信
- 需要覆盖更多用户
✅ PC 端游戏/网页游戏
- 用户在电脑上充值
- 支付宝的网页支付体验更好
- 支持账号登录支付
✅ 大额充值场景
- 支付宝的风控相对宽松
- 大额支付通过率更高
- 适合 ARPU 值高的游戏
✅ 需要国际化的游戏
- 支付宝的国际支付能力更强
- 覆盖更多国家和地区
- 支持多种货币
✅ 电商+游戏混合场景
- 如果你的平台既有虚拟商品又有实体商品
- 支付宝的担保交易模式更适合
实际建议:两个都要接
- 用户习惯不同,有人习惯用微信,有人习惯用支付宝
- 两个都接,可以提升支付成功率 10-20%
- 备用通道,一个挂了还有另一个
- 某些优惠活动可能只在某个平台
- 根据用户画像,决定优先展示哪个
- 如果是独立 App,根据统计数据选择
- 两个都支持,让用户自己选
- 记住用户上次的选择
- 根据数据反馈,调整优先级
- 动态调整推荐顺序
- 费率:两者基本相同(0.6% 左右,具体看行业)
- 开发成本:后端统一封装后,增加一个渠道成本不高
- 维护成本:需要同时关注两个平台的变更
决策流程图
开始
│
▼
┌───────────────┐
│ 是微信小游戏? │
└───────┬───────┘
│
┌──────────┴──────────┐
│Yes │No
▼ ▼
┌─────────────┐ ┌─────────────┐
│ 只接微信支付 │ │ 用户主要在?│
└─────────────┘ └──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐
│微信内 │ │独立App│ │ PC端 │
└───┬───┘ └───┬───┘ └───┬───┘
│ │ │
▼ ▼ ▼
┌──────────┐┌──────────┐┌──────────┐
│微信优先 ││两个都接 ││支付宝优先│
└──────────┘└──────────┘└──────────┘
[配图建议:决策树图,帮助选择支付方式的流程图]
十、对接时的"坑点"提醒
最后,分享几个最常见的坑:
微信的坑
⚠️ 坑1:openid 获取
问题描述:
- 不同公众号的 openid 不一样
- 同一用户在不同应用中 openid 不同
解决方案:
- 使用开放平台的 unionid 统一用户身份
- 提前规划账号体系
- 多个应用绑定到同一个开放平台账号
⚠️ 坑2:证书配置
问题描述:
- 退款接口需要双向证书
- 证书有有效期(通常1年)
- 不同环境需要不同证书
解决方案:
- 建立证书更新提醒机制
- 提前1个月申请更新
- 使用配置中心管理证书路径
⚠️ 坑3:签名类型
问题描述:
- V2 接口支持 MD5 和 HMAC-SHA256
- V3 接口只支持 RSA
- 混用会导致签名失败
解决方案:
- 明确每个接口的签名类型
- 不要混用不同版本的 SDK
- 查看错误信息中的签名类型提示
支付宝的坑
⚠️ 坑1:密钥管理
问题描述:
- 有应用私钥、应用公钥、支付宝公钥三种
- 容易混淆用错
解决方案:
- 应用私钥:自己生成,妥善保管,绝不上传
- 应用公钥:上传到支付宝开放平台
- 支付宝公钥:从开放平台获取,用于验签
- 使用 SDK 时注意区分
⚠️ 坑2:网关选择
问题描述:
- 有正式环境和沙箱环境
- 网关地址不同
- 沙箱密钥和正式密钥不同
解决方案:
- 测试时使用沙箱网关:https://openapi.alipaydev.com/gateway.do
- 上线时切换到正式网关:https://openapi.alipay.com/gateway.do
- 使用配置文件管理,避免硬编码
⚠️ 坑3:编码问题
问题描述:
- UTF-8 和 GBK 编码混用
- 中文参数签名失败
解决方案:
- 统一使用 UTF-8 编码
- 签名前检查编码格式
- 使用官方 SDK 避免编码问题
两者共同的坑
⚠️ 坑1:金额单位
问题描述:
- 微信:单位是"分",100 表示 1 元
- 支付宝:单位是"元",1.00 表示 1 元
- 这是最容易踩的坑!
解决方案:
// 微信支付
int totalFee = new BigDecimal("1.00")
.multiply(new BigDecimal(100))
.intValue(); // 100 分
// 支付宝支付
String totalAmount = "1.00"; // 直接用字符串
⚠️ 坑2:时间格式
问题描述:
- 微信:时间戳(秒)
- 支付宝:格式化字符串(yyyy-MM-dd HH:mm:ss)
- 时区问题
解决方案:
// 微信
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
// 支付宝
String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
.format(new Date());
⚠️ 坑3:幂等处理
问题描述:
- 同一个订单可能收到多次回调
- 网络重试导致重复通知
- 用户重复点击
解决方案:
1. 使用唯一订单号
2. 回调处理前检查订单状态
3. 使用数据库唯一索引
4. 使用分布式锁
代码示例:
// 伪代码
if (order.status == PAID) {
return "success"; // 已处理,直接返回成功
}
// 加锁处理
if (lock.tryLock(orderId, 30)) {
try {
// 双重检查
if (order.status == PAID) {
return "success";
}
// 处理支付成功逻辑
processPaySuccess(order);
} finally {
lock.unlock(orderId);
}
}
return "success";
⚠️ 坑4:回调地址
问题描述:
- 回调地址必须是外网可访问的
- 不能是 localhost 或内网 IP
- 必须支持 HTTPS(生产环境)
解决方案:
- 开发环境使用内网穿透工具(ngrok、frp)
- 测试环境部署到测试服务器
- 生产环境确保 HTTPS 证书有效
⚠️ 坑5:错误码处理
问题描述:
- 错误码定义不统一
- 同样的错误可能返回不同的错误码
- 错误信息不够详细
解决方案:
// 建立错误码映射表
Map<String, String> errorCodeMap = new HashMap<>();
errorCodeMap.put("SYSTEMERROR", "系统繁忙,请稍后重试");
errorCodeMap.put("PARAM_ERROR", "参数错误,请检查请求参数");
errorCodeMap.put("OUT_TRADE_NO_USED", "商户订单号重复");
// 统一错误处理
try {
// 调用支付接口
} catch (PayException e) {
String userMsg = errorCodeMap.getOrDefault(
e.getErrorCode(),
"支付失败,请联系客服"
);
throw new BusinessException(userMsg);
}
十一、总结:核心要点速记
📌 接入流程
- 微信: 严格但规范,资料要求齐全,审核 1-5 天
- 支付宝: 灵活但需自己把握,支持个人接入
📌 支付方式
- 微信: JSAPI/H5/小程序/APP/Native,各有场景
- 支付宝: 手机网站/APP/当面付/生活号,覆盖全面
📌 签名机制
- 微信: MD5/HMAC-SHA256,简单快速,对称加密
- 支付宝: RSA2,安全但复杂,非对称加密
📌 回调处理
- 都需要验证签名和幂等处理
- 微信重试更频繁(15次),支付宝相对克制(8次)
- 都要求 5 秒内响应
📌 API 差异
- 数据格式: 微信用 XML,支付宝用 JSON/表单
- 金额单位: 微信是分,支付宝是元
- 证书要求: 微信部分接口需要证书
📌 选型建议
- 游戏行业建议两个都接
- 微信小游戏只接微信
- 独立 App/PC 端优先支付宝
- 根据用户数据动态调整
📌 必须记住的三件事
- 金额单位 - 微信是分,支付宝是元!
- 幂等处理 - 回调可能重复,必须检查状态!
- 签名验证 - 回调必须验签,防止伪造!
支付对接没有银弹,理解差异、选对场景、做好容错,才能让用户的充值体验流畅无感 💰✨
- 支付篇第1篇:《支付系统的前世今生》
- 支付篇第2篇:《签名与验签:支付安全的基石》
- 支付篇第3篇:本文 ←
💬 评论 (0)