简介:这套基于Fabric的农产品溯源平台项目,覆盖区块链网络、小程序端、PC管理端与基础数据后台四大模块,适合区块链开发者、毕业设计选题或农产品溯源场景的技术验证。项目前端采用Vue.js、Element UI与mpvue,后端集成SpringBoot、Mybatis、FastDFS、Node.js等,区块链部分基于Fabric1.2并使用Golang编写智能合约,可在Ubuntu16.04与Docker环境中部署。压缩包共1371个文件,约18.15MB,其中java/js/vue构成业务代码,wxml/wxss为小程序界面,pem/crt/key及大量_sk文件为区块链证书与密钥,并含go合约文件、sql、yml配置与启动脚本。目前已有259人学习,适合需要完整工程源码、参考模块拆分或了解联盟链上链流程的读者;区块链部分采用solo共识与单orderer节点,结构简单易读,便于在此基础上扩展。资源内含四个子项目的完整代码与部署脚本,从节点配置到溯源查询均有实现,可帮助上手区块链溯源系统,同时其模块化结构和上链设计思路也对二次开发有参考价值。
1. 农产品溯源为什么盯上 Fabric:先想清楚链上到底存什么
「基于区块链(Fabric)的农产品溯源平台」翻译成大白话就是:把一颗白菜从播种、施肥、采收、运输到上架的关键动作,交给一条多方共同记账的联盟链来记录,而不是某一个企业自己的数据库。它解决的不是「能不能查到」,而是「查到的东西你信不信」。
适合谁?有真实的多方协作——生产基地、加工厂、物流商、监管方都要对同一批货负责的企业或园区,以及想把自己的追溯系统从「后台展示页」升级成「多方账本」的服务商。
我按落地这类项目的习惯,把选型、链码、部署和踩坑顺序讲透。先说结论:区块链在溯源项目里不是数据库的替代品,它是给数据库加了一把多方签字的锁。
2. 溯源平台的架构选型:Fabric 联盟链、链下库与哈希上链的边界
2.1 农产品溯源为什么是联盟链,而不是公链或中心库
农产品溯源的第一步不是写代码,而是先确定「这条链给谁用、谁能写、谁能查」。农产品从田间到餐桌要经过种植户、合作社、加工厂、仓储物流、批发商、零售商,每一方都有自己的系统,数据口径不一样,利益也不一致。中心化溯源平台之所以被诟病,是因为数据库在谁手里,谁就能改——今天检测报告不合格,明天后台就能换一张图。区块链的价值不是存储,而是「多方见证」。
| 维度 | 以太坊等公链 | Fabric 联盟链 | 中心化数据库 |
|---|---|---|---|
| 身份准入 | 匿名地址 | MSP 证书实名准入 | 后台账号 |
| 数据可见性 | 全网公开 | 通道隔离、私有数据可控 | 单方管控 |
| 共识开销 | PoW/PoS,成本高 | Raft,节点少、效率高 | 无共识 |
| 谁说了算 | 算力/质押 | 背书策略 | 数据库管理员 |
| 与溯源场景匹配度 | 弱 | 强 | 弱 |
实际项目中,农产品企业普遍要求「参与方实名、数据只对授权方可见、写入要经过多方同意」。Fabric 的通道(Channel)可以把某个品类的溯源数据隔离在单独的通道里,MSP 解决「你是谁、有没有权限」,背书策略解决「一条记录要几个组织签字才能生效」。这三点正好对应溯源场景的三个核心诉求,所以现在做农产品溯源,Fabric 是出现频率最高的联盟链选型,其次才是长安链、FISCO BCOS 这类国产框架。如果你的客户后续要求国产化,架构上哈希锚定这套思路是通用的,换链的成本主要在链码重写,不在业务设计。
2.2 先定数据模型:哪些字段进链,哪些留在链下
常见做法是「链上存哈希,链下存全文」,也叫哈希锚定。链上放一条结构精简的溯源事件,只包含可用于验证的最小字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| batchId | string | 生产批次号,一物一码的聚合维度 |
| eventType | string | planting / harvest / processing / transport / sale |
| operator | string | 操作方 MSP 组织或操作员 ID |
| eventTime | int64 | 交易时间,来自排序节点,不能信任客户端时钟 |
| dataHash | string | 链下完整农事记录/检测报告的 SHA-256 |
| prevHash | string | 该批次上一条事件的哈希,串成防篡改链 |
| txId | string | Fabric 自动生成的交易 ID,回写链下库供前端展示 |
设计要点:不要把完整的农事记录、施肥用量、检测报告正文直接塞进 PutState。链上存 32 字节哈希加几个关键索引字段就够了。一方面区块体积直接决定了排序节点的广播压力和 peer 的存储开销,图片、PDF 上链会让交易延迟从毫秒级恶化到秒级;另一方面,产量、价格、客户信息这些商业敏感数据,生产方并不想让通道里所有组织都看到。哈希上链后原始数据留在自己的业务库里,需要验证时再拿出来比对。
链下的业务表建议单独设计一张 trace_record,主键 record_id 对应链上事件的业务标识,字段里存完整 JSON:施肥品种、用量、作业照片 URL、检测报告 URL、操作人、位置等。这张表就是普通的关系库,完全可以用 MySQL,不用给它加任何「区块链数据库」的包装。
2.3 链上链下协同:哈希锚定的完整数据流
我在项目里一般把上链流程拆成五步,顺序不能乱:
- 业务系统先写链下库,生成完整业务记录;
- 对记录的核心内容(约定好的字段 JSON)算 SHA-256,得到 dataHash;
- 调用链码 RecordEvent,把 batchId、eventType、operator、dataHash 写入链上;
- Fabric 返回 txId,业务系统把 txId 回写到链下记录;
- 消费者扫码验证时,先从链下库取原始记录,重算 SHA-256,再查链上 dataHash 比对。
这套流程里有一个容易被忽略的细节:到底对哪些字段算哈希,必须在项目启动时就定下来,并写进接口文档。常见做法是对「除去展示性字段(如备注、图片URL)之外的核心业务字段」做 JSON 序列化后计算。如果前后两次序列化的字段顺序不一致,同名记录会算出不同的哈希,验证就会误报。我习惯在链下库里直接存一份「已序列化用于计算哈希的字符串」,验证时拿它比对,避免前端传参顺序导致的各种玄学问题。
提示:哈希锚定不是把原文加密存起来,而是给原文一个指纹。任何人拿到原文都能自己算指纹去和链上比对,这正是溯源场景需要的「可验证性」。
3. 用 Fabric 搭建最小溯源链:从网络启动到链码部署与 API 封装
3.1 准备 Fabric 网络:官方示例跑通的最短路径
最常见的起步方式是拉取官方 fabric-samples 仓库,用它自带的 test-network 脚本把网络跑起来。下面这条命令会同时拉取 fabric 二进制和 docker 镜像:
# 用官方安装脚本拉取 fabric-samples、fabric 二进制和 docker 镜像 curl -sSL https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh | bash -s cd fabric-samples/test-network # 启动网络并创建名为 trace 的应用通道,同时启动 CA ./network.sh up createChannel -c trace -ca参数说明:up表示启动网络,createChannel是创建应用通道的关键动作,-c trace把通道命名为 trace,-ca额外启动 Fabric CA 服务用于给各组织签发证书。如果只执行./network.sh up不带createChannel,网络会起来但通道不存在,后面链码无处部署。
启动后可以用docker ps确认 peer、orderer、CA 容器都起来了。看到 Org1、Org2 各两个 peer 和三个 orderer 容器,说明网络正常。很多新手在这一步卡在镜像拉取,原因是 docker 镜像源的问题,和 Fabric 本身没关系,把 docker registry 换成国内可达的镜像源再重试即可。
3.2 编写溯源链码:记录、查询、验证三个核心方法
链码是整个溯源平台最核心的部分。我用 Go 写一个最小可运行版本,把上面提到的数据模型直接落地。这段代码放到 fabric-samples 的任意 chaincode 目录下,再用 network.sh 部署即可:
package main import ( "crypto/sha256" "encoding/hex" "encoding/json" "strconv" "github.com/hyperledger/fabric-contract-api-go/contractapi" ) // TraceEvent 定义一次溯源事件 type TraceEvent struct { BatchID string `json:"batchId"` EventType string `json:"eventType"` Operator string `json:"operator"` Time int64 `json:"time"` DataHash string `json:"dataHash"` PrevHash string `json:"prevHash"` } // TraceContract 溯源链码 type TraceContract struct { contractapi.Contract } // RecordEvent 记录一次溯源事件,入参 dataHash 是链下完整记录算出的 SHA-256 func (c *TraceContract) RecordEvent(ctx contractapi.TransactionContextInterface, batchID, eventType, operator, dataHash string) error { prevHash, err := c.getLastHash(ctx, batchID) if err != nil { return err } event := TraceEvent{ BatchID: batchID, EventType: eventType, Operator: operator, Time: ctx.GetStub().GetTxTimestamp().Seconds, DataHash: dataHash, PrevHash: prevHash, } // 复合键里追加交易ID,避免同一批次同一类型事件在同一秒写入时互相覆盖 key, err := ctx.GetStub().CreateCompositeKey("trace", []string{batchID, eventType, strconv.FormatInt(event.Time, 10), ctx.GetStub().GetTxID()}) if err != nil { return err } eventJSON, _ := json.Marshal(event) return ctx.GetStub().PutState(key, eventJSON) } // getLastHash 取该批次最后一条事件的哈希,作为当前事件的 prevHash func (c *TraceContract) getLastHash(ctx contractapi.TransactionContextInterface, batchID string) (string, error) { resultsIterator, err := ctx.GetStub().GetStateByPartialCompositeKey("trace", []string{batchID}) if err != nil { return "", err } defer resultsIterator.Close() var lastHash string for resultsIterator.HasNext() { kv, err := resultsIterator.Next() if err != nil { return "", err } var event TraceEvent if err := json.Unmarshal(kv.Value, &event); err != nil { return "", err } lastHash = event.DataHash } return lastHash, nil } // QueryBatch 按批次号查询全部溯源事件,供前端渲染时间轴 func (c *TraceContract) QueryBatch(ctx contractapi.TransactionContextInterface, batchID string) ([]TraceEvent, error) { resultsIterator, err := ctx.GetStub().GetStateByPartialCompositeKey("trace", []string{batchID}) if err != nil { return nil, err } defer resultsIterator.Close() var events []TraceEvent for resultsIterator.HasNext() { kv, err := resultsIterator.Next() if err != nil { return nil, err } var event TraceEvent if err := json.Unmarshal(kv.Value, &event); err != nil { return nil, err } events = append(events, event) } return events, nil } // VerifyRecord 验证某段链下原始记录是否与链上哈希一致 func (c *TraceContract) VerifyRecord(ctx contractapi.TransactionContextInterface, batchID, rawJSON string) (bool, error) { hashBytes := sha256.Sum256([]byte(rawJSON)) dataHash := hex.EncodeToString(hashBytes[:]) events, err := c.QueryBatch(ctx, batchID) if err != nil { return false, err } for _, e := range events { if e.DataHash == dataHash { return true, nil } } return false, nil } func main() { chaincode, _ := contractapi.NewChaincode(&TraceContract{}) if err := chaincode.Start(); err != nil { panic(err) } }这段代码的逻辑说明:RecordEvent 先把同一批次已有事件的最后一条哈希取出来作为 prevHash,再把当前事件写进世界状态。这样一来,同一批次的所有事件通过 prevHash 串成了一条哈希链,任何中间一条被改动,后面所有记录的 prevHash 就对不上。QueryBatch 按复合键前缀扫出该批次全部事件,顺序就是事件写入顺序。VerifyRecord 是消费者扫码验证的后端逻辑:拿链下原始记录重算 SHA-256,再去链上比对。
参数说明:GetTxTimestamp().Seconds取的是排序节点背书的交易时间,不是客户端传入的时间,这正是 4.4 节那个坑的解法;CreateCompositeKey生成的键会被 Fabric 按字典序存储,所以同一批次的事件天然聚合在一起。生产环境状态数据库建议用 CouchDB,它能按 JSON 字段做富查询(比如「查某月所有施肥事件」),leveldb 只能按键查,功能上受限。部署命令:
# 部署链码到 trace 通道,链码名 tracecc ./network.sh deployCC -ccn tracecc -ccp ../trace-chaincode -ccl go -c trace参数说明:-ccn指定链码名 tracecc,-ccp指向链码目录,-ccl声明语言是 Go,-c指定部署到 trace 通道。测试环境默认背书策略是通道内任意一个组织即可背书,生产环境必须改,见 4.3 节。
3.3 用 fabric-gateway 封装溯源 REST API
Go 链码可以直接在 peer 上用 CLI 调用,但业务系统不可能让前端直接操作 gRPC,通常用 Node.js 的 fabric-gateway SDK 包一层 REST API。常见封装如下:
// 用 fabric-gateway 连接 peer 并提交交易 const { connect } = require('@hyperledger/fabric-gateway'); const grpc = require('@grpc/grpc-js'); async function submitRecord(batchId, eventType, operator, dataHash) { // identity 和 signer 来自 CA 签发的证书与私钥 const client = await connect({ identity: mspIdentity, signer: signer, // 提交超时按业务容忍度调整,溯源建议给到 30 秒 commitTimeout: 30000, }); const network = client.getNetwork('trace'); // 通道名 const contract = network.getContract('tracecc'); // 链码名 const txid = await contract.submitTransaction( 'RecordEvent', batchId, eventType, operator, dataHash ); return txid.toString(); }这里 mspIdentity 来自 CA 签发的证书,signer 对应的私钥从组织 admin 的 keystore 读取,这两个文件在 CA 签发后的 crypto-config 目录里。connect 里的 commitTimeout 建议设 30 秒,因为 Fabric 提交要等背书节点和排序节点都确认,网络抖动时 10 秒经常不够用。
常见做法是把这段封装成独立的 trace-api 服务,内部维护 grpc 连接池,外部暴露 /record、/query、/verify 三个 HTTP 端点。注意每个组织只连自己那台 peer 的 gRPC 端口,不要所有请求都打到 Org1 的 peer 上。
4. Fabric 溯源平台部署避坑:链码升级、背书策略与时间戳的四个常见问题
4.1 链码升级后旧数据「消失」,世界状态不兼容
现象:把链码升级到 v2 后,QueryBatch 返回空结果,peer 日志没有报错,前端溯源页面一片空白。
原因:世界状态数据库里存的是键值对,键的前缀由 CreateCompositeKey 的第一个参数决定。升级时如果顺手把前缀从trace改成了trace_v2,或者改了拼接字段的顺序,旧世界状态的键就全部对不上新链码的查询逻辑。数据还在库里,但新代码读不到了。
解决:升级前先用 peer 命令把现有键导出一份备查:
peer chaincode query -C trace -n tracecc -c '{"Args":["QueryBatch","B20240315001"]}'新链码务必保持复合键前缀和字段顺序不变。如果非改不可,写一个迁移链码,在 Init 里遍历旧前缀的键,重新映射后写入新键,再交给运维做灰度切换。最稳妥的做法是:溯源链码上线前就把键结构设计成「加字段不换前缀」,所有扩展通过新增对象类型实现,而不是改老键。
4.2 图片和 PDF 直接上链,区块体积失控
现象:上线两个月后 peer 磁盘暴涨,交易确认从几百毫秒变成十几秒,orderer 日志频繁出现超时,前端扫码转圈。
原因:前端把检测报告图片转成 Base64 后直接传进链码,链码原样 PutState。区块体积被撑到几 MB 甚至几十 MB,排序节点要把区块广播给所有 peer,带宽和磁盘双双被打满。这是新手最容易犯的错,以为链上存得越多越可信。
解决:链上只放 payload_hash 和摘要字段,原图放对象存储、报告正文放 MySQL。如果业务上确实有「报告原件必须链上存证」的要求,用 Fabric 的私有数据集合(Private Data Collection),把大文件放进私有数据,链上只存哈希,其他组织没有权限就读不到原文,但哈希验证照常进行。这是官方推荐的方案,别再走明文上链的老路。
4.3 背书策略没设对,溯源链条变成「一言堂」
现象:生产方在自己后台改了检测报告,审核方和消费者都没有感知,和没上链一样。
原因:测试网络部署链码时默认背书策略是 OR 任意单一组织,也就是说 Org1 自己就能提交一笔交易,不需要加工方、监管方签字。Fabric 的「不可篡改」依赖的是「多方见证」,单组织能独立写数据,区块链就退化成普通数据库。
解决:部署时显式指定签名策略,要求至少两个组织共同背书:
# 要求 Org1 和 Org2 共同背书,任何一方不能单独改数据 ./network.sh deployCC -ccn tracecc -ccp ../trace-chaincode -ccl go -c trace \ -ccs "AND('Org1MSP.member','Org2MSP.member')"注意-ccs参数不同版本的 network.sh 写法略有差异,以你拉取的 fabric-samples 版本为准。改完策略后,生产方要改一条记录,必须拉着加工方或监管方一起签字,篡改成本才真正上来。我的经验是:先分别验证「单组织提交被拒绝」和「双组织提交成功」两个场景,再上业务数据。
4.4 客户端时间戳导致同一批次事件顺序倒挂
现象:消费者扫码后看到「运输」事件的时间比「采收」还早三天,溯源时间轴看起来像断了链。
原因:采集端的 App 或网关用了本地时间作为事件时间,手机时间不准、服务器时钟没同步,就会出现前后事件时间倒挂。链码本身对传入参数做无状态校验,传什么存什么,问题出在业务层对时间来源没有约定。
解决:链码里一律用ctx.GetStub().GetTxTimestamp(),这个时间来自排序节点,同一笔交易在所有组织眼里时间是一致的。业务上关心的「实际发生时间」(比如几点采的摘)作为链下业务字段存进原始记录,算哈希的时候带上,展示时可以显示,但链上排序用的时间戳不依赖它。这样既保证链上顺序可信,又不丢失业务语义。
5. 接入真实业务流:一物一码、交易 ID 绑定与扫码验证
5.1 一物一码的设计:码里存批次,不存单件
农产品溯源里最常被问的一个问题是:能不能每颗白菜都有自己的链?我的回答是别这么做。溯源的业务粒度是「批次」,一箱菜、一袋米、一批次采摘的苹果共用一个批次号。二维码内容建议设计成 URL:
https://trace.example.com/v?b=B20240315001&e=harvestingb是批次号,e是默认展示的事件类型。消费者扫码后,服务端先查链下库,再调链码 VerifyRecord 逐条核对哈希,把验证通过的事件按时间渲染出来。扫码次数、扫码地理位置这些记录在链下服务里,不用上链。
5.2 验证链路的三个核对点
上线前做三次核对,缺一不可:
- 链上核对:用 peer 命令直接查通道状态,绕过应用层,确认链码真的把数据写进去了;
- 哈希核对:手动改一条链下检测报告的内容,重新调用 VerifyRecord,必须返回 false,页面显示「数据异常」;
- 多组织核对:从 Org2 的 peer 用同一通道再查一遍同一批次,确认在另一个组织视角下数据一致。
# 绕过应用层直接查链上状态,用于上线前核对 peer chaincode query -C trace -n tracecc -c '{"Args":["QueryBatch","B20240315001"]}'我在项目里养成了一个习惯:上线前让不懂区块链的产品同事拿手机实际扫一次码,亲眼看到「篡改后页面出现异常标记」,大家才会相信这套系统不是黑匣子。区块链项目的验收不是看链码跑通,而是看「恶意篡改是否真的能被发现」。希望帮到你。
本文还有配套的精品资源,点击获取