简介这是一套面向软件工程、计算机科学及区块链方向师生的毕业设计级开发资料围绕SpringBoot框架与以太坊技术构建项目评审系统适合作为课程实践、课题研究或项目原型开发的参考范本也便于具备编程基础的学习者进行功能拓展与定制。压缩包共248个文件约800KB以51个Java源码、22个FreeMarker模板、23个JavaScript脚本、9个CSS样式及6个XML配置为主另含Solidity合约、ABI接口文件、SQL脚本与部署命令脚本覆盖后端逻辑、前端页面、智能合约与数据库各层。目前已有24人学习关注。资料内含系统设计说明、实现代码与部署指南代码模块经多轮功能验证结构完整、可直接运行能帮助读者快速理解区块链评审系统的整体架构与关键实现并在此基础上完成二次开发或学术演示。1. 从一次评审扯皮说起SpringBoot 与以太坊怎么把「谁改过什么」钉死去年帮一个做科研项目管理的团队收拾烂摊子他们的评审系统跑在 SpringBoot 上MySQL 里存着评审意见和打分。问题出在一次结题验收三个专家对同一份材料给出了互相矛盾的评审记录后台日志只显示「update 成功」谁在什么时间把分数从 82 改成 91查不出来。数据库 binlog 有但没人愿意为一次扯皮去翻 binlog而且 binlog 本身也能被有权限的人清理。这件事让我彻底想明白一个道理评审系统里最值钱的不是表单和流程是「不可抵赖的评审痕迹」。基于 SpringBoot 与以太坊的区块链项目评审系统要解决的就是这件事。SpringBoot 负责业务项目申报、专家分配、材料上传、评审打分、结果公示以太坊负责存证把关键动作的哈希写进链上任何人改数据库都改不掉链上那条记录。适合谁做适合手里有 SpringBoot 基础、想找一个真实业务场景把区块链落地跑通的后端也适合做毕设或课程设计、需要一套能演示又能讲清原理的完整方案的人。下面我按「先想清楚存什么、再动手跑通、最后踩坑」的顺序讲。2. 存证边界怎么划哪些数据上链、哪些坚决不上链2.1 全量上链是新手最容易犯的错很多人一听区块链评审系统第一反应是把评审意见、项目材料、专家信息全塞进链上。这个方向从工程角度看是灾难。以太坊主网存 1KB 数据的 gas 成本高到离谱就算是私链或测试链全量上链也会让系统慢到没法用。更关键的是链上数据默认公开评审意见里往往有未公示的敏感内容全量上链等于把内部评审过程直接摊开。正确的边界是链上只存「指纹」和「状态变更事件」原文全部留在链下数据库或对象存储。所谓指纹就是原文的哈希值。评审意见正文存 MySQL同时算一个 SHA-256 哈希写进合约材料文件存 MinIO 或本地磁盘同样只把哈希上链。这样链上那条记录能证明「某份内容在某时刻存在且未被篡改」但内容本身不暴露。我一般会划三条线第一身份与权限不上链用 SpringBoot 的 RBAC 管第二大文本和文件不上链只上哈希第三关键状态流转上链比如「项目提交」「专家确认评审」「评分最终锁定」「结果公示」这四个动作每个动作写一条事件。这样链上数据量可控业务查询还是走 MySQL链只做存证和校验。2.2 合约该设计成什么样合约不需要复杂评审系统的存证合约核心就两个能力写入一条存证记录、按业务 ID 查询存证记录。下面是一个最小可用的 Solidity 合约字段设计对应上面说的边界。// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; contract ReviewEvidence { // 一条存证记录业务ID、阶段、内容哈希、操作人、时间戳 struct Evidence { string bizId; // 项目或评审单的业务ID string stage; // 阶段submit / review / lock / publish string dataHash; // 链下内容的 SHA-256 哈希 string operator; // 操作人标识工号或脱敏ID uint256 timestamp; // 区块时间戳 } // bizId 该业务下的所有存证 mapping(string Evidence[]) private records; event EvidenceAdded(string bizId, string stage, string dataHash, string operator, uint256 timestamp); // 写入存证只追加不修改 function addEvidence(string memory bizId, string memory stage, string memory dataHash, string memory operator) public { Evidence memory e Evidence(bizId, stage, dataHash, operator, block.timestamp); records[bizId].push(e); emit EvidenceAdded(bizId, stage, dataHash, operator, block.timestamp); } // 查询某业务下的存证数量 function getEvidenceCount(string memory bizId) public view returns (uint256) { return records[bizId].length; } // 按索引取一条存证 function getEvidence(string memory bizId, uint256 index) public view returns (string memory, string memory, string memory, string memory, uint256) { Evidence memory e records[bizId][index]; return (e.bizId, e.stage, e.dataHash, e.operator, e.timestamp); } }逻辑说明addEvidence只做追加没有提供任何修改或删除方法这是「不可抵赖」的合约层保证。records用mapping(string Evidence[])而不是数组套数组是为了按业务 ID 直接定位查询成本低。参数上bizId建议用项目编号加评审轮次拼成比如PRJ2024-001-R1保证唯一stage用固定枚举字符串方便前端过滤dataHash是链下内容算出来的十六进制字符串长度固定 64 字符operator不要直接存姓名存工号或系统内脱敏 ID避免链上暴露个人信息。提示合约部署到测试链还是私链取决于你的场景。做毕设或内部演示用 Ganache 或 Hardhat 本地节点足够要演示「多节点共识」再考虑搭一条小规模私链。不要一上来就碰主网gas 和私钥管理会把新手劝退。2.3 链下哈希怎么算才和链上对得上哈希对不上是这类系统最高频的翻车点。原因通常有三个编码不一致、字段拼接顺序不一致、换行符差异。我的做法是定义一个固定的「存证原文模板」前后端和合约调用方都按同一个模板拼字符串再算哈希。// SpringBoot 侧生成存证哈希 import java.nio.charset.StandardCharsets; import java.security.MessageDigest; public class EvidenceHashUtil { // 固定模板bizId|stage|content|operator用竖线分隔避免字段歧义 public static String buildHash(String bizId, String stage, String content, String operator) throws Exception { String raw bizId | stage | content | operator; MessageDigest digest MessageDigest.getInstance(SHA-256); byte[] bytes digest.digest(raw.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(String.format(%02x, b)); } return sb.toString(); } }逻辑说明raw的拼接顺序必须和链下校验时完全一致任何一方改了顺序哈希就对不上。StandardCharsets.UTF_8显式指定编码避免不同环境默认编码不同导致中文内容哈希漂移。%02x保证每个字节转成两位小写十六进制和前端crypto库、合约里字符串比较时格式统一。参数上content传的是评审意见正文或文件内容的字符串形式文件建议先算文件哈希再把这个哈希当 content 传入避免大文件读进内存。3. 用 SpringBoot 把链上链下串起来最小可跑通的调用链3.1 依赖选型和版本坑SpringBoot 调以太坊主流做法是 web3j。这里有个血泪经验SpringBoot 3.x 配 web3j 要注意 Jakarta 命名空间和旧版 javax 的冲突很多网上抄来的代码在 SpringBoot 3 下直接编译不过。我一般会锁一套经过验证的组合SpringBoot 2.7.x 配 web3j 4.9.x或者 SpringBoot 3.2.x 配 web3j 4.10.x 以上。别盲目追最新springboot版本太高带来的连锁反应在区块链依赖上尤其明显。!-- pom.xml 关键依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.web3j/groupId artifactIdcore/artifactId version4.10.3/version /dependency dependency groupIdorg.web3j/groupId artifactIdcodegen/artifactId version4.10.3/version /dependency逻辑说明core提供链交互能力codegen用来把 Solidity 编译后的 abi/bin 生成 Java 包装类省去手写交易组装。参数上版本号必须 core 和 codegen 一致混用会出现NoSuchMethodError。如果你的项目是 Gradle对应换成implementation org.web3j:core:4.10.3springboot gradle项目搭建时注意仓库里要能拉到 web3j。3.2 配置链节点连接# application.yml web3j: node-url: http://127.0.0.1:8545 contract-address: 0x你的合约部署地址 from-address: 0x你的调用账户地址 private-key: 你的测试账户私钥Configuration public class Web3jConfig { Value(${web3j.node-url}) private String nodeUrl; Bean public Web3j web3j() { // 关闭默认的轮询按需开启避免无谓请求 return Web3j.build(new HttpService(nodeUrl)); } }逻辑说明HttpService指向本地或测试链的 RPC 端口Ganache 默认 8545。private-key只用于测试环境生产环境必须走密钥管理服务或硬件签名绝不能明文写在配置文件里提交到仓库。参数上node-url换成你自己的节点地址contract-address是合约部署后返回的地址部署一次固定不变。3.3 调用合约写入存证Service public class EvidenceService { Autowired private Web3j web3j; Autowired private ReviewEvidence contract; // codegen 生成的包装类 Value(${web3j.from-address}) private String fromAddress; Value(${web3j.private-key}) private String privateKey; public String saveEvidence(String bizId, String stage, String content, String operator) throws Exception { // 1. 算链下哈希 String hash EvidenceHashUtil.buildHash(bizId, stage, content, operator); // 2. 组装交易 TransactionReceipt receipt contract.addEvidence(bizId, stage, hash, operator).send(); // 3. 返回交易哈希供前端展示和后续核验 return receipt.getTransactionHash(); } }逻辑说明send()是同步等待交易上链返回的TransactionReceipt里包含交易哈希和区块号。生产环境建议用sendAsync()配合回调避免阻塞业务线程。参数上bizId、stage、operator和算哈希时传入的必须完全一致否则链上存的是 A 的哈希、数据库存的是 B 的内容核验时必然失败。交易哈希要落库它是链上链下关联的唯一凭证。3.4 核验接口怎么写核验是评审系统里最实用的功能任何人拿到一份评审意见都能验证它是否和链上记录一致。public boolean verify(String bizId, String stage, String content, String operator, int index) throws Exception { // 重新算哈希 String localHash EvidenceHashUtil.buildHash(bizId, stage, content, operator); // 从链上取对应记录 Tuple5String, String, String, String, BigInteger e contract.getEvidence(bizId, BigInteger.valueOf(index)).send(); String chainHash e.component3(); // dataHash 在第三个位置 return localHash.equals(chainHash); }逻辑说明核验的本质是「用同样的输入重算哈希和链上存的比」。component3()对应合约返回元组的第三个字段位置不能错。参数上index是该业务下第几条存证前端一般会展示存证列表让用户选。如果核验失败先查拼接模板是否一致再查内容是否被改过最后查是不是取错了 index。4. 部署文档里最容易翻车的几个环节4.1 本地链、合约、后端的启动顺序部署文档如果只写「启动即可」新手一定卡住。正确的顺序是先起链节点再部署合约拿到地址把地址写进配置最后起 SpringBoot。顺序错了SpringBoot 启动时连不上节点或找不到合约直接报错退出。# 1. 启动本地链以 Ganache 为例 ganache --port 8545 --accounts 10 --defaultBalanceEther 100 # 2. 部署合约Hardhat 示例 npx hardhat run scripts/deploy.js --network localhost # 输出ReviewEvidence deployed to: 0x.... # 3. 把地址写入 application.yml 后启动后端 mvn spring-boot:run逻辑说明--accounts 10生成 10 个测试账户够演示多角色--defaultBalanceEther 100给每个账户足够余额付 gas。部署脚本输出的地址必须原样复制少一位都连不上。参数上端口要和node-url一致网络名要和 Hardhat 配置里的localhost对应。4.2 打包和部署到服务器vue打包放进springboot中是这类前后端一体项目常见的部署方式前端npm run build产出dist把dist放进 SpringBoot 的src/main/resources/static再打成一个 jar 部署。这样只需要暴露一个端口省去 Nginx 反代配置。# 前端打包 npm run build # 拷贝到后端静态目录 cp -r dist/* ../backend/src/main/resources/static/ # 后端打包 mvn clean package -DskipTests # 运行 java -jar target/review-system-1.0.jar --spring.profiles.activeprod逻辑说明-DskipTests在部署时跳过测试加快打包但首次部署建议先跑一遍测试。--spring.profiles.activeprod加载生产配置把链节点地址、数据库连接换成生产值。参数上static目录下的index.html会被 SpringBoot 默认欢迎页规则命中前端路由如果是 history 模式需要额外配置 fallback 到index.html否则刷新页面 404。注意宝塔docker部署springboot这类场景下链节点如果也在同一台机器容器网络要用 host 模式或自定义网络否则容器内127.0.0.1指向的是容器自己连不上宿主机的链节点。这是部署文档里必须写清的一条。4.3 数据库和链上数据的一致性链上写入成功、数据库写入失败或者反过来都会造成数据不一致。我的做法是把链上交易哈希先落库状态标记为「待确认」等交易回执确认后再更新为「已存证」。如果数据库写入失败链上那条记录就成了孤儿但因为是只追加不影响后续核验只是列表里少一条。反过来数据库写了但链上没写核验时会发现链上查不到这时候要触发补偿重试。5. 避坑与排查五条真实踩过的记录5.1 现象合约调用报 «insufficient funds for gas»原因调用账户余额不足以支付 gas或者 gas price 设得太低。本地链默认账户余额充足但换成测试链或自己配的账户经常忘充。解决先查账户余额web3j.ethGetBalance(address, DefaultBlockParameterName.LATEST).send()。本地链重启后账户余额会重置重新部署合约前确认余额。gas price 用web3j.ethGasPrice().send()取当前建议值不要写死。5.2 现象中文评审意见上链后核验失败原因哈希计算时编码不一致。Windows 默认 GBKLinux 默认 UTF-8同一段中文算出的字节不同哈希自然不同。解决所有涉及哈希的地方显式指定StandardCharsets.UTF_8包括 Java 侧、前端TextEncoder侧、以及任何脚本。数据库连接串也加上characterEncodingutf8避免存取过程中编码漂移。5.3 现象SpringBoot 启动报 «Unable to determine native type»原因web3j 版本和 SpringBoot 版本不匹配或者 codegen 生成的包装类是用旧版 abi 生成的字段类型对不上。解决core 和 codegen 版本锁一致重新用当前合约的 abi 生成包装类。如果升级了 SpringBoot 到 3.x检查是否有javax.annotation相关依赖需要换成jakarta.annotation。5.4 现象交易发出去了但一直不确认原因本地链出块间隔设置问题或者交易卡在 pending 池。Ganache 默认即时出块但如果用了--blockTime参数会按间隔出块。解决查交易状态web3j.ethGetTransactionReceipt(txHash).send()如果getReceipt()为空说明还没上链。本地链去掉--blockTime恢复即时出块测试链适当提高 gas price 加速。生产环境要设置合理的超时和重试。5.5 现象前端刷新页面 404原因Vue 用 history 路由模式打包进 SpringBoot 后刷新非根路径时 SpringBoot 找不到对应静态资源返回 404。解决加一个转发配置把所有非 API、非静态资源的请求转发到index.html。Controller public class SpaForwardController { // 匹配不含点非静态文件且不以 api 开头的路径 RequestMapping(value /{path:[^\\.]*}) public String forward() { return forward:/index.html; } }逻辑说明正则[^\\.]*排除带点的静态资源请求避免把 js、css 也转发。forward:/index.html交给前端路由处理。参数上如果你的 API 前缀不是/api需要相应调整排除规则。6. 进阶把存证做成可验证凭证而不是只存个哈希走到这一步系统能跑、能存、能核验但还差一层怎么让别人在不信任你数据库的前提下独立验证一条评审记录。答案是让链上记录本身携带足够信息配合一个公开的核验页面任何人输入业务 ID 就能拉出链上存证列表自己算哈希比对。具体做法是扩展合约的查询能力支持按业务 ID 一次性返回所有存证前端拿到后逐条展示阶段、操作人、时间戳和哈希。核验页面不依赖后端数据库直接读链。这样即使后端数据库被清空链上记录依然能证明「某份评审意见在某时刻存在过」。// 一次性返回某业务下所有存证的哈希和阶段供前端核验 function getAllEvidence(string memory bizId) public view returns (string[] memory, string[] memory, uint256[] memory) { Evidence[] memory list records[bizId]; string[] memory stages new string[](list.length); string[] memory hashes new string[](list.length); uint256[] memory times new uint256[](list.length); for (uint256 i 0; i list.length; i) { stages[i] list[i].stage; hashes[i] list[i].dataHash; times[i] list[i].timestamp; } return (stages, hashes, times); }逻辑说明返回三个平行数组而不是结构体数组是为了兼容 web3j 的元组解析结构体数组在部分版本里解析会出问题。参数上bizId为空时返回空数组前端要做空值判断。数据量大时这个接口会消耗较多 gasview 调用不花 gas但节点有计算限制评审系统的存证条数一般几十条以内够用。验证方法上我习惯做一次「篡改演练」手动改数据库里某条评审意见的内容然后跑核验接口确认返回 false再改回去确认返回 true。这个演练能证明整套存证链路真的有效而不是摆设。做毕设答辩时这个演示比讲十页 PPT 都管用。最后说个我自己的习惯每次部署新环境先跑一遍「提交-评审-锁定-公示」四个阶段的完整存证流程确认四条记录都能在链上查到、哈希都能对上再去调业务功能。这个顺序帮我省过很多次「业务跑通了但存证是坏的」的后悔药。区块链项目评审系统的价值不在链本身在于它让评审过程变得可追溯、可验证把扯皮的成本降到最低。希望帮到你。本文还有配套的精品资源点击获取