☰
SpringBoot+以太坊实战:构建区块链项目评审与哈希存证系统
2026/10/14 9:36:00 网站建设 项目流程

简介:这是一份基于SpringBoot与以太坊技术栈的区块链项目评审系统毕业设计资料包,面向软件工程、计算机科学、区块链及相关方向的师生和开发者,解决项目评审场景中的流程记录、结果存证与可追溯需求。包体共248个文件,涵盖Java源码、前端页面与脚本、Solidity智能合约及编译产物、SQL数据库脚本、部署命令与说明文档等,压缩包约800KB,结构完整,便于按模块理解与二次开发。系统源代码经多轮功能验证和学术评审,在最终答辩中表现优异,适合作为毕业设计、课程实践或课题研究的参考范本。具备编程经验者可在现有代码基础上进行功能扩展和定制调整,也能直接用于学术任务或项目演示。已有24人学习浏览。

1. 基于SpringBoot与以太坊做区块链项目评审系统,首先要回答的是:评审记录凭什么被信任?

专家评完项目,结果写进MySQL,后台能改、能删、能把评分从63改成88,传统审计手段很难发现。基于SpringBoot与以太坊的区块链项目评审系统,核心思路是把每次评审提交变成一笔以太坊交易:链上只留下评审人地址、评分、内容哈希和时间戳,链下存完整报告。事后任何人对上哈希,就能验证一份评审有没有被动过。这套做法适合科研课题评审、招投标评分、竞赛打分这些事后要接受质询的场景,对开发者来说也是把SpringBoot、Web3j、Solidity、Ganache这条链路一次跑通的好项目。

2. SpringBoot与以太坊的架构选型:评审系统为什么这么搭

2.1 系统整体架构:REST API、业务服务与链上合约的边界划分

常见做法是SpringBoot作为后端业务容器,向前端提供REST API,同时通过Web3j与以太坊节点通信。这套评审系统可以拆成四层。

第一层是前端展示与交互层,负责创建评审任务、填写评审意见、展示上链状态。第二层是SpringBoot的Controller层,负责接收请求、做参数校验、返回REST响应。第三层是Service层,也是核心业务层,这里干三件事:把评审内容做结构化处理、计算内容哈希、调用Web3j构建并发送以太坊交易。第四层是数据与链上存储层,MySQL保存项目信息、评审内容和交易回执,以太坊合约保存链上存证。

这套分层的关键在于:链上不承载完整业务数据,只承载不可篡改的证据,业务规则不停留在合约里,而是留在SpringBoot层。原因是评审系统的业务规则变化频率远高于存证需求——今天是百分制,明天可能改成等级制,如果把这套规则写进Solidity合约,每次调整都要重新部署合约、迁移数据,成本极高。

所以在设计上,我一般会把合约视为一个“存证接口”,不承担业务判断。合约里的函数越简单,越好验证,越不容易成为黑匣子。

2.2 合约状态变量还是事件日志:评审上链的最小模型

数据上链有两条路:写入合约的状态变量,或者写入交易日志(Event)。这两者的成本差异很大。SSTORE操作当前大约消耗20000 gas,而一条事件日志,每个topic消耗375 gas,每字节数据消耗8 gas。存同样的内容,日志比状态变量便宜一个数量级。

但代价是查询方式不同。状态变量可以被合约函数直接读取,对外部调用方来说就是一个getter方法,查询很直接。事件日志则要通过Bloom filter和indexed参数过滤,查询链路长一些,但不需要额外写合约方法。

这个评审系统的最小模型是两者结合:合约里保存一条ReviewRecord的数组,记录评审人地址、项目ID、分数、内容哈希和时间戳,同时提交评审的时候emit一个ReviewSubmitted事件。状态变量保证链上可查,事件保证系统可以异步监听评审提交动作、做链下数据同步。这是从我做类似系统踩过的坑里总结出来的——只做状态变量不发射事件,SpringBoot这边就只能轮询合约,轮询频率和时效性很难平衡,评完10分钟界面还显示“上链中”。

合约最小可用模型代码如下:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; contract ReviewContract { // 评审记录状态变量 struct ReviewRecord { string projectId; address reviewer; uint8 score; string contentHash; uint256 timestamp; } ReviewRecord[] public records; // 按项目ID索引记录下标 mapping(string => uint256[]) private projectReviewIndexes; // 事件:用于链下监听 event ReviewSubmitted( string projectId, address indexed reviewer, uint8 score, string contentHash, uint256 timestamp ); function submitReview( string memory _projectId, uint8 _score, string memory _contentHash ) external returns (uint256) { records.push(ReviewRecord({ projectId: _projectId, reviewer: msg.sender, score: _score, contentHash: _contentHash, timestamp: block.timestamp })); uint256 recordId = records.length - 1; projectReviewIndexes[_projectId].push(recordId); emit ReviewSubmitted(_projectId, msg.sender, _score, _contentHash, block.timestamp); return recordId; } function getRecord(uint256 _index) external view returns (string memory, address, uint8, string memory, uint256) { ReviewRecord storage r = records[_index]; return (r.projectId, r.reviewer, r.score, r.contentHash, r.timestamp); } function getProjectReviewCount(string memory _projectId) external view returns (uint256) { return projectReviewIndexes[_projectId].length; } }

这里有几个设计细节值得解释。score用uint8而不是uint256,是因为评分范围通常是0到100,一个字节足够,而且放进结构体后可以和相邻字段做存储打包,省gas。projectId用string而不是uint256,是为了跟SpringBoot业务系统中的项目ID对应,避免来回转换。submitReview函数返回recordId,给SpringBoot层用来和MySQL里的记录做关联。

对比项状态变量事件日志
存储成本SSTORE约20000 gas每条日志约375gas + topic费用,整体明显更便宜
读取方式合约getter直接读按Topic过滤或重放日志
适合场景需要链上可查的存证异步通知、审计流水
对SpringBoot的友好度同步调用,返回结构体Flowable异步监听,解耦业务

需要特别注意,合约里的timestamp是矿工打包交易时的区块时间,不是SpringBoot服务器的时间。评审人点击“提交”和实际上链之间可能相差几十秒甚至几分钟,这是区块链本身的特性。做业务展示时要以链上时间为准去对账,否则会出现“前端显示14:30提交,链上时间戳却是14:31”的误解。

2.3 Web3j连接以太坊节点的三种方式与选型建议

Web3j是Java生态里和以太坊节点打交道最主流的库,内部封装了JSON-RPC协议。SpringBoot整合Web3j,重点在于节点连接方式的选择。

方式一是HTTP连接,最常见,用一个HttpService包住节点地址就行,适合大多数RESTful交互场景。方式二是WebSocket连接,Web3j提供WebSocketService,节点和客户端保持长连接,适合需要低延迟监听的场景。方式三是IPC文件连接,只适用于以太坊节点和Java进程在同一台服务器上的情况,性能和连接可靠性都好,但部署受限。

这个评审系统我一般用HTTP连接,只有事件监听这种需要实时性的场景才考虑WebSocket。关键点在于,不要用裸的HttpService,因为默认的OkHttpClient没有连接池,也没配超时。要单独建一个OkHttpClient实例,连接超时10秒、读超时60秒,允许连接复用:

@Configuration public class Web3jConfig { @Value("${ethereum.rpc-url:http://localhost:8545}") private String rpcUrl; @Bean public Web3j web3j() { OkHttpClient okHttpClient = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .build(); return Web3j.build(new HttpService(rpcUrl, okHttpClient)); } }

如果你用的是SpringBoot 3.x,要留意一个兼容性问题:Web3j的老版本依赖javax.websocket,而SpringBoot 3.x迁移到Jakarta后,WebSocket链路可能报ClassNotFoundException。把Web3j升到支持Jakarta的4.9.0以上版本可以绕开这个坑,这也是不少人在升级SpringBoot后发现Web3j突然不可用的原因。

选型建议汇总:本地开发和测试直接用Ganache或Hardhat node,地址填http://localhost:8545即可;测试网和主网推荐Infura或Alchemy这类托管节点,地址是https协议;有条件的团队可以自建geth节点,链上数据都在自己手里,不受第三方限流影响。

3. 用SpringBoot与以太坊实现评审上链:合约部署与业务调用

3.1 合约编译、部署到Ganache与Java包装类生成

写好Solidity合约后,第一步是编译。常见做法是用solc直接命令行编译:

solc --optimize --optimize-runs 200 --bin --abi ReviewContract.sol -o build/

编译产物是build/ReviewContract.bin和build/ReviewContract.abi。bin是EVM字节码,部署合约时需要读取;abi是合约接口的JSON描述,生成Java包装类时要读它。

生成Java包装类有两种路径。第一种是Web3j的CLI命令,第二种是Maven或Gradle插件。CLI最直接:

web3j generate solidity -b build/ReviewContract.bin -a build/ReviewContract.abi -o ./src/main/java -p com.example.review.contract

生成出来的ReviewContract.java包含合约的加载、部署、方法调用、事件流式监听等封装。它本质上是把一个Java接口映射到链上的JSON-RPC调用,SpringBoot业务层直接调用这个类就行。

部署到Ganache本地链,可以用一个测试方法验证:

@SpringBootTest public class ContractDeployTest { @Autowired private Web3j web3j; @Test public void deployToGanache() throws Exception { // 用Ganache默认账户的私钥,开发环境专用,仅作示例 Credentials credentials = Credentials.create( "0x4f3edf983ac1e6f0a0c1e2b3c4d5e6f70000000000000000000000000000000001"); // Ganache本地节点对gas不敏感,直接用静态值 ContractGasProvider gasProvider = new StaticGasProvider( BigInteger.valueOf(20_000_000_000L), // 20 Gwei BigInteger.valueOf(300_000L) // 300k gas limit ); ReviewContract contract = ReviewContract.deploy( web3j, credentials, gasProvider).send(); System.out.println("contract address: " + contract.getContractAddress()); System.out.println("deploy tx hash: " + contract.getDeployTransactionHash()); } }

几个关键参数说明。Credentials代表签名身份,它持有私钥,Web3j发送交易时用该私钥对交易做ECDSA签名,同时推导出对应的以太坊地址作为交易的from。GasProvider的第二个参数gasLimit,经验值300k对这样简单合约足够,如果是生产环境,建议先用eth_estimateGas估算一遍再设,比如调用submitReview方法需要多少gas,乘以1.5做保险系数。

部署成功后会拿到一个合约地址,比如0x2E7d879b24AaB863a1E34eE2a2F4e6F7A9b8C3d1,把这个地址写进SpringBoot的application.yml。部署这笔交易本身也会永久记录在链上,部署人、部署时间都能通过交易哈希查到。

3.2 SpringBoot业务层:提交评审、签名与哈希上链

业务层核心是ReviewService,它要做的事包括接收Controller传入的评审内容、对内容做规范化处理、计算Keccak-256哈希、通过合约包装类发送交易。

@Service public class ReviewService { @Autowired private Web3j web3j; @Value("${contract.address}") private String contractAddress; @Value("${review.private-key}") private String privateKey; private ReviewContract contract; private Credentials credentials; @PostConstruct public void init() { credentials = Credentials.create(privateKey); ContractGasProvider gasProvider = new DynamicGasProvider(web3j); contract = ReviewContract.load(contractAddress, web3j, credentials, gasProvider); } public ReviewSubmitResult submitReview(ReviewRequest request) { // 1. 规范化评审内容,保证哈希可复现 String canonicalContent = canonicalize(request); byte[] contentBytes = canonicalContent.getBytes(StandardCharsets.UTF_8); String contentHash = Hash.sha3String(contentBytes); // 0x开头64位 // 2. 发送交易,调用合约submitReview方法 TransactionReceipt receipt = contract.submitReview( request.getProjectId(), request.getScore(), contentHash ).send(); // 3. 解析回执,返回业务状态 ReviewSubmitResult result = new ReviewSubmitResult(); result.setTxHash(receipt.getTransactionHash()); result.setBlockNumber(receipt.getBlockNumber()); result.setContentHash(contentHash); result.setStatus("PENDING_CONFIRM"); return result; } private String canonicalize(ReviewRequest request) { StringJoiner joiner = new StringJoiner("|"); joiner.add(request.getProjectId()); joiner.add(request.getReviewerId()); joiner.add(String.valueOf(request.getScore())); joiner.add(request.getComment() == null ? "" : request.getComment()); return joiner.toString(); } }

这里最容易被忽略的是哈希的规范化。如果直接把评审内容用JSON序列化后做哈希,一旦JSON库版本升级导致字段顺序变化,或者前端传过来的字段顺序不一致,相同内容会算出完全不同的哈希,链上链下就对不上了。所以要用定长、固定顺序的拼接字段,确保任何人在任何时间用相同输入都能得到同样的结果。

DynamicGasProvider是生产环境必写的类,它的职责是让每笔交易的gas price跟着网络实时行情走:

public class DynamicGasProvider implements ContractGasProvider { private final Web3j web3j; public DynamicGasProvider(Web3j web3j) { this.web3j = web3j; } @Override public BigInteger getGasPrice() { try { // 查询当前网络推荐的gas价格,再上浮10%作为缓冲 BigInteger current = web3j.ethGasPrice().send().getGasPrice(); return current.multiply(BigInteger.valueOf(110)).divide(BigInteger.valueOf(100)); } catch (IOException e) { // 取不到就退回一个保守值,避免交易直接失败 return BigInteger.valueOf(50_000_000_000L); } } @Override public BigInteger getGasLimit() { // 评审提交的操作固定,300k一般够用;合约复杂度变了要重新估算 return BigInteger.valueOf(300_000L); } }

3.3 事件监听:SpringBoot如何感知链上确认状态

交易发送成功不代表评审已确认,得等交易被打包进新区块。更准确地说,SpringBoot需要监听ReviewSubmitted事件来确认上链状态。

比较靠谱的做法是通过Web3j的Flowable接口订阅事件日志,解析事件里的projectId、score、contentHash,随后更新MySQL中对应记录的状态。以下是可用的监听器:

@Component public class ReviewEventProcessor { private static final Logger log = LoggerFactory.getLogger(ReviewEventProcessor.class); @Value("${contract.address}") private String contractAddress; @Autowired private Web3j web3j; @Autowired private ReviewRecordMapper reviewRecordMapper; // 上次处理到的区块号从数据库读,防止重启丢事件 private volatile BigInteger lastProcessedBlock = BigInteger.ZERO; @PostConstruct public void start() { BigInteger from = loadLastProcessedBlock(); ReviewContract contract = ReviewContract.load( contractAddress, web3j, credentials, new DynamicGasProvider(web3j)); contract.reviewSubmittedEventFlowable( new DefaultBlockParameterNumber(from), DefaultBlockParameterName.LATEST ).subscribe(event -> { reviewRecordMapper.markConfirmed( event.projectId, event.reviewer, event.score, event.contentHash ); lastProcessedBlock = event.getLog().getBlockNumber(); persistBlockProgress(lastProcessedBlock); }, error -> { log.error("review event processing failed, will retry on next block", error); }); } }

从最后处理区块继续扫描,是Web3j事件监听里最核心的避坑点。如果不记录lastProcessedBlock,服务一重启就会漏掉重启窗口内发生的链上事件。最早做这套系统时没写这个记录,重启两次丢了三条评审数据,数据库里出现了“链上已确认、业务状态未更新”的脏数据,对账时才暴露出来。

4. 评审系统的数据设计:链上存哈希、链下存全量

4.1 链上链下数据拆分的四个原则

这套评审系统数据设计的核心原则是:链上存证据,链下存全量。具体拆下来有四条。

第一,校验字段上链,完整内容留库。评审人地址、评分、内容哈希、时间戳是校验字段,写入合约;评语全文、附件清单、评分明细留在MySQL。需要仲裁或审计时,用链上哈希校验链下内容。

第二,上链字段必须是低维度的。哈希、分数、地址、时间戳都是固定长度的基础类型,不要上链JSON字符串,更不要把评审报告全文放上去。把评语全文写入事件的做法,不仅gas费用高,事件日志还有大小上限,几十KB的内容根本塞不进去。

第三,链上不存敏感信息。评审系统可能涉及专家个人信息,个人数据不应该写入公有链,因为一旦上链就不可删除,等于永久公开。

第四,链上链下要有明确的对账键。最常见的键是交易哈希和内容哈希。MySQL的每条评审记录都要存这两个字段,丢了交易哈希,后续定位链上证据会非常被动。

4.2 评审内容哈希的生成与验证流程

让哈希可复现是整个系统能否落地的关键。前面提到用固定拼接字符串做哈希输入,这里补充完整的验证代码。假设系统运行三个月后,审计人员要求验证某次评审结果是否被篡改:

@Service public class ReviewVerificationService { public VerifyResult verifyByProject(String projectId, Long recordId) { // 1. 从MySQL取出当时保存的评审内容和链上哈希 ReviewRecordDO record = reviewRecordMapper.selectByProjectAndId(projectId, recordId); if (record == null) { return VerifyResult.notFound(); } // 2. 重新计算哈希:使用与上链时完全相同的规范化规则 String canonicalContent = canonicalize(record); String localHash = Hash.sha3String(canonicalContent.getBytes(StandardCharsets.UTF_8)); // 3. 从链上拉取对应记录 String remoteHash = fetchRemoteHash(record.getTxHash()); // 4. 比对 boolean match = localHash.equalsIgnoreCase(remoteHash); return VerifyResult.of(match, localHash, remoteHash); } private String fetchRemoteHash(String txHash) { Optional<TransactionReceipt> receiptOptional = web3j.ethGetTransactionReceipt(txHash).send(); if (receiptOptional.isPresent()) { // 用合约包装类解析回执中的ReviewSubmitted日志,快捷取字段 List<ReviewContract.ReviewSubmittedEventResponse> events = contract.getReviewSubmittedEvents(receiptOptional.get()); if (!events.isEmpty()) { return events.get(0).contentHash; } } return null; } }

哈希比对通过,则证明三个事实:评审内容在链上时间戳之后没有被修改过;内容是当时提交的那份,因为哈希完全一致;提交者是该评审人地址对应的私钥持有者,因为以太坊交易的签名无法伪造。

实际运维中,这条验证链路可以做成对外REST接口,让评审专家输入交易哈希查他自己的评审是否原样上链。这也是这套系统价值主张成立的基础,没有这层可验证性,区块链只是额外负担。

4.3 MySQL表结构与交易回执的关联设计

数据库设计上,评审提交表是核心,需要同时覆盖业务数据和链上证据两类信息。常见的表结构如下:

CREATE TABLE review_submission ( id BIGINT AUTO_INCREMENT PRIMARY KEY, project_id VARCHAR(64) NOT NULL COMMENT '评审项目ID', reviewer_address VARCHAR(42) NOT NULL COMMENT '评审人以太坊地址', reviewer_id VARCHAR(32) NOT NULL COMMENT '评审人系统内ID', score TINYINT NOT NULL COMMENT '评分0-100', comment_text TEXT COMMENT '评语全文', content_hash VARCHAR(66) NOT NULL COMMENT '规范化内容的Keccak哈希', tx_hash VARCHAR(66) COMMENT '上链交易哈希', block_number BIGINT COMMENT '交易所在区块号', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待上链 1已确认 2失败', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, confirm_time DATETIME COMMENT '链上确认时间', UNIQUE KEY uk_project_reviewer (project_id, reviewer_id), UNIQUE KEY uk_tx_hash (tx_hash), KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评审提交与链上存证关联表';

几个字段设计值得说明。reviewer_address保存以太坊地址,42位是因为0x加上40个十六进制字符;content_hash用66位,0x加64位哈希值。reviewer_address是42位,content_hash是66位,这类带固定前缀的十六进制串,存CHAR比VARCHAR在索引上更紧凑,查询性能也更好。tx_hash是唯一键,一条交易对应一次评审提交,不可能两条评审共用一笔交易。status字段负责业务上链状态,程序启动后的事件监听和失败重跑都依赖它。

confirm_time和create_time一定要分开。create_time是SpringBoot服务器时间,confirm_time是链上事件确认时间,两者可能相差几十秒甚至几分钟。审计时要能区分这两个时间,不能混用。

5. 部署与运行的避坑指南:五个让评审上链翻车的问题

5.1 公共节点请求超时与限流

现象:系统刚上线第一周一切正常,第二周开始评审提交时好时坏,日志里频繁出现Connection timed out,而且越到晚上越严重。

原因:公共节点的免费计划有每秒请求上限,评审功能上线后,Web3j发交易、查回执、监听事件,每条链路都在消耗配额。再加上HttpService没有配置超时和连接池,一个慢请求就可能把业务线程卡住。

解决:给Web3j单独建OkHttpClient并配置连接池与超时;生产环境考虑自建节点或托管节点的高阶套餐;在SpringBoot的业务层给交易发送加一个退避重试,比如失败后等待2秒再试,连续三次失败才告警。给Web3j配多个RPC端点,通过Failover策略在节点异常时自动切换,也是我见过比较稳妥的工程做法。

5.2 Gas价格设置不合理导致交易卡死

现象:下午三点提交的评审,到晚上七点还在pending,用户刷新页面一直显示“上链中”。点进区块链浏览器一看,交易一直没被打包。

原因:用StaticGasProvider把gas price写死成某个值,下午三点这条链恰好进入拥堵状态,矿工优先打包高gas价交易,这笔交易就一直在交易池里等着。

解决:生产环境必须用动态gas provider,前面给出的DynamicGasProvider就是干这个的。以太坊切换到EIP-1559之后,用maxFeePerGas和maxPriorityFeePerGas替代原来的gas price,Web3j 4.8以上版本支持Eip1559GasProvider。还要注意,如果交易卡在pending池里太长时间,用相同nonce发一笔高gas的替换交易,可以把它“超车”出去。

5.3 私钥硬编码在配置文件里

现象:为了图方便,把评审人私钥直接写在application.yml里。代码仓库是内网的,以为不怕,后来加了外包协作者,仓库权限没控好,私钥泄露。

原因:开发阶段觉得本地或测试网私钥无所谓,但评审系统往往在测试网上就有真实业务数据,测试网私钥泄露被滥用,整条测试数据链路都会失去可信度。

解决:所有私钥统一从环境变量或配置中心注入,代码仓库里不出现任何私钥字面量。另一个更稳的做法是给每个评审人分配独立的以太坊地址,而不是所有评审共用一个系统私钥,这样签名可以追溯到单个评审人,避免单点私钥风险。

5.4 合约不可变与业务需求的冲突

现象:上线一个月后,需求方要求评分支持十个评审人加权汇总,但合约里只存了单人score字段,业务规则根本变不了。

原因:智能合约部署后代码不可变,这是区块链的基本特性。合约没有预留扩展位,业务需求一来就只能望链兴叹。

解决:方案一是设计合约时预留扩展字段,比如加一个uint8 reviewType或者uint8[] scores数组;方案二是引入代理合约模式,把逻辑合约和存储合约分开,升级时只重新部署逻辑合约。但对于这个评审系统,最务实的方案其实是把链上只做成哈希存证,评审业务逻辑全部留在SpringBoot,业务规则怎么变都不影响链上证据。这条思路前面反复强调过,因为它直接避开了合约升级这个最大的坑。

5.5 服务重启导致事件监听丢失

现象:凌晨发版重启服务,第二天发现有三条评审记录的status还是0“待上链”,但链上明明已经确认了。

原因:事件监听从LATEST开始,服务重启的几十秒里链上产生的交易完全没有被监听,又因为没有从历史区块重扫的逻辑,这批交易就永远没被处理。

解决:启动时从数据库记录的最后处理区块号开始重扫历史事件。前面事件处理器里已经给出具体实现,关键是把lastProcessedBlock持久化到MySQL而不是放在内存里。重扫区块较多时,控制每批拉取步长,比如一次1万个区块,避免一次拉太多触发RPC超时。

6. 验证评审上链数据的三个技巧:从交易回执到批量对账

6.1 从交易回执解析事件,直接验证单条评审

交易回执里不仅有交易状态,还有完整的事件日志。拿到回执,解析ReviewSubmitted事件里的contentHash和score,与数据库比对,是最直接也是最可靠的单条验证方式。

public VerifyResult verifyByTxHash(String txHash) { Optional<TransactionReceipt> receipt = web3j.ethGetTransactionReceipt(txHash).send(); if (!receipt.isPresent()) { return VerifyResult.pending(txHash); } // 从回执中过滤出ReviewSubmitted事件 ReviewContract.ReviewSubmittedEventResponse event = extractEventFromReceipt(receipt.get()); // 与MySQL中的记录比对 ReviewRecordDO record = reviewRecordMapper.selectByTxHash(txHash); boolean match = record != null && event.score.intValue() == record.getScore() && event.contentHash.equals(record.getContentHash()); return VerifyResult.of(match, event, record); }

每个评审人提交后,我把这笔交易的哈希展示在系统界面,也让评审人自己留底。事后有分歧,拿交易哈希到这个接口一查,数据有没有被后台改动一目了然。

6.2 用事件Flowable做批量对账

单条验证之外,可以做批量对账。定时任务拉取合约某个区块范围内的全部ReviewSubmitted事件,与数据库内容哈希做全量比对,发现不一致就告警。这个方法适合作为系统的自检功能,每周跑一次,让管理员在不主动查的情况下也能发现潜在的数据不一致,把被篡改的可能性降到最低。

批量拉取时要注意区块范围的分批查询,不要一次性把几万个区块的事件都拉下来。按天或按1万区块分段扫描,配合Web3j的flowable做背压控制,比直接在一个循环里拉全部日志要稳得多。

6.3 用合约getter抽查关键记录

如果担心事件日志解析环节出错,还有一个更底层的抽查方式:直接用合约的getRecord方法读取链上状态变量。

ReviewContract.ReviewRecord record = contract.getRecord(index).send();

这会把链上保存的score、contentHash直接取出来,不经过事件日志解析,是链上最原始的存证状态。抽查时取随机几条历史记录做哈希比对,成本低,但它验证的是“合约里实际存的内容”,与事件日志校验正好互补。事件校验负责“流水没错”,getter校验负责“存证没歪”。

我第一次做这个系统时,把评审报告全文直接塞进合约状态变量,想着“上链了就绝对安全”,结果一条报告几KB,Gas费用让业务方直接叫停。后来才想明白,区块链存证的核心不是把数据搬进区块链,而是给数据一个不可抵赖的哈希锚点。它不负责存储,只负责证明。按这个思路重做之后,系统才真正从演示变成了能跑进生产环境的工具。这套基于SpringBoot与以太坊的评审系统,从架构选型到部署避坑,希望帮到你。

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

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

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

立即咨询