1. 牧场管理系统到底在管什么从业务场景反推需求清单先聊个情况。最近在星球和几个技术社群里好几个人问我同一个问题毕设或者说练手项目到底选什么题目比较合适图书管理、员工管理这类系统说实话已经写烂了答辩评委一眼就能看出工作量不够。但如果直接上手电商、秒杀这种高并发项目又容易被 JVM 调优、分布式事务这些概念劝退。SpringBoot 牧场管理系统是个很巧妙的中间选择——它既有常规管理系统的增删改查又有畜牧行业特有的状态跟踪逻辑难度曲线温和业务故事又讲得圆。我在做这套系统之前特意去请教过一个在奶牛场做信息化实施的朋友。他给我讲了半天我总结下来牧场管理的核心痛点其实就四个字状态追踪。一头牛从出生、入栏、饲喂、防疫、配种、产犊到出栏是一条完整的时间线每一段都要留痕。同时圈舍的分配、饲料兽药的库存、员工的排班和操作记录也都需要一套系统串起来。这和传统的进销存系统有本质区别——货物只有在库和出库两种状态而牲畜的状态复杂得多。所以做这套系统之前别急着写代码先把角色和场景列出来。我最终梳理出的核心角色有四个。角色核心诉求对应功能系统管理员管人、管权限、看全局数据用户管理、角色分配、数据统计饲养员录入饲喂记录、查询牛只信息畜牧档案、饲养记录、圈舍管理兽医记录检疫、治疗、疫苗信息防疫管理、健康预警、繁殖管理仓库管理员管理饲料、兽药、耗材库存物资管理、出入库记录为了避免过度设计我没有一开始就上很复杂的流程引擎或工作流审批。系统功能边界收敛为六大模块系统管理、畜牧档案、圈舍管理、饲养管理、防疫繁殖管理、统计分析。这个边界适合大多数中小型牧场也符合毕设的体量。如果你是边学边做强烈建议先画一张业务流程图哪怕用纸笔画也行。画清楚一头牛进来到出去经历哪些环节再对照着去建表写接口思路会顺很多。这套系统适合谁一是做 Java Web 相关毕设的学生用来换皮改造成其他XX管理系统很轻松二是想系统学习 SpringBoot 完整开发流程的开发者因为它覆盖了登录鉴权、文件上传、数据统计、报表展示、部署上线这些常用技术点三是真正有小型牧场信息化需求的个人可以直接在此基础上二次开发。2. 技术选型为什么是 SpringBoot MyBatis Plus Vue 这套黄金组合技术选型这块我需要稍微展开讲一下因为这是面试和答辩时最常被追问的部分。别人问你为什么选 SpringBoot你不能只说因为好用得说出它相对于 SSH/SSM 解决了什么实际问题。SpringBoot 在我这个项目里的意义核心是零配置起步。早期用 SSM 搭项目需要写 web.xml、spring-mvc.xml、mybatis-config.xml 一长串配置而且版本之间经常不兼容稍不留神就给你抛个 BeanDefinitionStoreException。SpringBoot 把自动配置做到了极致我只需要在 pom.xml 里引入spring-boot-starter-web和spring-boot-starter-data-redis这些 starter一个加了SpringBootApplication注解的启动类项目就能跑起来。这一点对时间紧张的开发者来说省下的不是几小时是几天。持久层选了 MyBatis Plus 而不是原生 MyBatis理由也很实际。这套系统的基础 CRUD 操作占了差不多六成如果用原生 MyBatis我需要为每张表手写 Mapper 接口和 XML 文件工作量翻倍不说还容易在 SQL 占位符这种低级错误上栽跟头。MyBatis Plus 的BaseMapperT直接内置了 insert、deleteById、selectPage 这些方法单表操作几乎不用写 SQL。复杂查询再用Select注解或者 XML 去补充。这里有个小经验spring-boot mybatis项目里如果你想让表不存在时自动建表不开玩笑得写个配置类监听数据库连接然后执行CREATE TABLE IF NOT EXISTS语句。但 MyBatis Plus 社区里有人把ddl-auto的思路搬过来配合mybatis-plus.ddl-auto配置需要在依赖里加上mybatis-plus-ddl模块也能实现类似 JPA 的自动建表效果。我在演示项目里为了让用户拿源码就能跑其实更推荐直接执行项目附带的sql初始化脚本这样表结构版本可控不容易出幺蛾子。前端选了 Vue Element UI理由也简单前后端分离是目前的主流开发方式也是热搜词里反复出现的搭配。你搜springboot vue前后端分离能找到大量资料说明这套组合的生态非常成熟。SpringBoot 只负责提供 JSON 接口Vue 通过 Axios 调接口渲染页面。开发期用 Vite 代理解决跨域生产期用 Nginx 统一转发。其他配套组件Redis存储登录 Token 和验证码。选它是因为 Token 失效管理比 JWT 纯无状态更方便比如管理员可以强制把某个用户踢下线直接删 Redis 里的 key 就行。MinIO存储牛只照片和检疫报告附件。本地开发可以用文件系统代替但部署到服务器时 MinIO 的可视化控制台管理文件更直观。ECharts做存栏量统计、体重增长曲线等图表。MySQL 8.0数据库注意时区和字符集配置。这套组合的另外一个好处是学习资料丰富。SpringBoot 相关的面试题、配置技巧、排错经验随手一搜就是一大堆。真在开发中卡住了你很难遇到一个别人没踩过的坑。对新手来说站在别人的经验上往前走是最快的方式。3. 数据库设计一张牛档案表如何撑起整个牧场运营数据库设计是我每次做管理系统最重视的环节。表设计得好后面接口写起来行云流水设计得不好业务一复杂就要开始各种 join 缝合恨不得重构。这套牧场管理系统我最终拆了 9 张核心表下面按业务维度分组说。第一组基础组织架构sys_user系统用户表字段包括用户名、密码BCrypt 加密存储、真实姓名、手机号、角色 ID、状态。sys_role角色表预置管理员、饲养员、兽医、仓库管理员四种。paddock圈舍表字段有圈舍编号、名称、位置、容量、当前存栏量、状态启用/停用。圈舍表和我之前做的会议室管理系统里的会议室表很相似但多了一个当前存栏量字段。这个字段理论上可以通过统计畜牧表里paddock_id关联的牛只数量算出但我在设计时做了冗余。为什么因为在牧场实际场景里圈舍列表页需要实时显示剩余容量如果每次打开页面都去count()一遍数据量大了会影响体验。冗余字段牺牲了一点点一致性换来的是查询性能这在管理信息系统里是常见取舍。第二组畜牧业务核心livestock畜牧档案表这是系统的绝对核心。字段包括耳标号唯一索引、品种、性别、出生日期、入栏日期、体重kg、体高cm、父亲耳标、母亲耳标、所在圈舍 ID、当前状态在栏/出栏/死亡、照片 URL、备注。feeding_record饲养记录表记录每次投放饲料的情况字段包括关联牛只 ID、饲料类型、投喂量kg、投喂时间、操作人。health_record防疫/健康记录表字段包括关联牛只 ID、记录类型体检/疫苗接种/治疗、诊断结果、用药名称、用药剂量、记录时间、兽医 ID、下次提醒日期。设计livestock表时有个细节值得拿出来说。我没有把品种作为字符串直接存而是建了一个breed_type字典表来维护前端下拉框的选项也从这个表查。这背后的原因是品种名称在录入时如果靠手打很容易出现西门塔尔和西门塔而这种同义不同字的数据后期统计一塌糊涂。用字典表约束录入从源头保证了数据质量。类似的性别、状态这种枚举值字段我也用 tinyint 存数字在后端写一个LivestockStatusEnum来转换。第三组物资与统计分析material物资表字段包括物资名称、类别饲料/兽药/耗材、规格、库存总量、预警阈值、单位。material_record物资出入库流水表字段包括关联物资 ID、变动类型入库/出库、变动数量、关联单据号比如领用人、领用事由、操作时间。breeding_record繁殖记录表字段包括母牛 ID、配种日期、配种方式、公牛耳标、预产期、产犊日期、产犊数量、犊牛状态。在material表里加预警阈值是因为库存管理模块需要实现低于阈值自动提示补货。这个逻辑不复杂一条 SQL 就能查出来但如果没有这个字段就得在代码里写死阈值没有任何灵活性。很多管理系统只做出入库流水却忽略了库存预警这个真实业务中非常重要的需求这算是我从实际项目里总结出的经验点。统计功能方面我没有单独建统计表而是基于livestock表写聚合查询。比如统计每月存栏量就是按DATE_FORMAT(create_time, %Y-%m)分组统计品种分布就是按品种分组 count。ECharts 前端拿返回值直接画饼图和柱状图就行。这些 SQL 看着复杂其实把GROUP BY和条件筛选用好基本都能搞定。最后提一句表关系。畜牧表通过paddock_id关联圈舍表通过create_by关联用户表饲养记录和健康记录通过livestock_id回指畜牧表物资流水通过material_id关联物资表。整体是一套典型的主表 明细表结构没有多对多的复杂关系对新手非常友好。4. 核心功能实现从登录鉴权到健康预警的完整链路4.1 登录鉴权Spring Security Redis 实现可控制的 Token登录模块我选择了 Spring Security JWT Redis 的方案。流程是用户提交用户名密码后端校验通过后生成 JWTtoken 里只放用户 ID 和用户名然后把这个 token 写入 Redis设置过期时间为 24 小时。后续每次请求前端在 header 里带上 token后端通过拦截器实际上是一个 OncePerRequestFilter解析 token再查询 Redis 判断这个 token 是否有效。为什么不像很多教程那样直接用无状态 JWT因为实际业务中有退出登录和强制下线的需求。如果只把 token 发出去不记录管理员禁用某个用户后这个用户手里的 token 在过期前仍然有效这就产生了安全隐患。引入 Redis 之后退出登录时删掉 key用户立刻失效逻辑干净利落。当然代价是每次请求多一次 Redis 查询但系统并发量不大完全可接受。核心代码大致是这样的// JWT 认证过滤器 public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); String userId JwtUtil.parseToken(token); if (userId ! null redisTemplate.hasKey(login_token_ userId)) { // 从 Redis 中加载用户信息放入 SecurityContext LoginUser loginUser (LoginUser) redisTemplate.opsForValue().get(login_token_ userId); UsernamePasswordAuthenticationToken authenticationToken new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } filterChain.doFilter(request, response); } }这里有两个容易踩的坑。第一个是密码加密务必用BCryptPasswordEncoder千万不要把明文密码存数据库也不要用 MD5彩虹表破解太容易。第二个是 token 过期时间我设的 24 小时可以根据实际安全要求调整但要注意前端需要在 token 快过期时跳转到登录页不然用户会莫名其妙操作到一半被踢出去。4.2 畜牧档案的增删改查分页、多条件组合查询与文件上传畜牧档案模块是典型的 CRUD但细节上有几个值得优化的地方。分页 多条件组合查询列表页有筛选条件包括耳标号模糊查询、品种下拉选择、性别下拉选择、状态下拉选择、入栏日期范围。后端对应的 Controller 方法接收一个LivestockQuery对象在 Service 层用 MyBatis Plus 的 LambdaQueryWrapper 动态拼条件。public PageResultLivestockVO pageQuery(LivestockQuery query) { LambdaQueryWrapperLivestock wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(query.getEarTag()), Livestock::getEarTag, query.getEarTag()) .eq(query.getBreedType() ! null, Livestock::getBreedType, query.getBreedType()) .eq(query.getGender() ! null, Livestock::getGender, query.getGender()) .eq(query.getStatus() ! null, Livestock::getStatus, query.getStatus()) .between(query.getStartDate() ! null query.getEndDate() ! null, Livestock::getCreateTime, query.getStartDate(), query.getEndDate()) .orderByDesc(Livestock::getCreateTime); PageLivestock page livestockMapper.selectPage( new Page(query.getPageNum(), query.getPageSize()), wrapper); // 把实体转成 VO补充圈舍名称、品种名称等冗余信息 ListLivestockVO records page.getRecords().stream().map(this::toVO).collect(Collectors.toList()); return new PageResult(records, page.getTotal()); }文件上传畜牧档案里要传牛只照片。我单独写了一个FileController接收 MultipartFile保存到本地目录或者 MinIO返回访问 URL。需要注意保存文件时不要用用户上传的原始文件名因为可能存在路径穿越攻击。我用 UUID 重新生成文件名保存文件的物理路径和虚拟访问路径分开。PostMapping(/upload) public RString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return R.fail(上传文件不能为空); } // 用 UUID 生成新文件名防止路径穿越和重名覆盖 String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String newFilename UUID.randomUUID() suffix; File dest new File(uploadDir, newFilename); file.transferTo(dest); return R.ok(/files/ newFilename); }文件上传路径要在 application.yml 里配好同时还要配一个静态资源映射把/files/**映射到本地目录。这一块如果不配上传成功了但前端图片显示不出来是非常常见的问题。4.3 健康预警什么时候该给兽医发通知这个功能我觉得是整套系统里最有业务价值的模块也是大多数人做的管理系统里没有的东西。健康预警要解决的问题是牧场几千头牛兽医不可能每头都盯过来系统能不能自动把需要关注的牛筛出来我做了三个维度的预警。体重异常预警每头牛在建档和饲喂记录里都会更新体重。系统定期每天凌晨跑一次定时任务检查每头牛的体重与同品种、同龄段的平均体重对比如果偏离超过 20%标记为体重异常。疫苗到期提醒健康记录表里有next_remind_date字段记录下次需要接种疫苗的日期。定时任务查出当天需要提醒的记录生成待办推送。圈舍超容量预警如果圈舍的当前存栏量超过了容量列表页圈舍卡片就会变红。很多初学者会问定时任务用 Spring 的Scheduled注解不就行了吗对Scheduled(cron 0 0 1 * * ?)加在方法上就可以。但要注意如果以后部署了多个后端实例定时任务会被重复触发需要引入分布式锁或者独立的调度服务。我这套系统单机部署所以直接用注解简单够用。4.4 统计报表ECharts 数据展示的正确姿势统计报表模块是我最后一个开发的也是视觉效果最好的。前端用 ECharts 做了四个图表每月存栏量折线图、品种分布饼图、体重增长曲线图、物资出入库柱状图。后端接口返回的是前端绘图需要的格式化数据比如品种分布GetMapping(/breed/distribution) public RListMapString, Object breedDistribution() { ListMapString, Object list livestockMapper.selectMaps( new QueryWrapperLivestock() .select(breed_type, count(*) as count) .eq(status, 1) .groupBy(breed_type)); return R.ok(list); }前端拿到[{breed_type: 1, count: 20}, ...]这样的结构稍作映射喂给 ECharts 即可。这里有个小建议图表的数据格式后端可以直接按前端需要的格式返回不要在前端做复杂的数据加工。否则一旦多个图表都要数据前端逻辑会越来越乱。5. 系统部署从本地启动到 Docker Compose 容器化5.1 本地跑通的最低配置清单如果你拿到源码第一件事是想在本地跑起来按下面这个清单准备十分钟之内应该能看到登录页。安装 JDK 1.8 或 11我推荐 11Spring Boot 2.7 支持得比较好配置好 JAVA_HOME。安装 Maven 3.6配置阿里云镜像加速依赖下载。安装 MySQL 8.0执行项目里sql/init.sql初始化数据库。安装 Redis 5.0默认端口 6379。修改application.yml中的数据库账号密码和 Redis 密码。命令行执行mvn spring-boot:run启动后端默认端口 8080。前端项目npm install然后npm run dev默认端口 5173Vite 代理转发到 8080。这个流程我在不同电脑上跑过很多遍最容易出问题的就是 MySQL 连接串。如果连接串没带serverTimezoneAsia/Shanghai会报The server time zone value ...的错误需要把时区写清楚。5.2 Docker 部署一套命令搞定前后端项目做到后期要考虑部署到服务器。如果用传统方式先装 JDK、装 MySQL、装 Redis、装 Nginx再手动打包上传启动每次更新都要重复一遍部署流程。我选择用 Docker Compose 把一套环境编排起来服务器上只需要有 Docker 环境即可。大致结构是docker-compose.yml backend/ Dockerfile target/ranch-0.0.1-SNAPSHOT.jar frontend/ Dockerfile dist/ mysql/ init.sqldocker-compose.yml里定义四个服务version: 3.8 services: mysql: image: mysql:8.0 container_name: ranch-mysql environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: ranch ports: - 3306:3306 volumes: - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql - mysql-data:/var/lib/mysql redis: image: redis:7-alpine container_name: ranch-redis ports: - 6379:6379 command: redis-server --requirepass redis123456 backend: build: ./backend container_name: ranch-backend depends_on: - mysql - redis ports: - 8080:8080 environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/ranch?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: root123456 SPRING_DATA_REDIS_HOST: redis SPRING_DATA_REDIS_PASSWORD: redis123456 frontend: build: ./frontend container_name: ranch-frontend depends_on: - backend ports: - 80:80 volumes: mysql-data:这里有个关键细节容器内访问 MySQL 和 Redis不能用 localhost因为每个容器是一个独立网络要用服务名mysql和redis作为主机名。Spring Boot 的配置通过环境变量覆盖掉application.yml里的默认值这样本地开发和 Docker 部署就互不干扰了。前端 Dockerfile 分多阶段构建先用 node 镜像执行npm run build再把 dist 目录拷到 nginx 镜像里同时把 nginx 配置里 /api 代理到 backend:8080 写好。部署上线后访问服务器 IP 就能直接看到系统。配置 Nginx 时还有个点容易被忽略就是前端路由是 history 模式的话刷新非首页会 404需要在 nginx 配置里加try_files $uri $uri/ /index.html;。这个坑我踩过一次调试了半小时才反应过来。6. 这套系统开发中踩过的 5 个典型坑及排查思路最后一个部分专门讲我在开发这套 SpringBoot 牧场管理系统时切切实实遇到并解决的问题。这些问题如果你搜一下几乎都能找到对应答案但把完整的排查链路放在一篇帖子里会更直观。每个问题我都经历了从现象出现到定位根因再到修复验证的完整过程。6.1 启动报错Table ranch.xxx doesnt exist现象后端执行mvn spring-boot:run启动日志里出现Table ranch.xxx doesnt exist但明明已经执行过init.sql。排查过程第一反应是init.sql没执行成功于是登录 MySQL 客户端查看发现表确实存在命名空间也正确。接着怀疑是数据源连错了库检查application.yml数据库名是ranch没问题。最后打开 MyBatis Plus 的 SQL 日志发现执行的 SQL 里表名是livestock但 MySQL 实际建的表名是live_stock——我在init.sql和实体类上用的TableName不一致。根因书写 SQL 初始化脚本时我按自己的习惯把表名命名为livestock但后来在想加活体存栏量统计功能时把实体类复制出来改成了LiveStockStatistic的命名风格结果TableName写错了。修复统一用一份表名清单来管理所有表名实体注解和 SQL 建表脚本都必须从这份清单里抄。彻底解决了这个问题。6.2 前端请求 401登录状态失效与跨域同时出现现象前端登录成功后第一次调用列表接口返回 401 Unauthorized。排查过程先在浏览器开发者工具里看请求头确认Authorization带了 token。后端 Controller 的单测正常说明是过滤器拦截逻辑有问题。再仔细看发现第一次请求是从http://localhost:5173发往http://localhost:8080是跨域请求。跨域预检请求OPTIONS直接到了过滤器而我的过滤器把所有没有 token 的请求都直接返回 401没有放行OPTIONS。根因Spring Security 默认会拦截所有请求CORS 预检请求需要特殊放行。修复在 Spring Security 配置类里把/api/**加入permitAll并且调用cors()开启跨域支持。同时写一个全局的CorsFilter允许前端地址访问。6.3 LocalDateTime 序列化后前端拿到的是数组现象后端接口返回的createTime字段在swagger调试里是正常的字符串但前端拿到的是一个类似[2024, 1, 12, 15, 30, 0]的数组。排查过程搜索发现这是 Jackson 对 Java 8 时间类型的默认序列化方式导致的。后端application.yml里没有配置spring.jackson.date-formatJackson 会默认把LocalDateTime序列化成数组。根因spring-boot-starter-web默认会注册JSR310Module对LocalDateTime的处理方式是WRITE_DATES_AS_TIMESTAMPS。修复在application.yml里加配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai另外如果你用的是 Java 8 以上更彻底的方式是自定义ObjectMapper的序列化器。但配置文件方案最省事推荐先用这个。6.4 MyBatis Plus 分页不生效明明设置了 Page 参数结果却返回全部数据现象调用selectPage(new Page(1, 10), wrapper)前端拿到的 total 和当前页数据不对总数是全表的 count而不是过滤后的 count而且返回了所有行。排查过程这个问题几乎和 MyBatis Plus 相关的帖子绑定出现。我先检查wrapper条件有没有传对用 debug 确认条件正常。然后查 MyBatis Plus 的官方文档发现重点提示分页插件必须配置MybatisPlusInterceptor。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }根因没有注册分页插件MyBatis Plus 的selectPage实际执行时仍然是普通查询分页功能由内存假分页替代所以 result 是全量数据total 也是全量 count。修复加上上面的配置类。这个知识点几乎每次用 MyBatis Plus 做分页都会遇到建议大家彻底记住。6.5 Docker 部署后前端请求超时日志没有任何报错现象本地一切正常Docker Compose 部署到服务器后前端页面能打开但任何接口请求都超时后端日志没有任何报错。排查过程先确认后端容器是否正常启动docker ps显示三个容器都在运行。再进容器里面测试接口curl localhost:8080/api/...返回正常。然后从宿主机测试后端 8080 端口也通。问题聚焦到 Nginx 转发环节。查看 Nginx 容器日志发现大量upstream timed out。根因Nginx 容器内的proxy_pass配置的是http://127.0.0.1:8080但在容器网络里每两个容器是相互隔离的127.0.0.1指向的是 Nginx 容器自身并不指向后端。修复把proxy_pass改成http://backend:8080backend 就是 Compose 文件里定义的服务名。这个坑本质是容器网络模式原理不够熟悉但在 Docker 部署 SpringBoot 项目时非常常见值得单独记录。写在最后的个人体会这套系统我从数据库设计到部署上线断断续续用了三周回过头来看最大的收获其实不在写了多少行代码而是把一个模糊的牧场管理系统需求一步步拆解成功能模块、表结构、接口定义再落地的过程。如果你也准备动手做类似的管理系统我的建议是先把业务想清楚再动手写代码项目跑通之后再回头看看哪些表设计有冗余、哪些接口可以优化。源码版本的代码结构我做了分包清晰化处理controller、service、mapper、entity、vo各司其职直接改造成其他领域的XX管理系统也很快。最后再分享一个小技巧如果你的项目里也涉及定时任务一定要把任务日志打印出来否则某天预警没触发你根本不知道是任务没跑还是条件不满足。这算是我个人在实际操作中得到的最深刻的一条经验。