☰
SpringBoot与以太坊区块链项目评审系统:评审记录上链存证实践
2026/10/27 5:59:11 网站建设 项目流程

简介:这是一套面向软件工程、计算机科学及区块链方向师生的毕业设计级开发资料,围绕SpringBoot框架与以太坊技术构建项目评审系统,适合作为课程实践、课题研究或原型开发的参考范本,也便于具备编程基础的学习者进行功能拓展与定制调整。压缩包共248个文件,约800KB,以51个Java源文件与1个Solidity合约、1个abi接口文件构成核心业务与链上逻辑,辅以22个ftl模板、23个js脚本、9个css样式及gif演示素材,另有sql脚本、xml配置与properties文件支撑部署运行。目前已有24人学习关注。资料涵盖系统设计、实现代码与部署指南,代码模块经多轮功能验证,结构完整、可直接运行,能帮助读者快速理解区块链存证与评审流程的整合思路,并在此基础上完成二次开发或学术任务演示。

1. 基于SpringBoot与以太坊的区块链项目评审系统:评审记录上链到底解决了什么

做过项目评审的人都有一个共同痛点:评审意见写在 Word 里、审批流跑在 OA 里,过两个月想回溯“当时到底谁投了反对票、理由是什么”,翻出来的记录可能已经被改过、删过,甚至找不到。传统评审系统的信任基础是“数据库管理员不会乱改数据”,但这个假设在跨部门、跨机构的场景里非常脆弱。基于 SpringBoot 与以太坊的区块链项目评审系统,核心思路就是把评审过程中的关键动作——提交评审、打分、审批结论——做成链上交易,利用以太坊的不可篡改性和时间戳特性,让每一条评审记录都有可验证的存证。SpringBoot 负责业务逻辑、用户管理和链上链下数据协调,以太坊负责存证与共识。这套方案适合需要评审结果公信力的场景,比如科研项目验收、内部创新基金评审、供应商准入评审。下面从合约设计一路讲到部署文档里最容易翻车的地方。

2. 合约层怎么设计:评审状态机与链上数据结构

2.1 为什么不用合约存全量数据

一个常见的误区是把评审意见全文、附件、用户信息全部塞进以太坊合约的 storage 里。这样做有两个致命问题:第一,链上存储成本极高,以太坊上存 1KB 数据的 gas 费用在牛市可能达到几十美元;第二,链上数据公开可见,评审意见里可能包含敏感信息。我一般采用的方案是:链上只存关键字段的哈希值和状态标记,原文存在 MySQL 或 IPFS 里,通过哈希做完整性校验。具体来说,合约里存的是projectId、reviewerHash(评审人地址的哈希)、scoreHash(评分内容的哈希)、timestamp、status(评审状态枚举)。验证时拿链下原文重新计算哈希,和链上比对,一致就说明没被篡改。

2.2 评审状态机的 Solidity 实现

评审流程本质上是一个状态机:待评审 → 评审中 → 已提交 → 已确认。合约里用枚举定义状态,用 modifier 控制状态转换权限。下面是一个最小可用的合约骨架:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; contract ProjectReview { enum ReviewStatus { Pending, InProgress, Submitted, Confirmed } struct ReviewRecord { uint256 projectId; bytes32 reviewerHash; // 评审人地址哈希,保护隐私 bytes32 scoreHash; // 评分内容哈希 uint256 timestamp; ReviewStatus status; } // projectId => 评审记录数组 mapping(uint256 => ReviewRecord[]) public reviews; // 项目负责人地址 address public owner; modifier onlyOwner() { require(msg.sender == owner, "Not authorized"); _; } constructor() { owner = msg.sender; } // 提交评审:只有项目负责人可以调用 function submitReview( uint256 _projectId, bytes32 _reviewerHash, bytes32 _scoreHash ) external onlyOwner { reviews[_projectId].push(ReviewRecord({ projectId: _projectId, reviewerHash: _reviewerHash, scoreHash: _scoreHash, timestamp: block.timestamp, status: ReviewStatus.Submitted })); } // 查询某项目的评审数量 function getReviewCount(uint256 _projectId) external view returns (uint256) { return reviews[_projectId].length; } // 查询单条评审记录 function getReview(uint256 _projectId, uint256 _index) external view returns (bytes32, bytes32, uint256, ReviewStatus) { ReviewRecord storage r = reviews[_projectId][_index]; return (r.reviewerHash, r.scoreHash, r.timestamp, r.status); } }

这段合约的逻辑很直白:submitReview把评审记录的哈希和状态写入链上,getReview供前端查询验证。onlyOwner修饰器限制了写入权限,实际项目中可以扩展为多签或角色控制。参数方面,_reviewerHash建议用keccak256(abi.encodePacked(reviewerAddress, salt))生成,加盐是为了防止地址被暴力反推。_scoreHash同理,对评分 JSON 字符串做哈希。block.timestamp是矿工可轻微影响的,但对评审存证场景精度足够。

2.3 合约部署与测试的最小命令

用 Hardhat 部署到本地测试链,先起一个节点:

npx hardhat node

然后在另一个终端部署合约:

npx hardhat run scripts/deploy.js --network localhost

deploy.js里核心就三行:编译、部署、打印地址。部署完把合约地址和 ABI 复制到 SpringBoot 项目的配置文件里。本地测试链的账户私钥 Hardhat 会直接打印出来,拿第一个账户做 owner 即可。注意 Hardhat 默认的 chainId 是 31337,SpringBoot 里配置 Web3j 连接时要对应上,否则会报 chainId 不匹配。

3. SpringBoot 侧怎么接:Web3j 集成与链上链下数据协调

3.1 依赖选型与版本匹配的坑

SpringBoot 集成以太坊最常用的库是 Web3j。这里有一个血泪经验:Web3j 的版本和 SpringBoot 版本有兼容性要求。SpringBoot 3.x 要求 Java 17+,Web3j 6.x 才完整支持 Java 17。如果你用的是 SpringBoot 2.7 配 Web3j 4.x,在 Java 8 环境下没问题,但一旦升级 SpringBoot 版本,Web3j 的Credentials类和TransactionManager接口可能有变动。我一般会在pom.xml里锁定版本:

<dependency> <groupId>org.web3j</groupId> <artifactId>core</artifactId> <version>4.9.8</version> </dependency> <dependency> <groupId>org.web3j</groupId> <artifactId>crypto</artifactId> <version>4.9.8</version> </dependency>

如果你的项目用 Gradle,对应写法是implementation 'org.web3j:core:4.9.8'。版本号不要凭感觉写,去 Maven Central 查一下最新稳定版。另外,Web3j 依赖 Bouncy Castle 加密库,如果项目里同时引入了其他加密相关依赖,可能冲突,表现为NoSuchMethodError,这时候用mvn dependency:tree排查。

3.2 配置 Web3j 连接与合约调用

在application.yml里配置节点地址和合约信息:

web3j: node-url: http://127.0.0.1:8545 contract-address: "0x5FbDB2315678afecb367f032d93F642f64180aa3" owner-private-key: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"

然后写一个配置类初始化 Web3j 和合约服务:

@Configuration public class Web3jConfig { @Value("${web3j.node-url}") private String nodeUrl; @Value("${web3j.contract-address}") private String contractAddress; @Value("${web3j.owner-private-key}") private String privateKey; @Bean public Web3j web3j() { // 使用 HTTP 连接本地节点,超时设 30 秒 return Web3j.build(new HttpService(nodeUrl)); } @Bean public Credentials credentials() { return Credentials.create(privateKey); } @Bean public ProjectReview contract(Web3j web3j, Credentials credentials) { // 加载已部署的合约 return ProjectReview.load(contractAddress, web3j, credentials, new DefaultGasProvider()); } }

ProjectReview这个类是 Web3j 根据合约 ABI 自动生成的 Java 包装类,用web3j generate solidity命令生成。DefaultGasProvider会自动估算 gas 价格,本地测试够用,主网建议换成StaticGasProvider并手动设置合理的 gasPrice。Credentials.create(privateKey)里的私钥不要硬编码在代码里,生产环境用环境变量或密钥管理服务注入。

3.3 链上写入与链下存储的协调流程

评审提交的完整流程是:前端提交评分 → SpringBoot 计算哈希 → 调用合约写入 → 等待交易回执 → 把原文和交易哈希一起存 MySQL。关键代码如下:

@Service public class ReviewService { @Autowired private ProjectReview contract; @Autowired private ReviewRepository reviewRepository; public String submitReview(Long projectId, String reviewerId, String scoreJson) throws Exception { // 1. 计算评审人哈希和评分哈希 byte[] reviewerHash = Hash.sha3( (reviewerId + "salt_2024").getBytes(StandardCharsets.UTF_8)); byte[] scoreHash = Hash.sha3(scoreJson.getBytes(StandardCharsets.UTF_8)); // 2. 调用合约,发送交易 TransactionReceipt receipt = contract.submitReview( BigInteger.valueOf(projectId), Bytes32.wrap(reviewerHash), Bytes32.wrap(scoreHash) ).send(); // 3. 检查交易状态 if (!receipt.isStatusOK()) { throw new RuntimeException("链上交易失败: " + receipt.getStatus()); } // 4. 链下存原文和交易哈希 ReviewEntity entity = new ReviewEntity(); entity.setProjectId(projectId); entity.setReviewerId(reviewerId); entity.setScoreJson(scoreJson); entity.setTxHash(receipt.getTransactionHash()); entity.setBlockNumber(receipt.getBlockNumber().longValue()); reviewRepository.save(entity); return receipt.getTransactionHash(); } }

这段代码的核心逻辑是“先链上后链下”:链上交易成功后再写数据库,保证链下记录一定有链上存证对应。如果链上交易失败,直接抛异常,不会产生脏数据。Hash.sha3是 Web3j 提供的 Keccak-256 哈希方法,和 Solidity 里的keccak256一致。receipt.isStatusOK()判断交易是否成功,失败时getStatus()返回0x0。注意send()是同步阻塞的,本地节点很快,主网可能要等十几秒,生产环境建议用sendAsync()配合回调或 CompletableFuture。

4. 部署文档里最容易翻车的五个地方

4.1 本地链重启后合约地址变了

现象:每次重启 Hardhat 节点,部署的合约地址都不一样,SpringBoot 启动后调用合约报contract not found。原因:Hardhat 本地链默认不持久化状态,重启后链上数据清空,重新部署自然生成新地址。解决:开发阶段把合约地址写在配置文件里,每次重启后手动更新;或者用 Hardhat 的--network localhost配合hardhat node --fork做状态持久化。更稳妥的做法是写一个部署脚本,部署完自动把地址写入application-local.yml。

4.2 nonce 冲突导致交易卡住

现象:连续提交多笔评审交易,后面的交易一直 pending,查不到回执。原因:Web3j 默认用RawTransactionManager,nonce 从链上查询,如果前一笔交易还没打包,后一笔用的 nonce 可能重复。解决:改用FastRawTransactionManager,它会本地维护 nonce 递增。或者手动管理 nonce,每次发交易前getTransactionCount并加偏移量。本地测试链出块快,这个问题不明显,但部署到测试网就会暴露。

4.3 SpringBoot 版本太高导致 Web3j 不兼容

现象:项目用 SpringBoot 3.2 + Java 17,引入 Web3j 4.9.8 后启动报NoClassDefFoundError: javax/xml/bind/DatatypeConverter。原因:Java 11 之后移除了 JAXB 模块,Web3j 某些版本还在用。解决:升级 Web3j 到 5.x 或 6.x,或者手动引入jakarta.xml.bind-api依赖。如果项目里还用了其他依赖 JAXB 的库,统一迁移到 Jakarta 命名空间。

4.4 合约编译版本与节点 EVM 版本不匹配

现象:Solidity 0.8.19 编译的合约部署到某些测试网报invalid opcode。原因:不同以太坊客户端支持的 EVM 版本不同,老节点可能不支持 PUSH0 指令(Solidity 0.8.20 默认启用 Shanghai 升级)。解决:在hardhat.config.js里设置solidity: { version: "0.8.19", settings: { evmVersion: "paris" } },显式指定 EVM 版本。部署前用eth_call测试一下合约方法,确认节点兼容。

4.5 链下数据库和链上数据不一致

现象:链上交易成功了,但 SpringBoot 写数据库时抛异常,导致链上有记录、链下查不到。原因:链上写入和链下写入不在同一个事务里,无法回滚。解决:引入补偿机制——链下写入失败时,把交易哈希记到一张pending_review表里,定时任务重试写入。或者反过来,先写链下再写链上,链上失败就删链下记录。两种方案各有取舍,我一般选前者,因为链上交易一旦成功就无法撤销,链下重试成本更低。

5. 验证链上存证是否真的没被改过:一个手动校验脚本

部署完之后,怎么证明链上数据确实对应链下原文?写一个校验接口,拿链下原文重新算哈希,和链上比对。下面是一个可以直接跑的 Java 方法:

public boolean verifyReview(Long projectId, int index) throws Exception { // 1. 从链上读记录 ProjectReview.ReviewRecord record = contract.getReview( BigInteger.valueOf(projectId), BigInteger.valueOf(index)).send(); byte[] chainScoreHash = record.scoreHash; // 2. 从数据库读原文 ReviewEntity entity = reviewRepository .findByProjectIdAndIndex(projectId, index); byte[] localHash = Hash.sha3( entity.getScoreJson().getBytes(StandardCharsets.UTF_8)); // 3. 比对哈希 return Arrays.equals(chainScoreHash, localHash); }

这个方法的逻辑是:链上存的是哈希,链下存的是原文,重新计算原文哈希和链上比对,一致就说明没被篡改。参数projectId和index对应合约里的映射关系。实际使用时可以批量校验,把所有评审记录跑一遍,输出不一致的条目。注意Hash.sha3返回byte[],和合约返回的bytes32在 Web3j 里映射为byte[],直接用Arrays.equals比较即可。

一个我踩过的坑:早期版本里我用String存哈希,结果链上返回的bytes32转String时因为编码问题导致比对失败。后来统一用byte[]或Bytes32类型,问题消失。另外,校验脚本不要放在主流程里,单独写一个@Component定时跑或者手动触发,避免影响正常业务。

这套方案值不值得做?如果你的评审场景需要跨机构互信、需要审计追溯、需要防止事后抵赖,那链上存证带来的信任成本降低是实实在在的。如果只是内部小团队用,大家抬头不见低头见,传统数据库加操作日志就够了。技术选型永远看场景,不要为了区块链而区块链。希望帮到你。

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

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

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

立即咨询