
简介这份资料面向软件工程、计算机科学及区块链相关方向的师生与开发者提供一套基于SpringBoot框架与以太坊技术构建的项目评审系统完整实现方案可用于毕业设计、课程实践或课题研究等场景。压缩包共248个文件约800KB以51个Java源文件与22个FreeMarker模板构成后端与页面主体辅以23个JavaScript脚本、9个CSS样式及Solidity合约、ABI接口文件等区块链相关资源另含SQL脚本、XML配置与部署说明文档结构完整、层次清晰。目前已有24人学习关注。代码在学术评审中获得优异评价答辩成绩接近满分各模块经多轮功能验证运行稳定。具备编程基础的用户可据此快速理解系统架构与合约交互逻辑并在此基础上进行功能拓展与定制调整也可直接用于项目演示或原型开发为深入学习SpringBoot与以太坊结合应用提供系统化参考。1. 从一次评审扯皮说起这套 SpringBoot以太坊的评审系统到底能干什么如果你带过校级或企业级的项目评审大概率经历过这种场面评审结束三个月后要复核翻遍微信群和邮箱找到的评分表版本对不上某个专家的打分记录被质疑改过最后只能靠“当时大家都看到了”来搪塞。这套基于 SpringBoot 与以太坊的项目评审系统就是冲着这个场景来的——它把评审流程里的申报、分配、打分、公示几个关键动作搬到链上用智能合约固化规则用 SpringBoot 做业务外壳让每一次打分都留下不可篡改的痕迹。它适合三类人一是做毕业设计或课程设计、需要一套能跑通“区块链业务系统”完整链路的同学二是想搞清楚以太坊智能合约怎么和 Java 后端对接的初中级开发三是单位里真有评审流程、想做个可信存证原型的工程师。整套资料包含可运行源码、合约文件、部署文档拿到手能直接起服务不是那种只给几张截图的“论文配套”。2. 系统骨架拆解SpringBoot 分层与合约交互的边界在哪2.1 后端分层结构与关键依赖这套项目的后端是标准的 SpringBoot 分层controller 接请求、service 写业务、mapper 落库、contract 层专门封装对以太坊的调用。我拆包的时候特意看了 pom.xml核心依赖就三块——spring-boot-starter-web 做 REST 接口web3j 做 Java 与以太坊节点的通信mybatis-plus 管 MySQL 里的业务数据。这里有个选型细节值得说它没有用 JPA而是上了 mybatis-plus原因是评审系统里“按条件筛选申报项目”“统计某专家打分记录”这类查询偏多手写 SQL 更可控。合约部分用的是 Solidity 0.5.x 语法编译产物是一组 abi 和 bin 文件放在 resources 下的 contract 目录。web3j 在启动时会加载这些文件把合约部署到节点上拿到合约地址后写进配置文件。整个链路是前端提交打分 → controller 收参 → service 校验业务规则 → contract 层调合约方法 → 交易上链 → 返回交易哈希 → 落库记录哈希。注意链上只存关键凭证项目编号、专家编号、分数、时间戳项目描述、附件这些大字段还是留在 MySQL这是常见做法避免链上存储成本失控。2.2 合约部署与节点连接配置部署文档里给的节点方案是本地起一个以太坊私链用 geth 或者 Ganache 都行。我一般推荐 Ganache因为它开箱即用启动后直接给你十个带余额的测试账户省去挖矿配置的麻烦。启动命令大概是这样# 启动 Ganache指定端口和账户余额 ganache-cli -p 8545 -a 10 -e 1000 -m your mnemonic phrase here参数说明-p指定 RPC 端口默认 8545-a是生成账户数量-e是每个账户初始余额单位 ETH-m是助记词不写会随机生成。启动后终端会打印 RPC 地址和账户列表把这些信息填进 SpringBoot 的 application.yml。# application.yml 中区块链相关配置 ethereum: rpc-url: http://127.0.0.1:8545 contract-address: 0x... # 首次部署后由程序回写 admin-private-key: 0x... # 部署合约的管理员私钥 gas-price: 20000000000 gas-limit: 6721975这里gas-price和gas-limit两个参数最容易翻车。私链上 gas-price 设 0 也能跑但一旦你换到测试网设 0 交易直接卡死。gas-limit 要略高于合约部署的实际消耗Ganache 默认块 gas 上限是 6721975超过就报 “exceeds block gas limit”。我一般会先跑一次部署看回执里的 gasUsed再往上浮 20% 写进配置。2.3 评审流程上链的代码实现核心业务在 service 层以“专家提交打分”为例代码结构大致如下// ReviewService.java 中提交打分的核心逻辑 public String submitScore(Long projectId, Long expertId, int score) { // 1. 业务校验项目是否存在、专家是否被分配、分数是否在合法区间 Project project projectMapper.selectById(projectId); if (project null) throw new BizException(项目不存在); if (score 0 || score 100) throw new BizException(分数超出范围); // 2. 调用合约写入链上 String txHash; try { txHash contractService.addScore(projectId, expertId, score); } catch (Exception e) { // 链上失败不回滚业务记录异常供人工核查 log.error(上链失败 projectId{} expertId{}, projectId, expertId, e); throw new BizException(上链失败请稍后重试); } // 3. 落库保存交易哈希便于后续核验 ScoreRecord record new ScoreRecord(); record.setProjectId(projectId); record.setExpertId(expertId); record.setScore(score); record.setTxHash(txHash); scoreRecordMapper.insert(record); return txHash; }逻辑说明第一步做纯业务校验不碰链第二步调合约这里用 try-catch 包住因为链上交易可能因为 nonce 冲突、gas 不足、节点掉线等原因失败第三步把交易哈希存库这是事后核验的唯一线索。参数方面projectId和expertId在合约里是 uint256Java 侧用 Long 映射没问题但要注意前端传参时别传成字符串web3j 对类型敏感传错会抛 “invalid type” 异常。3. 从零跑通环境搭建、合约编译与前后端联调3.1 开发环境与版本对齐这套项目对版本比较敏感我踩过的坑是 JDK 用了 17结果 web3j 某个依赖在模块化下报错。稳妥的组合是 JDK 8 或 11、Maven 3.6、Node 14前端如果是 Vue 的话、Ganache 7.x。数据库用 MySQL 5.7 或 8.0 都行建库脚本在 sql 目录下直接 source 进去就行。合约编译需要 solc版本要和合约文件头部的pragma solidity ^0.5.x对上。如果你本地没装 solc可以用项目里带的编译脚本或者用 Remix 在线编译后把 abi 和 bin 拷出来。我一般用 solcjs# 安装指定版本的 solcjs npm install -g solc0.5.17 # 编译合约输出 abi 和 bin solcjs --abi --bin contracts/ReviewContract.sol -o build/编译完会在 build 目录下生成 .abi 和 .bin 文件把它们放到 SpringBoot 的 resources/contract 下注意文件名要和代码里读取的路径一致否则启动时报 “contract file not found”。3.2 合约部署与地址回写合约部署有两种方式一是用 Truffle 或 Hardhat 脚本部署二是让 SpringBoot 启动时自动部署。这套项目用的是后者在 contractService 的 init 方法里判断配置中的 contract-address 是否为空为空就调 deploy部署成功后把地址回写到配置文件。自动部署的好处是省事坏处是每次重启如果配置没持久化会重复部署新合约之前的数据就“失联”了。// ContractService.java 初始化时部署合约 PostConstruct public void init() { String addr environment.getProperty(ethereum.contract-address); if (addr null || addr.isEmpty()) { // 部署新合约 String deployed deployContract(); // 回写到本地文件避免重启重复部署 writeAddressToFile(deployed); this.contractAddress deployed; } else { this.contractAddress addr; } }参数说明PostConstruct保证 Bean 初始化后执行writeAddressToFile把地址写到项目根目录的 contract-address.txt下次启动先读这个文件。注意如果你把项目打成 jar 包运行写文件要写到外部路径别写到 jar 内部否则重启后文件丢失又会重新部署。3.3 前端联调与接口验证前端如果是 Vue 项目打包后放进 SpringBoot 的 static 目录这是热词里常说的“vue 打包放进 springboot 中”。联调时先起后端再起前端 dev server配好 proxy 指向后端端口。验证接口是否通可以用 curl 直接打# 测试提交打分接口 curl -X POST http://localhost:8080/api/review/score \ -H Content-Type: application/json \ -d {projectId:1,expertId:2,score:88}返回里会带 txHash拿这个哈希去 Ganache 的终端或者区块浏览器私链一般用 Ganache UI查交易详情能看到 input 数据里编码的 projectId、expertId、score。如果返回 500先看后端日志有没有 “insufficient funds” 或 “nonce too low”前者是账户余额不够付 gas后者是并发提交时 nonce 没管理好。4. 避坑与排查链上交互最容易翻车的五个点4.1 交易一直 pending 不打包现象调接口返回了 txHash但 Ganache 里查不到交易或者一直显示 pending。原因通常是 gas-price 设得太低私链虽然不挑但有些节点实现会过滤掉低价交易另一个原因是账户 nonce 冲突比如你手动发过交易没等确认就又发了一笔。解决办法把 gas-price 调到 20 Gwei 以上nonce 交给 web3j 的 TransactionManager 自动管理别自己手动设。4.2 合约地址重启后失效现象重启 SpringBoot 后之前上链的数据查不到了。原因就是 3.2 里说的自动部署逻辑没做持久化每次重启都部署新合约旧合约地址丢了。解决把 contract-address 写进 application.yml 并提交到版本库或者写到外部固定路径启动时优先读配置。4.3 中文乱码导致合约调用失败现象项目名称带中文调合约时报 “invalid UTF-8 sequence”。原因是 Solidity 的 string 类型对 UTF-8 编码有要求Java 侧如果没指定编码默认平台编码可能是 GBK。解决在 web3j 调用前把字符串转成 UTF-8 字节数组或者干脆链上只存项目 ID中文名称留库。4.4 MySQL 与链上数据不一致现象链上交易成功了但数据库里没记录或者反过来。原因是 2.3 里的代码是先上链再落库如果落库那步抛异常链上数据就“孤儿”了。解决加一张交易流水表先落库标记“待上链”上链成功后更新状态失败则重试或人工介入。这是最终一致性的常见做法别指望分布式事务。4.5 私钥硬编码在配置文件里现象代码提交到 Git 后私钥泄露。这是血泪经验我见过不止一个项目把 admin-private-key 直接写在 application.yml 里推上去了。解决用环境变量注入或者用 Jasypt 加密配置项。本地开发图省事可以写死但推代码前一定检查 .gitignore 有没有把配置文件排除掉。5. 进阶技巧用事件日志做评审记录审计合约里除了存分数还应该定义 event每次打分触发一个事件。这样做的好处是审计时不用遍历所有区块的交易 input直接按事件主题过滤就行效率高很多。合约里加一行// 定义打分事件 event ScoreSubmitted(uint256 indexed projectId, uint256 indexed expertId, uint256 score, uint256 timestamp);然后在 addScore 方法里 emit 这个事件。Java 侧用 web3j 的 EthFilter 订阅// 订阅 ScoreSubmitted 事件 EthFilter filter new EthFilter(DefaultBlockParameterName.LATEST, DefaultBlockParameterName.LATEST, contractAddress); filter.addSingleTopic(EventEncoder.encode(REVIEW_CONTRACT_SCORE_SUBMITTED_EVENT)); web3j.ethLogFlowable(filter).subscribe(log - { // 解析 log写入审计表 EventValues values FunctionReturnDecoder.decode(log.getData(), eventParameters); log.info(链上打分事件: projectId{} score{}, values.get(0), values.get(2)); });参数说明DefaultBlockParameterName.LATEST表示只监听最新区块如果要补历史数据改成EARLIEST到LATEST的范围。addSingleTopic里的 topic 是事件签名的 keccak256 哈希web3j 的 EventEncoder 会自动算。订阅到事件后可以写进一张独立的审计表和业务表分开这样即使业务库被改链上事件日志还能作为对照。验证方法很简单提交一次打分看审计表有没有新增记录再去 Ganache 里查对应交易的 logs对比 projectId 和 score 是否一致。如果订阅不到先检查 filter 的合约地址对不对再确认节点是否支持 eth_newFilter有些轻节点不支持。从那以后我每次做链上业务都强制走一遍“先落库待上链、再发交易、最后更新状态”的流程宁可多一张流水表也不让链上和库里的数据对不上。希望这套东西能帮到你少走点我当年踩过的弯路。本文还有配套的精品资源点击获取