Truffle投票DApp毕设实战:从Ganache部署到MetaMask交互

发布时间:2026/10/12 14:49:18
Truffle投票DApp毕设实战:从Ganache部署到MetaMask交互 简介这份资源是面向计算机、通信、人工智能、自动化等专业学生与开发者的区块链入门级毕设项目源码以 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 或 18Node 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 编写迁移脚本并部署到 Ganachemigrations文件夹里有两个文件1_initial_migration.js和2_deploy_contracts.js。前者部署Migrations.sol这是 Truffle 用来记录迁移状态的合约不用改。后者负责部署Voting.solconst 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 --registryhttps://registry.npmmirror.com。打包成功后src/js/app.bundle.js会更新用浏览器打开src/index.html即可看到页面。4.2 MetaMask 连接 Ganache 并导入账户MetaMask 默认连接以太坊主网需要手动切换到本地网络。点击网络选择器选择“自定义 RPC”填写参数值网络名称Ganache LocalRPC 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()用于读操作不消耗 gassend()用于写操作需要 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 环境下完整跑通。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询