
简介这套基于SpringBoot开发的区块链农产品溯源系统以多系统微服务方式组织源代码与数据库面向希望实战微服务拆分、区块链数据存证及农产品溯源业务闭环的开发者适合具备一定Java基础并想了解区块链落地场景的中高级学习者。压缩包共1370个文件约15.73MB覆盖255个Java后端代码文件、251个JavaScript与86个Vue前端文件、110个WXML及109个WXSS小程序页面文件、124个JSON配置文件另有大量PEM/CRT/KEY区块链证书密钥、12个Shell部署脚本、SQL数据库脚本以及多份docx文档与yml配置样例能支撑从链上身份配置、通道排序节点设置到前后端联调的全过程。已有459人学习下载。资源内包含完整的区块链节点密钥目录、多系统微服务工程结构和多端页面代码目录归类清晰适合用于毕业设计、课程项目或企业原型验证通过实际运行可掌握SpringBoot集成区块链、农产品溯源数据上链与查询等关键思路也可参考其多模块划分、配置命名与安全凭证管理方式。1. 从“扫码查不到”到“扫码查得准”SpringBoot区块链农产品溯源的落地样本SpringBoot 区块链 农产品溯源这个技术组合在过去几年被反复提及但真能跑起来、能回答“你凭什么证明这棵白菜没被换过”的源码包并不多。这份多系统微服务源代码加数据库的rar解决的不是“怎么把数据存进区块链”而是“从农户播种到消费者扫码全链路记录怎么进库、怎么上链、怎么证明没被改过”。拆开看里面是多个基于SpringBoot的微服务工程配合数据库脚本启动后能完整跑完种植、加工、物流、查询的溯源闭环。适合正在做农业信息化项目的开发者也适合想参考微服务拆分和区块链存证写法的Java工程师。先说结论这套系统真正的难点不在区块链节点本身而在业务数据和链上数据怎么对齐这也是本文重点拆的地方。2. 多系统微服务拆分六个服务怎么划、数据边界画在哪2.1 溯源流程天然适合微服务先理清种植、加工、物流、查询的边界农产品溯源是一个典型的跨部门长流程。生产端由农户或合作社产出播种、施肥、施药、采收记录加工端由分拣中心或屠宰场生成批次加工信息物流端由冷链车不断上报运输节点质检端独立出具检测报告监管方和消费者则是最终查询方。这五类角色分布在不同组织数据产生频率不同权限边界清晰天然适合用微服务划分。单体系统也能做溯源但一旦要接入多个种植基地、多个加工厂、多家物流商表结构和权限模型会越滚越复杂。生产端的录入页面挂了不应该影响消费者扫码查询物流端物流商调用接口慢也不应该拖垮整个大厅的监管大屏。按业务域拆成微服务之后每个服务可以独立部署、独立扩容故障影响面被缩小到具体业务域内。这是这套系统采用微服务架构最核心的理由不只是为了简历上多写一行微服务架构。技术栈上常见做法是Spring Cloud Alibaba这套组合Nacos做注册中心和配置中心Gateway做统一网关Feign做服务间声明式调用每个业务服务用SpringBoot MyBatis-Plus操作各自的MySQL库。源码包里的多系统我一般会拆成六个工程加一个公共模块网关服务、生产管理服务、质检服务、加工服务、物流服务、查询服务公共模块放统一返回体、通用工具类和公共实体。注意这些服务名不一定和源码包完全一致但拆分逻辑大体相同拿到包之后先对照自己的业务把服务边界画出来再去看代码会轻松很多。2.2 批次号贯穿全链路一张表看清数据从哪进、最终落在哪微服务拆分最怕的是数据边界模糊。这套系统里有一个贯穿全局的核心概念批次号batchCode。农户采收后生成一个批次后续加工、质检、物流、上架都围绕这个批次展开消费者扫溯源码本质上是拿溯源码关联到批次号然后聚合出这一个批次的全链路记录。下表是这套系统里常见六个模块的职责和核心数据表拿到源码包后建议先对照表定位代码而不是从controller开始翻服务模块核心职责主要数据表对外接口示例网关服务路由转发、Token校验、限流无业务表/api/** 统一入口生产管理服务种植计划、施药施肥、采收记录produce_plan、produce_record/api/produce/record质检服务检测报告上传、审核、报告编号quality_report、quality_item/api/quality/report加工服务批次加工、分拣包装、赋码绑定process_batch、process_step/api/process/batch物流服务运输节点、温控上报、签收logistics_node/api/logistics/node查询服务聚合多服务数据输出溯源页trace_agg聚合索引表/api/trace/query生产管理服务写入produce_record时带上batchCode质检服务写入quality_report时也带batchCode物流服务上报logistics_node时同样带batchCode这就是所有服务之间唯一的外键。查询服务不直接跨库join业务库而是先把各服务需要展示的字段同步到自己的只读表里或者通过Feign逐个查询后再拼装。前者性能好后者实现简单源码包里如果已经做了独立的查询服务建议优先用它的聚合能力后续接入新数据源时不用改前端页面。2.3 拆分的两条红线别按页面拆、别把查询都压到写库我第一次拆这种系统时踩过一个大坑按页面来拆服务。比如做“扫码页服务”“管理后台服务”“大屏服务”结果一个完整的业务事务被横向切到三四个服务里Feign调用链变成A调B、B调C、C调D中间任何一环超时整条链路失败。正确做法是严格按业务域拆一个服务能独立完成自己领域内的闭环不依赖别的服务才能写库。第二条红线是查询不能都怼业务库。查询服务为了展示一个溯源详情页如果for循环去调生产、质检、加工、物流四个服务的接口一次扫码请求就要串行等待4个200ms用户体感很差。我一般会在查询服务里放一张trace_agg聚合表或者用Canal这类数据库同步工具把各业务库需要的字段同步到只读库查询只走只读库写操作仍然走各自服务。这也是为什么带微服务的溯源系统往往都会配套数据库同步组件而不是全靠Feign现拉数据。但这里有个度每天几万次扫码量级下Feign聚合完全够用不需要一开始就上Canal同步组件带来的是额外运维成本。3. 上链存证环节的实现如何证明数据库里的记录没被改过3.1 不是所有数据都要上链先定存证范围再设计哈希链区块链在这套系统里解决的问题只有一个证明数据库里的记录在入库之后没有被改动过。它不适合直接存业务数据原因有两个一是链上写入需要等待节点确认无法承受高频入库的TPS二是链上存储成本远高于MySQL一段长文本或图片往链上塞交易体积和费用都会失控。常见做法是业务数据正常写MySQL上链的只是每条记录的哈希摘要。上链范围一般按三个原则来定影响消费者判断的关键信息产地、检测报告、加工时间、需要监管审计的信息施药记录、物流温度曲线、以及数据量小的核心字段。图片和视频这类大文件通常的做法是存到OSS或本地目录再把文件的SHA-256值上链消费者查看时下载原文件重算哈希做比对。为了防止有人把某一条记录连带哈希一起替换系统里还要做哈希链每一条新记录的哈希计算把上一条记录的哈希拼接进去这样篡改任意一条后面所有哈希都会对不上从结构上增加了篡改成本。下一节直接看核心代码实现。3.2 核心代码段溯源记录写入时的“业务落库 哈希上链”先看一个简化版的溯源记录实体里面承载了哈希链的关键字段public class TraceRecord { private Long id; // 主键 private String batchCode; // 批次号全局唯一 private String preHash; // 上一条记录的哈希构成链式结构 private String contentHash; // 当前这条记录计算出的哈希 private String chainTxId; // 区块链上的交易ID用于后续验证 private String contentJson; // 业务数据JSON写入时序列化 }再看存证核心逻辑Service public class TraceRecordService { Autowired private TraceRecordMapper traceRecordMapper; Autowired private BlockchainClient blockchainClient; Transactional(rollbackFor Exception.class) public boolean saveAndChain(TraceRecord record) { // 1. 批次号必传它是后续聚合和校验的唯一索引 if (!StringUtils.hasText(record.getBatchCode())) { throw new BizException(batchCode不能为空); } // 2. 查同一批次下最新一条记录的哈希作为链式前件 String preHash traceRecordMapper.selectLatestHash(record.getBatchCode()); if (preHash null) { // 首次写入的批次给一个初始占位值 preHash GENESIS; } // 3. 业务数据序列化后拼上 preHash 一起算哈希 String contentJson JSON.toJSONString(record.getContentData()); String contentHash DigestUtils.sha256Hex(contentJson preHash); record.setPreHash(preHash); record.setContentHash(contentHash); // 4. 先落MySQL保证业务可查 traceRecordMapper.insert(record); // 5. 再上链存证上链失败不能影响业务落库 try { String txId blockchainClient.storeHash(record.getBatchCode(), contentHash); traceRecordMapper.updateChainTxId(record.getId(), txId); } catch (TimeoutException e) { // 标记为待上链由定时任务补偿不在这里回滚事务 traceRecordMapper.markChainFailed(record.getId()); throw new BizException(上链超时已进入补偿队列); } return true; } }代码里有三个关键设计点。第一先落库再上链顺序不能反。如果先上链再写MySQL一旦事务回滚链上留下了哈希但库里没有记录校验时会出现“链上有、库里无”的孤儿数据。第二preHash拼在contentJson后面计算等于把当前记录和上一条记录绑定在一起这是哈希链能生效的关键。第三上链超时抛出异常但事务不回滚而是走markChainFailed标记补偿任务按batchCode contentHash的唯一索引幂等重试避免重复上链。参数层面SHA-256算出来的contentHash是固定64位十六进制字符串无论原始业务数据多大上链的字节数都是一样的。chainTxId是区块链交易哈希不同链长得不一样但大多数节点返回的都是64位左右字符串数据库字段放VARCHAR(128)足够。preHash没有特殊格式要求首次写入用“GENESIS”字符串只是为了标识链头校验时识别到它可以判断这是该批次的第一条记录。3.3 验证闭环把链上哈希取回来和本地库再比一次存证做完不算完查询端必须能把链上数据取回来做比对否则消费者看到“区块链存证”四个字也只是个图标。查询验证逻辑一般不重算整条哈希链而是取单条记录的chainTxId去链上节点查询对应哈希然后和本地contentHash比对public VerifyResult verifyTrace(Long recordId) { TraceRecord record traceRecordMapper.selectById(recordId); // 用交易ID从链上节点取回存证哈希 String onChainHash blockchainClient.getStoredHash(record.getChainTxId()); // 本地重算当前记录的期望哈希 String localHash DigestUtils.sha256Hex(JSON.toJSONString(record.getContentData()) record.getPreHash()); boolean ok onChainHash ! null onChainHash.equals(localHash); return new VerifyResult(ok, record.getBatchCode(), onChainHash, localHash); }这个比对里有个容易翻车的细节重算localHash时拼接的contentData必须是写入时序列化后的同一结构字段顺序不同算出来的哈希完全不同。我一般会把写入和验证共用的JSON序列化逻辑抽成一个统一方法不两边各写一遍拼字符串。false的情况要区分处理onChainHash为null说明这笔交易在链上不存在大概率是补偿任务还没跑完哈希不一致则说明这条记录被改过需要人工核查。4. 源码包启动与避坑从空环境到五个常见问题排查4.1 启动前置环境与初始化顺序从拿到rar到跑通第一步不是解压而是先确认环境。这个项目依赖的外部组件不少任何一个版本错位都会在启动时报出莫名其妙的错误。规格建议如下组件版本建议用途说明JDK1.8 或 11SpringBoot 2.x对高版本JDK兼容性不佳不建议一上来就用17Maven3.6多模块工程统一构建MySQL8.0utf8mb4字符集兼容溯源业务里的中文与特殊符号Redis6.x网关限流、Token缓存、验证码存储Nacos2.x注册中心和配置中心区块链节点视源码包文档而定常见的FISCO BCOS节点或以太坊私链测试网初始化分四步做。第一步建库导入脚本用全量SQL脚本初始化一次即可后面改表结构务必走增量DDLmysql -uroot -p -e CREATE DATABASE IF NOT EXISTS trace_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; mysql -uroot -p trace_db sql/trace_db.sql第二步启动Nacos默认端口8848启动后浏览器先确认控制台能打开再继续别在Nacos还没就绪时就去启动业务服务会让服务注册失败cd nacos/bin sh startup.sh -m standalone # 控制台地址 http://localhost:8848/nacos第三步检查各微服务的bootstrap.yml重点确认三处Nacos地址、命名空间、MySQL连接参数。源码包里一般会准备dev与prod两套配置本地跑dev即可。启动顺序上先启动业务服务生产、质检、加工、物流、查询最后启动网关避免网关先起来后有请求进路由但后端服务还没注册完成cd xu-service-quality mvn spring-boot:run -Dspring-boot.run.profilesdev4.2 避坑记录启动、连接与存证环节的五个常见问题问题一服务都能注册到Nacos但Feign调用总是报“Load balancer does not have available server”。现象控制台上所有服务状态都是UP但A服务调用B服务时找不到目标实例。原因服务注册到了不同的命名空间或分组。Nacos默认是public命名空间源码包如果手动建过dev命名空间某个服务的配置里忘了改就会出现在不同命名空间下互相看不到对方。解决打开Nacos控制台的服务列表页确认六个服务是否在同一个命名空间和分组把各服务bootstrap.yml里的namespace、group强制统一本地跑全部填public和DEFAULT_GROUP即可。问题二手贱把SpringBoot版本升级到3.x启动时直接抛javax.servlet相关的NoClassDefFoundError。现象项目原本跑得好好的升级SpringBoot 3.x后Maven依赖解析成功但启动类起不来报javax.servlet.ServletException找不到。原因SpringBoot 3.x把Java EE的javax包迁移到了jakarta老版本的Nacos客户端和部分第三方starter仍依赖javax两个体系冲突。解决源码包基于SpringBoot 2.x时老老实实保持原有版本。如果非要升3.x必须同步升级Nacos client到适配版本并检查所有依赖里是否还引着老javax。本地调通比追新更重要。问题三导入数据库脚本时报“Unknown collation utf8mb4_0900_ai_ci”。现象脚本执行到一半失败通常是建表语句里指定了排序规则。原因建表语句是在MySQL 8.0环境下生成用了8.0默认排序规则utf8mb4_0900_ai_ci但本地连的是MySQL 5.75.7不认识这个排序规则。解决把建表语句里的排序规则全局替换为utf8mb4_general_ci后重新导入。导入后还要检查表名大小写MySQL在lower_case_table_names1时表名不区分大小写实体类映射的TableName注释必须与实际表结构一致否则启动时MyBatis-Plus扫描会报找不到表。问题四区块链存证接口一直超时日志里出现Connection refused。现象业务记录能写入MySQL但调用区块链客户端的storeHash方法总是timeout。原因区块链节点RPC地址配置成了localhost但节点实际跑在另一台机器或Docker容器里或者本地调试时节点根本没有启动。解决先确认节点状态再curl一下RPC地址和端口看是否通curl -X POST http://192.168.1.10:8545 -H Content-Type: application/json --data {jsonrpc:2.0,method:net_version,params:[],id:1}通之后再改bootstrap.yml里的节点地址。同时保留代码里的补偿机制确认网络恢复后定时任务能把失败记录补上链。问题五Vue前端打包后放进SpringBoot的static目录打开页面空白、接口404。现象vue build生成的静态文件被拷贝到src/main/resources/static下启动后能访问index.html但刷新子页面就报404接口请求路径也对不上。原因前端路由用了history模式刷新某个路径时服务端没有对应的controller处理接口前缀也没有统一走网关。解决本地验证明白页面时把前端路由改成hash模式避免刷新问题接口调用全部走网关的/api前缀不要直连某个业务服务的端口。如果前端已经打包成了静态文件后端加一个简单的转发控制器把非/api开头的路径转发到index.html。5. 进阶给溯源系统做一次链上数据一致性“体检”系统跑了一段时间之后最担心的是数据库里的trace_record被内部人员改过。哈希链的价值这时候才真正体现出来不需要依赖第三方平台只要把一个批次的所有记录按写入顺序重算一遍哈希再和库里存的preHash、contentHash逐条比对就能快速定位是哪一条出了问题。public void verifyBatchChain(String batchCode) { ListTraceRecord records traceRecordMapper.selectByBatchOrder(batchCode); String expectedHash GENESIS; for (TraceRecord r : records) { String rehash DigestUtils.sha256Hex(JSON.toJSONString(r.getContentData()) expectedHash); if (!rehash.equals(r.getContentHash())) { System.out.println(批次 batchCode 记录 r.getId() 哈希不一致疑似被篡改); break; } // 当前记录验证通过后把它作为下一笔的前置哈希 expectedHash r.getContentHash(); } }这段校验有一个大前提写入时的contentData和校验时序列化出来的contentData结构完全一致。如果写入时只取了业务字段校验时把id、preHash也带进去哈希必然对不上还会误报篡改。我一般会把写入和校验共用的JSON构建逻辑放在同一个工具方法里两边都用它不各自拼接。我在实际用的项目里这个校验脚本会做成定时任务每天凌晨跑一遍所有在售批次的哈希链生成校验报告。消费者端展示溯源数据时不写“区块链存证”这种空泛口号而是直接显示“最近一次链上校验时间与结果”真实感和可信度完全不一样。从那以后我每次交付这个项目都会强制跑一遍全批次校验再发版顺手把连续两个批次的验证结果截图发给客户看比讲十页PPT管用得多。希望帮到你。本文还有配套的精品资源点击获取