
说实话接到这个农事管理系统项目的时候我心里是有点犯嘀咕的。毕竟Spring Boot做管理系统在很多开发者印象里就是“登录注册、增删改查、图表大屏”老三样。但真正把系统落地到一家五百亩地的家庭农场时我发现事情完全不是那么回事。农事管理有非常具体的业务约束农资库存不能超发、施肥打药计划必须贴合作物生长周期、采收数据要能算清楚成本毛利这些都不是靠堆CRUD能糊弄过去的。今天我就拿这个基于Spring Boot的农事管理系统当例子把从技术选型、数据库设计到前后端联调、部署上线的完整思路和踩坑记录捋一遍。如果你正准备做类似的管理系统或者正在用Spring Boot做实际项目这篇应该能帮你少走不少弯路。1. 项目背景与整体设计思路1.1 服务对象和业务场景这套系统到底在管什么做系统之前我和团队花了一周时间跟农场场长、技术员坐在一起聊业务。最开始的沟通其实很痛苦因为他们要的东西很散有人想要一个备忘录提醒哪天该打药有人想要一个库存台账搞清楚化肥还剩多少有人想要一张表看看去年种玉米到底赚没赚钱。把这些诉求汇总之后我发现农事管理系统的本质是一条完整的数字主线地块档案 - 农资准备 - 农事计划执行 - 采收记录 - 销售统计。这条主线串起来之后系统要服务的角色就很清晰了。农场主关心成本毛利和产量趋势技术员关心施肥打药是否按时执行仓库管理员关心农资库存是否充足普通工人只需要一个简单的“今天干什么活”的待办清单。基于这个分析我把系统模块划分成用户权限、地块档案、作物档案、农事计划、农资管理、采收销售、统计报表、系统日志八个部分。功能清单看着多但落到代码层面核心还是围绕“计划”和“库存”这两条业务链在转。当时还有人建议我加一个复杂的排班考勤模块我直接砍掉了。农场的工人流动性大考勤管理属于另一套系统的事硬塞进来只会让项目周期拉长而且工人不一定愿意用手机打卡。做管理系统功能边界一定要清楚什么都想做往往什么都做不好。1.2 技术选型为什么是Spring Boot MyBatis-Plus先说结论后端用Spring Boot 2.7.18 MyBatis-Plus 3.5.x前端用Vue 3 Element Plus数据库用MySQL 8.0。这套组合是目前做中小型管理系统最稳的搭配没有之一。Spring Boot的核心优势在于自动装配。你可能看过很多源码分析文章我这里用生活化的方式解释一下自动装配就像入住酒店前台看到你订的是行政房型就会把行政楼层该有的东西早餐券、迷你吧、行政酒廊使用权一次性给你备好而不是等你去前台一样一样要。Spring Boot通过EnableAutoConfiguration注解读取starter包里的AutoConfiguration.imports配置文件再根据当前项目的依赖和配置条件自动创建需要的Bean。理解了这一层后面遇到配置不生效的问题你就会下意识去查对应的starter是否引入、条件注解是否满足排查效率完全不一样。那为什么还要配MyBatis-Plus因为这套系统的数据模型虽然有固定的套路但统计报表里的SQL非常灵活。比如按月份汇总产量、按地块计算农药成本这种复杂聚合查询用JPA反而绕来绕去不如直接写SQL来得痛快。MyBatis-Plus在原生MyBatis基础上补齐了单表CRUD的自动化能力BaseMapper自带增删改查和分页插件既保留了手写SQL的灵活性又把最枯燥的重复代码省掉了。对比一下常见方案的取舍你可以看得更明白技术方案学习成本复杂查询支持维护体验适用场景Spring Boot MyBatis-Plus中等强支持XML手写SQL很舒服单表操作零SQL中小型管理类系统Spring Boot Spring Data JPA中高弱复杂查询需要JPQL单表很方便多表易混乱领域模型驱动的系统Spring Boot 原生MyBatis中等强单表CRUD也要手写SQL对SQL控制要求极高的场景我也想过要不要引入微服务架构后来掂量了一下农场的业务量峰值也就每天几千次请求单体应用完全扛得住。当时团队里有人提议用Redis缓存热点数据我评估后觉得前两个月可以先不上因为MySQL的查询性能完全够用多引入一个中间件就多一个运维负担。技术选型不是越新越好也不是越多越好而是刚好满足业务需求并且团队能Hold住这才是最合适的。1.3 Spring Boot版本避坑2.7还是3.x这里必须重点说一下版本选择因为我身边已经有好几个朋友在这个坑里栽过跟头。Spring Boot 3.x确实很香GraalVM原生镜像、Jakarta命名空间、更好的可观测性但代价是必须用JDK 17及以上。而农贸行业的服务器环境真的没有你想象中那么先进很多农企的服务器还是CentOS 7 JDK 8的老组合让他们升级JDK不光要动线上系统还得申请运维权限折腾一圈下来项目周期直接拉长。所以我在这个项目里锁定了Spring Boot 2.7.18。没别的原因就因为它是2.x系列最后一个支持JDK 8的版本意味着在旧服务器上可以零成本部署。这里也提醒所有准备做系统的朋友一个原则先确认目标部署环境的JDK版本再反过来选Spring Boot版本这个顺序千万不能反。前端我选了Vue 3 Element Plus构建工具用Vite。为什么不选JSP因为前后端分离之后后端接口可以同时被Web端、小程序端甚至未来的移动端复用这是一次性投入长期收益的事。Vue打包后生成的纯静态文件可以扔进Nginx也可以直接塞进Spring Boot的static目录部署方式非常灵活。这个细节后面专门讲。2. 数据库设计、后端分层与权限控制2.1 核心表设计把农事业务拆成六张主表数据库设计是整个系统最见功力的部分也是我这次花时间最多的地方。农事业务的数据关系并不复杂但表的字段设计和索引规划直接决定了后面写代码是顺滑还是煎熬。核心表我最终收敛为六张用户表、地块表、农事计划表、农资表、出入库流水表、采收记录表。用户表的设计比较常规关键是加了role字段区分管理员、技术员、仓库管理员和普通工人。这里不建议建一套完整的RBAC权限模型因为角色之间没有复杂的上下级关系用字段枚举就足够了。CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(64) NOT NULL COMMENT 登录名, password varchar(128) NOT NULL COMMENT BCrypt加密后的密码, real_name varchar(64) DEFAULT NULL COMMENT 姓名, role varchar(20) NOT NULL DEFAULT worker COMMENT admin/foreman/keeper/worker, phone varchar(20) DEFAULT NULL COMMENT 手机号, deleted tinyint NOT NULL DEFAULT 0 COMMENT 逻辑删除, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;农事计划表是核心中的核心。这里要特别设计一个status状态字段计划状态从待执行、执行中、已完成到已取消流转过程必须清晰。另一个关键字段是plan_type用来区分种植、施肥、打药、灌溉、采收等农事类型因为不同农事类型对应的计划内容、提醒文案和执行人角色都不一样。CREATE TABLE plan ( id bigint NOT NULL AUTO_INCREMENT, plan_no varchar(32) NOT NULL COMMENT 计划编号如PLAN20241015001, area_id bigint NOT NULL COMMENT 关联地块, crop_id bigint NOT NULL COMMENT 关联作物, plan_type varchar(20) NOT NULL COMMENT PLANTING/FERTILIZING/SPRAYING/IRRIGATING/HARVESTING, plan_date date NOT NULL COMMENT 计划执行日期, assignee varchar(64) DEFAULT NULL COMMENT 负责人, status varchar(20) NOT NULL DEFAULT PENDING COMMENT PENDING/DOING/DONE/CANCELLED, remark varchar(500) DEFAULT NULL, PRIMARY KEY (id), KEY idx_plan_date_status (plan_date, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;设计这张表时特别注意了联合索引idx_plan_date_status。因为系统最频繁的查询是“某一天有哪些待执行的计划”这个查询条件恰好命中plan_date和status联合索引能让查询走一次索引扫描而不是全表扫描。另外要注意农事计划表和实际执行记录表一定要分开设计因为计划允许调整和取消但实际执行的农事行为是不能抹掉的分开才能保证历史数据可追溯。农资表相对简单但有一个字段必须加上safety_stock安全库存阈值。农资库存低于这个值就应该预警。库存数量字段除了total_stock总库存我没有再拆多个仓库字段因为这个农场只有一个农资仓库拆了反而增加复杂度。数据模型设计要忠于现场业务形态不需要为了看上去“专业”而过度建模。2.2 后端分层controller-service-mapper的分工原则项目结构我保持了Spring Boot社区最标准的三层架构加DTO/VO分层包路径如下com.farm.system ├── controller # HTTP入口只做参数校验和结果包装 ├── service # 业务逻辑层事务开关在这里 │ └── impl ├── mapper # MyBatis-Plus的Mapper接口 ├── entity # 数据库实体 ├── dto # 接收前端参数的请求对象 ├── vo # 返回给前端的响应对象 ├── config # 配置类如WebMvc、线程池、定时任务 ├── common # 统一返回结果、异常处理、常量 ├── utils # JWT工具、日期工具等 └── task # 定时任务类分层的好处是每个人能快速找到自己该动的代码。但我强烈建议不要让Controller直接返回数据库实体一定要经过VO转换。举个例子sys_user表里有用户密码哈希如果用实体直接响应一旦忘记屏蔽字段就等于把密码哈希泄露给了前端。VO层还可以把字典类型的值翻译成中文描述比如用户角色字段数据库存的是admin返回给前端可以是带有文本描述的结构体验完全不同。另外所有接口的路径我统一加上了/api/v1前缀/api用来区分静态资源v1做版本管理。这样做的直接好处是以后接口大改版本时前端可以无缝切换也方便Nginx统一配置转发规则避免Spring Boot的静态资源拦截和接口请求互相冲突。2.3 权限控制JWT拦截器别一上来就上Spring Security全家桶这个项目的权限需求非常朴素不同角色能访问的菜单和接口不同仅此而已。我一向主张能用简单方案就不要上重型框架。Spring Security功能确实强大但配置项多、过滤链复杂对团队里刚接触后端的人来说学习成本偏高。所以我最终选择了JWT HandlerInterceptor 自定义注解的组合方案。用户登录成功后后端生成一个有效期为7天的JWT TokenToken里封装了用户ID和角色信息。前端每次请求在Header里带上Authorization: Bearer token。后端写一个拦截器在进入Controller之前解析Token并把用户信息放入ThreadLocal。这里有个细节我只定义了RequireRole注解标注在需要鉴权的接口上拦截器里做角色比对比在拦截器里写死接口路径白名单要优雅得多。Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String[] value(); }密码加密这块我用了BCryptPasswordEncoder绝对不能用MD5。MD5加盐虽然可以防彩虹表但BCrypt的适应性哈希能让暴力破解成本成倍上升这是业界普遍接受的密码存储方案。权限这块还有一个容易被忽略的点自定义拦截器一定要正确排除登录接口和静态资源路径否则前端打包后的首页都访问不了。3. 关键功能模块的落地实现3.1 农事计划与定时提醒状态机设计是核心农事计划模块是整个系统的门面农场技术员每天打开系统看最多的就是计划看板。这个模块的实现难点不在增删改查而在状态流转。计划从创建到完成至少要经历“待执行 - 执行中 - 已完成”这条主线还可能出现“待执行 - 已取消”的旁路。如果状态流转不设限制任何状态都能跳到任何状态过一个月数据就乱了。我在Service层写了一个状态机校验方法每次状态变更前先判断当前状态是否允许目标状态。比如“已完成”的计划不允许再回到“执行中”但“执行中”的计划允许异常状态下改回“待执行”。这个约束看似简单却避免了业务上“昨天已完成的活今天又变成待执行”这种数据打架的问题。定时提醒是这个模块最有价值的功能。每天早上7点系统查询当天所有待执行的计划把提醒消息推送给负责人。实现用的是Spring自带的Scheduled注解Component public class PlanRemindTask { Scheduled(cron 0 0 7 * * ?) public void remindTodayPlans() { ListPlan plans planMapper.selectList( new LambdaQueryWrapperPlan() .eq(Plan::getStatus, PENDING) .eq(Plan::getPlanDate, LocalDate.now())); for (Plan plan : plans) { String message String.format(【农事提醒】%s地块今天需要%s负责人%s, plan.getAreaName(), planTypeText(plan.getPlanType()), plan.getAssignee()); messageSender.send(plan.getUserId(), message); } } }这里我特意把消息发送抽象成了MessageSender接口目前只实现了站内信方式以后要接企业微信或者短信加一个实现类就行不用改动定时任务的主逻辑。这个设计原则叫面向接口编程在项目初期多花半小时抽象后期能省下不止半天。3.2 农资出入库一条SQL把超卖问题堵死农资库存看起来就是加减法实际坑很深。最典型的问题就是超卖工人拿着领料单来仓库领化肥系统显示还有100袋两个工人同时提交结果库存变成了负数。传统做法是先查库存再在Java代码里判断够不够最后执行更新这种“先查后改”在高并发下必然出问题。农场系统并发量虽然不高但农事高峰期多位工人同时领料是完全可能的。解决办法很经典一条UPDATE语句带上库存条件UPDATE material SET total_stock total_stock - #{quantity} WHERE id #{id} AND total_stock #{quantity}看到关键点了吗WHERE条件里额外要求total_stock #{quantity}。MyBatis执行后返回受影响的行数如果返回0说明库存不足事务直接回滚并抛出“库存不足”的业务异常。数据库本身的行锁机制保证了并发环境下这条SQL的原子性根本不需要在上层加分布式锁。出入库流水必须和库存变更放在同一个事务里。我在Service层加了Transactional注解先记录流水再更新库存任何一步失败都整体回滚。库存预警的实现也比较直接一个定时任务每天检查一遍total_stock safety_stock的农资项生成预警列表。预警消息可以合并成一条汇总消息推给仓库管理员避免一天弹十条通知惹人烦。3.3 统计报表聚合SQL与月份补零的坑农场主要看的报表其实就三类按月产量趋势、按地块投入成本、按作物毛利分析。这些数据来自不同的表聚合逻辑也不一样。我的做法是让Mapper直接返回处理好的统计VO前端拿到数据后只负责渲染图表不做二次计算。以月度产量统计为例SQL大概是这样的SELECT DATE_FORMAT(harvest_date, %Y-%m) AS month, SUM(yield_quantity) AS total_yield FROM harvest_record WHERE harvest_date BETWEEN #{startDate} AND #{endDate} GROUP BY month ORDER BY month写完这条SQL之后我遇到一个很经典的问题如果某个月没有任何采收记录结果集里就没有这个月前端折线图会直接断档。把SQL改复杂去补缺失月份不如在Java代码里做兜底。我拿到Mapper返回的Map之后先初始化一个包含全部12个月的Map再遍历查询结果填充缺失的月份自动补0。这种处理方式比硬写SQL简单得多也便于后续在代码里增加环比计算逻辑。报表查询要特别注意性能问题。采收记录表的harvest_date字段必须建索引否则跨年度查询会全表扫描。从项目第一版就把索引规划好后面数据量涨到几十万条也不会慌。另外报表接口可以考虑加一个参数控制数据范围默认只查最近三年避免一上来就沉淀五年的历史数据拖慢响应。3.4 Vue打包塞进Spring Boot单机部署的正确姿势前后端分离开发之后部署方式有两个选择一是用Nginx托管前端静态文件、反向代理后端接口二是把Vue构建产物直接复制到Spring Boot的static目录里由Spring Boot统一提供。这两种方案没有谁绝对好取决于服务器资源和运维习惯。我这里第二方案的原因是客户服务器配置一般能少装一个Nginx就少装一个。操作步骤说起来很简单前端执行npm run build得到dist文件夹把里面的内容全部复制到src/main/resources/static目录重新打包启动即可。但有一个大坑必须处理——Vue Router的history模式。这种模式下用户直接访问/plans路径Spring Boot会去查找名为plans的静态资源找不到就返回404页面直接白屏。解决办法是配置一个控制器把前端路由路径统一转发到index.htmlController public class SpaForwardController { GetMapping(value {/, /login, /plans, /areas, /materials, /reports, /**/{path:[^\\.]*}}) public String forward() { return forward:/index.html; } }注意正则只匹配不带点后缀的路径这样/assets/index.css这类静态资源不会被误拦。如果你有多个前端路由把路由路径加进去即可。如果你用了Nginx其实一行try_files $uri $uri/ /index.html;就搞定这也能看出来为什么很多人更推荐Nginx方案。我的经验是项目要快速上线、服务器资源有限就塞进Spring Boot项目生命周期长、后续会有多个前端应用就上Nginx分离部署。4. 踩坑实录与排查思路4.1 Spring Boot版本太高项目差点原地爆炸这个坑我必须放在第一个说因为教训太深刻了。项目最初的一版我用了Spring Boot 3.2.1本地开发环境是JDK 21一切跑得很流畅。结果部署到农场的服务器上那台机器安装的还是JDK 8应用启动直接报UnsupportedClassVersionError当时离农场主验收只剩三天一屋子人看着我排查环境问题。那天下午我把Spring Boot和JDK的版本兼容关系翻了个底朝天最后决定把整个工程迁移回Spring Boot 2.7.18。迁移过程虽然不算复杂但javax改jakarta相关代码已经写了不少得逐一调整。更头疼的是部分依赖的版本需要跟着降级比如某个工具库只支持Spring Boot 3.x的写法还得找到对应的旧版本。折腾了一整天才彻底跑通。这里真的要提醒大家立项第一天先问清楚部署环境把JDK版本、MySQL版本、操作系统版本记录下来再去确定技术栈版本。如果部署环境是JDK 8Spring Boot版本就锁死2.7.x如果能上JDK 17再考虑3.x。这个决策顺序能帮你省下整个项目周期里最糟糕的一次返工。4.2 CGLIB代理与循环依赖构造器注入为什么会炸这个坑属于Spring Boot进阶必须掌握的知识点。一次开发中PlanService需要调用NotificationService发通知而NotificationService又需要PlanService查计划详情两个Service相互依赖。一开始我用构造器注入服务启动时直接报错提示Bean循环依赖。背后的原理很值得讲清楚。Spring Boot 2.x默认使用CGLIB代理而不是老的JDK动态代理。CGLIB通过生成目标类的子类来实现代理对类的耦合更强。而Spring Boot 2.6版本之后默认禁止构造器循环依赖因为构造器必须在Bean实例化时完成注入两个Bean谁也没法先把自己构建出来。相比之下Set字段循环依赖能通过三级缓存做中间过渡但Spring官方已经明确不推荐依赖这套机制所以干脆默认禁用。我最终的重构方案是打破循环设计了一个独立的NotificationContext类来保存计划简要信息PlanService和NotificationService都不再直接互相依赖而是依赖这个上下文对象。循环依赖的解决办法不是硬绕而是重新审视依赖方向。如果项目里再次遇到这种问题优先考虑重构其次是加Lazy注解把自己会越弄越乱。4.3 定时任务重复执行与线程池阻塞定时任务在开发环境一切正常部署到生产环境后问题就来了。由于当时图省事把系统部署了两台机器做负载均衡每天早上7点的农事提醒被推送了两遍农户手机上的消息直接刷屏。这个问题的根源是Spring Boot的Scheduled调度器在单机应用里默认只在内存中排队多实例部署时每台机器都会执行一次定时任务而且互相之间没有感知。解决思路有两类。最彻底的是改用分布式调度框架让任务由唯一的主节点执行比如引入ShedLock在任务执行前通过数据库锁或Redis锁抢锁拿到锁的实例才执行。另一种思路更适合我这种规模的项目把定时任务模块从Web业务中拆出来单独部署成任务服务只保留一份调度实例。两条路各有权衡分布式框架功能强但要额外引入复杂度拆分服务简单可靠但要单独维护一个模块的生命周期。这里还有一个很容易忽略的坑Scheduled任务在Spring Boot默认情况下的线程池核心线程数只有1。也就是说如果你写了三个定时任务它们会排队依次执行其中某个任务卡在数据库查询上后面的任务全部延迟。配置一个任务线程池是必须的Configuration public class ScheduleConfig implements SchedulingConfigurer { Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5)); } }4.4 中文乱码、文件上传和启动端口这些细节这三个细节看着不值一提但在项目联调阶段没少折磨人。中文乱码的根源几乎都在连接串或表字符集。MySQL连接串必须显式加上characterEncodingutf8MySQL 8.0还要加serverTimezoneAsia/Shanghai否则服务器时区不匹配会报错。表字符集一律用utf8mb4别用utf8因为utf8在MySQL里连emoji都存不了更别提生僻字。我顺手在项目里加了统一异常处理和日志切面所有异常都带请求ID排查问题方便得多。文件上传主要用于农事照片留底比如施肥后的现场照片。Spring Boot的默认上传大小限制是1MB随便一张手机照片就超限。配置调整如下spring.servlet.multipart.max-file-size20MB spring.servlet.multipart.max-request-size50MB如果你用Nginx反代光改Spring Boot还不行Nginx默认的client_max_body_size也只有1MB必须同步调大。另外如果你在IDEA里开发想修改启动端口直接在application.yml里改server.port就行不需要去IDEA的编辑配置里找启动参数那个配置是给启动命令传参用的改错位置反而会掩盖问题。5. 部署上线与后续可扩展的方向5.1 打jar包、Docker部署与服务器配置项目交付阶段的部署工作我踩过不少次坑所以这次做了一个标准化的Docker部署方案。传统方式是mvn clean package -DskipTests打jar包然后通过java -jar启动配合宝塔面板做进程守护。这种方式简单直接但环境迁移能力很差——换一台服务器要重新装JDK、MySQL很不方便。用Docker之后整个运行环境通过镜像固定下来交付内容和运行依赖是自洽的。后端Dockerfile大致长这样FROM maven:3.8-openjdk-8 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM openjdk:8-jre-alpine WORKDIR /app COPY --frombuilder /app/target/farm-system.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -Xms512m, -Xmx1024m, -jar, app.jar, --spring.profiles.activeprod]这个Dockerfile用多阶段构建第一次运行会先拉依赖再编译后续只要源码没变化构建速度会快很多。服务器配置方面这套系统用2核4G内存完全够用JVM堆内存设定了1GB上限给操作系统留足余量。MySQL和Redis如果Docker部署建议打个单独的docker-compose.yml管理别跟应用塞在同一个容器里数据库变化频繁和应用容器分离能降低运维风险。5.2 后续扩展IoT、小程序、语音录入系统上线运行三个月后农场主开始觉得单纯依赖人工录入农事记录还是太麻烦了。这时候团队开始规划第二轮迭代扩展方向大体有三个。第一个方向是接入IoT气象站。农场里已经安装了温湿度、降雨量传感器数据可以通过定时任务批量采集到系统中结合作物生长模型生成浇灌建议。Spring Boot整合IoT设备数据非常成熟MQTT协议有现成的starter采集微气象数据直接入库再对接农事计划模块就能实现“天气异常自动提醒”。第二个方向是做移动端。很多农户不习惯用电脑小程序反而更适合田间地头使用。后端接口在第一轮就全部带上了/api前缀没有跟页面路由耦合小程序直接对接接口不需要改服务端代码。加上Token认证机制天然适用于移动端场景迁移成本很低。第三个方向是语音录入和自然语言处理。农户在田里做农事不方便掏手机打字如果能通过语音说一句“今天在三号地打了除草剂”系统自动识别并归档成农事记录体验会有明显提升。这块技术关键在于语音转文字服务和分词理解。农事术语比较固定用HanLP做领域分词效果不错Spring Boot也有成熟的整合方式。不过这类功能要等基础数据积累到一定量后再考虑否则识别的准确率会很难看。另外提一嘴农场一年的结构化数据量其实很有限MySQL完全能扛住暂时不需要引入Flink这类流式处理框架。我见过不少项目为了简历好看硬往系统里塞大数据组件最后运维成本成倍上涨业务却没有真正受益。技术的边界应该是业务需求而不是个人兴趣。最后说点个人体会。做农业系统最难的其实不是代码而是让人愿意把数据录进去。我们前后调研、埋点、说服农户每天登记农事记录花的精力比写代码多得多。这个项目做下来我的最大收获不是又熟练了几套Spring Boot用法而是想明白了一个道理管理系统成功的标志不是功能有多全而是用户真的在用。对于这类业务系统数据录入的便利性要放在首位数据库字段一定要预留扩展位因为农业上的业务模式变化很快轮作、认养、托管这些新模式说来就来。给未来留一点余地系统才能活得更久。