基于SpringBoot+Vue3+MyBatis的农事管理系统设计与实现

📅 发布时间:2026/10/16 10:55:50
基于SpringBoot+Vue3+MyBatis的农事管理系统设计与实现
1. 项目概述一套农事管理系统到底管什么在农业信息化项目里农事管理系统属于出现频率极高的那一类。我最近给一个种植合作社做技术方案时对方第一诉求就是“把每天的农事活动记录下来年底能算清楚每个地块的成本”。这类需求在国内农业园区、家庭农场、农资门店里非常普遍而且业务逻辑不算复杂特别适合用 SpringBoot Vue3 MyBatis 这套前后端分离方案落地。一套完整的农事管理系统核心是围绕“地块、作物、人、农资、时间”这五要素做串联。功能上通常包含基础信息维护地块档案、作物品种、员工账号、农事记录种植、施肥、施药、灌溉、采收、农资管理入库、出库、库存预警、农情记录病虫害、异常天气、统计报表按地块/作物/时间的用工与农资成本。听起来功能很多但实际上每个模块的数据关系都不深用关系型数据库存表、用 Vue 组件做表单和列表开发起来非常顺手。适配的人群也很明确如果你是刚学完 Java 后端和 Vue 前端的开发者想找一个综合度适中的实战项目练手那这套系统的模块划分、接口设计、权限控制都值得参考如果你在农业公司或者涉农软件公司工作需要快速搭一套内部管理工具那这套方案的架构和代码可以直接复用再根据业务微调字段就行。相比纯电商、纯 OA 系统农事管理系统业务术语多、行业特征强写简历和做项目展示时辨识度也更高。我在这篇文章里不打算只贴源码结构而是把“为什么这么设计”也讲清楚。从数据库建模、后端接口分层到前端 Vue3 的组合式 API 写法、部署时的常见问题全部过一遍。看完之后你自己复刻一套同类系统或者在现有源码上改业务都会轻松很多。2. 架构设计与关键选型为什么是这套组合2.1 技术栈的取舍逻辑先回答一个很多人纠结的问题SpringBoot、Vue3、MyBatis 这个组合现在还算不算主流算。尤其是涉农类管理系统甲方往往不追求极致性能更看重系统易用、好维护、代码有资料可查。SpringBoot 自带内嵌服务器配置简化到近乎“开箱即用”一个 jar 包就能跑起来MyBatis 保留了手写 SQL 的灵活性遇到复杂的多表统计报表可以精确控制查询逻辑不会像 JPA 那样在性能和可读性之间纠结Vue3 配合 Composition API 做中后台页面非常舒服表格、表单、弹窗这些高频组件写起来比 Vue2 的 Options API 更清晰。数据库选 MySQL 的理由更直观免费、稳定、运维资料多。农场或者合作社如果要招第三方维护MySQL 的门槛比 PostgreSQL 低很多。实际项目中我还见过直接用 MariaDB 的兼容性也没问题但默认推荐 MySQL 8.x。前端工程化工具选 Vite 而不是 Vue CLI主要看重启动速度和依赖安装体验。Vite 基于 esbuild 预构建依赖项目稍微大一点热更新速度优势非常明显。组件库方面我习惯用 Element Plus表格、表单、日期选择器、树形控件基本都是现成的对农事记录这种“表单密集型”应用特别友好。2.2 前后端分离的职责划分前后端分离在这个项目里的意义不只是“代码分两个仓库”更重要的是请求链路的清晰化。前端只负责渲染和交互所有数据都通过 HTTP 接口获取后端只暴露 JSON 数据接口不关心页面长什么样。项目里的工程结构大致是这样farm-manage ├── backend # SpringBoot 后端 │ ├── controller # 接收请求参数校验 │ ├── service # 业务逻辑层 │ ├── mapper # MyBatis 数据访问层 │ ├── entity # 数据库实体 │ ├── dto # 请求/响应参数封装 │ ├── config # 跨域、拦截器、全局异常处理 │ └── utils # JWT、日期处理等工具类 └── frontend # Vue3 前端 ├── src │ ├── api # 接口请求封装 │ ├── views # 页面组件 │ ├── components # 通用组件 │ ├── router # 路由配置 │ ├── store # Pinia 状态管理 │ └── utils # 请求拦截、权限判断后端统一返回ResultT结构体包含 code、message、data 三个字段。前端 axios 拦截器处理 token 附加和错误提示后端拦截器统一校验 JWT。这样每新增一个功能两边只需要按照约定格式写接口和调用不用反复沟通数据格式。2.3 权限模型设计农事系统的用户角色一般不会太复杂我常做的有三种管理员、农场员工、普通查看者。管理员可以管理地块、员工、农资、查看所有报表农场员工能够录入农事记录、领用农资查看者只读适合给公司老板或者合作农户开账号。权限校验做在拦截器里用一个注解标记接口需要的角色比如RequireRole(ADMIN)拦截器读取 JWT 里的角色字段做判断。不用引入专门的权限框架因为系统的角色和权限点很少引入太重反而增加学习成本。3. 数据库建模一张表怎么设计才能既好查又不出错3.1 核心表关系一套农事管理系统的表通常控制在 8 到 12 张以内没必要过度设计。我最常用的核心表如下表名作用关键字段sys_user用户账号id, username, password, real_name, roleland_block地块档案id, block_name, area, soil_type, responsible_personcrop_info作物品种id, crop_name, growth_days, remarksfarm_record农事记录主表id, block_id, crop_id, record_type, work_date, worker, costrecord_detail农事记录明细id, record_id, material_id, quantitymaterial_info农资档案id, material_name, spec, unit, stock_quantitymaterial_in农资入库单id, material_id, quantity, price, in_date, suppliermaterial_out农资出库单id, material_id, quantity, out_date, use_blockpest_record病虫害记录id, block_id, pest_name, level, discover_date, solutionoperation_log操作日志id, user_id, action, detail, create_time其中最关键的是farm_record和record_detail这对主从表结构。一条“施肥记录”会包含施了什么肥、每亩用量多少而多种肥料可能在同一次农事操作中出现所以用明细表存储农资消耗方便后续统计成本。主表存公共信息明细表存多行条目这种设计也符合业务习惯。3.2 字段设计的几个细节地块表里的soil_type推荐用字典值而不是自由文本。虽然“沙土”“黏土”“壤土”看上去只是几个字但在报表筛选和前端下拉框里自由文本会导致同一土质出现多种写法统计结果出现偏差。农事记录表里的work_date建议用 date 类型不要用 datetime。农事记录按天粒度记录足够如果我们精确到时分秒不仅录入麻烦而且按日分组查询还要额外写转换函数。真正需要精确时间的是操作日志那里用 datetime。农资库存字段stock_quantity不要只依赖前端计算。入库增加、出库减少都必须放在后端事务里做假如前端拿到库存后直接展示多人同时操作时会出现库存不一致。在 MySQL 中扣减库存的 update 语句最好带上库存充足的判断比如UPDATE material_info SET stock_quantity stock_quantity - #{quantity} WHERE id #{materialId} AND stock_quantity #{quantity};这条 SQL 利用数据库行锁保证原子性比“先查后减再更新”安全很多。3.3 索引与冗余字段farm_record表最常见的查询是按地块和时间范围过滤。我建的索引组合是(block_id, work_date)这样“某地块某年所有农事记录”的查询走覆盖索引速度非常快。区块数据到百万级别前这种索引完全够用。另外我在farm_record表里冗余了block_name和crop_name这种冗余字段。虽然三范式要求减少冗余但在农事系统这种读多写少的场景冗余字段能帮我们减少一次表和表的关联查询。列表页展示时直接取冗余名称不 join接口响应也能快不少。4. 核心模块实现农事记录、农资库存与预警4.1 农事记录模块的接口设计农事记录模块是整个系统的核心相关的接口一般有新增、分页查询、详情、删除、按地块统计成本。用一个简单的分页查询接口示例可以看清整个后端链路。Controller 层只做参数接收和结果返回RestController RequestMapping(/api/farm/record) public class FarmRecordController { Resource private FarmRecordService farmRecordService; PostMapping(/page) public ResultPageResultFarmRecordVO page(RequestBody FarmRecordQuery query) { PageResultFarmRecordVO result farmRecordService.queryPage(query); return Result.success(result); } PostMapping(/save) public ResultVoid save(RequestBody Valid FarmRecordSaveDTO dto) { farmRecordService.saveRecord(dto); return Result.success(); } }Service 层处理明细和主表的写入使用Transactional保证主表和明细要么都成功要么都失败Override Transactional(rollbackFor Exception.class) public void saveRecord(FarmRecordSaveDTO dto) { FarmRecord record new FarmRecord(); BeanUtils.copyProperties(dto, record); farmRecordMapper.insert(record); if (CollectionUtils.isNotEmpty(dto.getDetails())) { for (RecordDetail detail : dto.getDetails()) { detail.setRecordId(record.getId()); recordDetailMapper.insert(detail); } } }前端调用时用axios封装请求方法export function getRecordPage(data: RecordQuery) { return request.post(/farm/record/page, data) } export function saveRecord(data: RecordSaveDTO) { return request.post(/farm/record/save, data) }列表页的查询条件建议至少包含地块下拉选择、农事类型下拉、时间段日期范围。这三个条件基本覆盖了农场日常管理的常见筛选项再加一个关键词搜索冗余字段就行。4.2 农资库存模块的事务处理农资库存业务比较特殊容易出“脏数据”问题。比如同一批农药一边领用出库一边采购入库如果不在事务里操作库存就会出现负数或者虚高。我平时把入库和出库做成两个独立的 Service 方法各自打开事务Transactional(rollbackFor Exception.class) public void inbound(MaterialInDTO dto) { materialMapper.increaseStock(dto.getMaterialId(), dto.getQuantity()); materialInMapper.insert(dto); } Transactional(rollbackFor Exception.class) public void outbound(MaterialOutDTO dto) { int result materialMapper.decreaseStockWithCheck(dto.getMaterialId(), dto.getQuantity()); if (result 0) { throw new BizException(库存不足无法出库); } materialOutMapper.insert(dto); }这里的decreaseStockWithCheck就是前面提到的那条带库存判断的 SQL。这比“先查出库存量再在 Java 里判断、再 update”更可靠避免了并发下的超卖问题。前端农资模块的页面上库存列表需要显示“当前库存”和“单位”最好再加一个低库存阈值这样库存不足时表格的数值变成红色提示。4.3 农情预警用定时任务实现简易自动提醒很多系统把“预警”做得太重对农事项目来说简单的规则提醒反而更实用。我常用的预警包括库存低于阈值、连续 7 天无农事记录、病虫害记录超过一定次数。实现方式是在 SpringBoot 里加一个Scheduled定时任务每天定时扫描数据库Component public class WarnTask { Resource private MaterialMapper materialMapper; Resource private FarmRecordMapper farmRecordMapper; Scheduled(cron 0 0 8 * * ?) public void stockWarn() { ListMaterialInfo lowStockList materialMapper.selectLowStock(); for (MaterialInfo info : lowStockList) { // 生成预警消息保存到 warn_message 表 } } Scheduled(cron 0 30 8 * * ?) public void recordWarn() { ListLong blockIds farmRecordMapper.selectNoRecordBlocks(7); // 按地块生成预警 } }预警功能并不复杂关键是规则要明确不能“什么都提醒”否则时间一长就变成骚扰。库存阈值在农资档案表里加了safe_stock字段让管理员自己设定更符合实际场景。4.4 统计报表SQL 直接聚合还是后端内存聚合统计报表是农事系统的高频需求通常包括“按月份统计农事次数”“按地块统计农资成本”“按作物统计产量”。如果用户只查一年的数据数据量不大用 SQL 聚合就可以查询逻辑简单且效率高。比较典型的语句SELECT DATE_FORMAT(work_date, %Y-%m) AS month, COUNT(*) AS record_count FROM farm_record WHERE work_date BETWEEN #{start} AND #{end} GROUP BY DATE_FORMAT(work_date, %Y-%m) ORDER BY month;前端拿到结果后直接用 ECharts 的折线图或者柱状图展示。如果分组逻辑特别复杂比如同时按地块和月份分组再考虑在 Java 内存里做二次聚合。这里建议遵循一个原则第一版先让 SQL 完成 90% 的统计剩余复杂计算再交给代码不要一开始就把所有数据查出来在内存里算。5. 开发环境搭建与部署实战5.1 本地开发环境准备这套系统本地启动需要的软件比较常规软件版本建议用途JDK17 及以上运行 SpringBootMaven3.8依赖管理、后端构建Node.js18 LTS 以上运行前端工程MySQL8.0数据库Redis可选存储验证码或会话本项目可不依赖后端application.yml里的数据库连接配置要注意时区参数spring: datasource: url: jdbc:mysql://localhost:3306/farm_manage?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.DriverMySQL 8 的驱动类是com.mysql.cj.jdbc.Driver不是早期的com.mysql.jdbc.Driver不少新人在这里报错直接换成 8 的驱动类即可。前端启动进入项目目录后执行npm install npm run dev默认启动端口设在 5173后端设在 8080。前后端本地联调时我在前端.env.development文件里配置了代理VITE_API_BASE_URL/api同时在vite.config.ts里设置 proxy把/api转发到后端地址server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端页面里请求/api/farm/record/page实际转发到http://localhost:8080/api/farm/record/page避免跨域。5.2 生产环境部署步骤生产部署推荐前后端独立部署。后端编译打包mvn clean package -DskipTests生成的 jar 包用java -jar farm-manage.jar启动。如果机器内存不大可以加-Xms256m -Xmx512m参数限制占用。前端先构建npm run build构建产物在dist目录。然后把这个目录上传到服务器配置 Nginx 静态文件服务和接口反向代理server { listen 80; server_name yourdomain.com; root /opt/farm-frontend/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }注意location /api/里代理转发时路径保持完整Nginx 会把/api/farm/record/page原样转发到后端后端 Controller 中的RequestMapping(/api/farm/record)才能正确匹配。我在做历史项目时遇到过有人把/api给去掉了结果后端 404排查半天才发现是路径前缀问题。5.3 使用 Docker 简化部署如果服务器环境复杂比如一台机器上要部署多个 Java 项目推荐用 Docker。后端镜像的 Dockerfile 很简洁FROM openjdk:17-jdk-slim COPY farm-manage.jar /app/farm-manage.jar WORKDIR /app EXPOSE 8080 ENTRYPOINT [java, -jar, farm-manage.jar]前端用 Nginx 镜像配合上面的配置即可。用 Docker 的好处是环境隔离避免服务器上 Node、JDK、MySQL 版本互相干扰而且回滚版本只需要切换镜像标签。6. 常见问题排查与避坑建议6.1 高频问题速查表我在复现、调优这类系统时整理了一份高频问题表几乎每次都能用到问题现象可能原因解决办法前端请求接口报 404代理路径前缀和后端 Controller 路径不一致检查 Nginx 或 Vite 的 location 是否保留了/api后端报日期转换错误前端传了String后端期望Date在 DTO 字段上加JsonFormat(pattern yyyy-MM-dd)MyBatis 查询返回字段为 null数据库字段下划线实体属性驼峰未开启驼峰映射application.yml配置map-underscore-to-camel-case: true接口 401 循环跳转前端 axios 拦截器对 401 处理不当一直重复请求拦截 401 时只跳转一次登录页避免请求重新发农资库存出现负数出库未做库存检查将 update 带stock_quantity #{quantity}条件并开启事务数据接口响应慢列表页大量关联查询未走索引检查farm_record.block_id是否有索引必要时增加冗余字段6.2 时区问题农事管理系统里的日期很容易踩时区的坑。后端服务器默认时区是 UTCMySQL 也是 UTC 时前端用户看到的日期会比实际情况晚 8 小时。一个稳妥的做法是在 JDBC 连接串明确写serverTimezoneAsia/Shanghai同时后端 Jackson 序列化日期时统一格式化spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai前端展示时对后端返回的时间字符串不要再做new Date()转换并叠加本地时区直接用字符串展示。免得用户在浏览器里看到的日期莫名其妙又多了一天。6.3 JWT 登录与权限踩坑登录模块用的是 JWT令牌生成时把用户 id 和角色放进载荷前端存在 localStorage。后端拦截器每次请求读取请求头里的Authorization字段解析后把用户信息放入ThreadLocal。踩过的一个坑前端 axios 响应拦截器在收到 401 时如果不清除旧的 token 并跳转登录页而是直接刷新当前页面页面会循环刷新。我的处理方式是http.interceptors.response.use( response response.data, error { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )这里要避免在跳转登录页时带上redirect参数后再次触发请求。另外token 建议设置 12 小时过期太短会让农忙时节忘密码的老用户体验很差太长又带来安全隐患12 小时是平衡点。6.4 大列表分页的隐藏性能问题农事记录一旦录入多了分页查询也要注意性能。MyBatis 分页用 PageHelper 时count 查询会在每张关联表上都执行一次效率并不高。我后来改成在 Service 层先查询主表数据再根据主表 id 批量查明细减少 join。首页的最近农事动态不展示太多列只展示地块名、农事类型、日期、操作人这种轻度查询走默认索引即可不要一上来全字段查询。给前端返回的 VO 和数据库的 entity 尽量分开避免把内部字段全部暴露出去也方便接口裁剪。7. 二次开发思路这套源码还能怎么扩展农事管理系统做完基础版本之后扩展方向很多我见过比较有代表性的三个方向。第一个方向是接入物联网设备数据。地块里部署的温湿度传感器、土壤墒情仪通过 MQTT 上报到后台在农事记录页旁边展示实时环境数据形成“环境监测 农事操作”的闭环。这对大棚种植场景尤其有吸引力客户会愿意为实时数据能力付更高的预算。第二个方向是增加移动端适配。农场员工在田间地头录入农事信息不可能扛着电脑所以移动端或者移动优先的页面设计很重要。最省成本的方案是前端使用响应式布局让现有 Vue3 页面在手机端勉强可用体验更好的方案是用 uniapp 重写一套移动端接口完全复用后端。第三个方向是农资溯源与二维码结合。给每批农资入库时生成一个二维码出库扫码记录去向消费者扫描后可以看到农产品生产过程中用了哪些农资、什么时间用的、用量多少。这种溯源能力在高端农产品销售里很有价值也可以作为系统的增值模块。这些扩展都不会推翻现有架构因为前后端分离的本质就是业务逻辑和数据接口稳定表现层和新增接入层都可以灵活变化。我个人在实际操作中的体会是农事管理系统这类项目技术难度不高难在把农业术语和业务流程理解透然后转化为表结构和代码逻辑。代码可以复制但对业务的思考没法复制。你如果用这套源码做练习建议不看别人的代码自己先画一遍数据模型再开始写接口当你发现自己的设计和主流源码的思路一致时这套系统才算真的吃透了。最后再分享一个小技巧写农事记录的日期筛选时前端用日期范围组件组件默认会把开始日期设置为当天 00:00:00结束日期设置为当天 00:00:00这样会导致当天的记录被过滤掉。很多人第一次开发都会踩到。后端的查询条件最好把结束日期加一天或者前端把结束日期设置为 23:59:59。这类细节往往比那些复杂的架构设计更容易决定一个项目体验的好坏。