☰
基于SpringBoot+Vue的家禽养殖系统:从进雏到出栏的业务闭环与避坑指南
2026/10/14 3:44:46 网站建设 项目流程

简介:这是一套基于SpringBoot与Vue实现的家禽养殖管理系统源码,面向计算机专业学生、课程设计或毕业设计开发者,以及需要搭建养殖场信息化管理平台的初学者与中级开发者。系统围绕角色权限展开,管理员负责账户创建、权限分配与注册申请审核,普通员工完成家禽信息、饲养记录、生长健康状态等日数据导入,物资管理员则管理饲料、药品、医疗器械采购与库存监控,业务模块划分清晰,便于二次开发与功能扩展。压缩包为rar格式,共449个文件,约11.03MB,其中72个java文件承载后端业务逻辑,21个vue与22个js文件构成前端页面与交互,22个xml与yml、sql等文件负责配置与数据库脚本,另有少量png、jpg、md等资源与说明文件。目前已有176人学习下载,适合作为全栈项目练手、课程答辩参考或养殖管理场景的落地原型,帮助读者快速理解前后端分离架构与多角色权限设计的实现思路。

1. 从一张纸质喂料单说起:家禽养殖系统到底在解决什么

去年冬天,一个做养殖设备的朋友给我看他们场里的喂料记录:三本皱巴巴的笔记本,每本记着不同棚的投料量、死淘数、防疫日期。他说最怕两件事,一是月底对不上账,二是棚里出了状况却翻不到是哪天开始异常的。这不是个例,很多中小规模养殖场的信息化程度,还停留在「微信群里报个数、Excel 里凑个表」的阶段。基于 SpringBoot+Vue 的家禽养殖系统,要解决的就是把「棚—批次—日耗—防疫—出栏」这条链路搬到线上,让数据可查、可算、可预警。它适合两类人:一类是养殖场的技术负责人,想用一套自己能维护的系统替代手工台账;另一类是后端或全栈开发者,想找一个业务闭环完整、技术栈主流的练手项目。这篇文章不讲空泛的架构图,只讲这套系统怎么落地、参数怎么定、哪些地方最容易翻车。

2. 技术选型与数据模型:为什么是 SpringBoot + Vue,表该怎么拆

2.1 前后端分离的取舍:为什么不用模板引擎一把梭

很多同行第一反应是「养殖系统又不复杂,Thymeleaf 或者 JSP 直接渲染不就完了」。我一开始也这么想,直到遇到两个现实问题。第一,养殖场的网络环境普遍一般,棚区离办公区远,前端如果每次操作都整页刷新,体验很差;第二,场里往往不止一个角色——饲养员用手机端报数,场长用电脑看报表,财务导出 Excel,同一套后端要服务多种终端。前后端分离之后,后端只负责出 JSON,Vue 端可以打包成移动端 H5,也可以嵌到桌面浏览器里,复用同一套接口。

具体到版本组合,我一般会选 SpringBoot 2.7.x 搭配 Vue 3 + Vite。SpringBoot 2.7 是 2.x 的最后一个稳定分支,生态成熟,网上资料多,遇到问题好搜;Vue 3 的组合式 API 在写养殖这种表单密集、状态较多的页面时,逻辑聚合比选项式舒服很多。数据库用 MySQL 8.0,字符集统一 utf8mb4,因为养殖记录里偶尔会出现特殊符号或者 emoji 备注。

依赖上,后端核心就几个:spring-boot-starter-web、mybatis-plus、mysql-connector-j、hutool 工具包、easyexcel 做导出。权限用 Spring Security + JWT,不引入太重的框架。前端用 axios 封装请求,element-plus 做组件库,echarts 做报表。

<!-- pom.xml 关键依赖,版本按需调整 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.20</version> </dependency>

这段依赖里,mybatis-plus 是重点。养殖系统的查询大多是「按棚查、按批次查、按日期区间查」,用 MP 的 LambdaQueryWrapper 能省掉大量 XML。hutool 主要用来做日期计算和 Excel 导出,比手写工具类省事。

2.2 核心表结构:棚、批次、日记录三张表撑起主链路

养殖系统的数据模型,说复杂也复杂,说简单也简单。核心就三张表:棚舍表、批次表、每日记录表。其他像饲料库存、防疫记录、出栏记录,都是围绕这三张表展开的。

棚舍表存的是物理空间信息:棚号、类型(育雏棚/育肥棚)、容量、负责人。批次表是业务核心,一个批次对应「一批鸡从进雏到出栏」的完整周期,字段包括批次号、进雏日期、进雏数量、当前存栏、状态(在养/已出栏/已清栏)。每日记录表是流水,每天每个棚一条,记录投料量、饮水量、死淘数、温度、湿度、异常备注。

CREATE TABLE `poultry_house` ( `id` bigint NOT NULL AUTO_INCREMENT, `house_no` varchar(32) NOT NULL COMMENT '棚号', `house_type` tinyint DEFAULT '1' COMMENT '1育雏 2育肥', `capacity` int DEFAULT '0' COMMENT '设计容量', `manager` varchar(32) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_house_no` (`house_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `poultry_batch` ( `id` bigint NOT NULL AUTO_INCREMENT, `batch_no` varchar(40) NOT NULL COMMENT '批次号', `house_id` bigint NOT NULL, `start_date` date NOT NULL COMMENT '进雏日期', `init_count` int NOT NULL COMMENT '进雏数量', `current_count` int NOT NULL COMMENT '当前存栏', `status` tinyint DEFAULT '1' COMMENT '1在养 2已出栏 3已清栏', PRIMARY KEY (`id`), UNIQUE KEY `uk_batch_no` (`batch_no`), KEY `idx_house` (`house_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `daily_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `batch_id` bigint NOT NULL, `record_date` date NOT NULL, `feed_amount` decimal(10,2) DEFAULT '0.00' COMMENT '投料kg', `water_amount` decimal(10,2) DEFAULT '0.00' COMMENT '饮水L', `dead_count` int DEFAULT '0' COMMENT '死淘数', `temperature` decimal(5,2) DEFAULT NULL, `humidity` decimal(5,2) DEFAULT NULL, `remark` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_batch_date` (`batch_id`,`record_date`), KEY `idx_date` (`record_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有个关键设计:daily_record上建了(batch_id, record_date)的唯一索引。这是血泪经验——如果不加,饲养员手滑提交两次,存栏和料肉比全乱。加了唯一索引之后,后端捕获 DuplicateKeyException 直接返回「今日已填报」,比在代码里先查后插可靠得多。

2.3 存栏与死淘的联动逻辑:别把计算放在前端

很多新手会把「当前存栏 = 进雏数 - 累计死淘」这个计算放在 Vue 里做,页面一刷新就重算。这样做的问题是:一旦有人改了历史死淘记录,存栏就对不上了。正确做法是后端在每次新增或修改日记录时,用事务重算该批次的 current_count。

@Transactional(rollbackFor = Exception.class) public void saveDailyRecord(DailyRecordDTO dto) { // 1. 校验批次是否存在且在养 PoultryBatch batch = batchMapper.selectById(dto.getBatchId()); if (batch == null || batch.getStatus() != 1) { throw new BizException("批次不存在或已结束"); } // 2. 保存日记录,唯一索引兜底重复提交 DailyRecord record = BeanUtil.copyProperties(dto, DailyRecord.class); dailyRecordMapper.insert(record); // 3. 重算存栏:进雏数 - 该批次累计死淘 Integer totalDead = dailyRecordMapper.sumDeadByBatch(dto.getBatchId()); int current = batch.getInitCount() - (totalDead == null ? 0 : totalDead); batch.setCurrentCount(Math.max(current, 0)); batchMapper.updateById(batch); }

这段代码的重点在第三步:每次写日记录都全量重算,而不是增量加减。全量重算在单批次数据量(通常几十到几百条)下性能完全够用,而且天然避免了「改历史记录导致存栏漂移」的问题。Math.max(current, 0)是防御性写法,防止数据异常导致存栏为负。

3. 从进雏到出栏:把业务闭环跑通的最小实现

3.1 批次生命周期管理:状态机比 if-else 靠谱

批次的状态流转是这套系统的骨架:进雏后是「在养」,全部出栏后是「已出栏」,清栏消毒后是「已清栏」。这三个状态之间的流转必须受控,不能随便改。我见过有项目直接在页面上放个下拉框让用户改状态,结果出现「已清栏的批次还在产生日记录」这种脏数据。

常见做法是用一个简单的状态机来约束。在 Service 层定义允许的流转:

public enum BatchStatus { BREEDING(1, "在养"), SOLD(2, "已出栏"), CLEARED(3, "已清栏"); // 允许的流转:在养->已出栏->已清栏 public static boolean canTransfer(int from, int to) { if (from == 1 && to == 2) return true; if (from == 2 && to == 3) return true; return false; } }

出栏操作时,除了改状态,还要记录出栏数量和出栏日期,并校验出栏数量不能大于当前存栏。清栏操作则要求该批次没有未处理的异常记录。这些校验放在 Service 里,Controller 只做参数接收和结果包装。

3.2 日记录填报接口:参数校验和幂等怎么做

日记录填报是使用频率最高的接口,也是最容易出问题的地方。参数校验用 spring-boot-starter-validation,在 DTO 上打注解:

@Data public class DailyRecordDTO { @NotNull(message = "批次不能为空") private Long batchId; @NotNull(message = "日期不能为空") @PastOrPresent(message = "不能填报未来日期") private LocalDate recordDate; @DecimalMin(value = "0", message = "投料量不能为负") private BigDecimal feedAmount; @Min(value = 0, message = "死淘数不能为负") private Integer deadCount; @DecimalRange(min = -10, max = 50, message = "温度超出合理范围") private BigDecimal temperature; }

@PastOrPresent这个注解很关键。养殖场经常有人提前把明天的记录填了,结果当天实际数据对不上。限制不能填未来日期,能挡掉大部分误操作。温度范围 -10 到 50 是经验值,超出这个范围要么是传感器坏了,要么是填错了。

幂等性靠数据库唯一索引 + 全局异常处理。在 GlobalExceptionHandler 里捕获 DuplicateKeyException,返回友好提示:

@ExceptionHandler(DuplicateKeyException.class) public Result<?> handleDuplicateKey(DuplicateKeyException e) { return Result.fail("该批次今日记录已存在,请勿重复提交"); }

这样即使前端因为网络卡顿重复提交,后端也不会产生两条记录。

3.3 报表与导出:料肉比和死淘率怎么算

养殖场最关心的两个指标:料肉比和死淘率。料肉比 = 累计投料量 / 累计增重,死淘率 = 累计死淘数 / 进雏数。这两个指标在批次详情页实时计算,用 SQL 聚合比在 Java 里循环快得多。

SELECT b.batch_no, b.init_count, b.current_count, IFNULL(SUM(d.feed_amount), 0) AS total_feed, IFNULL(SUM(d.dead_count), 0) AS total_dead, ROUND(IFNULL(SUM(d.dead_count), 0) / b.init_count * 100, 2) AS dead_rate FROM poultry_batch b LEFT JOIN daily_record d ON d.batch_id = b.id WHERE b.id = #{batchId} GROUP BY b.id;

导出用 EasyExcel,把查询结果直接写进模板。注意导出时要加一个上限,比如单次最多导出 5000 行,防止有人导出全量数据把内存撑爆。前端用 axios 的 responseType: 'blob' 接收,拿到之后用 URL.createObjectURL 触发下载。

4. 避坑与排查:那些让我返工三次的问题

4.1 日期时区问题:数据库存进去差了一天

现象:前端选的日期是 2024-03-15,存到数据库变成 2024-03-14。原因:MySQL 的时区配置和 JVM 时区不一致,或者 JDBC 连接串没指定时区。解决:在 application.yml 的 JDBC URL 里显式加上serverTimezone=Asia/Shanghai,同时确认 MySQL 的time_zone参数。如果是 Docker 部署,还要检查容器时区。

spring: datasource: url: jdbc:mysql://localhost:3306/poultry?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

4.2 并发填报导致存栏算错

现象:两个饲养员同时给同一批次填日记录,存栏数偶尔会多算或少算。原因:重算存栏时没有加锁,两个事务同时读到旧的累计死淘数。解决:在批次表上加乐观锁版本号,或者用SELECT ... FOR UPDATE锁住批次行。我一般用乐观锁,侵入性小。

@Version private Integer version;

在 PoultryBatch 实体上加 @Version 注解,MyBatis-Plus 会自动处理版本比对。更新时如果版本不一致,会返回影响行数 0,Service 里判断后重试或提示用户。

4.3 前端表格分页参数传错

现象:报表页翻到第二页,数据还是第一页的。原因:Element Plus 的分页组件 current-page 从 1 开始,而 MyBatis-Plus 的 Page 对象当前页也是从 1 开始,但有些开发者习惯性减 1,导致偏移。解决:统一约定,前端传 pageNum 从 1 开始,后端直接new Page<>(pageNum, pageSize),不做加减。在接口文档里写清楚。

4.4 导出 Excel 中文乱码

现象:导出的文件名是乱码,或者打开后中文变问号。原因:响应头没设置编码,或者 EasyExcel 的写出的流没指定字符集。解决:设置 Content-Disposition 时用 URLEncoder 编码文件名,并指定Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet。

response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String fileName = URLEncoder.encode("养殖报表", StandardCharsets.UTF_8).replaceAll("\\+", "%20"); response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");

4.5 权限控制漏掉数据级隔离

现象:A 场的场长登录后能看到 B 场的数据。原因:只做了接口级权限,没做数据级过滤。解决:在棚舍表加org_id字段,所有查询都带上当前用户的 org_id 条件。这个字段在登录时从 JWT 里取,通过 MyBatis-Plus 的拦截器自动拼接,避免每个查询手写。

5. 进阶技巧:让系统从「能用」到「好用」

5.1 用定时任务做异常预警

养殖系统真正的价值不在记录,而在预警。我一般会加一个每天凌晨执行的定时任务,扫描前一天的日记录,发现以下情况就生成预警:死淘率超过阈值(比如育雏期超过 0.5%)、温度连续两天超出范围、投料量突然下降超过 30%。预警信息推到站内消息,也可以对接短信。

@Scheduled(cron = "0 0 2 * * ?") public void checkAbnormal() { LocalDate yesterday = LocalDate.now().minusDays(1); List<DailyRecord> records = dailyRecordMapper.selectByDate(yesterday); for (DailyRecord r : records) { PoultryBatch batch = batchMapper.selectById(r.getBatchId()); // 死淘率预警 if (r.getDeadCount() != null && batch.getInitCount() > 0) { double rate = r.getDeadCount() * 1.0 / batch.getInitCount(); if (rate > 0.005) { alertService.createAlert(batch.getId(), "死淘率偏高:" + String.format("%.2f%%", rate * 100)); } } // 温度预警 if (r.getTemperature() != null && (r.getTemperature().compareTo(new BigDecimal("35")) > 0 || r.getTemperature().compareTo(new BigDecimal("18")) < 0)) { alertService.createAlert(batch.getId(), "温度异常:" + r.getTemperature() + "℃"); } } }

阈值不是拍脑袋定的。育雏期第一周温度要求 32-35℃,之后每周降 2-3℃;死淘率育雏期控制在 0.5% 以内,育肥期 0.2% 以内。这些参数建议做成配置项存在数据库里,不同场、不同品种可以调。

5.2 用 Redis 缓存报表查询

批次详情页的聚合查询,如果每次刷新都打数据库,在批次多的时候会有压力。我一般把聚合结果缓存 5 分钟,key 用report:batch:{batchId}。注意在日记录新增或修改时主动删掉对应 key,避免脏数据。

public BatchReportVO getReport(Long batchId) { String key = "report:batch:" + batchId; BatchReportVO cached = redisTemplate.opsForValue().get(key); if (cached != null) return cached; BatchReportVO vo = batchMapper.selectReport(batchId); redisTemplate.opsForValue().set(key, vo, 5, TimeUnit.MINUTES); return vo; }

5.3 移动端适配:饲养员真正会用的是手机

最后说一个容易被忽略的点。很多开发者把系统做得漂漂亮亮,结果饲养员根本不用,因为棚里没有电脑。Vue 端一定要做移动端适配,表单字段尽量少,投料量和死淘数用数字键盘,日期默认今天。我一般会用 postcss-px-to-viewport 做整体缩放,关键操作按钮固定在底部,单手能点到。填报页面加载速度比功能丰富更重要,棚区信号不好时,首屏超过 3 秒基本就没人用了。

这套系统我从第一版到现在改了七八次,最大的教训是:别一上来就追求大而全。先把「进雏—日记录—出栏」这条主链路跑通,让饲养员愿意每天填,再慢慢加防疫、库存、报表。数据填不起来,功能再多也是摆设。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询