先说一下
目前云开发在对接虚拟支付功能上没有什么优势,只 因官方公告,不得不先进行一波适配
相关文档
基本配置
Appid、OfferId、沙箱AppKey、现网AppKey,这几个核心参数在 小程序后台-支付与交易-虚拟支付-基本配置 可以拿到
签名
虚拟支付涉及到两种签名
- 支付签名 paySig
- 用户态签名 signature
| 生成支付签名所需参数 | 说明 |
|---|---|
| appKey | 沙箱appKey或者现网appKey,取决于你当前的env |
| uri | 如果是基础库的wx.requestVirtualPayment,uri固定传requestVirtualPayment; 如果是后端接口,uri就传入api路径,例如查询创建订单接口传xpay/query_order |
| signData | 该参数是个json字符串,具体参考 wx.requestVirtualPayment的api文档 说明 |
| 生成用户态签名所需参数 | 说明 |
|---|---|
| sessionKey | 小程序会话密钥,通常情况下云开发不会用到这个参数,但是现在得对接了 小程序登录凭证校验 |
| signData | 参考 wx.requestVirtualPayment的api文档 说明 |
生成签名代码
const crypto = require('crypto');
/**
* hmacSha256Hex
* @param key
* @param data
* @returns {string}
*/
hmacSha256Hex(key, data) {
return crypto.createHmac('sha256', key).update(data, 'utf8').digest('hex');
}
/**
* 生成虚拟支付签名
* @param uri
* @param signData
* @param sessionKey
* @param appKey
* @returns {{paySig: string, signature: string}}
*/
generateVirtualPaymentSign({uri, signData, sessionKey, appKey}) {
const paySig = this.hmacSha256Hex(appKey, `${uri}&${signData}`);
const signature = this.hmacSha256Hex(sessionKey, signData);
console.log("paySig->", paySig, "signature->", signature)
return {paySig, signature};
}
获取session_key代码
/**
* 获取sessionKey
* @param code wx.login获取的code
* @param appid
* @param secret 小程序秘钥
* @returns {Promise<AxiosResponse<any>|void>}
*/
async code2Session({code, appid, secret}) {
const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appid}&secret=${secret}&js_code=${code}&grant_type=GRANT_TYPE`;
return await axios.get(url)
.then(res => {
console.log(res);
return res.data.session_key;
})
.catch(err => {
console.log(err);
});
}
其中code调用 wx.login 可以得到,secret是小程序秘钥,在小程序后台-开发者管理可得
发起虚拟支付
云函数端(道具模式)
/**
* 获取虚拟支付参数
* @param params
* @returns {Promise<*>}
*/
async getVirtualPaymentData(params) {
const {totalFee, mealCode, loginCode} = params;
const openid = this.wxContext.OPENID;
const appid = this.wxContext.APPID;
//生成订单
const orderInfo = await this._generalOrder(totalFee, appid, openid, mealCode, constants.ORDER_CATEGORY.NORMAL.code);
//业务订单号
const orderNo = orderInfo._id;
//虚拟支付基本配置
const virtualPaymentConfig = await this._getWxPaymentConfig(appid, "wxVirtualPayment");
const {offerId, appKeyProd, appKeySandbox} = virtualPaymentConfig;
//获取sessionKey
const sessionKey = await this.code2Session({
code: loginCode,
appid: virtualPaymentConfig.appid,
secret: virtualPaymentConfig.secret
});
//环境配置:0现网 1沙箱
const env = 0;
const signData = JSON.stringify({
buyQuantity: 1,
env,
offerId,
currencyType: "CNY",
productId: mealCode,//道具ID
goodsPrice: totalFee,//道具单价,要和道具ID对应
outTradeNo: orderNo,
attach: JSON.stringify({mealCode})//发货通知时会透传给开发者,根据自己需要透传数据
});
//获取支付签名、用户态签名
const {paySig, signature} = this.generateVirtualPaymentSign({
uri: "requestVirtualPayment",
signData,
sessionKey,
appKey: env ? appKeySandbox : appKeyProd
})
return {paySig, signature, signData, orderNo};
}
小程序端
/**
* 发起虚拟支付请求
* api.js是我自己封装的一些全局api
* @param totalFee:支付金额(道具单价)
* @param mealCode:套餐编码(道具ID)
* @returns {Promise<*>}
*/
const requestVirtualPayment = function (totalFee, mealCode) {
return new Promise(function (resolve, reject) {
api.login().then(loginCode => {
let start = new Date().getTime();
console.log("请求支付,订单信息:", totalFee, renewal, mealCode, activeCode);
//请求云函数获取虚拟支付参数
api.callCloudUserCenterFunction("PaymentHandler/getVirtualPaymentData", {
loginCode,
totalFee,
mealCode
}, res => {
console.log("操作结果:", JSON.stringify(res), "耗时:", new Date().getTime() - start);
console.log("获取到订单支付参数--->", res);
if (res.result.success) {
const {paySig, signature, signData, orderNo} = res.result.data;
api.requestVirtualPayment({
paymentData: {signData, paySig, signature, orderNo},
success: resolve, fail: reject
});
} else {
console.error("下单失败");
reject();
}
}, reject, () => {
});
}).catch(reject);
});
}
全局api.js
/**
* 登录
* 调用接口获取登录凭证(code)。
* 通过凭证进而换取用户登录态信息,包括用户在当前小程序的唯一标识(openid)、微信开放平台账号下的唯一标识(unionid,若当前小程序已绑定到微信开放平台账号)及本次登录的会话密钥(session_key)等。
* 用户数据的加解密通讯需要依赖会话密钥完成。
* @returns {Promise<unknown>}
*/
const login = function () {
return new Promise((resolve, reject) => {
wx.login({
success(res) {
console.log("登录结果:", res);
const {code} = res;
if (code) {
console.log("登录成功:", res);
resolve(code);
} else {
console.error("登陆失败", res);
reject(res);
}
},
fail: function (res) {
console.error("登陆异常", res);
reject(res);
}
});
});
}
/**
* 请求虚拟支付
* @param paymentData {signData, paySig, signature}
* @param mode
* @param success
* @param fail
* @param complete
*/
const requestVirtualPayment = function ({
paymentData,
mode = virtualPaymentMode.short_series_goods,
success,
fail,
complete
}) {
wx.requestVirtualPayment({
signData: paymentData.signData,
paySig: paymentData.paySig,
signature: paymentData.signature,
mode,
success(res) {
console.log("支付成功", res);
typeof success == 'function' && success(Object.assign(res, paymentData));
},
fail(res) {
console.warn("支付失败", res);
typeof fail == 'function' && fail(Object.assign(res, paymentData));
},
complete(res) {
console.log("支付完成", res);
typeof complete == 'function' && complete(Object.assign(res, paymentData));
}
})
};
到这里就能发起支付了
消息事件通知
现在云开发也支持接收虚拟支付消息了,需要在开发者工具上添加一下消息推送配置,具体参照下方截图
勾选需要接收的消息,比如道具模式一般配xpay_goods_deliver_notify xpay_refund_notify xpay_complaint_notify xpay_subscribe_ios_refund_query_notify这几个就够用了。
云函数消息推送的代码比较多,贴了一个代码片段,针对不同的通知事件用不同的处理器处理,可以参考一下
为了拿到支付结果通知,还需要对接一下 消息事件通知,云开发本来也是可以通过云函数来接收消息推送,遗憾的是目前仅支持客服消息推送。好在云函数支持HTTP访问,可以间接实现
- 开启HTTP访问服务
云函数如果要想接收支付结果通知(在虚拟支付功能里面是叫做发货通知)、退款结果通知、IOS退款问询通知等消息,得先到 腾讯云-cloudbase后台 给云开发环境开启HTTP访问服务,建议配一个自定义域名,然后域名关联你要收消息的云函数,操作如下:
- 小程序后台开启消息推送
后台-开发管理-开发设置-消息推送
这里配置的URL带了一个msgType参数,可以方便后续在云函数里面识别是微信推送的消息。
云函数消息推送的代码比较多,贴了一个代码片段,针对不同的通知事件用不同的处理器处理,可以参考一下
调试问题
调试过程中目前主要碰到以下问题:
- 虚拟支付道具如果是新增或者修改的话,大概需要10分钟时间才会生效,这期间这个道具调试的时候会报错:COIN_OR_PRODUCT_ID_CREATED_IN_RECENTLY
- IOS最低支付金额1元,这点文档里面也有写
- IOS得在现网环境调试,env=0
- IOS退款问询响应拒绝退款,还是会时不时收到问询消息
- 小程序后台操作退款,退款通知还是走HTTP,并不是云函数的消息推送,可能还有bug

现在云开发支持消息推送了。 可以看下
非常感谢兄弟, 帮了我很大的忙,我一直在找如何用云开发完成虚拟支付,也看了一下你列表文章,虽然很少有人回复,但都是实用干货,给你点赞
感谢支持🙏🏻