千家信息网

以太坊怎么实现中心化投票DApp与智能合约

发表于:2024-11-23 作者:千家信息网编辑
千家信息网最后更新 2024年11月23日,这篇文章主要介绍"以太坊怎么实现中心化投票DApp与智能合约"的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇"以太坊怎么实现中心化投票DApp与智能合约"文章能帮
千家信息网最后更新 2024年11月23日以太坊怎么实现中心化投票DApp与智能合约

这篇文章主要介绍"以太坊怎么实现中心化投票DApp与智能合约"的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇"以太坊怎么实现中心化投票DApp与智能合约"文章能帮助大家解决问题。

为什么要开发去中心化投票应用?

从本质上讲,利用区块链技术的去中心化应用程序允许你在没有可信赖的第三方的情况下执行与今天相同的操作(如转移资金)。最好的dApp具有特定的真实世界的用例,以便利用区块链的独特特征。

  • 从本质上讲,区块链是一个共享的,可编程的,加密安全的,可信赖的分类账本,没有任何一个用户可以控制,任何人都可以查询。- Klaus Schwab

即使投票应用对大家来说可能不是一个伟大的应用程序,但是我选择使用它作为本指南,这是因为区块链解决的主要问题:透明度,安全性,可访问性,可信任,是困扰当前民主选举的主要问题。

由于区块链是去中心化的交易(投票)的永久记录,因此每次投票都可以无可辩驳地追溯到它发生的时间和地点,而不会泄露选民的身份。此外,过去的投票也不能被改变,而现在也不能被黑客攻击,因为每个交易都是由网络中的每个节点验证的。任何外部或内部攻击者必须控制51%的节点才能改变记录。

即使攻击者能够在伪造用户输入真实身份证投票时也能实现这一点,但端到端投票系统可以让选民验证他们的投票是否在系统中正确输入,这使得系统极其安全。

以太坊的核心组成部分

我希望你读本指南的其余部分前,了解了区块链和以太坊。这里有一个很棒的指南,写了我想让你知道的核心组件的简要概述。

  • 智能合约充当后端逻辑和存储。合约是用Solidity一种智能合约语言编写的,是一个代码和数据的集合,驻留在以太坊区块链的特定地址。它与面向对象编程中的类非常相似,它包含函数和状态变量。智能合约以及区块链是所有权力下放应用程序的基础。像Blockchain一样,它们是不可变的和分布式的,这意味着如果它们已经在以太坊网络上,升级它们将是一种痛苦。幸运的是,这里有一些方法可以做到这一点。

  • 以太坊虚拟机(EVM)处理整个以太坊网络的内部状态和计算。将EVM视为这种大规模去中心化计算机,其中包含能够执行代码,更改数据和相互交互的addresses

  • Web3.js是一个Javascript API,允许你与区块链进行交互,包括进行交易和调用智能合约。此API抽象了与以太坊客户端的通信,允许开发人员专注于他们的应用程序的内容。你必须在浏览器中嵌入一个web3实例才能执行此操作。

我们将使用的其他工具

  • Truffle是以太坊的流行测试开发框架。它包括开发区块链,编译和迁移脚本,用于将合约部署到区块链,合约测试等。它使开发更容易!

  • Truffle Contracts是Web3 Javascript API之上的抽象,允许你轻松连接智能合约并与之互动。

  • Metamask将以太坊带入你的浏览器。它是一个浏览器扩展,提供链接到你的以太坊地址的安全web3实例,允许你使用去中心化应用程序。我们不会在本教程中使用Metamask,但它是人们在生产中与DApp交互的一种方式。相反,我们将在开发期间注入我们自己的web3实例。有关更多信息,请查看此链接。

开始吧!

为简单起见,我们实际上不会构建我之前描述的完整投票系统。为了便于说明,它只是一个单页应用程序,用户可以输入他们的ID并为候选人投票。还将有一个按钮,计算并显示每个候选人的投票数。

这样,我们将能够专注于在应用程序中创建智能合约并与之交互的过程。整个应用程序的源代码将在此存储库中,你需要安装Node.js和npm。

1.首先,让我们在全局范围内安装Truffle。
npm install -g truffle

要使用Truffle命令,必须在现有项目中运行它们。

git clone https://github.com/tko22/truffle-webpack-boilerplatecd truffle-webpack-boilerplatenpm install

这个存储库只是一个Truffle Box的框架,它是可以在一个命令中获得的样板或示例应用程序 - truffle unbox [box name]。但是,带有webpack的Truffle box未使用最新版本进行更新,并包含一个示例应用程序。因此,我创建了这个repo。

2.目录结构

你的目录结构应包括以下内容:

  • contracts/:包括所有合约的文件夹。不要删除Migrations.sol

  • migrations/:包含Migration files的文件夹,可帮助你将智能合约部署到区块链中。

  • src/:保存应用程序的HTML/CSS和Javascript文件。

  • truffle.js:truffle配置文件。

  • build/:在编译合约之前,你不会看到此文件夹。此文件夹包含构建文件,因此不要修改任何这些文件!构建文件描述了合约的功能和体系结构,并提供了有关如何与区块链中的智能合约进行交互的Truffle Contracts和web3信息。

1.写下你的智能合约

设置和介绍完,让我们开始写代码吧!首先,我们将编写我们的智能合约,这是用Solidity编写的(其他语言不那么受欢迎)。这可能看起来很不爽,但事实并非如此。

对于任何应用程序,你希望智能合约尽可能简单,甚至是非常简单。请记住,你必须为你所做的每笔计算/交易付费,而你的智能合约将永远存在于区块链中。所以,你真的希望它能够完美地运作--也就是说,它越复杂,就越容易犯错误。

我们的合约将包括:

  • 状态变量:包含永久存储在区块链中的值的变量。我们将使用状态变量来保存选民和候选人的名单和数量。

  • 函数:函数是智能合约的可执行文件。它们是我们要求与区块链进行交互的内容,具有不同级别的内部和外部可见性。请记住,无论何时你想要更改变量的值/状态,都必须进行交易--这要耗费以太币。你也可以calls区块链,这不会花费任何以太,因为你所做的更改将被销毁(当我们实际进行transactionscall时,在下面会有更多内容)。

  • 事件:每当调用事件时,传递给事件的值都将记录在交易日志中。这允许Javascript回调函数或已解析的promises查看你想要在交易之后传回的特定值。这是因为每次进行交易时,都会返回交易日志。我们将使用一个事件来记录新创建的候选者的ID,我们将显示该ID。

  • 结构类型 - 这与C结构非常相似。Structs允许你保存多个变量,并且对于具有多个属性的事物非常棒。Candidates只会有他们的名字和党派,但你绝对可以为他们添加更多属性。

  • 映射 - 将它们视为hash映射或字典,它具有键值对。我们将使用两个映射。

这里没有列出更多类型,但有些类型稍微复杂一些。这五个包含了智能合约通常使用的大部分结构。这里将更深入地解释这些类型。

作为参考,这是智能合约的代码。请注意,此文件应该被称为Voting.sol但我希望Github gist具有style,所以我给它一个.js扩展名。与本指南的其余部分一样,我将在代码中提供注释解释它正在做什么,然后我将在指出某些警告和逻辑的同时解释大体思路。

pragma solidity ^0.4.18;// written for Solidity version 0.4.18 and above that doesnt break functionalitycontract Voting {    // an event that is called whenever a Candidate is added so the frontend could    // appropriately display the candidate with the right element id (it is used    // to vote for the candidate, since it is one of arguments for the function "vote")    event AddedCandidate(uint candidateID);    // describes a Voter, which has an id and the ID of the candidate they voted for    struct Voter {        bytes32 uid; // bytes32 type are basically strings        uint candidateIDVote;    }    // describes a Candidate    struct Candidate {        bytes32 name;        bytes32 party;         // "bool doesExist" is to check if this Struct exists        // This is so we can keep track of the candidates         bool doesExist;     }    // These state variables are used keep track of the number of Candidates/Voters     // and used to as a way to index them         uint numCandidates; // declares a state variable - number Of Candidates    uint numVoters;        // Think of these as a hash table, with the key as a uint and value of     // the struct Candidate/Voter. These mappings will be used in the majority    // of our transactions/calls    // These mappings will hold all the candidates and Voters respectively    mapping (uint => Candidate) candidates;    mapping (uint => Voter) voters;        /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *     *  These functions perform transactions, editing the mappings *     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */    function addCandidate(bytes32 name, bytes32 party) public {        // candidateID is the return variable        uint candidateID = numCandidates++;        // Create new Candidate Struct with name and saves it to storage.        candidates[candidateID] = Candidate(name,party,true);        AddedCandidate(candidateID);    }    function vote(bytes32 uid, uint candidateID) public {        // checks if the struct exists for that candidate        if (candidates[candidateID].doesExist == true) {            uint voterID = numVoters++; //voterID is the return variable            voters[voterID] = Voter(uid,candidateID);        }    }    /* * * * * * * * * * * * * * * * * * * * * * * * * *      *  Getter Functions, marked by the key word "view" *     * * * * * * * * * * * * * * * * * * * * * * * * * */        // finds the total amount of votes for a specific candidate by looping    // through voters     function totalVotes(uint candidateID) view public returns (uint) {        uint numOfVotes = 0; // we will return this        for (uint i = 0; i < numVoters; i++) {            // if the voter votes for this specific candidate, we increment the number            if (voters[i].candidateIDVote == candidateID) {                numOfVotes++;            }        }        return numOfVotes;     }    function getNumOfCandidates() public view returns(uint) {        return numCandidates;    }    function getNumOfVoters() public view returns(uint) {        return numVoters;    }    // returns candidate information, including its ID, name, and party    function getCandidate(uint candidateID) public view returns (uint,bytes32, bytes32) {        return (candidateID,candidates[candidateID].name,candidates[candidateID].party);    }}

基本上,我们有两个Structs(包含多个变量的类型),用于描述选民和候选人。使用Structs,我们可以为它们分配多个属性,例如电子邮件,地址等。

为了跟踪选民和候选人,我们将它们放入单独的映射中,它们是整数索引的。候选人或选民的索引/密钥--让我们称之为ID--是函数访问它们的唯一方式。

我们还会跟踪选民和候选人的数量,这将有助于我们为他们编制索引。此外,不要忘记第8行中的事件,该事件将在添加时记录候选人的ID。我们的界面将使用此事件,因为我们需要跟踪候选人的ID以便为候选人投票。

  • 1.我知道,与我之前所说的关于使合约变得非常简单的说法相反,我认为这个合约与这个应用实际上做的相比有点复杂。但是,我这样做是为了让你们更容易进行编辑并在之后为此应用程序添加功能(最后更多内容)。如果你想制作一个更简单的投票应用程序,智能合约可以在不到15行代码。

  • 2.请注意,状态变量numCandidatesnumVoters未声明为public。默认情况下,这些变量具有internal可见性,这意味着它们只能由当前合约或派生合约直接访问(不用担心,我们不会使用它)。

  • 3.我们使用32bytes用于字符串而不是使用string类型。我们的EVM具有32字节的字大小,因此它被optimized以处理32字节的块中的数据。(当数据不是32字节的块时,编译器,例如Solidity,必须做更多的工作并生成更多的字节码,这实际上会导致更高的天然气成本。)

  • 4.当用户投票时,会创建一个新的Voter结构并将其添加到映射中。为了计算某个候选人的投票数,你必须遍历所有选民并计算投票数。候选人的行为相同。因此,这些映射将保留所有候选人和选民的历史。

2.实例化web3和合约

完成我们的智能合约后,我们现在需要运行我们的测试区块链并将此合约部署到区块链上。我们还需要一种方法来与它交互,这将通过web3.js完成。

在我们开始测试区块链之前,我们必须在/contracts文件夹中创建一个名为2_deploy_contracts.js的文件,告诉它在迁移时包含你的投票智能合约。

var Voting = artifacts.require("Voting")module.exports = function(deployer) {  deployer.deploy(Voting)}

要开始开发以太坊区块链,请转到命令行并运行:

truffle develop

由于Solidity是一种编译语言,我们必须首先将其编译为字节码,以便EVM执行。

compile

你现在应该在目录中看到一个文件夹build/。此文件夹包含构建文件,这对Truffle的内部工作至关重要,因此请勿修改它们!

接下来,我们必须迁移合约。migrations是一个truffle脚本,可帮助你在开发时更改应用程序合约的状态。请记住,你的合约已部署到区块链上的某个地址,因此无论何时进行更改,你的合约都将位于不同的地址。 迁移可帮助你执行此操作,还可帮助你移动数据。

migrate

恭喜!你的智能合约现在永远在区块链上。好吧,还不是真的...... 因为truffle develop会在每次停止时刷新。

如果你想拥有一个持久的区块链,可以考虑一下由Truffle开发的Ganache。如果你使用的是Ganache,则无需调用truffle develop。相反,你将运行truffle compiletruffle migrate。要了解在没有Truffle的情况下部署合约需要什么,请查看此博客文章。

一旦我们将智能合约部署到区块链,我们将不得不在应用程序启动时在浏览器上使用Javascript设置web3.0实例。因此,下一段代码将放在js/app.js的底部。请注意,我们使用的是web3.0版本0.20.1。

// When the page loads, we create a web3 instance and set a provider. We then set up the appwindow.addEventListener("load", function() {  // Is there an injected web3 instance?  if (typeof web3 !== "undefined") {    console.warn("Using web3 detected from external source like Metamask")    // If there is a web3 instance(in Mist/Metamask), then we use its provider to create our web3object    window.web3 = new Web3(web3.currentProvider)  } else {    console.warn("No web3 detected. Falling back to http://localhost:9545. You should remove this fallback when you deploy live, as it's inherently insecure. Consider switching to Metamask for development. More info here: http://truffleframework.com/tutorials/truffle-and-metamask")    // fallback - use your fallback strategy (local node / hosted node + in-dapp id mgmt / fail)    window.web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:9545"))  }  // initializing the App  window.App.start()})

如果你不理解这段代码,你真的不必太担心。只要知道这将在应用程序启动时运行,并将检查浏览器中是否已存在web3实例(Metamask)。如果没有,我们将创建一个与localhost:9545交互Truffle开发区块链。

如果你正在使用Ganache,你必须将端口更改为7545.一旦创建了一个实例,我们将调用start函数。

3.添加功能

我们需要做的最后一件事是为应用程序编写接口。这涉及任何Web应用程序的基本要素--HTML,CSS和Javascript(我们已经编写了一些用于创建web3实例的Javascript)。首先,让我们创建我们的HTML文件。

                      Ethereum Voting Dapp                    

Ethereum Voting Dapp



Add ID and click candidate to vote

这是一个非常简单的页面,带有用户ID的输入表单,以及用于投票和计票的按钮。点击这些按钮后,他们将调用投票的特定功能,并找到候选人的投票数。

但是有三个重要的div元素,其中有id:candidate-boxmsgvote-box,它们分别包含每个候选者的复选框,一条消息和一个投票数。我们还导入了JQuery,Bootstrap和app.js

现在,我们只需要与合约互动并实施投票和计算每个候选人的投票数量的功能。JQuery将控制DOM,当我们进行交易或调用Blockchain时,我们将使用Promises。以下是app.js的代码。

// import CSS. Webpack with deal with itimport "../css/style.css"// Import libraries we need.import { default as Web3} from "web3"import { default as contract } from "truffle-contract"// get build artifacts from compiled smart contract and create the truffle contractimport votingArtifacts from "../../build/contracts/Voting.json"var VotingContract = contract(votingArtifacts)/* * This holds all the functions for the app */window.App = {  // called when web3 is set up  start: function() {     // setting up contract providers and transaction defaults for ALL contract instances    VotingContract.setProvider(window.web3.currentProvider)    VotingContract.defaults({from: window.web3.eth.accounts[0],gas:6721975})    // creates an VotingContract instance that represents default address managed by VotingContract    VotingContract.deployed().then(function(instance){      // calls getNumOfCandidates() function in Smart Contract,       // this is not a transaction though, since the function is marked with "view" and      // truffle contract automatically knows this      instance.getNumOfCandidates().then(function(numOfCandidates){        // adds candidates to Contract if there aren't any        if (numOfCandidates == 0){          // calls addCandidate() function in Smart Contract and adds candidate with name "Candidate1"          // the return value "result" is just the transaction, which holds the logs,          // which is an array of trigger events (1 item in this case - "addedCandidate" event)          // We use this to get the candidateID          instance.addCandidate("Candidate1","Democratic").then(function(result){             $("#candidate-box").append(`
`) }) instance.addCandidate("Candidate2","Republican").then(function(result){ $("#candidate-box").append(`
`) }) // the global variable will take the value of this variable numOfCandidates = 2 } else { // if candidates were already added to the contract we loop through them and display them for (var i = 0; i < numOfCandidates; i++ ){ // gets candidates and displays them instance.getCandidate(i).then(function(data){ $("#candidate-box").append(`
`) }) } } // sets global variable for number of Candidates // displaying and counting the number of Votes depends on this window.numOfCandidates = numOfCandidates }) }).catch(function(err){ console.error("ERROR! " + err.message) }) }, // Function that is called when user clicks the "vote" button vote: function() { var uid = $("#id-input").val() //getting user inputted id // Application Logic if (uid == ""){ $("#msg").html("

Please enter id.

") return } // Checks whether a candidate is chosen or not. // if it is, we get the Candidate's ID, which we will use // when we call the vote function in Smart Contracts if ($("#candidate-box :checkbox:checked").length > 0){ // just takes the first checked box and gets its id var candidateID = $("#candidate-box :checkbox:checked")[0].id } else { // print message if user didn't vote for candidate $("#msg").html("

Please vote for a candidate.

") return } // Actually voting for the Candidate using the Contract and displaying "Voted" VotingContract.deployed().then(function(instance){ instance.vote(uid,parseInt(candidateID)).then(function(result){ $("#msg").html("

Voted

") }) }).catch(function(err){ console.error("ERROR! " + err.message) }) }, // function called when the "Count Votes" button is clicked findNumOfVotes: function() { VotingContract.deployed().then(function(instance){ // this is where we will add the candidate vote Info before replacing whatever is in #vote-box var box = $("
") // loop through the number of candidates and display their votes for (var i = 0; i < window.numOfCandidates; i++){ // calls two smart contract functions var candidatePromise = instance.getCandidate(i) var votesPromise = instance.totalVotes(i) // resolves Promises by adding them to the variable box Promise.all([candidatePromise,votesPromise]).then(function(data){ box.append(`

${window.web3.toAscii(data[0][1])}: ${data[1]}

`) }).catch(function(err){ console.error("ERROR! " + err.message) }) } $("#vote-box").html(box) // displays the "box" and replaces everything that was in it before }) }}// When the page loads, we create a web3 instance and set a provider. We then set up the appwindow.addEventListener("load", function() { // Is there an injected web3 instance? if (typeof web3 !== "undefined") { console.warn("Using web3 detected from external source like Metamask") // If there is a web3 instance(in Mist/Metamask), then we use its provider to create our web3object window.web3 = new Web3(web3.currentProvider) } else { console.warn("No web3 detected. Falling back to http://localhost:9545. You should remove this fallback when you deploy live, as it's inherently insecure. Consider switching to Metamask for deployment. More info here: http://truffleframework.com/tutorials/truffle-and-metamask") // fallback - use your fallback strategy (local node / hosted node + in-dapp id mgmt / fail) window.web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:9545")) } // initializing the App window.App.start()})

请注意,我在上一步中用于创建web3实例的代码也在这里。首先,我们导入必要的库和webpack内容,包括web3和Truffle Contracts。我们将使用Truffle Contracts,它建立在web3之上,与Blockchain进行交互。

要使用它,我们将获取在编译投票智能合约时自动构建的构建文件,并使用它们来创建Truffle Contracts。最后,我们在全局变量windows中设置函数,用于启动应用程序,投票给候选人,以及查找投票数。

要实际与区块链交互,我们必须使用deployed的功能创建松露合约的实例。反过来,这将返回一个承诺,该实例作为你将用于从智能合约调用函数的返回值。

有两种方法可以与这些功能进行交互:交易和调用。交易是一种写操作,它将被广播到整个网络并由矿工处理(因此,成本为Ether)。如果要更改状态变量,则必须执行交易,因为它将更改区块链的状态。

call是一种读操作,模拟交易但丢弃状态变化。因此,它不会花费以太。这非常适合调用getter函数(查看我们之前在智能合约中编写的四个getter函数)。

要使用Truffle Contracts进行交易,你可以编写instance.functionName(param1,param2),将instance作为deployed函数返回的实例(例如,检查第36行)。此事务将返回一个以交易数据作为返回值的promise。因此,如果在智能合约函数中返回一个值,但是使用相同的函数执行交易,则不会返回该值。

这就是为什么我们有一个事件会记录你想要写入要返回的交易数据的任何内容。在第36-37行,我们进行交易以添加一个候选人即Candidate。当我们确定promise时,我们在结果中有交易数据。

要获取我们使用事件AddedCandidate()记录的候选ID(检查智能合约以查看它0),我们必须检查日志并检索它:result.logs[0].args.candidateID

要真正了解正在发生的事情,请使用Chrome开发人员工具打印result并查看其result结构。

要进行调用,你将编写instance.functionName.call(param1,param2)。但是,如果某个函数具有关键字view,那么Truffle Contracts将自动创建一个调用,因此你无需添加.call`。

这就是我们的getter函数具有view关键字的原因。与进行交易不同,返回的调用promise将具有智能合约函数返回的任何返回值。

我现在将简要解释这三个函数,但如果你构建了从数据存储中检索/更改数据并相应地操作DOM的应用程序,那么这应该非常熟悉。将Blockchain视为你的数据库,将Truffle Contracts视为从数据库获取数据的API。

App.start()

创建web3实例后立即调用此函数。要使Truffle Contracts正常工作,我们必须将接口设置为创建的web3实例并设置默认值(例如你正在使用的帐户以及你要为交易支付的gas量)。

由于我们处于开发模式,我们可以使用任何数量的gas和任何帐户。在生产过程中,我们将采用MetaMask提供的帐户,并尝试找出你可以使用的最少量的gas,因为它实际上是真钱。

设置好所有内容后,我们现在将显示每个候选人的复选框,供用户投票。为此,我们必须创建合约实例并获取候选人的信息。如果没有候选人,我们将创建他们。为了让用户投票给候选人,我们必须提供该特定候选人的ID。因此,我们使每个checkbox元素具有候选ID的id(HTML元素属性)。另外,我们将把候选数量添加到全局变量numOfCandidates中,我们将在App.findNumOfVotes()中使用它。JQuery用于将每个复选框及其候选名称附加到.candidate-box

App.vote()

此功能将根据单击的复选框及其id属性为某个候选人投票。

  • 1.我们将检查用户是否输入了他们的userID,这是他们的身份。如果他们没有,我们会显示一条消息告诉他们需要这样做。

  • 2.我们将检查用户是否正在为候选人投票,检查是否至少有一个被点击的复选框。如果没有点击任何复选框,我们也会显示一条消息,告诉他们请投票给候选人。如果单击其中一个复选框,我们将获取该复选框的id属性,该属性也是链接候选人的ID,并使用该属性为候选人投票。

交易完成后,我们将解决退回的承诺并显示Voted已经完成投票的消息。

App.findNumOfVotes()

最后一个函数将找到每个候选人的投票数并显示它们。我们将通过候选人并调用两个智能合约函数,getCandidatetotalVotes。我们将解决这些承诺并为该特定候选人创建HTML元素。

现在,启动应用程序,你将在`http://localhost:8080/上看到它!

npm run dev

资源

我知道,这很多......当你慢慢开发这个应用程序并真正了解正在发生的事情时,你可能会暂时打开这篇文章。但那是在学习!请使用以太网,truffle以及我在下面提供的所有文档补充本指南。我试图点击本文中的许多关键点,但这只是一个简短的概述,这些资源将有很大帮助。

  • 关于Solidity和Smart Contracts的一切:我的意思是一切。

  • 关于truffle一切

  • truffle contract 文档

  • Web3 Javascript API:这将是很好的知识和参考,但松露合约抽象了很多部分。

  • 有用的DApp模式

  • 以太坊文件:看目录,有很多东西。

  • CryptoKitties代码说明:作者介绍了CryptoKitties的智能合约的重要部分。

  • 智能合约最佳实践:必读。

关于"以太坊怎么实现中心化投票DApp与智能合约"的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注行业资讯频道,小编每天都会为大家更新不同的知识点。

0