:docloud:`微信支付 ` ========================================= 功能概述 ------------------- 本文档主要介绍微信支付功能,您可以通过本文示例体验体验下单、支付、退款等支付类的功能。 体验功能 ------------------- .. image:: https://main.qcloudimg.com/raw/c3bffdddfa54ecd95b5a569b10f71840.png 体验 DEMO ------------------- 本章的案例代码,请参考代码 `tcb-demo-basic `_ 。 DEMO 接入流程 ------------------- 代码下载完成后,请按照以下步骤操作: 1. 在云开发中创建好至少一个环境。 2. 在 `app.js` 文件中,如果使用默认环境,则没有改动。如果需要使用非默认环境, 则需要在 `wx.cloud.init` 中传入环境 ID。如果使用非默认环境,在云函数的 `cloud.init` 处,也需要补充环境 ID。如下图所示: .. image:: https://main.qcloudimg.com/raw/69d8d7590284cc80776021f3f8883948.png 示例代码如下: .. code:: js wx.cloud.init({ env: 'xxxx', // 环境 id traceUser: true }); 3. 在云函数根目录 cloud/functions 中找到函数 pay,在 config 目录中新建 index.js, 配置环境 ID、微信支付商户号、微信支付商户密钥、和双向证书中的 apiclient_cert.p12, 该证书放也放在 config 目录中,用 npm 安装两个云函数的依赖,并上传该函数。 .. image:: https://main.qcloudimg.com/raw/70c58a00ca5f69ee1b449dd06f1734a1.png 4. 找到云函数 `pay-message`,在 `config` 目录中新建 `index.js`。 .. image:: https://main.qcloudimg.com/raw/532124c3661e43c86cddca24a85ad508.png 5. 配置小程序 AppSecret 和模板 ID(选择合适的模板,合适的字段即可),用 npm 安装两个云函数的依赖,然后上传该函数。 .. image:: https://main.qcloudimg.com/raw/10e976f5ac788aa0fbb92d8d072d1efd.png 6. 在云开发数据库中,新建两个 `colleciton`,第一个名为 `goods`,权限设置为 所有用户可读, 仅创建者及管理员可写,而另一个名为 orders,权限保持不变。 7. 在云开发存储中,上传 `cloud/storage` 中的图片到 `goods` 目录下。 .. image:: https://main.qcloudimg.com/raw/6387d368b33ab39c3b89bfcba465dd78.png 8. 然后复制文件 `fileID`,并填入 `cloud/database/goods.json` 中。 .. image:: https://main.qcloudimg.com/raw/441b6544a6775160b26b7bc3f35765b2.png 9. 在 `collections goods` 中,导入 `cloud/database/goods.json`。 源码介绍 ------------ 准备工作 ~~~~~~~~~~~~~~~~~~~~~ 微信支付,最主要的包括订单创建、发起支付、订单查询、申请退款、退款查询等等,本节主要介绍如何基于云开发实现微信支付。 开发前,建议先阅读以下相关的文档:微信支付小程序文档 和 微信小程序支持接口。 订单创建 ~~~~~~~~~~~~~~~~~~~~~ 在 DEMO 里,我们有三个页面,分别是 pay-list(商品列表), pay-order (订单列表) 和 pay-result (订单详情)。 我们在商品列表里,直接单击【下单】,就会帮我们调用云函数 pay 进行订单的创建。 云函数 pay 是一个复合的云函数, 通过 switch 对支付的不同操作类型进行分支处理,创建订单的类型是 unifiedorder。 本节截选了部分云函数 pay 的代码。我们借助 wx-js-utils 封装好微信支付的能力。 初始化的时候,我们要填入小程序 appId, 微信支付商店号, 微信支付密钥等参数。 .. note:: 由于某些敏感操作。例如,申请退款需要使用 证书,因此我们也需要带上证书并进行读取, 但对于云函数来说,由于服务器里有内置 CA 证书,因此不需要填写 caFileContent 参数。 请参考微信支付 统一下单 文档,先拼凑好请求参数。 .. note:: notify_url 参数,由于目前云函数还没支持 HTTP 触发器和公开的外网地址,因此这里可以先随便填一个, 可以是自有业务的地址。支付成功后,建议由小程序端主动去触发订单查询并更新状态。 示例代码如下: .. code:: js 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); 发起支付与订单查询 ~~~~~~~~~~~~~~~~~~~~~ 发起支付的过程,除了后台接口,还涉及到小程序的 wx.requestPayment 接口。 下面代码截选自 pay-result 订单详情页面,进入页面后,会先通过 getOrder 方法查询订单并获取订单所有数据, 用户单击支付后,会根据订单的数据拼凑参数。然后调用 wx.requestPayment 发起支付, 再去请求云函数 pay,在 payorder 处理分支中,进行订单信息的更新。 .. code:: js // 获取订单详情 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 查询微信服务端,如果支付成功, 就会更新订单数据,并取发送一条模板消息告诉用户支付成功。详情请参考 模板消息文档。 .. code:: js // 进行微信支付及更新订单状态 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 } } 申请退款 ~~~~~~~~~~~~~~~~~~~~~ 申请退款是针对已经支付过的订单。申请退款属于敏感操作,需要使用双向证书。 通过云函数 pay 的 refund 操作分支申请退款,退款不是马上生效的,可能会有一定的延迟。 示例代码如下: .. code:: js // 申请退款,但不会马上退 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 方法进行退款申请即可。示例代码如下: .. code:: js // 申请退款 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: {} } } 退款查询 ~~~~~~~~~~~~~~~~~~~~~ 申请退款也有 notify_url 回调参数的,不过是可选项。但由于不支持 HTTP 触发和公网地址, 因此建议您在小程序端主动触发更新订单信息。示例代码如下: .. code:: js // 查询退款情况 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 中查询并更新退款订单。示例代码如下: .. code:: js // 查询退款情况 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 } }