简介:这份资源是面向高校计算机相关专业毕业设计、期末大作业与课程设计场景的完整项目源码,主题为基于Springboot与fabric信用区块链的慈善救助系统,适合希望将区块链存证与信用机制融入公益救助流程的同学参考。项目难度适中,源码经本地编译可运行,评审得分达到98分,内容经助教老师审定,能够满足毕业设计答辩与课程实践需求。压缩包共172个文件,约591KB,以50个java业务代码、55个pem证书、20个crt证书、10个key密钥、8个yaml配置、6个xml配置为主,另含tx交易配置、properties、jar依赖与go脚本等,覆盖区块链网络搭建、链码交互与后端业务实现。目前已有87人学习下载。读者可据此掌握Fabric证书体系、通道与链码调用方式,以及Springboot整合区块链的接口设计与目录组织,快速搭建可演示的慈善救助系统并完成论文与答辩准备。
1. 慈善救助系统为什么需要 Fabric 来做信用存证
做过几套公益类管理系统的同行大概都有个共识:钱和物的流转记录,用普通关系库存,技术上没问题,但一旦有人质疑“这笔救助金到底有没有到人手里”,你拿出来的数据库截图是没有说服力的。毕业设计里选“基于 SpringBoot + Fabric 信用区块链的慈善救助系统”这个题,核心要解决的就是这个信任问题——把救助申请、审核、拨款、签收这几个关键节点写进区块链,让每一笔救助都有不可篡改的存证。
这个方向适合两类人:一类是正在做毕设、想拿高分的学生,需要一套能跑通、能演示、有技术深度的方案;另一类是想把区块链真正落到业务系统里的初中级开发,想搞清楚 Fabric 到底怎么和 SpringBoot 配合,而不是停留在“区块链就是比特币”的层面。下面我按自己搭这套系统的实际路径,把选型、代码、参数和踩过的坑讲清楚。
2. 系统架构与 Fabric 网络的最小可用搭建
2.1 为什么选 Fabric 而不是自己写一条链
很多毕设一上来就想“从0开始搭建一个区块链平台”,自己写 PoW、写 P2P 网络,结果时间全耗在共识算法调试上,业务逻辑反而没写几行。Fabric 是联盟链框架,天然适合慈善救助这种有明确参与方(基金会、医院、学校、监管方)的场景。它把共识、账本、成员管理都封装好了,你只需要写链码(智能合约)和业务层。
具体到这套系统,我一般会划分四个组织:CharityOrg(慈善机构)、DonorOrg(捐赠方)、SuperviseOrg(监管方)、BeneficiaryOrg(受助方代表)。每个组织跑自己的 Peer 节点,Orderer 用 Solo 或 Raft 都行,毕设演示用 Solo 足够。通道(Channel)建一个 charitychannel,所有救助相关的交易都走这个通道。
选 Fabric 的另一个理由是它支持私有数据集合(Private Data Collection)。受助人的身份证号、家庭住址这些敏感信息不能上链明文存,用私有数据集合可以只把哈希上链,原文存在各组织的本地账本里,兼顾了存证和隐私。
2.2 用 Docker 拉起 Fabric 测试网络
Fabric 官方提供了 fabric-samples 里的 test-network,这是最快的起步方式。假设你已经装好了 Docker 和 Docker Compose,Go 环境也配好了,按下面步骤走。
# 进入 fabric-samples 的 test-network 目录 cd fabric-samples/test-network # 关掉可能残留的网络容器,避免端口冲突 ./network.sh down # 启动网络,创建名为 charitychannel 的通道,使用默认的 cryptogen 生成的证书 ./network.sh up createChannel -c charitychannel -s couchdb这里-s couchdb表示状态数据库用 CouchDB 而不是 LevelDB。慈善救助系统里经常要按“受助人姓名+救助类型+时间范围”做富查询,LevelDB 只支持键查询,CouchDB 支持 JSON 查询,后面链码里写复杂查询会方便很多。
启动成功后你会看到几个关键容器:orderer.example.com、peer0.charity.example.com、peer0.donor.example.com 等。用docker ps确认它们都是 Up 状态。
# 确认通道创建成功 ./network.sh deployCC -ccn charitycc -ccp ../asset-transfer-basic/chaincode-go -ccl go -c charitychannel这条命令把官方示例链码部署上去,先验证网络通不通。-ccn是链码名字,-ccp是链码源码路径,-ccl是语言。部署成功后,用peer chaincode invoke发一笔测试交易,能返回 200 就说明网络层没问题。
2.3 SpringBoot 侧连接 Fabric 的网关配置
SpringBoot 连 Fabric 不要用老式的 Fabric SDK,用 Fabric Gateway 客户端更简洁。在pom.xml里加依赖:
<dependency> <groupId>org.hyperledger.fabric</groupId> <artifactId>fabric-gateway</artifactId> <version>1.5.0</version> </dependency>然后在application.yml里配置连接信息。注意这里的路径要指向 test-network 生成的证书目录,每个人的路径不一样,别直接抄。
fabric: msp-id: Org1MSP channel-name: charitychannel chaincode-name: charitycc peer-endpoint: localhost:7051 override-auth: peer0.charity.example.com cert-path: /path/to/fabric-samples/test-network/organizations/peerOrganizations/charity.example.com/users/User1@charity.example.com/msp/signcerts/cert.pem key-path: /path/to/fabric-samples/test-network/organizations/peerOrganizations/charity.example.com/users/User1@charity.example.com/msp/keystore/ tls-cert-path: /path/to/fabric-samples/test-network/organizations/peerOrganizations/charity.example.com/peers/peer0.charity.example.com/tls/ca.crt写一个配置类把 Gateway 和 Network 对象初始化成单例,避免每次请求都重新建连接。Gateway 连接是长连接,频繁创建会拖慢响应,也容易把 Peer 的连接数打满。
@Configuration public class FabricConfig { @Value("${fabric.msp-id}") private String mspId; // 省略其他注入字段 @Bean(destroyMethod = "close") public Gateway gateway() throws Exception { // 读取证书和私钥,构建身份 Identities identities = ...; // 建立 gRPC 连接,注意 TLS 配置 ManagedChannel channel = Grpc.newChannelBuilder(peerEndpoint, tlsCredentials).build(); return Gateway.newInstance() .identity(identity) .connection(channel) .evaluateOptions(options -> options.withDeadlineAfter(5, TimeUnit.SECONDS)) .endorseOptions(options -> options.withDeadlineAfter(15, TimeUnit.SECONDS)) .submitOptions(options -> options.withDeadlineAfter(5, TimeUnit.SECONDS)) .commitStatusOptions(options -> options.withDeadlineAfter(1, TimeUnit.MINUTES)) .connect(); } @Bean public Network network(Gateway gateway) { return gateway.getNetwork(channelName); } }evaluateOptions是查询交易的超时,endorseOptions是背书超时,submitOptions是提交超时,commitStatusOptions是等待提交状态超时。这几个参数在本地测试网络里默认值够用,但如果你把 Peer 部署到性能一般的云主机上,背书超时经常需要从 15 秒调到 30 秒以上,否则会报ENDORSEMENT_TIMEOUT。
3. 链码设计:救助信用数据怎么上链
3.1 救助申请与信用积分的链码结构
链码是跑在 Peer 节点上的智能合约,用 Go 或 Java 写都行。毕设里我倾向用 Go,因为 Fabric 本身是 Go 写的,示例多,编译出来的链码包也小。下面是一个救助申请上链的核心结构体。
// CharityRecord 定义一笔救助记录的链上结构 type CharityRecord struct { RecordID string `json:"recordId"` // 救助记录唯一ID ApplicantID string `json:"applicantId"` // 受助人哈希ID,不存明文 Amount float64 `json:"amount"` // 救助金额 Status string `json:"status"` // APPLIED/APPROVED/PAID/CONFIRMED CreditScore int `json:"creditScore"` // 本次救助产生的信用积分 Timestamp string `json:"timestamp"` // 上链时间戳 TxID string `json:"txId"` // 交易ID,用于溯源 }ApplicantID存的是受助人身份证号的 SHA256 哈希,不存明文。这样链上能通过哈希关联同一个人的多次救助,但看不到具体是谁。CreditScore是这套系统的信用核心:每次按时签收、材料真实,加 10 分;逾期未签收或材料造假被驳回,扣 20 分。积分累积到一定程度,下次申请可以走快速通道。
链码里写一个CreateRecord方法:
func (s *SmartContract) CreateRecord(ctx contractapi.TransactionContextInterface, recordJSON string) error { var record CharityRecord err := json.Unmarshal([]byte(recordJSON), &record) if err != nil { return fmt.Errorf("解析救助记录失败: %v", err) } // 检查记录ID是否已存在,防止重复上链 exists, err := s.RecordExists(ctx, record.RecordID) if err != nil { return err } if exists { return fmt.Errorf("记录 %s 已存在", record.RecordID) } // 写入账本 recordBytes, _ := json.Marshal(record) return ctx.GetStub().PutState(record.RecordID, recordBytes) }RecordExists用GetState查一下键是否存在。这里有个坑:Fabric 的GetState在键不存在时返回nil, nil,不是报错,所以判断要写if recordBytes == nil,别写成if err != nil。
3.2 信用积分的累加与查询逻辑
信用积分不能只存在单条记录里,需要一个单独的键来存每个人的累计积分。链码里用复合键:credit~{applicantHash}。
func (s *SmartContract) UpdateCredit(ctx contractapi.TransactionContextInterface, applicantHash string, delta int) error { creditKey, err := ctx.GetStub().CreateCompositeKey("credit", []string{applicantHash}) if err != nil { return err } // 读取当前积分 currentBytes, err := ctx.GetStub().GetState(creditKey) current := 0 if currentBytes != nil { current, _ = strconv.Atoi(string(currentBytes)) } newScore := current + delta // 积分不能为负 if newScore < 0 { newScore = 0 } return ctx.GetStub().PutState(creditKey, []byte(strconv.Itoa(newScore))) }CreateCompositeKey是 Fabric 提供的复合键工具,底层用\u0000分隔各部分,查询时用GetStateByPartialCompositeKey可以按前缀扫。比如要查所有信用积分记录,传"credit"作为前缀就能遍历。
查询某个受助人的积分:
func (s *SmartContract) QueryCredit(ctx contractapi.TransactionContextInterface, applicantHash string) (int, error) { creditKey, err := ctx.GetStub().CreateCompositeKey("credit", []string{applicantHash}) if err != nil { return 0, err } bytes, err := ctx.GetStub().GetState(creditKey) if bytes == nil { return 0, nil // 没有记录说明积分为0 } return strconv.Atoi(string(bytes)) }3.3 用 CouchDB 富查询做救助记录溯源
前面启动网络时用了 CouchDB,现在就能用富查询。假设要查某个受助人所有状态为 PAID 的记录,在链码里写:
func (s *SmartContract) QueryRecordsByApplicant(ctx contractapi.TransactionContextInterface, applicantHash string) ([]CharityRecord, error) { // CouchDB 富查询语句,注意字段名要和结构体 json tag 一致 queryString := fmt.Sprintf(`{ "selector": { "applicantId": "%s", "status": "PAID" }, "sort": [{"timestamp": "desc"}] }`, applicantHash) resultsIterator, err := ctx.GetStub().GetQueryResult(queryString) if err != nil { return nil, err } defer resultsIterator.Close() var records []CharityRecord for resultsIterator.HasNext() { queryResponse, _ := resultsIterator.Next() var record CharityRecord json.Unmarshal(queryResponse.Value, &record) records = append(records, record) } return records, nil }CouchDB 富查询的selector里字段名必须和链码结构体的jsontag 完全一致,大小写敏感。我见过有人结构体 tag 写applicant_id,查询里写applicantId,结果永远查不到数据,排查半天以为是链码没部署成功。
另外,CouchDB 查询默认不分页,数据量大了会拖垮 Peer。毕设数据量小无所谓,但如果要往生产靠,记得在查询里加limit和skip,或者用 Fabric 的分页查询接口GetQueryResultWithPagination。
4. SpringBoot 业务层与区块链的对接
4.1 救助申请提交:从 Controller 到链码调用
SpringBoot 这边,Controller 接收前端表单,Service 层组装成链码需要的 JSON,再通过 Gateway 提交交易。先看 Controller:
@RestController @RequestMapping("/api/charity") public class CharityController { @Autowired private CharityService charityService; @PostMapping("/apply") public Result apply(@RequestBody CharityApplyDTO dto) { // 参数校验:金额必须大于0,受助人ID不能为空 if (dto.getAmount() <= 0 || dto.getApplicantId() == null) { return Result.fail("参数不合法"); } String txId = charityService.submitApply(dto); return Result.ok(txId); } }Service 层的关键是把 DTO 转成链码能解析的 JSON,并调用Contract.submitTransaction:
@Service public class CharityService { @Autowired private Network network; public String submitApply(CharityApplyDTO dto) { Contract contract = network.getContract(chaincodeName); // 构造链码入参,字段名和 Go 结构体 json tag 对应 Map<String, Object> record = new HashMap<>(); record.put("recordId", UUID.randomUUID().toString()); record.put("applicantId", sha256(dto.getApplicantId())); record.put("amount", dto.getAmount()); record.put("status", "APPLIED"); record.put("creditScore", 0); record.put("timestamp", Instant.now().toString()); record.put("txId", ""); try { byte[] result = contract.submitTransaction("CreateRecord", JSON.toJSONString(record)); return new String(result); } catch (Exception e) { throw new RuntimeException("上链失败: " + e.getMessage()); } } }submitTransaction是同步提交,会等交易被 Orderer 打包、Peer 提交后才返回。毕设演示够用,但如果并发高,应该用submitAsync拿 Future,避免阻塞 Tomcat 线程。
4.2 交易状态回写与 MySQL 的配合
区块链只存关键存证,业务查询还是走 MySQL。我的做法是:提交上链成功后,把txId和业务数据一起写回 MySQL 的charity_record表。这样前端列表页查 MySQL,详情页拿txId去链上验真。
@Transactional public String submitApply(CharityApplyDTO dto) { // ... 上链逻辑 String txId = new String(result); // 回写 MySQL CharityRecordEntity entity = new CharityRecordEntity(); entity.setRecordId(record.get("recordId").toString()); entity.setTxId(txId); entity.setStatus("APPLIED"); charityRecordMapper.insert(entity); return txId; }这里有个事务边界问题:上链和 MySQL 写入不在同一个事务里。如果上链成功但 MySQL 写入失败,链上会有记录但业务库没有。解决办法是加一张pending_tx表,先写 pending,上链成功后再更新状态,用定时任务补偿。毕设里如果不想搞这么复杂,至少要在 catch 里打日志,别让异常静默吞掉。
4.3 链上数据验真接口的实现
验真接口接收txId,去链上查交易详情,返回给前端展示。Fabric Gateway 提供了getTransaction方法:
@GetMapping("/verify/{txId}") public Result verify(@PathVariable String txId) { try { Contract contract = network.getContract(chaincodeName); // 用 evaluateTransaction 做查询,不产生新交易 byte[] recordBytes = contract.evaluateTransaction("QueryRecord", txId); return Result.ok(new String(recordBytes)); } catch (Exception e) { return Result.fail("链上未找到该交易"); } }evaluateTransaction和submitTransaction的区别要搞清楚:前者是查询,走 Peer 的账本副本,不经过 Orderer,不产生新区块;后者是提交,要经过共识排序。验真用evaluateTransaction就够了,速度快,也不消耗交易费(虽然 Fabric 没有 gas 概念,但会产生区块)。
前端拿到链上记录后,和 MySQL 里的业务数据做比对,金额、状态、时间戳一致就显示“已上链存证”。这个比对逻辑建议放在后端做,前端只展示结果,避免把链上原始数据暴露给不可信客户端。
5. 避坑与常见问题排查
5.1 链码部署后调用报“chaincode not found”
现象:./network.sh deployCC显示成功,但 SpringBoot 调submitTransaction时报chaincode charitycc not found on channel charitychannel。
原因:链码部署时指定的通道名和 SpringBoot 配置里的channel-name不一致,或者链码包虽然安装了但没在通道上 approve 和 commit。Fabric 2.x 的链码生命周期是“安装 → 批准 → 提交”三步,少一步都不行。
解决:用peer lifecycle chaincode querycommitted -C charitychannel确认链码已提交到通道。如果没提交,重新走一遍deployCC,注意-c参数要和配置文件一致。
5.2 交易提交超时 ENDORSEMENT_TIMEOUT
现象:本地测试正常,部署到云服务器后,提交交易经常等 15 秒然后报超时。
原因:云服务器的磁盘 IO 或网络延迟比本地高,Peer 背书和 Orderer 排序的时间变长。默认的 15 秒背书超时不够用。
解决:在 FabricConfig 里把endorseOptions的超时调到 30 秒甚至 60 秒。同时检查 Peer 容器的 CPU 和内存限制,docker stats看一下是不是资源被限死了。
5.3 CouchDB 查询返回空但数据明明存在
现象:链码里用富查询查status: "PAID"的记录,返回空数组,但用GetState按 ID 查能查到。
原因:CouchDB 的索引没建,或者查询字段名和结构体 json tag 不一致。CouchDB 富查询需要对应的索引才能高效执行,没有索引时可能返回不完整结果。
解决:在链码目录下建META-INF/statedb/couchdb/indexes/文件夹,放索引定义 JSON:
{ "index": { "fields": ["applicantId", "status", "timestamp"] }, "ddoc": "indexCharityDoc", "name": "indexCharity", "type": "json" }重新部署链码后,CouchDB 会自动建索引。另外用docker exec进 CouchDB 容器,直接查_find接口验证数据是否存在。
5.4 SpringBoot 启动时报证书路径找不到
现象:FileNotFoundException,提示cert.pem或keystore目录不存在。
原因:test-network 每次./network.sh down再up会重新生成证书,路径里的组织名或用户名可能变化。另外,Windows 和 Linux 的路径分隔符不同,配置里写死了/在 Windows 上可能出问题。
解决:用File.separator拼接路径,或者把证书路径配成相对于项目根目录的相对路径。每次重启网络后,用ls确认证书文件确实存在。如果用的是 Fabric 2.5 以上版本,证书目录结构可能有调整,以实际生成的为准。
5.5 链码里 JSON 解析中文乱码
现象:救助记录里的受助人姓名或备注是中文,上链后再查出来变成乱码。
原因:Go 的json.Unmarshal默认按 UTF-8 处理,但如果 SpringBoot 这边用new String(bytes)没指定字符集,或者 HTTP 传输时 Content-Type 没带charset=UTF-8,就会乱码。
解决:SpringBoot 里统一用new String(bytes, StandardCharsets.UTF_8),Controller 的@PostMapping加produces = "application/json;charset=UTF-8"。链码这边不用特殊处理,Go 原生支持 UTF-8。
6. 让信用积分真正跑起来的两个进阶技巧
第一个技巧是把信用积分和救助额度挂钩,做成动态授信。链码里加一个CalculateQuota方法,根据当前积分返回本次可申请的最高金额:
func (s *SmartContract) CalculateQuota(ctx contractapi.TransactionContextInterface, applicantHash string) (float64, error) { score, err := s.QueryCredit(ctx, applicantHash) if err != nil { return 0, err } // 基础额度1000,每分信用加50,上限5000 quota := 1000.0 + float64(score)*50 if quota > 5000 { quota = 5000 } return quota, nil }这样信用积分就不是一个摆设数字,而是直接影响受助人能申请多少钱。演示的时候,先提交一笔按时签收的记录,积分涨上去,再申请时额度变大,评委一眼就能看懂信用体系的价值。
第二个技巧是用 Fabric 的事件机制做异步通知。链码里在CreateRecord成功后SetEvent("RecordCreated", recordBytes),SpringBoot 侧用contract.addContractListener监听事件,一旦有新的救助记录上链,自动发短信或站内信通知监管方。这个在毕设答辩时是个加分项,因为它体现了“链上链下联动”的完整闭环。
contract.addContractListener(event -> { if ("RecordCreated".equals(event.getName())) { String payload = new String(event.getPayload(), StandardCharsets.UTF_8); // 解析 payload,触发通知逻辑 notificationService.notifySupervisor(payload); } });监听器要注册在 Gateway 连接建立之后,且注意异常处理——监听器里抛异常不会影响链码执行,但会静默丢失事件。我一般会在监听器里包一层 try-catch,把失败的事件写进本地重试队列。
最后说个我自己的习惯:每次改完链码,先别急着重启 SpringBoot,用peer chaincode invoke在命令行手动调一遍,确认链码逻辑没问题,再去调 Java 层。这样能把“链码 bug”和“Java 对接 bug”分开排查,省掉很多来回翻日志的时间。这套系统我前后搭了三遍,第一遍卡在证书配置,第二遍卡在 CouchDB 索引,第三遍才把信用积分和额度联动跑通。希望你少走点弯路,顺利把毕设拿下。
本文还有配套的精品资源,点击获取