评论

小程序云开发接入虚拟支付指引

小程序云开发接入虚拟支付指引

先说一下

目前云开发在对接虚拟支付功能上没有什么优势,只官方公告,不得不先进行一波适配

相关文档

虚拟支付接入指引文档
小程序api文档

基本配置

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
最后一次编辑于  03-18  
点赞 5
收藏
评论

2 个评论

  • 发飙的蜗牛
    发飙的蜗牛
    03-17

    现在云开发支持消息推送了。 可以看下

    03-17
    赞同
    回复 6
    • showms
      showms
      03-17
      嗯是的
      03-17
      回复
    • 发飙的蜗牛
      发飙的蜗牛
      03-18回复showms
      他们这个退款通知有么。  就是在公众号平台提交退款通知,然后云开发给消息推送啊,我测了好几遍都不行呢
      03-18
      回复
    • showms
      showms
      03-18回复发飙的蜗牛
      有推送,但是走的是HTTP推送,估计有bug
      03-18
      回复
    • 发飙的蜗牛
      发飙的蜗牛
      03-18回复showms
      我还跟他们反馈这个问题了。 测试了 好多遍都不行!
      03-18
      回复
    • 发飙的蜗牛
      发飙的蜗牛
      03-18回复showms
      http推送的你试了可以是吧
      03-18
      回复
    查看更多(1)
  • 坤头
    坤头
    03-15

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

    03-15
    赞同
    回复 1
    • showms
      showms
      发表于小程序端
      03-15

      感谢支持🙏🏻

      03-15
      回复
登录 后发表内容