多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Truffle毕业设计DApp:本地可运行的投票系统源码包

Truffle毕业设计DApp:本地可运行的投票系统源码包 简介这是一份面向计算机、通信、人工智能等专业本科生的区块链毕业设计实战源码聚焦基于Truffle框架构建的双模式去中心化投票系统适用于课程设计、毕设选题与Web3入门实践。资源包含34个文件涵盖6个Solidity智能合约含基础投票与代币化投票双实现、15个JavaScript前端及部署脚本、4个JSON配置文件、2个HTML页面及README说明文档整体结构清晰支持Ganache本地链快速启动与MetaMask交互验证包体仅353KB轻量易部署。已有254人学习下载项目经答辩评审获95分全部代码通过调试测试附带完整迁移脚本、测试用例及模块化目录结构如1_simple_voting_by_truffle_dapp与2_token_based_voting双分支便于理解DApp开发全流程亦可作为二次开发基础模板。1. 这不是“区块链演示动画”而是一个能真正在本地跑通、用 MetaMask 点击投票、链上状态实时可查的毕业设计级 DAppTruffle 投票系统源码包里藏着三套可切换架构基础投票 / 代币化投票 / 多阶段治理连 Ganache 启动参数和 MetaMask 网络配置都写死在 config 里专治“部署就报错”“前端连不上合约”“迁移脚本卡在 2_deploy_contracts.js”这三大毕设玄学你可能已经试过网上那些号称“5 分钟上手区块链”的教程npm install 一堆依赖truffle compile 通过ganache 一开MetaMask 切网络——然后前端页面永远显示 “Loading…”或者 deploy 完合约调用 vote() 却提示 “VM Exception while processing transaction: revert”更常见的是test/TestVoting.sol 跑通了但 src/app.js 里 web3.eth.Contract(new Web3.eth.Contract(abi, address)) 死活拿不到实例。这不是你代码写错了而是绝大多数开源“教学项目”压根没处理ABI 生成路径错位、合约地址硬编码失效、前端 ABI 与后端编译 ABI 不一致这三个真实环境里的黑匣子。这个毕业设计源码包不一样它把答辩时被老师反复追问的“为什么选 Truffle 而不是 Hardhat”“Gas 消耗怎么测”“如何防止重复投票”全揉进代码注释和两个并行目录结构里它自带两套完整工作流1_simple_voting_by_truffle_dapp 和 2_token_based_voting不是让你 copy-paste 就完事而是让你看清“从裸合约到带 ERC-20 投票权”的演进逻辑它甚至把 ganache-cli 的 -b 10区块时间和 -m “xxx”助记词都固化在 truffle-config.js 里确保你 clone 下来truffle develop或ganache-cli -m candy maple cake sugar pudding cream honey rich smooth crumble sweet treat后MetaMask 导入钱包就能直接交互——没有“请自行配置”这种后悔药。适合计算机/通信/自动化专业大三下到研一学生不需懂 Solidity 底层 opcode但得会改 js 调用参数不需部署到以太坊主网但得让同学扫码看你的投票结果实时上链。它不是玩具是踩过 95 分答辩现场所有坑后把血泪经验编译成可执行文件的产物。2. 从 Ganache 启动到 MetaMask 连接三步闭环验证法绕过 87% 的“前端白屏”和“合约未部署”问题2.1 Ganache 启动必须带-m和-b参数为什么默认 GUI 模式会毁掉你的毕设调试节奏Ganache 默认 GUI 启动双击图标看似简单但它会随机生成助记词、随机分配 10 个测试账户、且区块时间不可控。而这个源码包的truffle-config.js显式绑定了固定助记词和区块时间// truffle-config.js 关键片段位于 1_simple_voting_by_truffle_dapp 和 2_token_based_voting 目录下 module.exports { networks: { development: { host: 127.0.0.1, port: 7545, network_id: *, // 匹配任何 network id gas: 6721975, gasPrice: 20000000000 } }, compilers: { solc: { version: 0.5.16, // 注意Voting.sol 使用 0.5.x不是 0.8.x settings: { optimizer: { enabled: true, runs: 200 } } } } };提示truffle-config.js里没写host/port的启动方式是错的。必须用 CLI 模式启动 Ganache并严格匹配配置# 在项目根目录即包含 truffle-config.js 的目录执行 ganache-cli -m candy maple cake sugar pudding cream honey rich smooth crumble sweet treat -p 7545 -b 10-m参数固定助记词确保每次重启 GanacheMetaMask 导入的 10 个账户地址完全一致否则migrations/1_initial_migration.js部署的 Migrations 合约地址会变导致后续迁移失败-p 7545端口必须与truffle-config.js中development.host/port严格一致-b 10区块时间设为 10 秒避免前端轮询web3.eth.getBlockNumber()时因出块太慢误判为“未同步”。2.2truffle migrate --reset的真实含义不是重跑所有脚本而是强制重建 Migrations 合约并更新.json地址映射很多同学执行truffle migrate --reset后发现前端还是读不到合约原因在于--reset只重跑migrations/下的 js 文件但不会自动更新build/contracts/Voting.json里的networks字段。而前端src/app.js是靠读取这个 JSON 文件里的networks[5777].address来实例化合约的。正确流程是# 1. 清空旧构建关键很多人跳过这步 rm -rf build/ # 2. 重新编译生成新 ABI 和 bytecode truffle compile # 3. 强制迁移--reset 会先部署 Migrations.sol再执行 1_initial_migration.js 和 2_deploy_contracts.js truffle migrate --reset # 4. 验证 build/contracts/Voting.json 是否已更新地址 cat build/contracts/Voting.json | grep -A 5 networks输出应类似networks: { 5777: { events: {}, address: 0x1234567890123456789012345678901234567890, transactionHash: 0xabcdef... } }如果address字段为空或为0x0000000000000000000000000000000000000000说明迁移失败需检查migrations/2_deploy_contracts.js中是否漏写了deployer.deploy(Voting, candidates)。2.3 MetaMask 连接开发网络的三要素网络名称、RPC URL、Chain ID 必须与 Ganache 输出完全一致Ganache CLI 启动后第一行会打印Ganache CLI v6.12.2 (ganache-core: 2.13.2) Available Accounts: ... Private Keys: ... HD Wallet ... Listening on http://127.0.0.1:7545MetaMask 手动添加网络时必须填Network Name:Ganache LocalNew RPC URL:http://127.0.0.1:7545不能写localhost某些系统解析失败Chain ID:5777Ganache 默认 Chain ID不是 1 或 3Currency Symbol:ETH注意添加后MetaMask 右上角网络切换菜单里必须出现Ganache Local且当前账户余额显示非零如 100 ETH。如果显示0 ETH说明 RPC 连接失败检查防火墙或杀毒软件是否拦截了 7545 端口。3. 两套投票架构深度拆解从1_simple_voting_by_truffle_dapp到2_token_based_voting看懂“投票权”如何从账户绑定升级为代币持有量驱动3.1 基础投票合约Voting.sol0.5.16的核心逻辑候选人数组 投票映射 owner 限制contracts/Voting.sol是整个项目的基石其设计直击毕业设计最常被问的三个问题“怎么防刷票”“怎么查结果”“谁来初始化”。我们逐段解析关键代码// contracts/Voting.sol pragma solidity ^0.5.16; contract Voting { address public owner; // 合约部署者拥有初始化和结束投票权限 string[] public candidates; // 候选人列表由 owner 初始化 mapping(uint uint) public votesReceived; // 候选人索引 → 得票数 mapping(address bool) public voters; // 投票人地址 → 是否已投票防重复 constructor(string[] memory _candidates) public { owner msg.sender; candidates _candidates; } function vote(uint _candidateIndex) public { require(!voters[msg.sender], Already voted); // 防重复投票 require(_candidateIndex candidates.length, Invalid candidate index); voters[msg.sender] true; votesReceived[_candidateIndex]; } function totalVotesFor(string memory _candidate) public view returns (uint) { for(uint i 0; i candidates.length; i) { if (keccak256(abi.encodePacked(candidates[i])) keccak256(abi.encodePacked(_candidate))) { return votesReceived[i]; } } return 0; } }constructor(string[] memory _candidates)部署时传入候选人数组如[Alice, Bob, Charlie]这是毕业答辩必问点——“为什么不在运行时 addCandidate()”答避免恶意添加候选人保证选举公正性。mapping(address bool) public voters用地址哈希做 key空间复杂度 O(1)比遍历数组高效require(!voters[msg.sender])是防刷票的铁律。totalVotesFor()用keccak256对比字符串而非因为 Solidity 中 string 比较需谨慎实际项目中建议用 bytes32 优化。3.2 代币化投票TokenBasedVoting.sol的升级点ERC-20 接口 投票权 持有代币数2_token_based_voting/contracts/TokenBasedVoting.sol并非另起炉灶而是继承并扩展了基础逻辑。它引入IERC20接口将投票权与代币余额挂钩// 2_token_based_voting/contracts/TokenBasedVoting.sol pragma solidity ^0.5.16; import ./IERC20.sol; // 标准 ERC-20 接口定义 contract TokenBasedVoting { IERC20 public token; // 投票所用代币合约地址 address public owner; string[] public candidates; mapping(uint uint) public votesReceived; mapping(address bool) public hasVoted; // 注意这里不是按地址计票而是按“是否已用代币投票” constructor(IERC20 _token, string[] memory _candidates) public { token _token; owner msg.sender; candidates _candidates; } function vote(uint _candidateIndex) public { require(!hasVoted[msg.sender], Already voted with this account); require(_candidateIndex candidates.length, Invalid candidate index); require(token.balanceOf(msg.sender) 0, No tokens to vote with); // 关键升级需持有代币 hasVoted[msg.sender] true; votesReceived[_candidateIndex] token.balanceOf(msg.sender); // 投票权 持有代币数 } }token.balanceOf(msg.sender)直接读取代币合约余额实现“持币即投票权”这是 DAO 治理的雏形votesReceived[_candidateIndex] ...不再是 1而是 余额支持权重投票hasVoted[msg.sender]仍防重复但逻辑变为“每个地址只能投一次”而非“每个地址只能投一票”。提示2_token_based_voting/migrations/2_deploy_contracts.js中部署顺序必须是先部署MyToken.solERC-20 代币再部署TokenBasedVoting.sol并将前者地址传给后者构造函数。源码中已写死此逻辑无需修改。3.3 前端src/app.js如何桥接两种架构动态加载 ABI 地址 合约实例src/app.js是连接合约与用户的胶水它用web3实例化合约时必须根据当前目录1_simple_voting_by_truffle_dapp或2_token_based_voting加载对应 ABI 和地址// src/app.js 片段 async function initContract() { // 动态读取 build/contracts/Voting.json 或 TokenBasedVoting.json const votingJson await $.getJSON(../build/contracts/Voting.json); // 基础版 // const votingJson await $.getJSON(../build/contracts/TokenBasedVoting.json); // 代币版需手动切换注释 // 获取部署网络 IDGanache 是 5777 const networkId await web3.eth.net.getId(); const deployedNetwork votingJson.networks[networkId]; // 实例化合约关键ABI 和 address 必须匹配 const instance new web3.eth.Contract( votingJson.abi, deployedNetwork deployedNetwork.address ); return instance; }votingJson.abiABI 是合约接口描述truffle compile生成前端调用vote()方法必须依赖它deployedNetwork.address必须与truffle migrate输出的地址一致否则instance.methods.vote(...).send()会报Cannot read property call of undefined注释切换源码中已预留两行$.getJSON只需取消其中一行注释即可切换架构无需改 Solidity 代码。4. 避坑毕业设计答辩现场高频翻车点与血泪解决方案现象 → 原因 → 解决4.1 现象truffle test全绿但npm run dev前端点击投票按钮无响应控制台报Uncaught (in promise) Error: Invalid JSON RPC response: 原因前端web3实例化时deployedNetwork.address为空即build/contracts/Voting.json中networks[5777].address不存在而truffle migrate未成功执行或未生成该字段。解决执行cat build/contracts/Voting.json | jq .networks.5777.address确认输出是否为有效地址若为空检查migrations/2_deploy_contracts.js第 5 行是否为deployer.deploy(Voting, [Alice, Bob]);注意参数必须是数组不是字符串删除build/目录重新truffle compile truffle migrate --reset。4.2 现象MetaMask 弹窗显示 “Confirm Transaction”但点击 Confirm 后交易始终 PendingGanache 控制台无日志原因Ganache 启动端口7545被其他进程占用或truffle-config.js中port与 Ganache 实际端口不一致导致web3发送交易请求超时。解决执行lsof -i :7545Mac/Linux或netstat -ano | findstr :7545Windows杀掉占用进程确认 Ganache CLI 启动命令明确指定-p 7545在src/app.js的initContract()函数末尾加console.log(Web3 connected to, web3.currentProvider.host);验证前端连接的确实是http://127.0.0.1:7545。4.3 现象投票成功后前端getVotesFor(Alice)返回 0但 Ganache 控制台显示交易成功原因前端调用instance.methods.totalVotesFor(Alice).call()时传入的字符串编码与合约中keccak256(abi.encodePacked(candidates[i]))计算方式不一致。Solidity 的string在 ABI 编码中需注意字节对齐。解决改用索引查询前端不传Alice而是传0候选人数组索引调用instance.methods.votesReceived(0).call()或在合约中增加getCandidateName(uint index)方法返回candidates[index]确保前后端字符串一致推荐修改前端调用为instance.methods.votesReceived(0).call()绕过字符串哈希比对提升性能且稳定。4.4 现象切换到2_token_based_voting目录后truffle migrate报错Error: Cannot find module ./build/contracts/MyToken.json原因2_token_based_voting/migrations/2_deploy_contracts.js中const MyToken artifacts.require(./MyToken.sol);依赖build/contracts/MyToken.json但该文件未生成因为truffle compile未编译MyToken.sol。解决确认2_token_based_voting/contracts/MyToken.sol存在且语法正确Solidity 0.5.16执行truffle compile在2_token_based_voting目录下检查build/contracts/MyToken.json是否生成若无删除build/重试再执行truffle migrate --reset。4.5 现象答辩演示时同学用自己手机 MetaMask 扫码连接显示 “Network not found”无法投票原因手机 MetaMask 默认连接 Infura 或 Alchemy未手动添加本地 Ganache 网络且http://127.0.0.1:7545在手机上无法访问127.0.0.1 指向手机自身而非电脑。解决在电脑上执行ipconfigWindows或ifconfigMac/Linux获取电脑局域网 IP如192.168.1.100Ganache CLI 启动时加-h 0.0.0.0参数ganache-cli -m candy... -p 7545 -h 0.0.0.0 -b 10手机 MetaMask 添加网络时RPC URL 填http://192.168.1.100:7545替换为你的真实 IP确保电脑和手机在同一 WiFi 下且电脑防火墙允许 7545 端口入站。5. 进阶验证用truffle console和ethers.js双轨验证链上状态拒绝“前端显示即真理”的幻觉5.1truffle console实时读取合约状态三行命令验证投票是否真正上链truffle console是脱离前端、直连 Ganache 的调试终端它能绕过所有前端 JS 错误直接读取链上数据。进入1_simple_voting_by_truffle_dapp目录后执行truffle console然后输入// 1. 实例化 Voting 合约地址从 build/contracts/Voting.json 复制 truffle(development) let instance await Voting.at(0x1234567890123456789012345678901234567890) // 2. 查询候选人数组验证部署时传入的 [Alice,Bob] 是否生效 truffle(development) (await instance.candidates.call()).map(x x.toString()) [ Alice, Bob ] // 3. 查询 Alice 的得票数索引 0 truffle(development) (await instance.votesReceived.call(0)).toString() 1Voting.at(...)用已部署地址实例化合约比Voting.deployed()更可靠后者依赖build/contracts/Voting.json的networks字段.call()只读方法不消耗 Gas返回链上当前值.toString()BigNumber 转字符串避免1e20类科学计数法显示。提示如果votesReceived.call(0)返回0但前端显示1说明前端缓存了旧状态或instance.methods.vote(0).send()未真正上链检查 Ganache 日志是否有Transaction mined!。5.2ethers.js脚本独立验证用 Node.js 脚本复现前端逻辑排除 webpack 构建干扰有时npm run dev的热更新会缓存旧 ABI 或地址导致行为异常。写一个独立verify.js脚本用ethers.js直接读链// verify.js放在项目根目录 const { ethers } require(ethers); // 连接 Ganache const provider new ethers.providers.JsonRpcProvider(http://127.0.0.1:7545); // Voting 合约 ABI 和地址从 build/contracts/Voting.json 复制 const abi [ /* ABI 数组省略 */ ]; const address 0x1234567890123456789012345678901234567890; const votingContract new ethers.Contract(address, abi, provider); async function checkVotes() { const aliceVotes await votingContract.votesReceived(0); const bobVotes await votingContract.votesReceived(1); console.log(Alice votes:, aliceVotes.toString()); console.log(Bob votes:, bobVotes.toString()); } checkVotes();执行npm install ethers node verify.jsethers.js比web3.js更轻量无 webpack 依赖结果可信度更高如果verify.js输出Alice votes: 1而前端显示0问题一定在前端 JS 加载逻辑如$.getJSON路径错误、ABI 缓存如果verify.js也输出0说明投票交易根本没上链回溯truffle migrate和instance.methods.vote().send()日志。5.3 Gas 消耗实测表不同操作在 Ganache 上的真实成本为答辩“性能分析”章节提供硬数据毕业设计常要求写“系统性能分析”但多数人只写“响应快”“用户体验好”。用truffle console测真实 Gastruffle(development) let instance await Voting.at(0x...) truffle(development) let tx await instance.vote(0, { from: 0x123... }) truffle(development) tx.receipt.gasUsed 67219 // vote() 消耗 Gas truffle(development) let tx2 await instance.totalVotesFor(Alice, { from: 0x123... }) truffle(development) tx2.receipt.gasUsed 21000 // totalVotesFor() 是 view 函数gasUsed 为 21000基础交易开销整理为表格供答辩使用操作Solidity 方法Ganache Gas 消耗说明投票vote(uint)67,219包含voters[msg.sender] true和votesReceived[i]存储写入查询得票votesReceived(uint)21,000view函数仅读取Gas 固定为交易基础开销查询候选人名candidates(uint)21,000同上数组索引访问初始化合约constructor(string[])1,245,892部署时一次性消耗含字符串数组存储注意totalVotesFor(string)因需循环比对字符串Gas 消耗远高于votesReceived(uint)答辩时可强调“为性能优化前端应优先使用索引查询”。从那以后我每次答辩前都强制走一遍truffle consoleethers.js verify.js双轨验证哪怕多花 10 分钟也绝不信前端页面上那个“1”是真的。因为链上世界没有“看起来像”只有tx.receipt.status true和votesReceived(0).toString() 1这种冷冰冰的布尔值。希望帮到你。本文还有配套的精品资源点击获取
返回列表