一、前言
境外微信支付的实现方式,与国内支付的方式大不同,即不是V2,也不是V3,而是经由海外支付服务商,通过间连的方式来实现的。(香港地区融合钱包除外)。
以下专门介绍具体的实现攻略:
二、开通
首先,你需要先了解当地有哪些第三方支付服务商,然后选择其中一家,接入他们提供的支付服务,从而实现微信支付的开通。
1、全球第三方支付服务商:
https://pay.weixin.qq.com/index.php/public/wechatpay_en/partner_search#/
找到当地服务商的联系方式,与他们联系,他们会告诉你所需提供哪些资料,并帮你开通商户号,接入微信支付;
资料信息包括:
营业执照
翻译认证
公司银行账户
法人及联系人信息
其他信息等。
2、香港融合钱包
融合钱包以下不做介绍,技术方面和v3支付完全一样。
技术方案可以参考:
https://pay.weixin.qq.com/wiki/doc/api/wxpay/en/pages/MiniProgramPay_fw.shtml
三、第三方服务商提供的支付服务
每个第三方服务商所提供的支付服务各不相同,以下分别举例:
Yaband Pay (Europe PayPro B.V.):
微信支付, 支付宝, PayPal, SOFORT, IDEAL, 银联支付, List
2paynow (Europe Sepay B.V.):
微信支付, 支付宝
VR Payment (Europe CardProcess GmbH):
微信支付, 支付宝
Supay (Australia):
微信支付, 信用卡支付
其他地区第三方支付服务商,提供哪些服务,需要向他们咨询。
注:
1、境外小程序对除微信支付之外的支付方式,卡得并不象国内这样严,一般情况下,PayPal, 信用卡等方式,都支持并审核通过,支付宝会稍难点;
2、小程序里实现其他支付方式的唯一方案:生成支付链接,复制到剪贴板,在手机浏览器或微信浏览器中打开,完成支付流程。
四、第三方支付账号和接入文档
第三方帮你接入微信支付后,会回复邮件给你,并提供相关支付信息:
1. 一个属于第三方的支付账号
比如Yaband pay,支付账号类似如下:
ID: yourname@yoursite.com
Key: f4f70db6107xxxxxxxb75efa1f1490b4
2paynow,支付账号类似如下:
ID: 541102252
Key: osVx_0POxxxxF9h3JSCls
2. 该第三方支付服务商支付接入文档
比如Yaband pay 支付文档:
https://www.yabandmedia.com/api/cn-api.html
比如2paynow支付文档,是由技术支持人员发送给你的PDF文档,具体内容暂不介绍。
3. 第三方支付服务商的商家管理后台
你可以登录该后台进行配置,并查看详细的订单数据。
五、云开发实现境外支付
因为商家可以自主选择第三方支付服务商,所以我们在实现支付功能时,需要同时支付多个第三方支付服务商。
用云开发来实现,一般会有两种方式:
(1)、每个第三方的支付功能,分别用一个云函数实现;
(2)、一个云函数集成所有第三方支付功能;(我们采用的是该方式)
1、云函数pay.js:
按支付接口、支付服务商、支付方式分流:
const cloud = require('wx-server-sdk')
const rp = require('request-promise')
const CryptoJS = require('crypto-js')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
exports.main = async (event, context) => {
switch (event.action) {//根据小程序端传入的支付相关接口分流
case 'unifiedorder': return await unifiedorder(event)//统一下单
case 'queryorder': return await queryorder(event)//订单查询
case 'refund': return await refund(event)//退款
default: return 'wrong action'
}
}
async function unifiedorder(event) {
let res = await db.collection('config').doc('payment').get().catch()
if (res && res.data) { } else return 'wrong action'//注意:res?.data在云函数中不支持
let config = res.data//云数据库中保存了支付配置信息
switch (config.pay_sp) {//根据支付服务商pay_sp来分流
case '2paynow': return await unifiedorder_2paynow(event, config)
case 'yaband': return await unifiedorder_yaband(event, config)
case 'cloudpay': return await unifiedorder_cloudpay(event, config)
case 'supay': return await unifiedorder_supay(event, config)
case 'vr': return await unifiedorder_vr(event, config)
case 'v2': return await unifiedorder_v2(event, config)
case 'v3': return await unifiedorder_v3(event, config)
case 'v2sp': return await unifiedorder_v2(event, config)//v2服务商模式
case 'v3sp': return await unifiedorder_v3(event, config)//v3服务商模式
default: return 'wrong action'
}
}
async function queryorder(event) {
let res = await db.collection('config').doc('payment').get().catch()
if (res && res.data) { } else return 'wrong action'
let config = res.data
switch (config.pay_sp) {
case '2paynow': return await queryorder_2paynow(event, config)
case 'supay': return await queryorder_supay(event, config)
case 'vr': return await queryorder_vr(event, config)
default: return 'wrong action'
}
}
async function refund(event) {
let res = await db.collection('config').doc('payment').get()
if (res && res.data) { } else return 'wrong action'
let config = res.data
switch (res.data.pay_sp) {
case '2paynow': return await refund_2paynow(event, config)
case 'yaband': return await refund_yaband(event, config)
case 'cloudpay': return await refund_cloudpay(event, config)
case 'supay': return await refund_supay(event, config)
case 'vr': return await refund_vr(event, config)
default: return 'wrong action'
}
}
2、统一下单(Yaband pay为例)
接口文档:https://www.yabandmedia.com/api/cn-api.html
// -------------------yaband-----------------------------------------------------------
async function unifiedorder_yaband(event, config) {
let notify_url = config.notify_url
let user = config.mchid
let key = config.key
let timeStamp = parseInt(Date.now() / 1000)
let payMethod = event.pay_method
if (payMethod == 'wechat') {//微信支付
const wxContext = cloud.getWXContext()
let param = {
"user": user,
"method": "v3.CreatePaymentsWechatMiniPay",
"time": timeStamp,
}
let data = {
"pay_method": "online",
"sub_pay_method": "WeChat Pay",
"order_id": event.out_trade_no,
"amount": event.total_fee || "0.1",
"currency": event.fee_type || config.currency,
"description": event.body || "YabandPay pay",
"demo": event.demo || "order",
"timeout": "0",
"notify_url": notify_url,
"sub_app_id": wxContext.FROM_APPID || wxContext.APPID,
"sub_open_id": wxContext.FROM_OPENID || wxContext.OPENID
}
let sign = getSign_yaband({ ...param, ...data }, key)
let body = { ...param, sign, data }
let res = await rp({
url: "https://mapi.yabandpay.com/Payments",
method: 'POST',
body,
json: true
})
let payment = res.data.parameters
return { payment, sp_trade_no: res.data.trade_id }
}
if (payMethod == 'sofort') {//省略
}
if (payMethod == 'alipay') {//省略
}
if (payMethod == 'paypal') {//Paypal支付
let param = {
"user": user,
"method": "v3.CreatePayments",
"time": timeStamp,
}
let data = {
"pay_method": "online",
"sub_pay_method": "PAYPAL/RECURRING",
"order_id": event.out_trade_no,
"amount": event.total_fee || "0.1",
"currency": event.fee_type || config.currency,
"description": event.body || "YabandPay pay",
"demo": event.demo || "order",
"post_email": "xin.liu@elbsino.com",
"timeout": "0",
"redirect_url": "https://www.klarna.com/de/",
"notify_url": notify_url
}
let sign = getSign_yaband({ ...param, ...data }, key)
let body = { ...param, sign, data }
let res = await rp({
url: "https://mapi.yabandpay.com/Payments",
method: 'POST',
body,
json: true
})
let url = res.data.url
return { url }//除微信支付外,都必须返回一个url:支付链接
}
}
async function refund_yaband(event, config) {
console.log('refund_yaband')
let notify_url = config.notify_url
let user = config.mchid
let key = config.key
let timeStamp = parseInt(Date.now() / 1000)
let param = {
"user": user,
"method": "v3.CreateRefund",
"time": timeStamp,
}
let data = {
"trade_id": event.trade_id,
"refund_amount": event.refund_amount || "0.1",
"refund_currency": config.refund_currency,
"refund_description": event.refund_description || "test",
"notify_url": notify_url
}
let sign = getSign_yaband({ ...param, ...data }, key)
let body = { ...param, sign, data }
console.log(body)
return await rp({
url: "https://mapi.yabandpay.com/Payments",
method: 'POST',
body,
json: true
})
}
function getSign_yaband(args, key, sa = []) {//每个支付服务商的签名方式都不一样
for (let k in args) sa.push(k + '=' + args[k])
sa = sa.sort()
let signStr = sa.join('&')
let hash = CryptoJS.HmacSHA256(signStr, key)
return CryptoJS.enc.Hex.stringify(hash)
}
function getSign_2paynow(args, key) {//2paynow的签名算法。
let signStr = args.function + args.mid + args.timestamp + key
let hash = CryptoJS.MD5(signStr, key)
return CryptoJS.enc.Hex.stringify(hash)
}
踩坑实录:
1、每次统一下单,都会产生三种订单号:
商家订单号
支付服务商的支付订单号
微信支付订单号
记住:一定要保存支付服务商的支付订单号,因为有些支付服务商在订单查询和退款接口中,只认该订单号。
2、每个服务商的签名方案是不同的,有的甚至不需要签名,这个需要分别对待。
六、支付成功异步通知
多个服务商的异步通知,用一个云函数来实现。
云函数:notify.js
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
exports.main = async (event, context) => {
let res = await db.collection('config').doc('payment').get()
switch (res.data.pay_sp) {
case '2paynow': return await notify_2paynow(event)
case 'yaband': return await notify_yaband(event)
case 'cloudpay': return await notify_cloudpay(event)
case 'vr': return await notify_vr(event)//vr暂时不支持回调通知
case 'supay': return await notify_supay(event)
case 'v2': return await notify_v2(event)
case 'v3': return await notify_v3(event)
default: return 'wrong action'
}
}
async function notify_2paynow(event) {
console.log('notify_2paynow')
let qs = event.queryStringParameters
if (qs.merchant_trade_no) { } else return 'success'
await db.collection('payment').add({ data: qs }).then(res => console.log(res))
if (qs.merchant_trade_no && qs.trade_status == 'TRADE_SUCCESS') {
await onPaymentSuccess(qs.merchant_trade_no, qs.trade_no, qs.original_trade_no)
}
return 'success'
}
async function notify_yaband(event) {
console.log('notify_yaband')
if (event.body) { } else return { statusCode: 200, body: 'ok' }
let body = JSON.parse(event.body)
let pay = body.data
await db.collection('payment').add({ data: pay }).then(res => console.log(res))
if (pay.transaction_id && pay.state == 'paid') {
await onPaymentSuccess(event.outTradeNo)
}
return { statusCode: 200, body: 'ok' }
}
async function notify_cloudpay(event) {//云支付pay_cb
console.log('notify_cloudpay')
if (event.outTradeNo) {
await db.collection('payment').add({ data: event }).then(res => console.log(res))
if (event.returnCode == 'SUCCESS' && event.resultCode == 'SUCCESS') {
await onPaymentSuccess(event.outTradeNo)
}
}
return { "errcode": 0, "errmsg": 'SUCCESS' }
}
踩坑实录:
1、有些国外支付服务商,并不提供异步通知功能,比如VR Payment,这就很尴尬了,因为我们业务的流程里,是非常依赖这个功能的,因此,后期造成了大量的代码修改工作,建议大家从一开始就考虑这种情况。
七、币种选择
统一下单时,支持两种币种:
1、CNY
2、当地币种,比如EUR
境外退款,一般只能使用当地币退款,不支持RMB。
八、境外小程序云开发禁用后的选择
目前境外小程序的云开发功能已经被禁用,可以通过环境共享的模式支持以上云开发境外支付方案;
1、共享在禁用令之前的同主体小程序的云环境;
2、我们是采用开放平台第三方服务商批量云开发模式;
九、境外小程序相关文章
1、https://developers.weixin.qq.com/community/develop/article/doc/000aec921e4fd8a320ec0f9795bc13
2、https://developers.weixin.qq.com/community/develop/article/doc/0000e805af0900b37f6c900c356c13
3、https://developers.weixin.qq.com/community/develop/article/doc/000a68f1a24a687242eb2427556013
4、https://developers.weixin.qq.com/community/develop/article/doc/000860b434cd180a749b9268f51c13
大家还有哪些关于境外支付要了解的内容,请留言,我来补充。