前言

本片文章全文12000字,细致讲述从申请账户到小程序注册到打印机的选型最后到实现微信小程序云开发订单支付更新与小票,标签打印完整的高效流程。

一、流程设定

1、如何开通云支付流程

无论是云支付还是其他平台语言的原生支付接入都有共同一点:非个人,是组织,企
业,个体商家[1],说白了就是有合理合法经营的营业执照,具体可参考[1]

1.1 开通商户,获取商户号,商户号获取指引文档[2]

1.2 注册微信小程序,微信小程序注册要是“组织,企业,个体商家”,并且主体要与注册
商户号的主体保持一致。注册完成后等待认证完成即可进入微信公众平台[3]

微信公众平台

1.3 进入微信小程序开发者平台后填写相关的资料并进行类目确定。再次有一次非常需要注意的事项。

注意:根据交易类小程序运营规范,对于小程序内提供珠宝玉石、3C 数码、盲盒、服饰内衣、海淘、美妆、酒类、家用电器、玩具、箱包皮具、鞋靴、运动户外等商品在线销售及配送服务时,不可使用云调用支付能力。

1.4 对接商户号,进行JASP支付相关确认,接入准备文档[4]

注意:微信小程序云开发支付是一种免鉴权,免密钥配置的安全的支付方式,所以不
需要配置相关的密钥,直接进行到下一步1.5

云函数支付的官网流程图

1.5 云开发控制台确认

此处请参考微信开发者文档[5],文档内容详细。

2、以订单下单为例的支付流程

2.1 业务场景介绍

客户A在我们的点餐平台上点了一杯咖啡,所以客户要在选好后付款,在付款完成后商家的小票打印机和标签打印机打印出本订单的相关信息。如下:图2.1为小票打印机,图2.2为标签打印机。

小票打印机
图2.1 小票打印机
在这里插入图片描述
图2.2 标签打印机

2.2 业务场景流程图

在这里插入图片描述

二、代码与代码文件组成

1、页面JS

作用:发起支付请求与回调后续相关逻辑操作,例如:返回上一级,或跳转到相关页面。

wx.cloud.database().collection('orderUser').add({
   data:{
         subTime:this.getDateandTime().time,/* 下单时间 */
         subDate:this.getDateandTime().date,/* 下单日期 */
         orderCode:hexiaoCode,//核销码
         orderRows:[this.data.rows],
         orderGroups:tempInfo,//点单详情
         status:'制作中', // 出餐情况
         style:"点餐", //用餐方式
         pre_title:'**** 微信支付点单',
         pay_status:'wait', //交易状态
         orderNumber:orderTemp, //交意单号
         order_price:this.data.priceAll, //价格
		 userName:wx.getStorageSync('userInfo').userName,
         phone:wx.getStorageSync('userInfo').phone,
         orderRemarks:this.data.orderRemarks
         },success:(event)=>{
           //支付接口
           /* 价格处理 */
           var price = parseFloat(this.data.priceAll)
           // 订单处理
           wx.cloud.callFunction({
              name:'payPre',
              data:{
                  pro_name:'***店自取订单',
                  pro_codeNum:orderTemp,
                  pro_price:price*100,
               },success:(res_3)=>{
                   wx.hideLoading()
                   console.log("支付接口",res_3);
                  // 调起支付
                  wx.requestPayment({
                       timeStamp: res_3.result.payment.timeStamp,
                       nonceStr: res_3.result.payment.nonceStr,
                       package: res_3.result.payment.package,
                       signType: 'MD5',//加密方式
                       paySign: res_3.result.payment.paySign,
                       success (res_4) {
                             //这里写付款完成后的逻辑函数
                       },fail (res_4) {
                             console.log('发起支付窗口失败');
                       }
                  })
                  },fail(res_3){
                      console.log('调用payPre失败:',res_3);
                  }
            })
       },fail:(res)=>{
            console.log("创建订单出错误",res);
         }
 })

2、云函数payPre

作用调用云函数支付接口与返回支付的信息

// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init({ env: '' }) // 使用当前云环境
// 云函数入口函数
exports.main = async (event, context) => {
    const res = await cloud.cloudPay.unifiedOrder({
        "body" :event.pro_name, // 订单名称
        "outTradeNo" :event.pro_codeNum, // 订单号
        "spbillCreateIp" : "127.0.0.1",
        "subMchId" : "*****", //商户号ID
        "totalFee" : event.pro_price, // 价格
        "envId": "",//云环境ID
        "functionName": "pay_cb" //回调函数必须有
      })
      return res
}

3、支付回调函数pay_cb

作用:支付完成后的回调函数,这个函数很重要,所有的支付完成的云操作都在这里边进行。

3.1 准备条件

由于用到了第三方库:flatted,所有要下载好Node.js并且使用NPM命令进行远程下载

return {
      errcode: 0, //必须要写,否则微信服务器没有响应则会一直调用
      errmsg: "订单未找到",
};

3.2 必要认知

pay_cb作为回调函数,则必须要有一个固定返回形式,参考文档如[6]

在这里插入图片描述

npm install flatted

3.3 pay_cb 完整函数如下

const cloud = require('wx-server-sdk');
const { stringify, parse } = require('flatted');

// 此处的作用是:设置此函数的超时,主要是防止大量数据的处理时间造成的报错

cloud.init({ 
    env: '', // 云环境环境ID
    timeout: 10000 // 设置超时时间为10秒
});

const db = cloud.database();

// 更新订单状态并查询订单信息函数
const updateOrderStatusAndFetch = async (outTradeNo) => {
    try {
        // 更新订单状态
        await db.collection("orderUser").where({
            orderNumber: outTradeNo
        }).update({
            data: {
                pay_status: "订单完成"
            }
        });
        // 查询订单信息
        const res = await db.collection("orderUser").where({
            orderNumber: outTradeNo
        }).get();
        return res.data[0];
    } catch (error) {
        console.error("订单更新或查询出错", error);
        throw error;
    }
};

// 标签打印函数
const printLabel = async (allOrder) => {
    try {
        const result = await cloud.callFunction({
            name: 'printBiaoQian',
            data: {
                allOrder
            }
        });
        return { name: "标签打印", value: result.result };
    } catch (error) {
        console.error("标签打印出错", error);
        throw error;
    }
};

// 小票打印函数
const printTicket = async (printLabelObj) => {
    try {
        const result = await cloud.callFunction({
            name: 'printer',
            data: parse(stringify({
                payStyle: printLabelObj.payStyle,
                time: printLabelObj.time,
                ordercode: printLabelObj.ordercode,
                takecode: printLabelObj.takecode,
                info: printLabelObj.info,
                price: printLabelObj.price,
                phone: printLabelObj.phone,
                name: printLabelObj.name,
                remarks: printLabelObj.remarks
            }))
        });
        return { name: "小票打印", value: result.result };
    } catch (error) {
        console.error("小票打印出错", error);
        throw error;
    }
};

// 云函数入口函数
exports.main = async (event, context) => {
    const { outTradeNo, resultCode } = event;
    let resultAll = [];

    if (!outTradeNo || outTradeNo.length === 0) {
        return {
            errcode: 0, //必须要写,否则微信服务器没有响应则会一直调用
            errmsg: "订单未找到",
        };
    }
    try {
        console.log("开始处理订单支付");

        // 立即响应微信服务端
        const response = {
            errcode: 0,
            errmsg: event.returnCode,
        };

        // 异步处理订单更新和打印逻辑
        updateOrderStatusAndFetch(outTradeNo)
            .then(async (order) => {
                if (!order) {
                    console.error("订单未找到");
                    return;
                }

                console.log("订单信息查询完成");

                // 准备标签打印数据
                console.log("准备标签打印数据中...");
                const allOrderGroups = order.orderGroups;
                const tempNowTemp = allOrderGroups.map(element => {
                    const orderShuXingStr = element.orderShuXing.map(el => `#${el.value}`).join('');
                    return {
                        sumNum: element.orderNum,
                        title: element.goodGroups.shopTitle,
                        tuple: `(${orderShuXingStr})`,
                        order: order.orderCode,
                        time: order.subTime,
                        price: element.priceAll,
                        style: "小程序点单"
                    };
                });

                // 准备小票打印数据
                console.log("准备小票打印数据中...");
                const tupleInfoArray = allOrderGroups.map(element => {
                    const orderShuXingStr = element.orderShuXing.map(el => `#${el.value}`).join('');
                    return {
                        tuple: orderShuXingStr,
                        title: element.goodGroups.shopTitle,
                        num: element.orderNum
                    };
                });

                const printLabelObj = {
                    payStyle: "小程序微信支付",
                    time: order.subTime,
                    ordercode: order.orderNumber,
                    takecode: order.orderCode,
                    info: tupleInfoArray,
                    price: order.order_price,
                    phone: order.phone,
                    name: order.userName,
                    remarks: order.orderRemarks
                };

                // 标签打印
                console.log("标签打印中...");
                const printBiaoQianResult = await printLabel(tempNowTemp);
                resultAll.push(printBiaoQianResult);
                console.log("标签打印完成");

                // 小票打印
                console.log("小票打印中...");
                const printerResult = await printTicket(printLabelObj);
                resultAll.push(printerResult);
                console.log("小票打印完成");

                // 打印完成后的处理
                console.log("支付处理完成");
            })
            .catch((error) => {
                console.error("异步处理订单出错", error);
            });

        // 立即返回响应
        return response;
    } catch (error) {
        console.error("支付处理出错", error);
        return {
            errcode: 0,
            errmsg: "支付处理出错",
        };
    }
};

4、标签打印函数

在本文章当中,我所使用的标签打印机是“芯烨云”的打印机,SDK比较明确,主流打印机,使用后端语言比较多,具体请参考官网文档[7]

// 引入云函数 SDK 和 axios
const cloud = require('wx-server-sdk');
const axios = require('axios'); //提前用npm下载此库

// 初始化云环境
cloud.init({
  env: '' //云开发云函数环境ID
});

// 生成签名的函数
function generateSignature(user, userKey, timestamp) {
  const crypto = require('crypto');
  const str = user + userKey + timestamp;
  return crypto.createHash('sha1').update(str).digest('hex');
}

// 打印标签的云函数
exports.main = async (event, context) => {
  // 获取传入的订单数组
  const allOrder = event.allOrder;
  // 设置相关参数
  const user = ''; //开放用户账户名
  const userKey = ''; // 用户密钥
  const timestamp = Math.floor(Date.now() / 1000);
  const sign = generateSignature(user, userKey, timestamp);

  // 添加打印机
  const addPrinterResponse = await axios.post('https://open.xpyun.net/api/openapi/xprinter/addPrinters', {
    user: user,
    timestamp: timestamp,
    sign: sign,
    items: [
      {sn: '打印机sn号', name: '店铺名称'}
    ]
  }, {
    headers: {
      'Content-Type': 'application/json;charset=UTF-8'
    }
  });
  if (addPrinterResponse.data.code !== 0) {
    // 添加打印机失败
    return addPrinterResponse.data;
  }

  // 遍历订单数组,生成打印内容并发送打印请求
  for (const order of allOrder) {
    for (let i = 1; i <= order.sumNum; i++) {
      // 初始化单个订单的打印内容
      let printContent = '';
      printContent += `<TEXT x="10" y="10" font="9" w="1" h="1" r="0">${order.style} #${i}/${order.sumNum}</TEXT>\n`;
      printContent += `<TEXT x="10" y="40" font="9" w="1" h="1" r="0">取餐号:${order.order}</TEXT>\n`;
      printContent += `<TEXT x="10" y="70" font="9" w="1" h="1" r="0">${order.title}</TEXT>\n`;
      printContent += `<TEXT x="10" y="100" font="9" w="1" h="1" r="0">${order.tuple}</TEXT>\n`;
      printContent += `<TEXT x="10" y="130" font="9" w="1" h="1" r="0">${order.price}元</TEXT>\n`;
      printContent += `<TEXT x="10" y="160" font="9" w="1" h="1" r="0">${order.time}</TEXT>\n`;
      printContent += `<TEXT x="10" y="190" font="9" w="1" h="1" r="0">NFIVE COFFEE</TEXT>\n`;
      // 构建打印请求体
      const printRequestBody = {
        sn: '',
        content: printContent,
        user: user,
        timestamp: timestamp,
        sign: generateSignature(user, userKey, timestamp), // 重新生成签名
        debug: '0'
      };
      // 发送打印请求
      const printResponse = await axios.post('https://open.xpyun.net/api/openapi/xprinter/printLabel', printRequestBody, {
        headers: {
          'Content-Type': 'application/json;charset=UTF-8'
        }
      });
      // 检查打印结果
      if (printResponse.data.code !== 0) {
        // 打印失败
        return printResponse.data;
      }
    }
  }
  // 所有订单打印成功
  return { code: 0, msg: 'All orders printed successfully' };
};

5、小票打印函数

在本文章当中,我所使用的小票打印机是“优声云”的打印机,SDK比较明确,使用后端语言比较多,具体请参考官网SDK文档链接[8]

// 云函数入口文件
const cloud = require('wx-server-sdk')
const crypto = require('crypto');  //提前用npm下载此库
//axios版本
const axios = require('axios')  //提前用npm下载此库
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
// 云函数入口函数
exports.main = async (event, context) => {
    //event为客户端上传的参数
    console.log('event : ', event)
    //通用参数
    var appid = ''; //应用ID,申请的ID,值为number类型
    var appsecret = ''; //申请的appsecret
    var sign = ''; //签名,不需要填写,会自动生成。
    var timestamp = parseInt((new Date().getTime()) / 1000); //当前时间戳,不需要填写
    var deviceid = ''; //设备号
    var devicesecret = ''; //设备密钥
    // 构建打印内容
    var printdata = '<B1><C>小程序订单</C></B1>\n';
    printdata += '<B1><C></C></B1>\n';
    printdata += '--------------------------------\n';
    printdata += '<B1><C>' + event.payStyle + '</C></B1>\n';
    printdata += '下单时间:' + event.time + '\n';
    printdata += '订单编号:' + event.ordercode + '\n';
    printdata += '取餐码:' + event.takecode + '\n';
    printdata += '--------------------------------\n';
    event.info.forEach(item => {
        printdata += item.title + ' x ' + item.num + '\n';
        printdata += item.tuple + '\n';
    });
    printdata += '--------------------------------\n';
    printdata += '实付金额:' + event.price + '\n';
    printdata += '下单手机号:' + event.phone + '\n';
    printdata += '客户姓名:' + event.name + '\n';
    printdata += '备注:' + event.remarks + '\n';
    //打印
    var parameter = {
        appid: appid,
        timestamp: timestamp,
        deviceid: deviceid,
        devicesecret: devicesecret,
        printdata: printdata,
        //请在此添加您需要的参数,格式为上面定义的变量名为key值和value值
    }
    //键值排序
    function objKeySort(obj) { //排序的函数
        var newkey = Object.keys(obj).sort();
        //先用Object内置类的keys方法获取要排序对象的属性名,再利用Array原型上的sort方法对获取的属性名进行排序,newkey是一个数组
        var newObj = {}; //创建一个新的对象,用于存放排好序的键值对
        for (var i = 0; i < newkey.length; i++) { //遍历newkey数组
            newObj[newkey[i]] = obj[newkey[i]]; //向新创建的对象中按照排好的顺序依次增加键值对
        }
        return newObj; //返回排好序的新对象
    }
    var newObj = objKeySort(parameter); //函数执行
    var valueJian = '';
    for (var x in newObj) {
        if (!newObj[x]) {
            break;
        }
        valueJian += x + newObj[x]
    }
    valueJian += appsecret;
    //console.log(valueJian);
    sign = crypto.createHash('md5').update(valueJian).digest("hex")
    //需要的参数对象列表
    parameter.sign = sign;
    try {
        const response = await axios.post('https://open-api.ushengyun.com/printer/print', parameter);
        // 处理循环引用
        const data = JSON.parse(JSON.stringify(response, (key, value) => {
            if (typeof value === 'object' && value !== null) {
                const seen = new WeakSet();
                if (seen.has(value)) {
                    return;
                }
                seen.add(value);
            }
            return value;
        }));

        console.log(data);
        return data;
    } catch (error) {
        console.error('Error:', error);
        return { errorCode: 1, errorMessage: error.message };
    }
}

三、回调函数常见问题

在接下来的回调函数我们统一使用pay_cb表示

1、提高云开发数据库订单更新与查询效率

面临大量的数据并发,会造函数执行超时,对于这个云数据库操作我们有以下方法

1.1 在云开发数据库集合当中增加索引,对于经常使用的查询键,可以增加到索引当中去。如图
在这里插入图片描述 1.2 减少数据库调用

将查询和更新操作合并为一个操作,减少数据库调用的次数。具体请仔细查看pay_cb

回调函数中的订单更新与查询操作语句写的方法。

1.3 提高云函数超时时间

在云函数的配置中增加超时时间,可以在调用 cloud.init 时设置 timeout 参数。

cloud.init({ 
    env: '',
    timeout: 10000 // 设置超时时间为10秒
});

2、避免阻塞与防止回调函数一直回调

由于在付款完成后,回调函数执行当中很多时间浪费在云数据库操作查询更新上,更新的时候使用了await,阻塞了应答微信服务端的应答,应答晚了,微信服务端认为你没有应答,所以就造成微信服务器一直在回调
所以我们可以按照pay_cb完整代码当中那样的结构去写,通过在接收支付回调时立即返回响应,然后异步更新数据库,这样做可以避免阻塞微信服务端的响应时间。

五、参考文档

1. 云调用对接微信支付
2. 商户号获取指引文档
3. 微信公众平台
4. 接入准备文档
5. 微信开发者文档云支付确认
6. 芯烨云标签打印机参考官网文档
7. 优声云小票打印机参考官网文档

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐