微信小程序云开发订单微信支付与小票和标签打印的完整高效流程

一个字“全”!!!

  • 前言
  • 一、流程设定
    • 1、如何开通云支付流程
    • 2、以订单下单为例的支付流程
      • 2.1 业务场景介绍
      • 2.2 业务场景流程图
  • 二、代码与代码文件组成
    • 1、页面JS
    • 2、云函数payPre
    • 3、支付回调函数pay_cb
      • 3.1 准备条件
      • 3.2 必要认知
      • 3.3 pay_cb 完整函数如下
    • 4、标签打印函数
    • 5、小票打印函数
  • 三、回调函数常见问题
    • 1、提高云开发数据库订单更新与查询效率
    • 2、避免阻塞与防止回调函数一直回调
  • 五、参考文档

前言

本片文章全文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: '', // 云环境环境IDtimeout: 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 = ''; //申请的appsecretvar 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. 优声云小票打印机参考官网文档

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/387593.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Llama 4训练已开启!Meta科学家最新采访,揭秘Llama 3.1是如何炼成的

Llama 3.1的诞生标志着人工智能领域的一个重要里程碑&#xff0c;它不仅是Meta在大型语言模型&#xff08;LLM&#xff09;研发上的一次重大突破&#xff0c;也代表了开源AI模型在技术进步和应用潜力上的新高度。以下是对Llama 3.1的炼成过程、观点阐述以及未来发展趋势的分析。…

莫斯科的社会生态环境之一瞥

题记 社会生态&#xff0c;它是指人类随着利用科技对环境的作用所呈现出的人和人的关系&#xff0c;人和人群的关系&#xff0c;人群和环境的关系 。而生态文明的理念&#xff0c;提倡的不止是尊重自然、顺应自然、保护自然&#xff0c;也包含注重规律的和谐性、可持续性、稳定…

如何使用CANoe自带的TCP/IP Stack验证TCP的零窗口探测机制

如果想利用CANoe自带的TCP/IP协议栈验证TCP的零窗口探测机制,就必须添加一个网络节点并配置独立的CANoe TCP/IP协议栈,作为验证对象。而与它进行TCP通信的对端也是一个网络节点,但不要配置TCP/IP协议栈,而是使用CAPL代码在底层组装TCP报文模拟TCP通信过程。这样可以尽量减少…

2024年最强网络安全学习路线,详细到直接上清华的教材!

关键词&#xff1a;网络安全入门、渗透测试学习、零基础学安全、网络安全学习路线 首先咱们聊聊&#xff0c;学习网络安全方向通常会有哪些问题前排提示&#xff1a;文末有CSDN官方认证Python入门资料包 &#xff01; 1、打基础时间太长 学基础花费很长时间&#xff0c;光语…

医院体检信息管理系统,C#体检系统源码,健康体检系统PEIS

体检服务全流程 检前 检前注意事项提醒-体检预约-套餐选择-体检签到-费用缴纳 检中 科室队列提醒-增项检中支付 检后 报告查询-体检百科-报告解读-问卷调查 体检管理系统模块介绍 一、登记管理模块 登记体检者基本信息&#xff0c;包括唯一的体检编号&#xff0c;姓名、…

【Golang 面试 - 基础题】每日 5 题(八)

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/UWz06 &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;收藏…

Miniconda快速安装conda

关注B站可以观看更多实战教学视频&#xff1a;hallo128的个人空间 安装官方网址&#xff1a;https://docs.anaconda.com/miniconda/#quick-command-line-install 1. Miniconda for Windows curl https://repo.anaconda.com/miniconda/Miniconda3-latest-Windows-x86_64.exe -o …

【LeetCode】56. 区间合并

区间合并 题目描述&#xff1a; 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间&#xff0c;并返回 一个不重叠的区间数组&#xff0c;该数组需恰好覆盖输入中的所有区间 。 示例 1&#xff1a; …

捷径,这世上有没有捷径

Q&#xff1a;大师&#xff0c;这个世界上有没有捷径&#xff1f; A&#xff1a;有呀&#xff0c;有捷径呀 Q&#xff1a;大师&#xff0c;那我要怎么走&#xff1f; A&#xff1a;你错啦&#xff0c;不要想着走捷径&#xff0c;因为捷径不是用来走的&#xff0c;捷径是用来飞的…

SNN系列论文阅读:梦开始的地方

论文地址:https://igi-web.tugraz.at/people/maass/psfiles/85a.pdf 1. nn分类 一开始论文将nn分为三类, 1. 最初的MP多层感知机 2. 加入非线性激活, 并可以反向传播训练的神经网络 3. SNN 注意分类不是普通的fc网络,rnn网络和snn网络 2. 理解脉冲 LIF模型,全称Leak…

CUDA_Occupancy_Calculator计算公式

CUDA_Occupancy_Calculator计算公式

【设计模式:单例模式】

单例模式的特点&#xff1a; 单例类只允许一个实例单例类必须自己创造自己的唯一实例单例类必须给所有其他对象提供这一实例 单例模式底层如何实现&#xff1a; 私有化构造函数&#xff0c;类外部无法创造类对象&#xff0c;实现了单例类只允许有一个实例对象的特点类定义中含有…

【技术支持案例】使用S32K144+NSD8381驱动电子膨胀阀

文章目录 1. 前言2. 问题描述3. 理论分析3.1 NSD8381如何连接电机3.2 S32K144和NSD8381的软件配置 4.测试验证4.1 测试环境4.2 测试效果4.3 测试记录 1. 前言 最近有客户在使用S32K144NSD8381驱动电子膨胀阀时&#xff0c;遇到无法正常驱动电子膨胀阀的情况。因为笔者也是刚开…

c#中使用数据验证器

前言 在很多情况下&#xff0c;用户的输入不一定满足我们的设计要求&#xff0c;需要验证输入是否正确&#xff0c;传统的方案是拿到控件数据进行逻辑判定验证后&#xff0c;给用户弹窗提示。这种方法有点职责延后的感觉&#xff0c;数据视图层应该很好的处理用户的输入。使用…

如何处理selenium Webdriver中的文本框?

文本框或字段在整个网页中广泛使用,本文将介绍如何在Java中使用Selenium Webdriver处理文本框。可以有各种文本字段,我们将尝试包括其中的大多数,并执行各种操作,如清除和输入文本。 我们将使用我们的Selenium游乐场网站- testkru,与各种文本框进行交互。您也可以使用同一…

后端采用SpringBoot框架开发的:ADR药物不良反应智能监测系统源码,用于监测和收集药品在使用过程中发生的不良反应的系统

ADR药物不良反应智能监测系统是一套用于监测和收集药品在使用过程中发生的不良反应&#xff08;Adverse Drug Reaction, ADR&#xff09;的系统。该系统基于医院临床数据中心&#xff0c;运用信息技术实现药品不良反应的智能监测、报告管理、知识库查询、统计分析等功能&#x…

厚积薄发,详解 IoTeX 2.0 如何推动 DePIN 赛道迈向新台阶

背 景 DePIN 是加密货币行业的一个新兴垂直领域&#xff0c;也是本轮牛市最重要的叙事之一。DePIN 通常通过发行和分配代币来激励参与者&#xff0c;用户可以通过提供资源、维护网络、参与治理等方式获得代币奖励并产生直接的经济收益&#xff0c;从而重新洗牌财富分配方…

Java线程阻塞:原因

Java线程阻塞&#xff1a;原因 1. sleep()2. suspend() 和 resume()&#xff08;不推荐&#xff09;3. yield()4. wait() 和 notify()/notifyAll() &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 线程阻塞是一个重要的概念&#xff0c;它决…

17K star!30秒偷走你的声音,开源声音克隆工具

现在的AI发展越来越快&#xff0c;生成一段语音不是难事&#xff0c;那如果生成的是你自己的声音&#xff0c;你觉得如何&#xff1f; 今天我们分享一款开源的声音克隆工具&#xff0c;只需30秒的一般音源&#xff0c;他就可以偷走你的声音&#xff0c;它就是&#xff1a;Open…

前端开发不得不知道的那些事

文章目录 一、技能提升篇vueuseJavaScript中文网JavaScript.infoRxJsWeb安全学习书栈网码农之家 二、UI篇iconfont&#xff1a;阿里巴巴矢量图标库IconPark3dicons美叶UndrawError 404摹克 三、CSS篇You-need-to-know-cssCSS TricksAnimate.cssCSS ScanCSS Filter 四、颜色篇中…