7.4. 微信支付

7.4.1. 功能概述

本文档主要介绍微信支付功能,您可以通过本文示例体验体验下单、支付、退款等支付类的功能。

7.4.2. 体验功能

https://main.qcloudimg.com/raw/c3bffdddfa54ecd95b5a569b10f71840.png

7.4.3. 体验 DEMO

本章的案例代码,请参考代码 tcb-demo-basic

7.4.4. DEMO 接入流程

代码下载完成后,请按照以下步骤操作:

  1. 在云开发中创建好至少一个环境。

  2. app.js 文件中,如果使用默认环境,则没有改动。如果需要使用非默认环境, 则需要在 wx.cloud.init 中传入环境 ID。如果使用非默认环境,在云函数的 cloud.init 处,也需要补充环境 ID。如下图所示:

https://main.qcloudimg.com/raw/69d8d7590284cc80776021f3f8883948.png

示例代码如下:

wx.cloud.init({
  env: 'xxxx', // 环境 id
  traceUser: true
});
  1. 在云函数根目录 cloud/functions 中找到函数 pay,在 config 目录中新建 index.js, 配置环境 ID、微信支付商户号、微信支付商户密钥、和双向证书中的 apiclient_cert.p12, 该证书放也放在 config 目录中,用 npm 安装两个云函数的依赖,并上传该函数。

https://main.qcloudimg.com/raw/70c58a00ca5f69ee1b449dd06f1734a1.png
  1. 找到云函数 pay-message,在 config 目录中新建 index.js

https://main.qcloudimg.com/raw/532124c3661e43c86cddca24a85ad508.png
  1. 配置小程序 AppSecret 和模板 ID(选择合适的模板,合适的字段即可),用 npm 安装两个云函数的依赖,然后上传该函数。

https://main.qcloudimg.com/raw/10e976f5ac788aa0fbb92d8d072d1efd.png
  1. 在云开发数据库中,新建两个 colleciton,第一个名为 goods,权限设置为 所有用户可读, 仅创建者及管理员可写,而另一个名为 orders,权限保持不变。

  2. 在云开发存储中,上传 cloud/storage 中的图片到 goods 目录下。

https://main.qcloudimg.com/raw/6387d368b33ab39c3b89bfcba465dd78.png
  1. 然后复制文件 fileID,并填入 cloud/database/goods.json 中。

https://main.qcloudimg.com/raw/441b6544a6775160b26b7bc3f35765b2.png
  1. collections goods 中,导入 cloud/database/goods.json

7.4.5. 源码介绍

7.4.5.1. 准备工作

微信支付,最主要的包括订单创建、发起支付、订单查询、申请退款、退款查询等等,本节主要介绍如何基于云开发实现微信支付。

开发前,建议先阅读以下相关的文档:微信支付小程序文档 和 微信小程序支持接口。

7.4.5.2. 订单创建

在 DEMO 里,我们有三个页面,分别是 pay-list(商品列表), pay-order (订单列表) 和 pay-result (订单详情)。 我们在商品列表里,直接单击【下单】,就会帮我们调用云函数 pay 进行订单的创建。 云函数 pay 是一个复合的云函数, 通过 switch 对支付的不同操作类型进行分支处理,创建订单的类型是 unifiedorder。

本节截选了部分云函数 pay 的代码。我们借助 wx-js-utils 封装好微信支付的能力。 初始化的时候,我们要填入小程序 appId, 微信支付商店号, 微信支付密钥等参数。

注解

由于某些敏感操作。例如,申请退款需要使用 证书,因此我们也需要带上证书并进行读取, 但对于云函数来说,由于服务器里有内置 CA 证书,因此不需要填写 caFileContent 参数。

请参考微信支付 统一下单 文档,先拼凑好请求参数。

注解

notify_url 参数,由于目前云函数还没支持 HTTP 触发器和公开的外网地址,因此这里可以先随便填一个, 可以是自有业务的地址。支付成功后,建议由小程序端主动去触发订单查询并更新状态。

示例代码如下:

const {
  WXPay,
  WXPayConstants,
  WXPayUtil
} = require('wx-js-utils');

// 此处省略其它代码


const pay = new WXPay({
    appId: APPID, // 小程序 appID
    mchId: MCHID, // 微信支付商户号
    key: KEY, // 微信支付密钥
    certFileContent: CERT_FILE_CONTENT, // 微信支付证书
    timeout: TIMEOUT, //  超时时间
    signType: WXPayConstants.SIGN_TYPE_MD5, // 加密方式
    useSandbox: false // 不使用沙箱环境
  });

  // 此处省略其它代码

// 拼凑订单参数
const curTime = Date.now();
const tradeNo = `${goodId}-${curTime}`; // 自这义的trade number
const body = good.name; // 商品名作为 body 内容
const spbill_create_ip = ip.address() || '127.0.0.1'; //  获取服务器的 ip 地址
const notify_url = 'http://www.qq.com'; //'云函数暂时没有外网地址和HTTP触发起,暂时随便填个地址。建议填自己站点的域名'
const total_fee = good.price; // 商品的价格
const time_stamp = '' + Math.ceil(Date.now() / 1000); // 订单创建的时间
const out_trade_no = `${tradeNo}`;
const sign_type = WXPayConstants.SIGN_TYPE_MD5; // 加密方式

let orderParam = {
    body,
    spbill_create_ip,
    notify_url,
    out_trade_no,
    total_fee,
    openid: OPENID,
    trade_type: 'JSAPI',
    timeStamp: time_stamp,
};

// 在微信支付服务端生成该订单
const {
    return_code,
    ...restData
} = await pay.unifiedOrder(orderParam);

7.4.5.3. 发起支付与订单查询

发起支付的过程,除了后台接口,还涉及到小程序的 wx.requestPayment 接口。 下面代码截选自 pay-result 订单详情页面,进入页面后,会先通过 getOrder 方法查询订单并获取订单所有数据, 用户单击支付后,会根据订单的数据拼凑参数。然后调用 wx.requestPayment 发起支付, 再去请求云函数 pay,在 payorder 处理分支中,进行订单信息的更新。

// 获取订单详情
async getOrder() {
const {result} = await wx.cloud.callFunction({
    name: 'pay',
    data: {
    type: 'orderquery',
    data: {
        out_trade_no: this.data.out_trade_no
    }
    }
})

const data = result.data || {}

this.setData({
    order: data
}, () => {
    // 如果状态是退款中,则每次进来都会查询一下退款情况
    if (data.status === 3) {
    this.queryRefund()
    }
})
},

// 发起支付
pay() {
const orderQuery = this.data.order
const out_trade_no = this.data.out_trade_no

const {
    time_stamp,
    nonce_str,
    sign,
    prepay_id,
    body,
    total_fee
} = orderQuery

wx.requestPayment({
    timeStamp: time_stamp,
    nonceStr: nonce_str,
    package: `prepay_id=${prepay_id}`,
    signType: 'MD5',
    paySign: sign,
    success: async () => {
    wx.showLoading({
        title: '正在支付',
    })

    wx.showToast({
        title: '支付成功',
        icon: 'success',
        duration: 1500,
        success: async () => {
        this.getOrder()

        await wx.cloud.callFunction({
            name: 'pay',
            data: {
            type: 'payorder',
            data: {
                body,
                prepay_id,
                out_trade_no,
                total_fee
            }
            }
        })
        wx.hideLoading()
        }
    })
    },
    fail() {}
})
},

通过云函数 pay 的 payorder 处理分支,主要就是通过 pay.orderQuery 查询微信服务端,如果支付成功, 就会更新订单数据,并取发送一条模板消息告诉用户支付成功。详情请参考 模板消息文档。

// 进行微信支付及更新订单状态
case 'payorder': {
    const {
    out_trade_no,
    prepay_id,
    body,
    total_fee
    } = data

    const {return_code, ...restData} = await pay.orderQuery({
    out_trade_no
    })

    if (restData.trade_state === 'SUCCESS') {
    await orderCollection
        .where({out_trade_no})
        .update({
        data: {
            status: 1,
            trade_state: restData.trade_state,
            trade_state_desc: restData.trade_state_desc
        }
        })

    // console.log('======restData======');
    // console.log(restData);

    const curDate = new Date()
    const time = `${curDate.getFullYear()}-${curDate.getMonth() + 1}-${curDate.getDate()} ${curDate.getHours()}:${curDate.getMinutes()}:${curDate.getSeconds()}`
    try {
        const messageResult = await cloud.callFunction({
        name: 'pay-message',
        data: {
            formId: prepay_id,
            openId: OPENID,
            appId: APPID,
            page: `pages/pay-result/index?id=${out_trade_no}`, // pages 前面不允许带有 "/",否则识别为非法参数
            data: {
            keyword1: {
                value: out_trade_no // 订单号
            },
            keyword2: {
                value: body // 物品名称
            },
            keyword3: {
                value: time// 支付时间
            },
            keyword4: {
                value: (total_fee / 100) + '元' // 支付金额
            }
            }
        }
        })
        console.log('=======message=========')
        console.log(messageResult)
    } catch (e) {
        console.log('===========')
        console.log(e)
    }
    }

    return {
    code: return_code === 'SUCCESS' ? 0 : 1,
    data: restData
    }
}

7.4.5.4. 申请退款

申请退款是针对已经支付过的订单。申请退款属于敏感操作,需要使用双向证书。 通过云函数 pay 的 refund 操作分支申请退款,退款不是马上生效的,可能会有一定的延迟。 示例代码如下:

// 申请退款,但不会马上退
async refund() {
wx.showLoading({
    title: '正在申请退款',
})

const result = await wx.cloud.callFunction({
    name: 'pay',
    data: {
    type: 'refund',
    data: {
        out_trade_no: this.data.out_trade_no
    }
    }
})

wx.hideLoading()

if (!result.code) {
    const order = this.data.order
    order.trade_state_desc = '正在退款'
    order.status = 3
    order.trade_state = 'REFUNDING'

    this.setData({
    order
    })
} else {
    this.showToast({
    title: result.message,
    icon: 'none'
    })
}
},

在云函数中,主要需要的参数是 out_trade_no 和 total_fee, 分别作为退款的订单号和退款金额传给 pay.refund 方法进行退款申请即可。示例代码如下:

// 申请退款
case 'refund': {
    const {out_trade_no} = data
    const orders = await orderCollection.where({out_trade_no}).get()

    console.log(orders)

    if (!orders.data.length) {
    return {
        code: 1,
        message: '找不到订单'
    }
    }

    const order = orders.data[0]
    const {
    total_fee,
    } = order

    const result = await pay.refund({
    out_trade_no,
    total_fee,
    out_refund_no: out_trade_no,
    refund_fee: total_fee
    })

    const {return_code} = result

    if (return_code === 'SUCCESS') {
    try {
        await orderCollection.where({out_trade_no}).update({
        data: {
            trade_state: 'REFUNDING',
            trade_state_desc: '正在退款',
            status: 3
        }
        })
    } catch (e) {
        return {
        code: 1,
        mesasge: e.message
        }
    }
    } else {
    return {
        code: 1,
        mesasge: '退款失败,请重试'
    }
    }

    return {
    code: 0,
    data: {}
    }
}

7.4.5.5. 退款查询

申请退款也有 notify_url 回调参数的,不过是可选项。但由于不支持 HTTP 触发和公网地址, 因此建议您在小程序端主动触发更新订单信息。示例代码如下:

// 查询退款情况
async queryRefund() {
const {result} = await wx.cloud.callFunction({
    name: 'pay',
    data: {
    type: 'queryrefund',
    data: {
        out_trade_no: this.data.out_trade_no
    }
    }
})

// 退款成功,则更新本地数据状态
if (!result.code && result.data) {
    const data = result.data
    const order = this.data.order
    order.trade_state_desc = data.trade_state_desc
    order.status = data.status
    order.trade_state = data.trade_state

    this.setData({
    order
    })
}
},

在云函数 pay 中查询并更新退款订单。示例代码如下:

// 查询退款情况
case 'queryrefund': {
    const {out_trade_no} = data

    const result = await pay.refundQuery({
    out_trade_no
    })

    const {refund_status_0, return_code} = result

    if (return_code === 'SUCCESS' && refund_status_0 === 'SUCCESS') {
    try {
        await orderCollection.where({out_trade_no}).update({
        data: {
            trade_state: 'REFUNDED',
            trade_state_desc: '已退款',
            status: 4
        }
        })

        return {
        code: 0,
        data: {
            trade_state: 'REFUNDED',
            trade_state_desc: '已退款',
            status: 4
        }
        }
    } catch (e) {
        return {
        code: 0
        }
    }
    } else {
    return {
        code: 0
    }
    }

    // eslint-disable-next-line no-unreachable
    return {
    code: 0
    }
}