境外小程序支付攻略(云开发篇),看这一篇就够了。
一、前言 境外微信支付的实现方式,与国内支付的方式大不同,即不是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