☰
Truffle投票DApp毕设实战:从Ganache部署到MetaMask交互
2026/10/12 14:49:01 网站建设 项目流程

简介:这份资源是面向计算机、通信、人工智能、自动化等专业学生与开发者的区块链入门级毕设项目源码,以 Truffle 框架搭建投票 DApp,配合 Ganache 启动本地链、MetaMask 完成交互,帮助初学者快速理解智能合约编写、部署与前端调用的完整流程。压缩包共 34 个文件,约 353KB,以 15 个 js 脚本、6 个 sol 合约、4 个 json 配置及 html、md 文档为主,涵盖合约源码、迁移脚本、测试用例与前端页面,目录按 simple_voting 与 token_based_voting 两个阶段组织,便于循序渐进学习。目前已有 254 人学习下载。项目代码经过调试测试,答辩评审分达 95 分,可直接运行,既可作为课程设计、大作业或毕业设计的参考模板,也适合基础较好的读者在此基础上修改扩展,实现代币投票等差异化功能。

1. 从一份 95 分毕设说起:Truffle 投票 DApp 到底能跑出什么

如果你正在找一份能直接跑起来、结构清晰、还能在答辩时讲出技术深度的区块链毕业设计,那这份基于 Truffle 的投票系统源码值得认真拆一遍。它不是那种只丢几个合约文件、连迁移脚本都跑不通的“半成品”,而是包含两套完整实现——1_simple_voting_by_truffle_dapp和2_token_based_voting,从基础投票到代币加权投票,层层递进。整个项目用 Ganache 启动本地链,MetaMask 做交互入口,前端用 Webpack 打包,测试用例覆盖了核心投票逻辑。适合计算机、通信、自动化等专业的学生拿来做课设或毕设底稿,也适合刚接触区块链开发的工程师用来理解 DApp 的完整链路。我拿到这份源码后,第一件事就是把它跑通,然后逐行拆了合约和迁移脚本,下面把复现路径和踩过的坑一次讲清。

2. 环境搭建与合约编译:把 Ganache、Truffle 和 MetaMask 串成一条线

2.1 为什么选 Truffle + Ganache 这套组合

区块链开发环境的选择直接决定你后面调试的顺畅程度。Hardhat 现在很火,但对毕设场景来说,Truffle 的目录结构更“教科书”——contracts、migrations、test三个文件夹把合约、部署脚本、测试用例分得清清楚楚,答辩时老师一眼就能看懂你的工程组织。Ganache 则提供了一个本地内存链,启动后直接给你 10 个带 100 ETH 的测试账户,不用同步主网数据,也不用申请测试币。MetaMask 作为浏览器插件钱包,负责把前端页面和 Ganache 链连接起来。这三者组合,基本是 Truffle 官方文档推荐的入门路径,也是这份源码默认适配的环境。

源码里有两个子项目,1_simple_voting_by_truffle_dapp是基础版,2_token_based_voting是代币加权版。建议先从基础版跑通,再切到代币版,否则容易在代币转账和授权那一步卡住。

2.2 安装依赖与启动 Ganache

先确认 Node.js 版本。Truffle 对 Node 版本比较敏感,建议用 Node 16 或 18,Node 20 以上在某些老版本 Truffle 上会有兼容问题。我实测 Node 18.20.4 跑这套源码没问题。

# 全局安装 Truffle 和 Ganache CLI npm install -g truffle npm install -g ganache # 启动 Ganache,指定端口 7545,网络 ID 5777 ganache --port 7545 --networkId 5777 --deterministic

--deterministic这个参数很关键,它让每次启动 Ganache 时生成的账户地址和私钥都一致。这样你 MetaMask 里导入的账户不会因为重启链而失效,省去反复导入的麻烦。--port 7545是 Ganache GUI 的默认端口,Truffle 的truffle-config.js里通常也配的这个端口,保持一致可以少改配置。

启动后终端会输出 10 个账户地址和对应的私钥,把第一个账户的私钥复制出来,待会导入 MetaMask。

2.3 配置 truffle-config.js 并编译合约

进入1_simple_voting_by_truffle_dapp目录,先看truffle-config.js的 networks 配置:

module.exports = { networks: { development: { host: "127.0.0.1", port: 7545, network_id: "5777", }, }, compilers: { solc: { version: "0.8.0", settings: { optimizer: { enabled: true, runs: 200, }, }, }, }, };

这里有几个参数需要核对。host和port必须和 Ganache 启动参数一致,network_id对应--networkId 5777。solc.version指定 Solidity 编译器版本,源码里的Voting.sol用的是pragma solidity >=0.4.22 <0.9.0,所以 0.8.0 可以覆盖。optimizer.enabled打开后能减少合约字节码大小,部署时省 gas,但调试时如果发现变量值不对,可以先关掉 optimizer 再编译一次,排除优化导致的变量内联问题。

# 在子项目根目录执行编译 truffle compile

编译成功后会在build/contracts下生成Voting.json和Migrations.json。如果报错Source file requires different compiler version,说明truffle-config.js里的 solc 版本和合约 pragma 不匹配,调整版本号即可。如果报Cannot find module 'truffle',检查是否在子项目目录下执行,而不是在压缩包根目录。

2.4 编写迁移脚本并部署到 Ganache

migrations文件夹里有两个文件:1_initial_migration.js和2_deploy_contracts.js。前者部署Migrations.sol,这是 Truffle 用来记录迁移状态的合约,不用改。后者负责部署Voting.sol:

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

artifacts.require的参数是合约名,必须和contracts/Voting.sol里的contract Voting完全一致,大小写敏感。deployer.deploy可以传构造函数参数,但这份源码的Voting合约没有构造函数参数,所以直接部署即可。

# 部署合约到 Ganache truffle migrate --network development --reset

--reset表示重新部署所有迁移脚本,而不是从上次中断的地方继续。如果你改了合约代码,不加--reset可能会跳过已经执行过的迁移,导致链上还是旧合约。部署成功后终端会输出合约地址、交易哈希和花费的 gas,把Voting的合约地址记下来,前端配置里要用。

3. 合约逻辑拆解:投票、代币加权与测试用例怎么写

3.1 Voting.sol 的核心数据结构与投票流程

打开contracts/Voting.sol,先看状态变量:

contract Voting { mapping(address => bool) public voters; mapping(bytes32 => Candidate) public candidates; bytes32[] public candidateList; address public chairperson; struct Candidate { bytes32 name; uint voteCount; } }

voters映射记录每个地址是否已经投过票,防止重复投票。candidates用bytes32作为键,存储候选人姓名和得票数。candidateList保存所有候选人键值,方便遍历。chairperson是部署合约的账户地址,拥有添加候选人的权限。

投票函数vote的逻辑:

function vote(bytes32 candidate) public { require(!voters[msg.sender], "Already voted."); require(validCandidate(candidate), "Invalid candidate."); voters[msg.sender] = true; candidates[candidate].voteCount += 1; }

require(!voters[msg.sender])是第一道防线,确保同一地址只能投一次。validCandidate检查候选人是否在candidateList中。通过后把voters[msg.sender]置为true,然后给对应候选人票数加一。这里没有用SafeMath,因为 Solidity 0.8.0 内置了溢出检查,voteCount += 1不会溢出。

常见坑是bytes32和string的转换。前端传参时如果直接传字符串,Truffle 会报类型错误。需要用web3.utils.fromAscii或ethers.utils.formatBytes32String转成 bytes32。源码的前端src/js/app.js里已经做了处理,但如果你自己改前端,这一步容易翻车。

3.2 代币加权投票的差异点

2_token_based_voting子项目在基础版上增加了代币逻辑。核心变化是投票权重不再是一票一权,而是根据持有的代币数量决定。合约里多了一个ERC20代币合约或者一个简化的代币映射:

mapping(address => uint256) public tokenBalance; function vote(bytes32 candidate) public { require(!voters[msg.sender], "Already voted."); require(validCandidate(candidate), "Invalid candidate."); require(tokenBalance[msg.sender] > 0, "No tokens to vote."); voters[msg.sender] = true; candidates[candidate].voteCount += tokenBalance[msg.sender]; }

关键差异在candidates[candidate].voteCount += tokenBalance[msg.sender],票数增量等于投票者持有的代币数。这意味着代币分配机制直接影响投票结果。源码里代币是在部署时一次性分配给部署者,然后通过transfer函数转给其他账户。测试时如果发现投票权重不对,先检查tokenBalance映射里对应地址的余额。

3.3 测试用例 TestVoting.sol 与 Voting.js 的配合

test文件夹里有两个文件:TestVoting.sol和Voting.js。TestVoting.sol是 Solidity 测试合约,用 Truffle 的Assert库做断言;Voting.js是 JavaScript 测试脚本,用 Mocha 和 Chai 写。两者覆盖的测试点类似,但 JS 测试更灵活,可以模拟多个账户交互。

const Voting = artifacts.require("Voting"); contract("Voting", (accounts) => { let votingInstance; before(async () => { votingInstance = await Voting.deployed(); }); it("should initialize with candidates", async () => { const candidateList = await votingInstance.getCandidates(); assert.equal(candidateList.length, 2, "Initial candidates count mismatch"); }); it("should allow a voter to vote", async () => { await votingInstance.vote(web3.utils.fromAscii("Alice"), { from: accounts[1] }); const voteCount = await votingInstance.totalVotesFor(web3.utils.fromAscii("Alice")); assert.equal(voteCount.toNumber(), 1, "Vote count should be 1"); }); it("should reject double voting", async () => { try { await votingInstance.vote(web3.utils.fromAscii("Bob"), { from: accounts[1] }); assert.fail("Expected revert not received"); } catch (error) { assert(error.message.includes("Already voted"), "Expected 'Already voted' error"); } }); });

before钩子在所有测试前部署合约实例。第一个用例检查候选人列表长度,第二个用例用accounts[1]投票并验证票数,第三个用例尝试同一账户再次投票,捕获 revert 并检查错误信息。web3.utils.fromAscii把字符串转成 bytes32,和合约里的bytes32类型对齐。

# 运行测试 truffle test --network development

如果测试全部通过,终端会显示绿色的勾和用例数量。如果某个用例失败,先看错误信息是 revert 还是断言失败。revert 通常是require条件没满足,断言失败则是实际值和期望值不一致。常见问题是 Ganache 重启后账户余额变化,导致accounts[1]没有足够 gas 投票,这时重新部署合约即可。

4. 前端交互与 MetaMask 联调:从合约地址到页面按钮

4.1 前端项目结构与 Webpack 配置

app文件夹是前端根目录,src下放 HTML、JS 和 CSS。webpack.config.js定义了入口和输出:

module.exports = { entry: "./src/js/app.js", output: { path: __dirname + "/src/js", filename: "app.bundle.js", }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: "babel-loader", }, }, ], }, };

入口是src/js/app.js,输出到同目录的app.bundle.js。babel-loader负责把 ES6+ 语法转成浏览器兼容的 ES5。package.json里的scripts通常有"dev": "webpack --mode development"和"build": "webpack --mode production"。开发时用npm run dev生成未压缩的 bundle,方便调试。

# 安装前端依赖并打包 cd app npm install npm run dev

如果npm install卡住,检查网络或换用npm install --registry=https://registry.npmmirror.com。打包成功后src/js/app.bundle.js会更新,用浏览器打开src/index.html即可看到页面。

4.2 MetaMask 连接 Ganache 并导入账户

MetaMask 默认连接以太坊主网,需要手动切换到本地网络。点击网络选择器,选择“自定义 RPC”,填写:

参数值
网络名称Ganache Local
RPC URLhttp://127.0.0.1:7545
链 ID5777
货币符号ETH

保存后切换到该网络。然后导入 Ganache 启动时输出的第一个账户私钥。导入后账户余额应该显示 100 ETH。如果余额为 0,检查 Ganache 是否还在运行,以及 RPC URL 的端口是否和truffle-config.js一致。

4.3 前端调用合约的完整链路

app.js里用web3或truffle-contract连接合约。核心步骤:

import Web3 from "web3"; import VotingArtifact from "../../build/contracts/Voting.json"; const init = async () => { const web3 = new Web3(Web3.givenProvider || "http://127.0.0.1:7545"); const accounts = await web3.eth.getAccounts(); const networkId = await web3.eth.net.getId(); const deployedNetwork = VotingArtifact.networks[networkId]; const voting = new web3.eth.Contract( VotingArtifact.abi, deployedNetwork.address ); // 渲染候选人列表 const candidateList = await voting.methods.getCandidates().call(); // 绑定投票按钮 document.querySelector("#voteButton").addEventListener("click", async () => { const candidate = document.querySelector("#candidateInput").value; await voting.methods .vote(web3.utils.fromAscii(candidate)) .send({ from: accounts[0] }); }); };

Web3.givenProvider会优先使用 MetaMask 注入的 provider,如果没有则回退到本地 Ganache 的 HTTP 端点。VotingArtifact.networks[networkId]从编译产物里读取对应网络的合约地址,这样切换网络时不用手动改地址。call()用于读操作,不消耗 gas;send()用于写操作,需要 MetaMask 确认交易。

常见坑是networkId和chainId混淆。Ganache 的--networkId 5777对应web3.eth.net.getId()返回的 5777,而 MetaMask 里填的“链 ID”也是 5777。如果前端报Cannot read property 'address' of undefined,说明VotingArtifact.networks[networkId]为空,检查合约是否部署到了当前网络,以及truffle migrate是否成功。

5. 避坑与排查:那些让 DApp 跑不起来的细节

5.1 现象:truffle migrate 报 “Could not connect to your Ethereum client”

原因:Ganache 没有启动,或者truffle-config.js里的 host/port 和 Ganache 实际监听的不一致。常见情况是 Ganache GUI 默认端口 7545,但 CLI 启动时用了 8545,而配置文件没改。

解决:先确认 Ganache 终端是否还在运行,然后检查truffle-config.js的networks.development.port是否和启动参数一致。如果用的是 Ganache GUI,在设置里看 RPC SERVER 的端口。改完配置后重新执行truffle migrate --network development --reset。

5.2 现象:MetaMask 显示余额为 0,但 Ganache 终端显示账户有 100 ETH

原因:MetaMask 连接的链 ID 和 Ganache 的 networkId 不匹配,或者导入的私钥对应的账户不是 Ganache 生成的账户。有些同学从别处复制了一个私钥导入,那个账户在 Ganache 链上不存在。

解决:在 MetaMask 里删除当前网络重新添加,确保链 ID 填 5777。然后从 Ganache 终端复制第一个账户的私钥重新导入。导入后如果余额还是 0,在 MetaMask 里点击“切换账户”再切回来,强制刷新余额。

5.3 现象:前端页面点击投票按钮后 MetaMask 弹出交易确认,但确认后票数没变

原因:交易虽然被 MetaMask 签名并发送,但可能因为 gas 不足或 nonce 冲突被链上拒绝。Ganache 默认 gas limit 很高,但如果你在 MetaMask 里手动调低了 gas price,交易可能一直处于 pending 状态。

解决:打开 Ganache 终端,看是否有新的交易记录。如果没有,在 MetaMask 的“活动”标签里查看交易状态。如果是 pending,点击“加速”或取消后重新发起。另外检查vote函数的require条件是否满足,比如是否已经投过票、候选人是否有效。可以在 Truffle console 里用voting.methods.voters(accounts[0]).call()查看投票状态。

5.4 现象:truffle test 报 “Invalid JSON RPC response: undefined”

原因:测试脚本连接的网络和 Ganache 不一致,或者 Ganache 在测试过程中被关闭。也有可能是truffle-config.js里同时配置了多个网络,truffle test默认连了development以外的网络。

解决:执行truffle test --network development显式指定网络。如果问题依旧,重启 Ganache 后先truffle migrate --reset再跑测试。确保测试过程中不要关闭 Ganache 终端。

5.5 现象:代币加权版投票时,票数增量不等于代币余额

原因:tokenBalance映射在部署时没有正确初始化,或者代币转账后没有更新映射。源码里代币分配是在2_deploy_contracts.js里通过deployer.deploy的第三个参数传入的,如果迁移脚本没写对,余额就是 0。

解决:打开migrations/2_deploy_contracts.js,确认代币合约的部署和转账逻辑。可以在 Truffle console 里用token.methods.balanceOf(accounts[0]).call()查余额。如果余额为 0,检查迁移脚本里是否调用了transfer函数,以及转账金额是否大于 0。

6. 进阶改造与验证:把投票系统改成你自己的毕设

跑通基础版和代币版之后,下一步就是把它改成能写进论文的“自己的东西”。最省力的切入点是改投票规则。比如把“一票一权”改成“按持币时间加权”,在Voting.sol里加一个mapping(address => uint256) public lockTime,投票时用block.timestamp - lockTime[msg.sender]作为权重系数。这样既保留了原有结构,又增加了创新点,答辩时也容易讲清楚。

另一个方向是加候选人注册功能。基础版里候选人是在迁移脚本里硬编码的,你可以加一个registerCandidate函数,让 chairperson 在运行时动态添加候选人:

function registerCandidate(bytes32 name) public { require(msg.sender == chairperson, "Only chairperson can register."); require(!validCandidate(name), "Candidate already exists."); candidates[name] = Candidate(name, 0); candidateList.push(name); }

require(msg.sender == chairperson)限制权限,validCandidate防止重复注册。前端加一个输入框和按钮,调用registerCandidate并传入web3.utils.fromAscii(name)。这样你的 DApp 就有了完整的“管理员添加候选人 → 用户投票 → 实时查看结果”闭环。

验证改造是否成功,除了跑truffle test,还可以在 Truffle console 里手动调用:

truffle console --network development

进入 console 后:

const voting = await Voting.deployed(); await voting.registerCandidate(web3.utils.fromAscii("Charlie"), { from: accounts[0] }); const list = await voting.getCandidates(); console.log(list.map(c => web3.utils.toAscii(c)));

如果输出里包含Charlie,说明注册成功。然后切换账户投票,检查票数是否正确累加。这套流程我每次改完合约都会走一遍,比单纯跑测试用例更能发现边界问题。

从那以后我每次拿到新的 Truffle 项目,都强制自己先跑truffle compile、再truffle migrate --reset、最后truffle test,三步全绿才动前端。这份源码的两个子项目我都按这个顺序验证过,基础版和代币版都能在 Ganache + MetaMask 环境下完整跑通。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询