做果园溯源系统之前,我一直觉得这不就是个扫码看信息的页面吗?真正把“可追溯果园生产过程管理系统”从零搭起来才发现,溯源只是冰山一角,背后是一整套生产数据采集、批次流转、投入品管控和销售流向记录的闭环。系统本身用Java技术栈落地,核心是SpringBoot做容器和自动装配,ORM层用SSM体系里的MyBatis负责数据访问,前端配合Vue做管理后台,消费者端则是一个公开的追溯查询页面。这套东西既适合农企做内部生产过程数字化管理,也适合外包团队或开发者接农业信息化项目时当作可复用的基础框架,能真正解决“果子从哪棵树上摘的、施过什么肥、打过什么药、最后卖给谁”的全链路追问。
1. 项目定位:可追溯管理的核心逻辑
1.1 可追溯到底“溯”什么
很多人一提溯源就想到二维码,觉得扫码能出个页面就算完事。实际上,二维码只是消费者看到的最后一环,真正值钱的是二维码背后沉淀下来的批次数据链条。一个完整的可追溯果园系统,要回答的是这几个问题:这批果子是哪块地种的,用了什么品种的树苗,生长周期内施过几次肥、用的什么牌子什么批次的肥料,打过哪些农药、安全间隔期够不够,采收是哪天、经手人是谁,入库之后存放在哪里,最后发给了哪家批发商或者超市。
这一连串信息不是录到Excel里就完事,而是要通过数据模型把每一次操作都挂到具体的“批次”和“地块”上。比如张三在A1地块给红富士苹果打了杀菌剂,系统里就要生成一条农事记录,记录里不仅要有操作时间和操作人,还要关联农药的入库批号。如果这瓶农药本身是从某个供应商进的货,那么供应商信息也一并串起来。这就是真正的可追溯:从投入品源头一直追到终端销售,中间任何一个环节都不能断。
1.2 生产过程管理的业务闭环
果园生产过程管理的业务闭环,比我们做普通的管理系统要复杂一些,因为它是典型的“作物生命周期 + 供应链”双重模型。从业务流程上拆,大致是:果园档案建立、地块划分、品种种植、日常农事操作(施肥、打药、灌溉、修剪、疏花疏果)、投入品出入库、采收登记、批次加工、库存管理、销售出库、消费者扫码溯源。
这里面有一个特别容易踩坑的点:农事操作和物资消耗是不在一个时间点上发生的,或者说它们的对应关系不是天然的。比如你5月1日施了一次复合肥,实际用的肥料可能是一个月前入库的某个批次。这时候系统里就要做“农事操作单”和“库存出库单”的关联,而不是简单地在农事记录里填个肥料名称就完了。我见过不少项目图省事,在农事记录表里直接存肥料名称和用量,结果溯源的时候只能显示文字,拿不出这批肥料的真实采购来源,等于溯源溯了个寂寞。
所以我在设计的时候,把农事记录和投入品消耗拆成两张表,通过一个中间关联字段挂接。施肥、打药这类操作必须关联库存批次,否则不允许提交。这样既保证了业务闭环,也逼着用户把数据录全。
1.3 角色与权限体系设计
果园可追溯系统里的角色,不能简单按“管理员、普通用户”来分。从实际作业场景出发,至少要划分这几类:
- 系统管理员:管账号、管基础数据字典、管系统配置
- 农场负责人/园长:查看全局数据、审核农事记录、管理批次和销售
- 一线作业人员:负责录入农事操作、领用物资、登记采收
- 质检员:录入质检报告、检测结果,可以驳回不规范的批次
- 消费者:只能通过扫码查询公开的溯源信息,不进入管理后台
这套角色体系在SpringBoot里用拦截器加注解就能实现,不用上Spring Security那么重的框架。我给每人分配一个role字段,登录后把角色信息放进session或者JWT里,在Service层用AOP切面做权限校验。比如一线作业人员调新增农事记录的接口没问题,但如果他试图删除一条已审核的记录,接口就要返回403。前期业务逻辑没那么复杂的时候,这种轻量级权限方案比引入安全框架之后研究一堆Filter和UserDetailsService更高效。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型解析:SpringBoot + SSM的方案取舍
2.1 SpringBoot版本选型与JDK适配
关于SpringBoot版本,这是我每次都要重点强调的:不要盲目追新。市面上很多教程一上来就是最新的SpringBoot 3.x,结果环境装的是JDK8,项目根本跑不起来。SpringBoot 3.0开始强制要求JDK17,而SpringBoot 2.7.x最高支持到JDK8的最后一个正式版本。我自己的习惯是:如果服务器上装的是JDK8,就老老实实用SpringBoot 2.7.x;如果开发机已经是JDK17,那可以考虑3.x,但后续所有依赖版本都要跟着升级,很容易陷入“依赖不兼容—换版本—又引来新的不兼容”的循环。
果园管理系统这种偏业务型项目,本质上对SpringBoot的新特性没有硬性需求,稳定压倒一切。所以我最终选用的是SpringBoot 2.7.18 + JDK8 + MyBatis-Plus 3.5.x。这个组合网上资料最多,遇到问题随便一搜都有答案,不像SpringBoot 3.x出了问题还得去翻英文Issue。
提示:如果你的项目里已经拿到了别人给的源码,第一件事不是看代码逻辑,而是先核对pom.xml里的SpringBoot版本和你本机JDK版本匹不匹配。我之前遇到过SpringBoot版本太高导致项目启动直接报“不支持发行版本17”的错,把pom里的parent版本降回2.7.x后用JDK8编译,一分钟解决。
2.2 传统SSM在SpringBoot下的“复活”
说“SpringBoot + SSM”,很多人会觉得奇怪:SSM不是Spring+SpringMVC+MyBatis的整合吗?SpringBoot不是能完全替代SpringMVC的配置吗?这两者怎么共存?
实际上,SpringBoot并没有替代SpringMVC,它只是把Spring和SpringMVC的配置自动化了。SpringBoot底层仍然依赖Spring Framework,Web模块仍然是SpringMVC那一套HandlerMapping和DispatcherServlet在处理请求,只是你不用再手写applicationContext.xml、spring-mvc.xml这些配置文件了。至于项目里说“SSM”,更多的是指项目的数据访问层仍然沿用MyBatis这套持久层框架,而没有被Spring Data JPA替代。
我这次用的是MyBatis-Plus,它是MyBatis的增强工具,内置了通用的CRUD方法,单表操作根本不用写XML,直接调用baseMapper的selectPage、insert、updateById就行。比如地块列表的分页查询:
java复制Page<OrchardBlock> page = new Page<>(current, size);
LambdaQueryWrapper<OrchardBlock> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(name), OrchardBlock::getName, name)
.eq(OrchardBlock::getOrchardId, orchardId)
.orderByDesc(OrchardBlock::getCreateTime);
orchardBlockMapper.selectPage(page, wrapper);
这段代码不用写一行SQL,就能完成带姓名模糊查询、按果园过滤、按创建时间倒序的分页。MyBatis-Plus的LambdaQueryWrapper用起来比拼接字符串靠谱多了,字段名通过方法引用传入,数据库字段和Java属性对应错了编译期就能发现,比XML里的“字段写错但运行才报错”的体验强太多。
2.3 前后端分离的真实考量
果园管理系统要不要前后端分离?我的建议是:如果只是校内项目或者小规模农企内部使用,完全没必要上Vue + SpringBoot的分离式架构,直接用Thymeleaf模板渲染反而更省事。但如果你后续有对接小程序、App或者给多个农场做SaaS化的打算,前后端分离是必然选择。
这个项目我最终用了前后端分离,管理端用Vue2 + Element UI,后端只提供JSON接口。好处是开发调试灵活,接口写好后前端拿Mock数据也能并行开发;但代价是需要额外处理跨域、Token认证、接口文档维护这些事。跨域我做了一个全局配置类,实现WebMvcConfigurer的addCorsMappings方法,允许本地开发环境的请求通过。生产环境则通过Nginx反向代理,把前后端部署在同一个域名下,彻底规避跨域问题。
接口文档这块我强烈建议大家用Swagger。Controller层的方法上加@Api和@ApiOperation注解,前端人员自己就能在Swagger页面里测试接口,强类型返回结果一目了然。没有接口文档的联调就是灾难,我在项目里吃过这个亏,后来长了记性。
3. 核心功能模块与数据库表设计
3.1 果园档案与地块编码
果园档案是系统的地基。果园表存的是果园名称、地址、负责人、经纬度、面积、成立时间这些静态信息。地块表则挂在果园下面,一个果园有多个地块,每个地块有独立的面积、土壤类型、种植品种、定植时间。
地块编码是整个溯源链路的关键。我设计的编码规则是:果园拼音首字母 + 两位数字序号,比如某果园叫“绿源果园”,那第一块地就是LY-01,第二块LY-02。农事记录、采收批次、溯源码全都引用这个地块编码,编码一旦定了就不要改,否则历史数据全对不上。
地块表设计如下:
sql复制CREATE TABLE `orchard_block` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`orchard_id` bigint(20) NOT NULL COMMENT '所属果园ID',
`block_code` varchar(20) NOT NULL COMMENT '地块编码,如LY-01',
`block_name` varchar(50) NOT NULL COMMENT '地块名称',
`area` decimal(10,2) DEFAULT NULL COMMENT '面积(亩)',
`soil_type` varchar(50) DEFAULT NULL COMMENT '土壤类型',
`crop_variety` varchar(50) DEFAULT NULL COMMENT '种植品种',
`plant_date` date DEFAULT NULL COMMENT '定植日期',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_block_code` (`block_code`)
) ENGINE=InnoDB COMMENT='地块表';
3.2 农事操作记录与联动校验
农事记录是溯源链路上数量最庞大的一块数据,施肥、打药、灌溉、修剪、疏果、套袋、除草每一项都要记录。农事记录表的核心字段是:地块ID、操作类型、操作日期、操作人、操作内容描述、关联投入品批次ID、天气情况、备注。
这里有一个业务逻辑难点——不同操作类型的必填字段不一样。打药必须关联农药入库批次,施肥必须关联肥料入库批次,灌溉则不需要关联任何投入品。如果做成一张表一个统一表单,要么字段冗余,要么用户不知道该填什么。我的方案是加一个operationType字段,前端根据类型动态渲染表单,后端在Service层写一个校验方法,根据类型决定是否校验投入品批次。
联动校验的代码大概长这样:
java复制public void validateOperation(AgriOperation operation) {
String type = operation.getOperationType();
if ("FERTILIZE".equals(type) || "PESTICIDE".equals(type)) {
if (operation.getMaterialBatchId() == null) {
throw new BusinessException("施肥/打药操作必须关联投入品批次");
}
// 校验该批次库存是否充足
MaterialBatch batch = materialBatchMapper.selectById(operation.getMaterialBatchId());
if (batch.getStockQuantity() < operation.getMaterialQuantity()) {
throw new BusinessException("批次库存不足,当前剩余:" + batch.getStockQuantity());
}
}
}
这个校验逻辑非常关键,它从源头上保证了生产记录的真实性和可信度。很多溯源项目查不到数据,不是因为数据库里没存,而是录入的时候缺少这种强制校验,导致内容不完整。
3.3 投入品库存与批次追溯
投入品管理是整个可追溯系统里最容易做砸的模块,因为它在传统进销存的基础上多了一个“批次追溯”的维度。普通的进销存系统,管好物料名称、数量、供应商就差不多了,但溯源系统要求每一批次的肥料、农药都能追溯到供应商,还能和具体的农事操作记录关联。
我设计了两个关键表:material_batch(投入品批次表)和material_stock_record(库存流水表)。material_batch存的是每个批次的物料名称、规格、入库日期、供应商、生产日期、有效期、总入库量、剩余量。material_stock_record则记录每一次入库、出库、报损的流水,通过direction字段区分是入库还是出库。
重量级的坑:同一种农药在不同的农事操作中被分多次使用,每次出库都要减少对应批次的剩余量。如果剩余量的增减直接用update语句做,高并发下容易产生数据不一致。我在项目里用了一个相对简单但有效的方案:出库时用带条件更新的SQL:
java复制int updated = materialBatchMapper.updateStock(batchId, deductQuantity);
// SQL: UPDATE material_batch SET stock_quantity = stock_quantity - #{deductQuantity}
// WHERE id = #{batchId} AND stock_quantity >= #{deductQuantity}
if (updated == 0) {
throw new BusinessException("库存不足或批次不存在");
}
这种写法避免了先查再更新的竞态问题,虽然单机项目并发量不高,但养成这个习惯总没坏处。
3.4 采收、销售与溯源批次管理
采收和销售是溯源链路里连接“生产过程数据”和“消费者可见数据”的桥梁。采收时,系统根据地块、采收日期、品种生成一个harvestBatchNo(采收批次号),这个批次号就是后续溯源码的核心依据。
采收批次生成之后,进入库存管理和销售环节。销售订单里关联采收批次号,出库时记录流向客户信息。最终消费者扫码的时候,系统根据溯源码反查采收批次,再通过采收批次关联到这个批次所有的农事记录、投入品记录和质量检测记录。
我设计了一个trace_batch表来统一管理溯源批次:
sql复制CREATE TABLE `trace_batch` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`trace_code` varchar(64) NOT NULL COMMENT '溯源码',
`harvest_batch_no` varchar(64) NOT NULL COMMENT '采收批次号',
`product_name` varchar(100) DEFAULT NULL COMMENT '产品名称',
`variety` varchar(100) DEFAULT NULL COMMENT '品种',
`origin` varchar(200) DEFAULT NULL COMMENT '产地',
`harvest_date` date DEFAULT NULL COMMENT '采收日期',
`quality_report` varchar(255) DEFAULT NULL COMMENT '质检报告文件路径',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_trace_code` (`trace_code`)
) ENGINE=InnoDB COMMENT='溯源批次表';
这样把溯源相关字段单独抽成一张聚合表,查询的时候一次就能拿到全部要展示的核心数据,再根据这些核心数据去关联农事记录和投入品流水,效率上比在采收表里反复join快得多,也便于后期做报表统计。
4. 关键实现:溯源码生成与查询链路
4.1 溯源码编码规则设计
溯源码的编码规则必须同时满足三个要求:唯一、可解析、难以伪造。我设计的编码规则是:
LY + 采收日期(YYYYMMDD) + 地块编号 + 3位流水号
比如LY-20250618-01-001,代表绿源果园(LY)在2025年6月18日从01号地块采收的第一批产品。这样设计的好处是,即使不查数据库,光从编码本身就能快速判断这个批次来自哪个果园、哪天采收的、哪个地块,方便线下人员口播核对信息。
生成溯源码的逻辑放在了采收登记完成之后的后置处理器里。用户新增采收记录,系统自动调用一个生成批次号的方法,从数据库里查当天该地块已经有多少个批次,序号自动加一,然后拼出完整编码。这个逻辑要注意并发下的唯一性问题,如果同一时间两个人同时给同一地块做采收登记,序号可能冲突。我的方案是在表里加了唯一索引兜底,冲突时捕获DuplicatedKeyException自动重试一次。
4.2 二维码生成与绑定
二维码本身不复杂,我用的是Google的ZXing库,依赖就一个:
xml复制<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.2</version>
</dependency>
生成的二维码内容是一个URL地址,指向我部署的溯源查询页面。URL后面带上traceCode参数,消费者扫码后自动跳转到这个页面并根据参数查询对应的溯源信息。值得注意的是:二维码里存的是一个支持GET请求的URL,不是直接把JSON数据塞进去。因为如果产品已经包装好甚至发货了,你还能在后端修改这个批次要展示的信息,二维码不用重新打印,灵活性高很多。
生成二维码的操作放在采收批次确认之后。系统把二维码图片保存到服务器本地目录,同时把图片访问路径存进trace_batch表的qrcode_url字段。前端在批次列表页可以直接预览和下载二维码,方便农场打印出来贴到产品包装上。
4.3 溯源查询接口与页面
溯源查询接口是对外公开的,不需要登录,所以要注意两点:接口要简洁、响应要快。我设计了一个getTraceInfo接口,入参是traceCode,返回的数据包含三块:产品基础信息(品名、品种、产地、采收日期)、生产记录列表(按时间排列的农事操作记录)、检测报告信息。
生产记录列表从农事记录表按批次关联查询,用VO组装后返回。这里我特别注意一个细节:返回给消费者的字段不能一股脑全暴露,比如操作人姓名可以显示,但操作人的手机号绝对不能返回,农药的商品名可以显示,但农药的供应商电话不能给消费者。数据脱敏在开发阶段就要想清楚,别等上线了被用户投诉泄露隐私再补救。
消费者端的溯源页面是个简洁的移动端适配页面,绿色的清新风格,核心就是展示上述三类信息。页面右侧加了一个“扫码次数统计”,记录这个批次被扫码查询的次数,数据存到trace_record表。这个数据挺有价值,既能让农场主知道哪些产品流通更广,也能用来做防伪参考。
5. 实操过程:从建表到联调的完整流程
5.1 数据库初始化与MyBatis-Plus配置
数据库我选用的是MySQL 8.0。建库时直接执行一个初始化SQL脚本,把所有的表按照第三章的设计一次性建好,同时插入默认的管理员账号。这个脚本要保证可以反复执行不报错,我的做法是脚本开头用DROP TABLE IF EXISTS先把已有的表删掉再重建,虽然粗暴但开发阶段很方便。
MyBatis-Plus的配置有几个必须注意的点。第一个是逻辑删除,我给所有业务表都加了deleted字段,并且配置了全局逻辑删除,这样所有删除操作都变成update,防止误删后数据无法恢复:
yaml复制mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
第二个是自动填充,createTime和updateTime这两个字段我不想每个插入和更新都手动赋值,就实现了MetaObjectHandler接口,在insertFill和updateFill方法里统一填充。
5.2 后端核心代码结构
后端代码我按标准的三层结构组织:Controller、Service、Mapper。Controller层只做参数接收和结果封装,不写业务逻辑;Service层负责所有业务规则,加@Transactional注解保证事务;Mapper层继承MyBatis-Plus的BaseMapper接口。
一个典型的Controller方法长这样:
java复制@ApiOperation("新增农事记录")
@PostMapping("/operation")
public Result<?> addOperation(@RequestBody @Valid AgriOperationDTO dto) {
AgriOperation operation = new AgriOperation();
BeanUtils.copyProperties(dto, operation);
operation.setCreateBy(SecurityUtil.getCurrentUserId());
agriOperationService.addOperationWithStockCheck(operation);
return Result.success();
}
这里有个我自己一直坚持的实践:Controller层不建议直接用实体类接收前端参数,而是定义一个DTO(Data Transfer Object)。因为前端传的字段和数据库表字段往往不是一一对应的,比如DTO里可能是batchCode而不是batchId,需要Service层根据编码查出对应的Id再存库。用DTO做隔离层,以后表结构变了也不影响接口。
Service层里,凡是涉及多表操作的,都要加上@Transactional。比如新增农事记录时不仅要插入agri_operation表,还要减少投入品批次库存、插入库存流水表,这三个操作必须同时成功或同时失败。不加事务的话,农事记录插进去了但库存没扣,溯源的时候批次还显示有货,数据就乱了。
5.3 前端页面与接口对接
前端管理后台用Vue2 + Element UI,核心页面包括:登录页、果园信息页、地块管理页、农事记录页、投入品库存页、采收批次页、销售订单页、溯源批次管理页。我对前端进行异步请求的工具封装是统一封装一个request.js,基于Axios拦截器,自动附加Token,统一处理错误码:
javascript复制service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
this.$message.error(res.msg || '请求失败')
return Promise.reject(new Error(res.msg))
}
return res
},
error => {
if (error.response && error.response.status === 401) {
router.push('/login')
}
return Promise.reject(error)
}
)
这个拦截器太重要了,没有它,每个页面都得自己写错误判断逻辑,代码重复一百遍。
前后端联调时最典型的坑是字段命名。后端Java习惯用驼峰命名(harvestBatchNo),前端JS也习惯驼峰,但如果后端返回的JSON里字段名和前端页面绑定的字段名不一致,渲染出来就是一片空白。我的做法是后端统一配上Jackson的驼峰命名策略,同时要求前端严格按照接口文档里返回的字段名来绑定数据,不在前端做多余的字段映射。
5.4 调试文档与部署验证
调试文档和讲解配套这些“周边产物”,说实话才是这类项目的交付关键。我一般把调试文档分成三份:技术文档(框架选型、表结构、接口清单)、部署文档(从安装JDK到部署前端后端的完整步骤)、使用说明(给农场管理员看的操作手册)。部署文档里要写清楚每一个命令,包括打包命令、启动命令、日志查看命令,方便非技术背景的同事也能执行。
实际部署我用了两台服务器的方式:一台跑Nginx托管前端静态文件并做反向代理,一台跑SpringBoot后端服务。前端打包后生成dist目录,扔到Nginx的html目录下,然后配置反向代理把/api路径转发到后端的8080端口。启动后端用nohup java -jar命令,日志输出到指定文件。这套部署架构不复杂,但生产环境完全够用。
6. 常见问题与排查技巧实录
6.1 SpringBoot版本太高引发的兼容性危机
这个真的是老生常谈但又不得不说的坑。有一次我拿到一份据说“能跑”的源码,pom里SpringBoot版本是3.2.1,我自己开发机是JDK8,导入后启动直接报错。排查之后发现mybatis-spring-boot-starter的版本只支持到SpringBoot 2.x,换用3.x专用的mybatis-spring-boot-starter 3.0.3版本,同时又发现JDK8编译不了,最终只能把SpringBoot降级到2.7.x。折腾了整整一个下午,就为了一个版本问题。
经验是,项目启动前先做三件事:1)确认JDK版本和SpringBoot版本的兼容性;2)确认MyBatis-Plus等核心依赖的版本和SpringBoot主版本匹配;3)确认Maven仓库里能拉到所有依赖。这三件事做完,后面能省一大半排查时间。
6.2 SpringBoot自动装配失败和扫描不到Bean
虽然SpringBoot号称自动装配,但MyBatis-Plus的Mapper扫描还是有讲究。项目里的Application类默认在根包下,它只扫描自己所在包及子包。如果某个Mapper接口放在了其他包路径下,没有加上@MapperScan注解,启动就会报找不到对应的Bean。
我的做法是在启动类上统一加上:
java复制@MapperScan("com.example.orchard.mapper")
指定Mapper接口所在的包路径。这样所有Mapper都能被扫描到,不用在每个Mapper接口上重复加@Mapper注解,代码也简洁一些。如果加了@MapperScan还是报找不到Bean,那就看一下MapperXML文件有没有配置正确的namespace,这一步也很容易漏。
6.3 事务不生效的几个典型场景
SpringBoot的事务,用起来简单,但失效场景五花八门。我遇到过的最典型的三种情况:第一种,方法没加@Transactional注解,或者类上加了但方法不是public的,事务不生效;第二种,同一个类内部调用自己带@Transactional的方法,比如A方法调B方法,B方法虽然加了@Transactional,但因为是this.B()调用,事务代理没有介入,B方法里的异常不会触发回滚;第三种,异常被catch住了没有往外抛,事务管理器感知不到异常,自然也不会回滚。
我的习惯是:每个涉及多表更新的Service方法都单独设计成public方法,尽量不要在同类里的另一个public方法里调用它。如果业务确实需要,就把方法拆到另一个Service类里去,或者注入自己(@Resource private XxxService self)来调用,这样Spring的AOP代理才会正常生效。
6.4 前端上传图片和文件大小限制
果园系统里要上传的东西不少,地块的照片、农事操作的现场图、质检报告PDF等。开发时我用SpringBoot默认的文件上传配置,结果传一个10MB的照片就报错了。这个问题本质是SpringBoot对multipart格式请求有默认大小限制,单文件默认1MB,请求总大小默认10MB,要改配置:
yaml复制spring:
servlet:
multipart:
max-file-size: 20MB
max-request-size: 50MB
同时,前端Axios的请求默认没有设置Content-Type为multipart/form-data时,文件上传会莫名报错。文件上传组件里必须设置:
javascript复制const formData = new FormData()
formData.append('file', file)
axios.post('/api/common/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
上传后的文件我统一存在服务器本地指定的目录,并生成年月子目录防文件太杂,文件的访问路径直接存数据库,配合SpringBoot的静态资源映射对外开放。如果部署到Docker容器里,记得挂载一个volume把上传目录映射出来,否则容器一删数据全没了,这个坑我替你们踩过。
6.5 生产环境排查与日志记录
项目上线后,日志就是唯一的眼睛。我一般习惯用Logback,配合logback-spring.xml配置,按天滚动生成日志文件,保留30天。更重要的是,我在Controller层加了一个简单的切面,自动打印每个接口请求的耗时、参数和返回状态。排查问题时,直接搜这个接口的路径就能快速定位是哪次请求出了异常:
java复制@Aspect
@Component
@Slf4j
public class LogAspect {
@Around("execution(* com.example.orchard.controller.*.*(..))")
public Object log(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
log.info("{} 耗时 {}ms", pjp.getSignature().getName(), System.currentTimeMillis() - start);
return result;
}
}
一个实用的排查技巧:如果用户上报说某个微信小程序扫码后查不到数据,先别急着查代码,先通过拦截器或者Nginx的access.log看清请求路径和参数。很多时候不是后端接口报错,而是前端把traceCode拼错了,比如编码里的大写字母L和小写字母l在URL传递中被混淆了。这种问题看日志一眼就能定位。
最后分享一个我个人的体会:类似果园可追溯系统这种业务型项目,技术栈无非是SpringBoot加个ORM框架,代码量也不至于多到吓人,真正决定交付质量的永远是业务建模是否严谨、操作流程是否落地、数据采集是否闭环。开发的时候多花一点时间在设计阶段把业务规则想清楚,别急着敲代码,后面联调、部署、验收都会顺很多。这套源码连同配套的调试文档、部署手册和讲解视频整理在一起,适合做二次开发或者学习参考,遇到具体问题也欢迎一起交流。
