基于hyperleger fabric区块链的校园化妆品交易平台搭建

源码资料等获取方式在文章末尾

一、大数据与区块链解决方案概述
选题背景:
目前不少同学在校园里进行二手交易没有一个大众认可的平台,很多都是私下交易,但会存在很多虚假交易,甚至出现诈骗事件,没有一个让校园同学认可放心的二手化妆品交易平台,基于这个交易问题,我们将校园二手化妆品交易与区块链技术结合来处理交易存在的问题,给校园内学生提供提供一个安全、公开、方便、美观的交易平台,让学生在校园交易的放心,买得放心,用的安心。不但可以交易平台的资产也可以添加我的售卖,自己成为卖家。

采用区块链技术架构的优点:
区块链采用了分布式核算和存储,不存在中心化的硬件或管理机构,因此使得任意节点的权利和义务都是均等的。区块链的系统的一个开放性质的,除了交易各方的私有信息被加密外,区块链的数据对所有人公开的。在区块链中,任何人为的干预都是不起作用的,将对“人”的信任改成了对机器的信任。使得整个系统中的所有节点能够在去信任的环境自由安全的交换数据。在区块链中一旦信息经过验证并添加至区块就会永久的存储起来无法进行修改。
区块链作为当下被推崇的一门技术主要优点为分布式记账、全流程记录、不可篡改性和加密技术。在区块链中都必须遵循同一记账交易规则,基于密码的算法同时每笔交易需要网络内其他用户的批准,核心分布式去中心化。区块链不可篡改和加密安全性采取了单向哈希算法,时间的不可逆性导致任何试图入侵篡改区块链内数据信息的行为都很容易被追溯,提高了对应的安全性。

二、应用场景需求分析

应用场景分析:
基于目前不少同学在校园里进行二手交易没有一个大众认可的平台,很多都是私下交易,但会存在很多虚假交易,甚至出现相关二手化妆品交易诈骗事件,目前没有一个让校园同学认可放心的二手化妆品交易平台,基于这个交易问题,我们将校园二手化妆品交易与区块链技术结合来处理交易存在的问题,给校园内学生提供提供一个安全、公开、方便、美观的交易平台,让学生在校园交易的放心,买得放心,用的安心。不但可以交易平台的资产也可以添加我的售卖,自己成为卖家。

需求分析
在校园内搭建一个基于Fabirc区块链的化妆品二手交易平台,学生以学院为单位(=>Org=>peer节点)加盟该平台;学生使用客户端连接本组织的peer节点参与交易。可以售卖化妆品、也可以购买化妆品;阅读过管理员平台公告后进入登录页面,学生通过学号认证后通过登录,学生在个人中心可以查看所有可交易的化妆品也可以查看自己的交易情况是否已经完成,也可以注册平台个人账号进行商品评论和交流查看个人信息,不但可以交易平台的资产也可以添加我的售卖,自己成为卖家;用户可以根据自己的需求选择不同的化妆品类型比如口红、香水、面部护理等。查看化妆品详情,收藏化妆品、加入购物车完成资产交易,退出的时候直接注销用户清除个人登陆记录保护用户隐私。这样校园内学生交易大大提高了安全性和大众的认可,满足了很多学生的交易需求。

用户需求分析和用户组织结构设计:

三、平台功能设计及功能结构设计图





四、数据存储结构设计与链码开发

1、数据存储结构
交易资产以资产键值对(Key-Value)的方式存储:

Key:id value {brand: '化妆品品牌名称',type: '化妆品类型',price: '$化妆品价格',owner: '拥有者',describe:'化妆品介绍',},


2、链码开发与实现

启动startFabric.sh javascript脚本启动网络并部署链码:
打开终端,进入到fabcar:

cd /home/bsxy/github.com/hyperledger/fabric-samples/fabcar

然后启动脚本:

./startFabric.sh javascript


通过调用startFabric.sh脚本,我们就很方便的把区块链网络启动成功了。

对于我们的开发Fabric应用程序 ,我们需要按照下面操作来进行启动:
新建终端输入:

cd /home/bsxy/github.com/hyperledger/fabric-samples/fabcar/javascript

然后我们需要在这个文件路径下安装一个art-template依赖包:

npm install art-template


(如果这里安装art-template依赖包还是不行的话,试一下npm install)
然后我们再新建一个终端进入同一个目录:

cd /home/bsxy/github.com/hyperledger/fabric-samples/fabcar/javascript

该目录包含使用Node.js对应的Fabric SDK 开发的示例程序:

ls -ll


当我们创建网络的时候,一个管理员用户( admin)被证书授权服务器(CA)创建成了 注册员 。我们第一步要使用 enroll.js 程序为 admin 生成私钥、公钥和 x.509 证书。这个程序使用一个 证书签名请求 (CSR)——现在本地生成公钥和私钥,然后把公钥发送到 CA ,CA 会发布会一个让应用程序使用的证书。这三个证书会保存在钱包中,以便于我们以管理员的身份使用 CA 。

我们登记一个 admin 用户:

node enrollAdmin.js

这个命令将 CA 管理员的证书保存在 wallet 目录。 您可以在 wallet/admin.id 文件中找到管理员的证书和私钥。

注册和登记应用程序用户:

node registerUser.js

更新账本:

node invoke.js

然后我们就可以启动我们的应用程序l了:

node app.js


然后我们可以对链上的资产进行增删改查:

我们简单修改链上资产数据:
把资产编号MAKEUP1的迪奥香水价格把809修改为999



在终端利用query脚本查询一下链资产数据信息:

node query.js


这里我们也是成功修改链上资产数据。

下面给大家详细介绍一下系统功能:

功能一:调用智能合约初始化自定义链码:

peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n fabmakeup --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"initLedger","Args":[]}'

功能二:通过调用智能合约初始化查询特定键值的资产:

peer chaincode query -C mychannel -n fabmakeup -c '{"function":"queryMakeup","Args":["MAKEUP1"]}'

功能三:通过调用智能合约初始化查询特定键值范围的资产:

peer chaincode query -C mychannel -n fabmakeup -c '{"function":"queryMakeupsByRange","Args":["MAKEUP3",""]}'

功能四:通过调用智能合约新增资产:

peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n fabmakeup --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"createMakeup","Args":["MAKEUP14","迪奥","香水","450","Lala","美白保湿"]}'


功能五:通过调用智能合约修改资产属性:

peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n fabmakeup --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"updateMakeup","Args":["MAKEUP1","迪奥","香水","$820","gala","女士香水 魅惑清新淡香氛50ml 清新甜韵"]}'


功能六:通过调用智能合约删除指定资产:

peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n fabmakeup --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"deleteMakeup","Args":["MAKEUP14"]}

功能七:通过调用智能合约实现资产交易:

peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n fabmakeup --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"changeMakeupOwner","Args":["MAKEUP3","Lisksk"]}'

功能八:调用链码实现更新资产信息 :

peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n fabmakeup --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"updateMakeup","Args":["MAKEUP1","迪奥","香水","$820","gala","女士香水 魅惑清新淡香氛50ml 清新甜韵"]}'

功能九:判断资产是否存在:

peer chaincode query -C mychannel -n fabmakeup -c '{"function":"makeupExists","Args":["MAKEUP1"]}'

四、应用程序前端开发

在站长素材-分享综合设计素材的平台 (chinaz.com)找到项目合适的前端网页模板,然后修改数据和更改网页部分样式,最后通过前后端链接实现资产交易。客户端的主要作用是与Fabric区块链交互,实现对区块链的操作。区块链操作分为管理类和链码类的两种,管理类操作包括启停节点和配置网络等;链码类操作主要是链码的生命周期管理,如安装、实例化以及调用链码。最常用的客户端是命令行客户端(CLI),此外是使用Fabric SDK开发的应用客户端。
开发者创建客户端应用和智能合约( chaincode ), Chaincode 被部署到区块链网络的 Peer 节点上面。通过 chaincode 来操作账本,当你调用一个交易 transaction 时,你实际上是在调用 Chaincode 中的一个函数方法,它实现业务逻辑,并对账本进行 get , put , delete 操作。客户端应用提供用户交互界面,并提交交易到区块链网络上。
前端页面以用户体验效果为主,主页化妆品里面有化妆品分类、热销推荐、折扣推荐、化妆品详情、化妆品资讯,也可以进入我的个人主页查看个人订单和交易情况等。尽可能满足用户交易各方面需求,一些折扣和热销推荐也方便用户做出交易决策。用户挑好自己合适喜欢的就可以放进购物车进行资产交易。每个化妆品分类页面又有各种不同的产品热销和折扣供用户挑选,

源主页面及其文件:


修改源码和完善后的主页面和文件:

使用pycharm对前端网页模板源码进行修改:
通过元素对应的源码进行更改删除或添加相关标签,完善页面布局和整体效果。

页面一:主页面及其商品分类和商品推荐:
化妆品分类有:健康护理、皮肤护理、口红、面部护理、香水、自然护理。

对应路由: /index
页面二
显示化妆品所有资产信息

对应路由:

else if(url_path == '/list'){query().then(async function(contract){const result = await contract.evaluateTransaction('queryAllMakeups');const result_json = JSON.parse(result);//response.end('Transaction has been evaluated, result is: ' + result.toString());const view = path.resolve(__dirname, 'views', 'list.html');list1 = template(view, {data:result_json});response.end(list1);});


通过else if语句,用于检查url_path是否等于’/list’。如果是,它会调用query()函数,并使用返回的contract对象来评估事务’queryAllMakeups’。然后,将结果解析为JSON,并存储在result_json变量中。

接下来,代码使用path.resolve()方法将view变量设置为’list.html’文件的路径。然后,调用template()函数,将view路径和result_json数据作为参数传递进去。返回的HTML模板存储在list1变量中。

最后,代码使用response.end()方法将list1 HTML模板作为响应发送给客户端。

页面三:进行化妆品资产交易

对应路由:

对应路由:

else if(request.url == '/modify'){let formData = '';//接收post参数request.on('data',function(param){formData += param;});//post参数接收完毕request.on('end',function(){console.log(formData);const params = new URLSearchParams(formData);query().then(async function(contract){await contract.submitTransaction('updateMakeup', params.get('id'), params.get('brand'), params.get('type'), params.get('price'), params.get('owner'),params.get('describe'));//301代表重定向 response.writeHead(301,{Location: '/list'});response.end();});});else if(url_path == '/modify'){query().then(async function(contract){const result = await contract.evaluateTransaction('queryMakeup',params.get('id'));const result_json = JSON.parse(result);

当收到 /modify 的 POST 请求时,服务器会监听请求的数据流,将数据拼接到 formData 变量中。
当数据流结束(request.on(‘end’, …)),将 formData 解析为 URLSearchParams 对象,其中包含 POST 请求中提交的参数。使用 query() 函数返回的智能合约对象,调用 submitTransaction 方法执行更新操作,传递 POST 请求中的参数。执行更新操作后,通过响应头设置重定向到 /list,并结束响应。

当收到 /modify 的 GET 请求时,使用 query() 函数返回的智能合约对象,调用 evaluateTransaction 方法执行查询操作,传递可能的参数(这里可能应该是请求中的参数,如 params.get(‘id’))。
查询结果以字符串形式返回,通过 JSON.parse 解析为 JSON 对象,存储在 result_json 中。
在注释的位置,可以添加逻辑来处理查询结果,例如将其用于渲染修改页面。

页面四:添加交易化妆品资产
用户可以看到自己售卖化妆品的交易订单,也可以继续添加自己售卖的化妆品资产,在平台进行交易。



对应路由:

 if(request.method == 'POST'){if(request.url == '/add'){let formData = '';//接收post参数request.on('data',function(param){formData += param;});//post参数接收完毕request.on('end',function(){console.log(formData);const params = new URLSearchParams(formData);query().then(async function(contract){await contract.submitTransaction('createMakeup', params.get('id'), params.get('brand'), params.get('type'), params.get('price'), params.get('owner'),params.get('describe'));//301代表重定向 response.writeHead(301,{Location: '/list'});response.end();});});

HTTP 方法检查:首先,代码检查请求的 HTTP 方法是否为 POST。

路径检查:接着,代码检查请求的路径是否为 ‘/add’。

数据接收:如果满足上述两个条件,代码开始监听请求的数据流 (request.on(‘data’, …)),将数据拼接到 formData 变量中。

数据解析:当数据流结束 (request.on(‘end’, …)),将 formData 解析为 URLSearchParams 对象,其中包含 POST 请求中提交的参数。

智能合约操作:使用 query() 函数返回的智能合约对象,调用 submitTransaction 方法执行创建操作,传递 POST 请求中的参数。

重定向:执行创建操作后,通过响应头设置重定向到 ‘/list’。

结束响应:最后,结束响应。

页面五:删除交易化妆品资产(选择所需要删除的化妆品资产)

对应路由:

else if(url_path == '/remove'){query().then(async function(contract){await contract.submitTransaction('deleteMakeup',params.get('id'));response.writeHead(301,{Location: '/list'});response.end();});

路径检查:首先,代码检查请求的路径是否为 ‘/remove’。

智能合约操作:如果请求路径为 ‘/remove’,则调用 query() 函数,该函数返回智能合约对象。然后,使用合约对象的 submitTransaction 方法执行 ‘deleteMakeup’ 操作,传递参数 params.get(‘id’),即从请求中获取的特定 ID。

重定向:在删除操作执行成功后,通过响应头设置重定向到 ‘/list’。

结束响应:最后,结束响应。

这段代码的作用是处理 ‘/remove’ 路径下的请求,调用智能合约的 deleteMakeup 方法来删除特定 ID 对应的记录,然后重定向到 ‘/list’。

六、应用程序服务端开发(注:对于分工内容,需详细描述设计思路和实现细节,非分工内容,只需截图)
1、路由一
Get:通过params参数获取,

query().then(async function(contract){await contract.submitTransaction('createMakeup', params.get('id'), params.get('brand'), params.get('type'), params.get('price'), params.get('owner'),params.get('describe'));//301代表重定向 response.writeHead(301,{Location: '/list'});response.end();});

首先,通过调用 query() 函数获取智能合约对象。然后,使用合约对象的 submitTransaction 方法执行 ‘createMakeup’ 操作,传递了一系列参数,包括 params.get(‘id’)、params.get(‘brand’)、params.get(‘type’)、params.get(‘price’)、params.get(‘owner’)、和 params.get(‘describe’)。

在智能合约事务执行成功后,通过响应头设置重定向到 ‘/list’。response.writeHead(301,{ Location: ‘/list’ }); 表示使用 301 状态码进行重定向,其中 301 表示永久性移动。最后,结束响应。

总体来说,这段代码的作用是在智能合约上调用 createMakeup 方法来创建一个新的化妆品条目,然后重定向到 ‘/list’。

Post:

if(request.method == 'POST'){if(request.url == '/add'){let formData = '';//接收post参数request.on('data',function(param){formData += param;});//post参数接收完毕request.on('end',function(){console.log(formData);const params = new URLSearchParams(formData);

首先,代码检查HTTP请求的方法是否为POST。这是通过 if(request.method == ‘POST’) 条件语句实现的。接着,代码检查请求的URL是否为’/add’,通过 if(request.url == ‘/add’) 条件语句。如果方法和URL检查都通过,代码创建一个空字符串 formData 用于存储接收到的POST参数。然后,通过监听 ‘data’ 事件,每当有新的数据块到达时,将其附加到 formData。

当所有POST参数都接收完毕时,通过监听 ‘end’ 事件,执行回调函数。在这个回调函数中,输出接收到的所有POST参数 console.log(formData) 并将其解析为 URLSearchParams 对象,存储在 params 变量中。

总体来说,这段代码用于在接收到POST请求时,检查URL是否为’/add’,然后将POST参数收集到 formData 变量中,并通过 URLSearchParams 将其解析为键值对。

2、路由二

Get:else if(url_path == '/modify'){query().then(async function(contract){const result = await contract.evaluateTransaction('queryMakeup',params.get('id'));const result_json = JSON.parse(result);

首先,代码检查URL路径是否为’/modify’。这是通过 else if(url_path == ‘/modify’) 条件语句实现的。

调用query函数:如果URL路径检查通过,代码调用一个 query 函数。这个函数返回一个 Promise,可能是一个异步函数,因为在 query().then(async function(contract){}) 中使用了 async 关键字。

调用智能合约方法:在 then 的回调中,使用 contract 对象(可能是从 query 函数返回的)调用了 evaluateTransaction 方法。这看起来像是与某个智能合约的交互,其中使用了 ‘queryMakeup’ 作为交易名称,还传递了参数 params.get(‘id’)。

解析结果:接着,使用 JSON.parse(result) 将智能合约的返回结果(result)解析为 JSON 对象,存储在 result_json 变量中。

Post:

let formData = '';//接收post参数request.on('data',function(param){formData += param;});//post参数接收完毕request.on('end',function(){console.log(formData);const params = new URLSearchParams(formData);query().then(async function(contract){await contract.submitTransaction('updateMakeup', params.get('id'), params.get('brand'), params.get('type'), params.get('price'), params.get('owner'),params.get('describe'));//301代表重定向 response.writeHead(301,{Location: '/list'});response.end();});});

当服务器收到POST请求时,通过 request.on(‘data’, function(param){…}) 监听数据传输事件,并将数据逐块拼接到 formData 变量中。当数据传输完毕时,通过 request.on(‘end’, function(){…}) 触发回调函数,这里执行数据处理的逻辑。

日志输出:console.log(formData) 打印接收到的数据,可能用于调试目的。

使用 URLSearchParams 对象解析 formData,将其转换为包含所有参数的键值对。这些参数可能是通过POST请求发送的表单数据。使用 query 函数获取智能合约对象,然后调用 submitTransaction 方法,将从表单中获取的参数传递给 ‘updateMakeup’ 交易。这个过程可能会将新的化妆品信息提交到区块链或智能合约中。通过 response.writeHead(301, { Location: ‘/list’ }) 设置HTTP响应头,将浏览器重定向到’/list’页面。最后,通过 response.end() 结束响应。

总体来说,这段代码的作用是处理POST请求,将接收到的表单数据传递给智能合约的 ‘updateMakeup’ 交易,并在处理完成后将浏览器重定向到’/list’页面。

Get:

else if(url_path == '/remove'){query().then(async function(contract){await contract.submitTransaction('deleteMakeup',params.get('id'));response.writeHead(301,{Location: '/list'});response.end();});

Post:

 let formData = '';//接收post参数request.on('data',function(param){formData += param;});//post参数接收完毕request.on('end',function(){console.log(formData);const params = new URLSearchParams(formData);query().then(async function(contract){await contract.submitTransaction('updateMakeup', params.get('id'), params.get('brand'), params.get('type'), params.get('price'), params.get('owner'),params.get('describe'));//301代表重定向 response.writeHead(301,{Location: '/list'});response.end();});});

解析一下:
let formData = ‘’;:声明一个空字符串 formData 用于存储接收到的POST参数。
request.on(‘data’, function(param){…}):通过 request 对象监听 ‘data’ 事件,当有数据传输时,将数据累加到 formData 中。这是一个逐块接收POST数据的过程。
request.on(‘end’, function(){…}):监听 ‘end’ 事件,当数据传输完成时触发。在这个事件中,进行参数的处理和后续操作。

console.log(formData);:打印接收到的formData,可能用于调试和检查接收到的数据。

const params = new URLSearchParams(formData);:使用 URLSearchParams 对象解析 formData,将其转换为包含所有参数的键值对。这是将POST请求中的数据解析成易于处理的形式。

query().then(async function(contract){…}):调用 query 函数,该函数返回一个Promise。一旦Promise解决,执行一个包含智能合约操作的异步函数。

await contract.submitTransaction(‘updateMakeup’, params.get(‘id’), params.get(‘brand’), params.get(‘type’), params.get(‘price’), params.get(‘owner’),params.get(‘describe’));:调用智能合约的 submitTransaction 方法,将从表单中获取的参数传递给 ‘updateMakeup’ 交易。这个过程可能会将新的化妆品信息提交到区块链或智能合约中。

response.writeHead(301, { Location: ‘/list’ });:设置HTTP响应头,将浏览器重定向到’/list’页面。HTTP状态码301表示永久性重定向。

response.end();:结束HTTP响应。

这段代码的目的是处理接收到的POST请求,解析表单数据,调用智能合约提交交易,然后通过HTTP状态码301将浏览器重定向到’/list’页面。

七、应用程序扩展

八、应用程序整体展示

仔细看平台公告后然后进入用户认证:

管理员账号、密码:admin admin
普通用户账户、密码:user1 user123456

进入认证成功后登录主页:

通过商品分类选择自己想要的产品:



也可以注册和登录自己的私人账户:


选择合适的化妆品加入购物车进行交易:

也可以查看我的订单和所有可交易的化妆品资产:

关于化妆品品牌排行榜:

查看相关资讯:

也可以在评论区进行交流查看化妆品评价:

有问题和疑问可以向平台进行反馈:

应用平台流程图:

源码资料等获取方式

各位有兴趣的小伙伴 可以扫码要项目开发文档、完整项目源码和其它相关资料。
各位有兴趣的小伙伴 可以扫码要项目开发文档、完整项目源码和其它相关资料。
各位有兴趣的小伙伴 可以扫码要项目开发文档、完整项目源码和其它相关资料。