接手这个项目之前,我在帮某个小型农场做信息化的需求梳理。农场主给我的第一句话是:“地里的记录都在Excel里,不同的人在填,填完也没人看。去年种了什么品种、每亩用了多少肥、哪块地产量高,根本说不清楚。”这就是做农事管理系统最真实的出发点——不是炫技,而是把农业生产里的地块、农事、农资、人员、记录这些散落的信息整理成一套可查询、可统计、可追溯的数据闭环。
这篇文章把完整的项目拆开讲:业务需求怎么提炼成功能模块,数据库表怎么设计才能支撑农事作业闭环,后端用SpringBoot+MyBatis怎么写核心接口,前端用Vue怎么把页面串起来,最后是本地跑通源码的完整步骤和我在开发过程中踩过的坑。适合正在做毕业设计、想拿完整项目练手的开发者,也适合准备给中小型农场做信息化的小团队参考。
1. 农事管理系统到底要管什么:业务需求拆解
很多同学拿到这类题目第一反应是上网抄一套通用后台管理模板,把用户管理、角色管理、菜单管理做出来就觉得完事了。这恰恰是本末倒置。农事管理系统的核心不是“管理用户”,而是“管理农事”。用户管理只是支撑系统运转的骨架,地块、农事任务、农资库存、农事记录才是血肉。
1.1 地块档案:一切农事数据的锚点
农事管理的基本单位是“地块”。在真实的农场里,一块地有编号、面积、土壤类型、种植作物、负责人、地理位置等属性。所有后续的播种、施肥、打药、灌溉、采收记录都要挂到地块上,才能回答“这块地今年种了什么、花了多少成本、产出如何”。
项目里我设计了“地块档案管理”模块,字段包括地块编号、地块名称、面积、土壤类型、当前作物、状态(空闲/种植中/休耕)、负责人、备注。这里最关键的一点是,地块编号必须唯一且稳定,它是农事记录外键关联的基础。如果地块可以随意删除,历史记录就会失去锚点,所以地块模块一般做逻辑删除,不物理删除。
1.2 农事任务与农事记录:计划到执行的双层结构
农场日常工作不是“想做就做”,而是按农时安排。播种、施肥、打药、灌溉、采收都有时间窗口。我把农事拆成两层:农事任务(计划)和农事记录(执行)。
农事任务是计划层,包含任务类型(播种、施肥、打药、灌溉、采收、除草等)、关联地块、计划开始时间、计划结束时间、负责人、任务状态(待执行/执行中/已完成/已取消)、任务描述。农事记录是执行层,由执行人提交,包含实际执行时间、实际用量、用药用肥明细、操作人、现场情况说明。
为什么要拆两层?因为农场的考核和管理需要对比“计划 vs 实际”。比如计划在3月20日前完成施肥,实际拖到3月28日,系统里就能看出偏差。如果不拆分,只有一条记录,很难做这种对比分析。我在设计时让“新建农事记录”可以从某个农事任务一键生成,自动带出地块、类型、负责人,执行人只要补充实际数据即可。
1.3 农资库存与出入库:闭环里的成本账
农资管理如果只做“进出库流水”,那只是一个记账工具。我在项目里把它做成了带预警的库存闭环。
农资档案包括种子、化肥、农药、农机具等,属性有物资名称、类别、规格、单位、库存数量、预警阈值、供应商、单价。出入库单记录每一次变动:入库单(采购入库、退库入库)、出库单(农事领用、损耗出库)。最关键的设计是:出库单可以选择关联农事任务或农事记录。这样到月底就能算出来“这块地这个月领了多少化肥,花在哪个任务上”,从而实现按地块、按任务维度的成本归集。
我还加了一个低库存预警逻辑,当库存数量低于预警阈值时,系统自动标记,主页看板会提示需要采购。这一条在实际使用中农户非常认可,因为以前都是用到地里才发现药不够了,临时采购往往耽误农时。
1.4 环境记录与统计报表:给决策提供依据
环境数据(温度、湿度、降雨量)对农业生产有直接影响。系统里保留了一个环境记录表,可以手动录入,也可以为将来接入物联网传感器留接口。模块不复杂,就是按日期和地块记录环境指标,配合可视化图表展示。
统计报表是系统的价值出口。我做了三个核心报表:按地块统计全年农事记录数量与产量趋势、按农资类型统计使用成本、按人员统计任务完成率。这些数据从农事记录表和出入库表聚合而来,用SQL的GROUP BY就能完成,但页面展示上用了ECharts折线图和柱状图,让农场管理者一眼能看到问题地块和成本大户。
2. 技术选型的底层逻辑:为什么是SpringBoot+Vue+MyBatis
网上关于技术选型的讨论很多,但对于这个项目,SpringBoot + Vue + MyBatis + MySQL的组合不是随便选的,它非常适合“中小型管理信息系统”这个定位。
2.1 后端:SpringBoot 2.7 + MyBatis
SpringBoot是目前Java后端开发成本最低的框架,内嵌Tomcat,简化了Spring的XML配置,Maven一键打包,项目的启动入口就是一个main方法。对做毕业设计或者小团队交付来说,SpringBoot能显著降低部署和运维的复杂度。
MyBatis和SpringBoot的配合很成熟。我选择MyBatis而不是JPA,原因有两点:第一,农事管理系统的查询条件复杂多变——按地块查、按时间范围查、按任务类型查、按状态查,还可能多个条件组合。MyBatis的XML里可以手写动态SQL,用if标签灵活拼接条件,比JPA的Specification更直观可控。第二,报表统计需要写多表JOIN和GROUP BY聚合查询,这种SQL在MyBatis里就是直接写XML,改起来方便,执行计划也可控。
如果你不太熟悉MyBatis的动态SQL,建议花半小时过一遍where、set、foreach、if这几个标签。这个项目90%的查询接口都要用到它们。
2.2 前端:Vue 2 + Element UI
前端选择Vue 2是基于生态成熟度的考虑。Vue 2 + Element UI后台管理组件的组合在大量项目中得到验证,网上的解决方案多,遇到问题容易查到资料。Vue的响应式数据绑定非常适合表单类和列表类页面,数据变化自动驱动视图更新,不需要手动操作DOM。
Element UI提供了表格、表单、日期选择器、对话框、消息提示这些后台系统高频组件,直接降低页面开发量。比如农事记录列表页,用el-table绑定数据,加上分页组件,几行代码就出来了。
我建议如果你自己做项目且不熟悉前端,先别急着上Vue 3和TypeScript,虽然它们更新,但Vue 2 + JS的学习曲线更平缓,先把业务跑通更重要。等到整体架构熟练了,再迁移到Vue 3也不迟。
2.3 前后端分离的结构设计
项目采用前后端分离结构,后端只提供RESTful API,前端通过axios调用。开发环境用Vue CLI的devServer代理解决跨域,生产环境用Nginx反向代理。这样前后端可以独立开发、独立部署,代码维护也清晰。
整体结构分为三层:前端是Vue项目,包含登录页、首页看板、地块管理、农事任务、农事记录、农资库存、统计报表、系统管理这几个页面;后端是SpringBoot单体应用,按controller、service、mapper分层;数据库用MySQL 8.0,字符集utf8mb4。单体应用对这个量级的系统完全够用,不需要微服务。
3. 数据库设计:五张核心表的关系与建表细节
数据库设计是整个项目中最重要、也最容易被低估的环节。表结构设计得好,后面写Mapper和页面都会顺手;设计得不好,后期改表会连带改动大量代码,痛苦不堪。
3.1 用户与角色:别在用户表上堆字段
用户模块我不做复杂的RBAC权限模型,只做用户表和角色字段。农场系统用户量本来就不大,角色无非是管理员、场长、技术员、操作工几种。在user表里用一个role字段区分即可,不需要role表、user_role表、menu_role表那套复杂设计,那会显著增加工作量。
user表字段:id、username、password(BCrypt加密存储)、real_name、phone、role、status、create_time。
登录时用JWT生成token,前端把token存在localStorage,每次请求头带Authorization。后端写一个拦截器校验token,解析用户信息后放行。
3.2 地块、农事任务、农事记录、农资、出入库五张核心表
这五张表是系统的业务主干。我列出简化版字段,方便你理解关系:
| 表名 | 核心字段 | 说明 |
|---|---|---|
| farmland(地块) | id, code, name, area, soil_type, current_crop, status, manager_id | 业务锚点 |
| farm_task(农事任务) | id, task_type, farmland_id, plan_start, plan_end, assignee_id, status, description | 计划层 |
| farm_record(农事记录) | id, task_id, farmland_id, record_type, execute_time, operator_id, detail, pesticide_info | 执行层,task_id可空 |
| material(农资档案) | id, name, category, spec, unit, stock, warn_threshold, supplier, price | 库存主数据 |
| material_io(出入库单) | id, io_type, material_id, quantity, related_task_id, create_time, creator_id, remark | 库存流水 |
farm_record里冗余了farmland_id,为什么不只通过task_id去关联?因为农事记录允许不挂任务直接录,比如临时发现病虫害,马上打药,不一定要先建一条计划任务。冗余字段可以避免每次查询都要JOIN任务表,提高列表页性能。这是典型的“以空间换效率”设计,在报表统计时很有用。
3.3 建表SQL的几个容易忽略的细节
第一,所有表的主键用BIGINT自增,不要用UUID字符串做主键,BIGINT在索引和JOIN性能上更好。第二,时间字段统一用datetime,不要用timestamp,避免2038年问题和时区换算困扰。第三,字符集统一utf8mb4,排序规则utf8mb4_general_ci,这样中文、emoji、特殊符号都能存。第四,金额和面积字段用DECIMAL,不要用FLOAT/DOUBLE,避免精度误差。比如农药单价12.35元,float可能存成12.349999。
外键我建议只在逻辑层面关联,不建物理外键约束。物理外键在插入、删除时会带来额外的锁和校验开销,而且在分页、批量操作时容易出问题。维护数据一致性靠service层事务控制,性能更好,灵活性更高。
初始化SQL脚本里,我会预置一个管理员账号(admin/admin123)和一些演示地块、农资数据,方便第一次启动就能看到页面有内容。
4. 后端实现:登录鉴权与农事任务闭环
4.1 JWT登录流程与拦截器
登录接口接收用户名和密码,查user表,用BCryptPasswordEncoder匹配密码,匹配成功则生成JWT。JWT的payload里放userId和username,设置24小时有效期。我习惯在JWT里不放敏感信息,密码、手机号这些坚决不往token里塞,因为JWT的payload是Base64编码,等于明文。
拦截器继承HandlerInterceptor,重写preHandle方法,从request头取Authorization,去掉“Bearer ”前缀后解析token。解析失败或过期直接返回401。配置类里用registry.addInterceptor注册拦截器,并放行登录接口、静态资源、跨域预检请求OPTIONS。这一步如果不放行OPTIONS,前端调用接口时会一直报跨域错误,后面前端章节会细说。
4.2 农事任务的CRUD与状态流转
农事任务模块是后端写得最多的部分。列表接口用动态SQL,传入taskType、status、farmlandId、时间范围等条件,mapper XML里用where加if拼接。分页用PageHelper插件,一行startPage就可以自动生成limit语句。
新增任务的逻辑比较简单,直接insert。比较关键的是状态流转。我把状态机定义为:
- 待执行:任务创建后默认状态
- 执行中:负责人开始作业后更新
- 已完成:执行人提交农事记录后自动变为已完成
- 已取消:管理员手动取消
提交农事记录时,后端会执行一个事务方法:插入farm_record,然后更新对应farm_task状态为已完成。这样才能保证“记录有了,任务状态也变了”这两个操作要么都成功,要么都失败。
4.3 农资出入库的事务控制
出入库是另一个涉及事务的关键点。出库单的保存逻辑是:先插入material_io记录,再更新material表库存数量(stock = stock - 数量)。如果插入流水成功但库存更新失败,数据就不一致了,所以两个操作必须在一个事务里。
我还在service层加了库存不足校验:出库前查询当前库存,如果小于出库数量,直接抛出业务异常,事务回滚。采购入库的逻辑类似,只是数量做加法。
这里补充一个我的实现技巧:用Spring的@Transactional注解时,注意只有RuntimeException和Error才会触发默认回滚,受检异常不会。所以自定义业务异常要继承RuntimeException。我项目里定义了一个BusinessException,service层直接throw new BusinessException("库存不足")就能触发回滚。
5. 前端实现:列表、表单、看板的落地细节
5.1 项目初始化与请求封装
前端用Vue CLI创建项目,安装vue-router、axios、element-ui、echarts。页面结构上,整体布局用Element UI的el-container,左侧是侧边菜单,右侧是内容区,顶部是用户信息。
axios需要封装一下,统一处理baseURL、超时时间、token携带和响应拦截。我在request.js里做了三件事:请求拦截器从localStorage取token并加到请求头;响应拦截器判断后端返回的code,code为200时直接返回数据,code为401时跳转登录页,code为500时用Element UI的Message提示错误信息。
这样封装之后,每个页面的接口调用代码就很简洁,不再需要重复处理错误。
5.2 农事日历与任务列表页
任务列表页是所有页面里最复杂的。顶部是查询条件区(任务类型下拉、状态下拉、日期范围、地块选择),中间是el-table展示任务列表,操作列有“开始执行”“查看”“提交记录”按钮。
新增和编辑共用同一个弹窗表单,提交时校验必填项。这里有个前端小技巧:编辑弹窗打开时,要用深拷贝把行数据复制给表单对象,不能直接赋值引用,否则表单里改了没点保存,表格数据已经被改了。
农事日历是我给这个项目加的亮点页面。用echarts的日历坐标系,按日期展示当月任务数量,颜色深浅代表任务密度。配合一个表格,点日历日期就能看到当天的具体任务列表。做起来并不复杂,api返回的数据按日期分组即可,但使用体验比纯表格直观很多。
5.3 统计看板的图表页面
统计报表页面分三个Tab:地块产量趋势(折线图)、农资成本分布(柱状图)、人员任务完成率(饼图)。数据接口都在后端写好了聚合SQL,前端只需要接收标准结构,用echarts的setOption渲染。
ECharts的选项配置比较多,我建议把每个图表的option封装成独立的函数,接收数据返回配置,不要在组件里写一长串option,后期维护会很难受。图表容器一定要设定width和height,否则初始化时会是0像素,图表不显示,这是新手常踩的坑。
6. 从源码到运行:本地部署完整步骤
拿到完整源码之后,怎么在本地跑起来?我按后端和前端两部分说明,跟着做一遍基本就能启动成功。
6.1 环境准备清单
| 软件 | 版本建议 | 说明 |
|---|---|---|
| JDK | 1.8及以上 | 项目基于JDK8开发,11也能跑 |
| Maven | 3.6+ | 用于后端依赖管理和打包 |
| MySQL | 8.0 | 5.7也可以,但建议8.0 |
| Node | 14/16 | Vue CLI 5要求Node版本不能太低 |
| IDE | IntelliJ IDEA | 后端用IDEA,前端可以用IDEA或VSCode |
第一步,创建一个空的MySQL数据库,执行项目里提供的init.sql,脚本会自动建库建表并写入演示数据。注意MySQL 8.0的默认认证插件是caching_sha2_password,如果连接报错,可以在创建用户时指定mysql_native_password,或者升级mysql-connector-java版本。
6.2 后端配置与启动
修改application.yml,把数据库地址、用户名、密码改成你的本地配置。我建议单独建一个专用账号,不要用root,避免误操作。
启动后端之前确认三件事:Maven依赖是否下载完整、数据库是否已初始化、端口8080是否被占用。如果8080被占用,在application.yml里改server.port。启动成功后,浏览器访问http://localhost:8080,配合swagger或者直接访问一个测试接口,能返回JSON说明后端OK。
6.3 前端启动与代理配置
前端项目在IDE里打开终端,执行npm install安装依赖。这一步在国内经常卡在下载,可以先用nrm把npm源切换到国内镜像源,速度提升明显。安装完成后执行npm run serve,默认启动在8080端口,如果和后端冲突,config目录下的index.js里改port。
关键点是vue.config.js里配置devServer代理:
module.exports = { devServer: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }这样前端请求/api/login会被代理到后端的http://localhost:8080/api/login,规避跨域问题。注意后端的Controller统一以/api开头,否则代理路径还要加rewrite逻辑。
启动完成后,浏览器访问http://localhost:3000,看到登录页就说明环境跑通了。
7. 踩坑清单:这几个问题我排查了最久
项目开发过程中我遇到了不少问题,挑几个影响最大、最容易复现的记录下来,这些在官方文档里往往不会直接写明。
7.1 跨域与拦截器放行冲突
前端调用后端接口报跨域,第一反应是配置CorsFilter。配置之后预检请求OPTIONS通过了,但带token的正式请求还是失败。原因在于拦截器在CORS处理之前就把OPTIONS请求拦截了,还没到CorsFilter就返回了401。
解决办法有两个:一是拦截器里统一放行OPTIONS方法,代码很简单:
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; }二是SpringBoot的CorsMvcConfigurer和拦截器注册顺序配合。我建议两个都做,稳一点。这个问题排查了我一个下午,因为控制台报的是CORS错误,但实际根因是拦截器,表面现象很容易误导。
7.2 MyBatis下划线转驼峰没生效
数据库字段是create_time,实体类是createTime,查询返回的createTime一直为null。查了很久,发现application.yml里缺了配置:
mybatis: configuration: map-underscore-to-camel-case: true没有这个配置,MyBatis查询结果映射时不会自动把下划线转驼峰。需要注意的是,在application.yml里写这个配置,如果用mybatis-plus可能写法不同,它是mybatis-plus.configuration.map-underscore-to-camel-case。这个细节很容易踩。
7.3 时间格式化时差问题
农事记录列表页的时间比数据库实际时间少了8小时。原因有两层:MySQL连接URL里的serverTimezone没设置,或者设置成了UTC;加上fastjson在序列化LocalDateTime时默认格式化的时区不对。
我在连接URL里加了serverTimezone=Asia/Shanghai,并在application.yml里统一配置jackson的时间格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai实体类的时间字段我用LocalDateTime,配合@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")。这样前后端显示的时间就一致了。
7.4 库存扣减的并发问题
测试的时候用两个账号同时对同一农资出库,库存从10变成了8,而不是预期的6。这是典型的并发覆盖问题:两个请求都查到库存10,各自扣2后写回8,最后一次写覆盖了前一次。
解决方法是出库SQL用原子更新:
UPDATE material SET stock = stock - #{quantity} WHERE id = #{materialId} AND stock >= #{quantity}affected rows为0说明库存不足,抛出业务异常。这样不需要用悲观锁,也不需要引入分布式锁,一个SQL就能解决并发场景下的库存扣减。这是我在实际项目中验证过的方案,对这个量级的系统足够用。
写在最后的个人建议
项目全部做完之后,我把源码里的注释、文档和SQL重新梳理了一遍。一个很重要的体会是:农事管理系统这类项目,业务逻辑并不复杂,真正花时间的反而是数据库设计和那些不起眼的细节配置。如果让我重做一次,我会在项目开始时先花一周时间和最终用户聊业务流程,把地块怎么分、农资怎么管、任务怎么派这些问题彻底搞清楚,再动手写代码。业务模型对了,技术实现是水到渠成的事。
另外,这个项目最大的扩展空间在于数据接入层——环境传感器、气象接口、智能水肥控制,这些都可以作为独立的数据源接入现有系统。做毕业设计的同学可以考虑在报表模块加一两个这样的数据来源,展示出来的效果会比纯增删改查亮眼很多。最后提醒一句,一定要自己动手把代码敲一遍,源码看一眼和亲手写一遍,掌握程度完全是两回事。