ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于SpringBoot+Vue的家禽养殖系统:从数据对不上到全链路跑通

基于SpringBoot+Vue的家禽养殖系统:从数据对不上到全链路跑通

简介:这是一套基于SpringBoot与Vue实现的家禽养殖管理系统源码,面向计算机专业学生、课程设计开发者及需要企业级管理后台参考的Java学习者。系统围绕养殖场日常运营展开,划分管理员、普通员工与物资管理员三类角色:管理员负责账户创建、权限分配与注册申请审核;普通员工完成家禽信息、饲养记录、生长健康状态等日数据导入,并执行喂养与环境清洁任务;物资管理员则管理饲料、药品、医疗器械的采购与库存监控,维护供应商列表以保障物资质量。资源包共449个文件,以72个Java后端源码、21个Vue组件、22个JavaScript脚本及22个XML配置为主,另含SQL建表脚本、YAML配置与少量图片资源,压缩包约11.03MB,前后端结构完整。目前已有176人学习下载,适合作为毕业设计、课程作业或全栈练手项目,帮助读者快速理解角色权限设计与养殖业务模块的实现思路。

1. 家禽养殖系统为什么总在“数据对不上”这一步翻车

养了三年鸡的老张最近很烦。他那个存栏两万只的养殖场,每天喂了多少料、死了多少只、产了多少蛋,全靠三本手写台账。月底一算账,饲料消耗比理论值高了百分之八,死淘率对不上兽医记录,产蛋数跟收购单差了两百多枚。问题出在哪?不是他不会养鸡,是数据在鸡舍、仓库、财务三个环节各记各的,中间全靠人脑和微信语音传递。

基于SpringBoot+Vue的家禽养殖系统,要解决的就是这个“数据对不上”的问题。它把进雏、饲喂、免疫、用药、产蛋、死淘、出栏、库存、成本核算串成一条线,让每个环节的数据从哪来到哪去都有迹可循。适合谁用?中小型养殖场的场长、技术员,以及想用一套完整项目练手全栈开发的学生或初级工程师。这篇文章不讲空泛的架构图,只讲怎么把这条数据链跑通、参数怎么设、哪里容易翻车。

2. 从鸡舍到数据库:家禽养殖系统的领域建模与表结构设计

2.1 先搞清楚“一批鸡”在系统里怎么表示

家禽养殖的核心对象是“批次”。一批鸡从进雏到出栏,所有生产数据都挂在批次下面。常见做法是建三张主表:批次表、日记录表、事件表。批次表存进雏日期、品种、初始数量、当前存栏、鸡舍编号;日记录表存每天的采食量、饮水量、产蛋数、死淘数、平均体重;事件表存免疫、用药、消毒、转群这些非每日发生但必须留痕的操作。

为什么不用一张大宽表?因为日记录是高频写入,事件是低频但字段差异大。分开之后,日记录表可以按批次和日期做联合索引,查询“某批次最近七天采食趋势”时不会扫全表。事件表用类型字段区分,免疫记录存疫苗名称和剂量,用药记录存药品名称和休药期,字段用JSON扩展列存,避免频繁改表结构。

-- 批次表:一批鸡的身份证 CREATE TABLE batch ( id BIGINT PRIMARY KEY AUTO_INCREMENT, batch_no VARCHAR(32) NOT NULL UNIQUE COMMENT '批次编号,如2024-B01', breed VARCHAR(64) COMMENT '品种,如白羽肉鸡、海兰褐', house_no VARCHAR(16) COMMENT '鸡舍编号', initial_count INT NOT NULL COMMENT '进雏数量', current_count INT NOT NULL COMMENT '当前存栏', start_date DATE NOT NULL COMMENT '进雏日期', status TINYINT DEFAULT 1 COMMENT '1在养 2已出栏 3已清栏', INDEX idx_start_date (start_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 日记录表:每天一条,高频写入 CREATE TABLE daily_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, batch_id BIGINT NOT NULL, record_date DATE NOT NULL, feed_kg DECIMAL(10,2) COMMENT '采食量公斤', water_l DECIMAL(10,2) COMMENT '饮水量升', egg_count INT DEFAULT 0 COMMENT '产蛋数', dead_count INT DEFAULT 0 COMMENT '死淘数', avg_weight_g INT COMMENT '平均体重克', UNIQUE KEY uk_batch_date (batch_id, record_date), INDEX idx_batch (batch_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

参数说明:batch_no用业务编号而不是自增ID对外,方便场里人沟通。current_count不通过触发器自动减,而是由服务层在死淘事件里更新,因为死淘可能补录,触发器容易算错。uk_batch_date唯一索引防止同一天重复录入,这是血泪经验——不加上这个约束,月底对账时你会发现同一天有三条记录,分别是技术员、场长、财务各录了一遍。

2.2 存栏数为什么不能只靠“初始减死淘”

新手最容易踩的坑:以为当前存栏 = 初始数量 - 累计死淘。实际养殖中还有淘汰、转群、出售、调拨。比如一批鸡养到中期,把两千只转到另一个鸡舍,这批的存栏要减,那批的要加。如果只记死淘,转群之后两边数据全乱。

常见做法是引入“存栏变动流水表”,每次数量变化都插一条记录,字段包括批次ID、变动类型(死淘/转群出/转群入/出售/淘汰)、变动数量、变动后存栏、操作时间。当前存栏直接读批次表的冗余字段,但每天凌晨跑一次对账任务,用流水表重算存栏,和冗余字段比对,不一致就告警。这样既保证查询快,又保证数据可追溯。

// 存栏变动服务:每次变动都写流水并更新冗余 @Service public class StockService { @Transactional public void changeStock(Long batchId, String type, int delta, String remark) { Batch batch = batchMapper.selectById(batchId); int newCount = batch.getCurrentCount() + delta; if (newCount < 0) { throw new BizException("存栏不能为负,当前" + batch.getCurrentCount() + ",变动" + delta); } // 写流水 StockFlow flow = new StockFlow(); flow.setBatchId(batchId); flow.setChangeType(type); flow.setDelta(delta); flow.setAfterCount(newCount); flow.setRemark(remark); stockFlowMapper.insert(flow); // 更新冗余 batch.setCurrentCount(newCount); batchMapper.updateById(batch); } }

逻辑说明:delta用正负表示增减,转群出传负数,转群入传正数。newCount < 0校验必须做,否则一次误操作就能把存栏搞成负数,后面所有报表全废。对账任务用SELECT batch_id, SUM(delta) FROM stock_flow GROUP BY batch_id和批次表比对,差异超过阈值就发通知。

2.3 免疫和用药记录怎么和休药期联动

家禽养殖绕不开用药,但用药之后有休药期,休药期内产的蛋不能卖。系统如果不做联动,全靠人记,迟早出事。设计上,用药事件表加两个字段:drug_name和withdraw_days。当录入一条用药记录时,自动计算withdraw_end_date = 用药日期 + withdraw_days,并写入批次表的lock_until字段。产蛋记录提交时,如果当前日期小于lock_until,弹窗提示“该批次处于休药期,产蛋不可销售”,但允许记录为“非销售蛋”。

这个逻辑不复杂,但参数要留活口。不同药品休药期不同,同一药品不同给药方式休药期也不同。常见做法是建一个药品字典表,存药品名、默认休药期、给药方式,录入时带出默认值,允许手动改。改的时候要填原因,留审计痕迹。

3. 用SpringBoot把生产数据接口跑通:从录入到校验的完整链路

3.1 日记录录入接口的防重与校验

日记录是系统里写入最频繁的接口,一天可能几百条。设计不好,要么重复录入,要么并发写冲突。常见做法是用batch_id + record_date做唯一键,插入时用INSERT ... ON DUPLICATE KEY UPDATE,但这样会覆盖之前的数据,不适合需要留痕的场景。更稳的做法是先查后插,查的时候加行锁。

@PostMapping("/daily") public Result saveDaily(@RequestBody @Valid DailyRecordDTO dto) { // 1. 校验批次是否存在且在养 Batch batch = batchMapper.selectById(dto.getBatchId()); if (batch == null || batch.getStatus() != 1) { return Result.fail("批次不存在或已结束"); } // 2. 校验日期不能晚于今天 if (dto.getRecordDate().isAfter(LocalDate.now())) { return Result.fail("不能录入未来日期"); } // 3. 查重,加锁防并发 DailyRecord exist = dailyMapper.selectForUpdate(dto.getBatchId(), dto.getRecordDate()); if (exist != null) { return Result.fail("该批次当日记录已存在,请勿重复录入"); } // 4. 死淘数不能超过当前存栏 if (dto.getDeadCount() > batch.getCurrentCount()) { return Result.fail("死淘数超过当前存栏"); } // 5. 保存并更新存栏 DailyRecord record = new DailyRecord(); BeanUtils.copyProperties(dto, record); dailyMapper.insert(record); if (dto.getDeadCount() > 0) { stockService.changeStock(dto.getBatchId(), "DEAD", -dto.getDeadCount(), "日记录死淘"); } return Result.ok(); }

参数说明:selectForUpdate在Mapper里写SELECT * FROM daily_record WHERE batch_id=? AND record_date=? FOR UPDATE,利用InnoDB行锁防止两个请求同时查到空然后都插入。deadCount校验用>而不是>=,因为死淘等于存栏是允许的,意味着清栏。死淘更新存栏走StockService,保证流水和冗余一致。

3.2 产蛋数据的异常值过滤

产蛋数受品种、日龄、季节、光照影响,波动正常,但突然翻倍或减半大概率是录入错误。系统里加一层软校验:根据批次品种和日龄,查标准产蛋曲线,实际值偏离标准值超过百分之三十时,标记为“待确认”,不直接拒绝,但推送给技术员复核。

// 产蛋异常检测:偏离标准曲线30%以上标记 public boolean isEggCountAbnormal(Batch batch, int eggCount, LocalDate date) { int ageDays = (int) ChronoUnit.DAYS.between(batch.getStartDate(), date); // 标准曲线从配置表读,按品种和日龄查 StandardCurve curve = curveMapper.selectByBreedAndAge(batch.getBreed(), ageDays); if (curve == null) { return false; // 没有标准曲线就不校验 } double expected = curve.getEggRate() * batch.getCurrentCount(); if (expected == 0) { return false; } double deviation = Math.abs(eggCount - expected) / expected; return deviation > 0.3; }

逻辑说明:eggRate是产蛋率,从标准曲线表读,比如海兰褐在200日龄产蛋率约0.92。expected是理论产蛋数。偏离超过百分之三十标记异常,前端显示黄色感叹号,技术员点确认后才进入统计。这个阈值可以配置,不同品种、不同季节可以调。注意不要用硬编码,否则换品种就得改代码。

3.3 批次成本核算的定时任务

养殖场最关心的是这批鸡到底赚没赚钱。成本包括鸡雏、饲料、药品、疫苗、人工、水电、折旧。前四项能从系统数据算,后三项通常按批次分摊。常见做法是每天凌晨跑一个定时任务,汇总当天各批次的饲料消耗、药品消耗,写入成本日汇总表。出栏时再算总成本。

@Scheduled(cron = "0 30 1 * * ?") // 每天凌晨1:30执行 public void calcDailyCost() { List<Batch> activeBatches = batchMapper.selectActive(); for (Batch batch : activeBatches) { LocalDate yesterday = LocalDate.now().minusDays(1); // 饲料成本:从日记录读采食量,乘以饲料单价 DailyRecord record = dailyMapper.selectByBatchAndDate(batch.getId(), yesterday); BigDecimal feedCost = BigDecimal.ZERO; if (record != null && record.getFeedKg() != null) { BigDecimal price = feedPriceService.getPrice(batch.getBreed(), yesterday); feedCost = record.getFeedKg().multiply(price); } // 药品成本:从事件表读当天用药记录 BigDecimal drugCost = eventMapper.sumDrugCost(batch.getId(), yesterday); // 写入成本汇总 CostSummary summary = new CostSummary(); summary.setBatchId(batch.getId()); summary.setCostDate(yesterday); summary.setFeedCost(feedCost); summary.setDrugCost(drugCost); costSummaryMapper.insert(summary); } }

参数说明:cron表达式按服务器时区,如果服务器在UTC,要改成对应时间。feedPriceService.getPrice按品种和日期查,因为饲料价格会波动,不能用一个固定值算全程。sumDrugCost在事件表里按类型过滤,只算用药和疫苗,不算消毒。这个任务要加失败重试,某天没跑成功,第二天要补算,否则成本缺一天。

4. Vue前端怎么把“鸡舍现场”搬进浏览器

4.1 批次看板:一屏看清所有在养批次

场长最常看的页面是批次看板。需求很简单:每个在养批次一张卡片,显示批次号、品种、日龄、当前存栏、今日采食、今日产蛋、死淘率。数据从/api/batch/overview一次拉回,前端用v-for渲染。关键点是日龄计算和死淘率格式化。

// 批次看板组件核心逻辑 export default { data() { return { batches: [], loading: false }; }, methods: { async fetchOverview() { this.loading = true; try { const res = await axios.get('/api/batch/overview'); this.batches = res.data.map(item => ({ ...item, // 日龄:从进雏日期到今天 ageDays: dayjs().diff(dayjs(item.startDate), 'day'), // 死淘率:累计死淘/初始数量 deadRate: item.initialCount > 0 ? ((item.initialCount - item.currentCount) / item.initialCount * 100).toFixed(2) : '0.00' })); } finally { this.loading = false; } } }, mounted() { this.fetchOverview(); // 每5分钟刷新一次 this.timer = setInterval(this.fetchOverview, 5 * 60 * 1000); }, beforeDestroy() { clearInterval(this.timer); } };

逻辑说明:dayjs().diff算日龄,注意时区问题,如果服务器返回的是UTC日期,要转成本地再算。deadRate用累计死淘除以初始数量,不是除以当前存栏,否则分母越来越小,死淘率会虚高。定时刷新用setInterval,组件销毁时要clearInterval,否则内存泄漏。这个页面在手机浏览器上也要能看,卡片布局用flex换行,不要写死宽度。

4.2 日记录表单的联动校验

录入日记录时,前端要做几项即时校验:日期不能选未来、死淘数不能超过当前存栏、采食量不能为负。这些校验后端也会做,但前端先拦一道,减少无效请求。用Element UI的el-form规则加上自定义校验函数。

// 日记录表单校验规则 rules: { recordDate: [ { required: true, message: '请选择日期', trigger: 'change' }, { validator: (rule, value, callback) => { if (value && dayjs(value).isAfter(dayjs(), 'day')) { callback(new Error('不能录入未来日期')); } else { callback(); } }, trigger: 'change' } ], deadCount: [ { required: true, message: '请输入死淘数', trigger: 'blur' }, { validator: (rule, value, callback) => { if (value > this.currentBatch.currentCount) { callback(new Error('死淘数不能超过当前存栏' + this.currentBatch.currentCount)); } else { callback(); } }, trigger: 'blur' } ], feedKg: [ { type: 'number', min: 0, message: '采食量不能为负', trigger: 'blur' } ] }

参数说明:dayjs(value).isAfter(dayjs(), 'day')比较到天,同一天允许。this.currentBatch从父组件传入,切换批次时要重置表单。feedKg用type: 'number'而不是type: 'integer',因为采食量可能有小数。校验触发用blur而不是change,避免输入过程中频繁报错。

4.3 用ECharts画产蛋曲线和采食趋势

技术员需要看趋势,判断鸡群是否正常。产蛋曲线和采食趋势用ECharts折线图,数据从/api/report/trend?batchId=x&days=30拉。关键点是横轴日期要连续,不能因为某天没记录就断掉。

// 产蛋趋势图配置 initEggChart(data) { const dates = []; const eggCounts = []; const expectedCounts = []; // 补齐缺失日期 const start = dayjs().subtract(29, 'day'); for (let i = 0; i < 30; i++) { const d = start.add(i, 'day').format('YYYY-MM-DD'); dates.push(d); const found = data.find(item => item.recordDate === d); eggCounts.push(found ? found.eggCount : null); expectedCounts.push(found ? found.expectedCount : null); } this.chart.setOption({ xAxis: { type: 'category', data: dates }, yAxis: { type: 'value', name: '产蛋数' }, series: [ { name: '实际产蛋', type: 'line', data: eggCounts, connectNulls: false }, { name: '标准曲线', type: 'line', data: expectedCounts, lineStyle: { type: 'dashed' } } ], tooltip: { trigger: 'axis' } }); }

逻辑说明:connectNulls: false让缺失日期断开,一眼能看出哪天没录。标准曲线用虚线,实际用实线,偏离大时技术员能直接看到。expectedCount从后端算好返回,前端不做业务计算。图表容器要设固定高度,否则ECharts在flex布局里可能高度为0。

5. 避坑与排查:家禽养殖系统落地时最容易踩的五个坑

5.1 坑一:存栏数越用越乱,最后没人信系统

现象:用了两个月,场里人发现系统显示的存栏和实际对不上,差几十只,于是又回去用手写台账。原因:死淘录入不及时,或者转群没录,或者补录时只改了日记录没走存栏变动。解决:第一,日记录提交时死淘必须走StockService,不允许直接改批次表;第二,每天凌晨对账任务跑一遍,差异超过千分之五就推送告警给场长;第三,转群操作做成独立功能,必须选转出批次和转入批次,两边同时更新。关键是让场里人知道,系统数据比手写准,他们才会用。

5.2 坑二:休药期没锁住,产蛋记录照样能提交

现象:技术员录了用药记录,但产蛋录入时没提示休药期,鸡蛋照样标记为可销售。原因:用药事件和产蛋记录之间没有联动,或者lock_until字段没写进批次表。解决:用药保存时同步更新批次表的lock_until,产蛋提交时查这个字段,在休药期内强制标记为“非销售蛋”,并且前端用红色横幅提示。如果业务允许手动解锁,必须填解锁原因并记录操作人。

5.3 坑三:日记录重复录入,月底对账发现同一天三条

现象:技术员录了一次,场长又录了一次,财务补录一次,同一天三条记录,统计时不知道用哪条。原因:没有唯一约束,或者前端没做防重提交。解决:数据库加uk_batch_date唯一索引,接口层先查后插加行锁,前端提交按钮点击后置灰三秒。如果确实需要修改,走编辑接口,保留修改日志。不要用INSERT ON DUPLICATE KEY UPDATE,那样会静默覆盖,出了事查不到。

5.4 坑四:成本核算漏算饲料价格波动,算出来全是错的

现象:出栏一算,每只鸡成本比实际低了两块,场长觉得系统在瞎算。原因:饲料价格用了固定值,但实际饲料价格每月都在变。解决:建饲料价格表,按品种和生效日期存价格,成本核算时按日记录日期查对应价格。如果价格表没维护,成本任务要告警而不是用默认值硬算。另外,人工水电这些分摊项要允许手动录入,不能全指望自动算。

5.5 坑五:前端图表在手机上一片空白

现象:场长用手机看产蛋趋势,图表区域白的,什么都没有。原因:ECharts容器高度用了百分比,父元素没有固定高度,算出来是0。解决:图表容器设固定高度,比如height: 300px,或者用vh单位。另外,移动端要监听resize事件,调chart.resize(),否则横竖屏切换后图表变形。如果还不行,检查数据是不是空数组,空数据时ECharts不报错但也不显示。

6. 把系统真正用起来:从演示到日常的最后一公里

很多养殖系统死在演示之后。演示时数据漂亮,真用起来场里人嫌麻烦,录两天就不录了。我自己的习惯是,上线第一周只开一个功能:日记录录入。其他功能全隐藏。等日记录录顺了,再开产蛋和死淘。再一周,开免疫用药。再一周,开报表。每次只加一个功能,场里人不会觉得一下子多了很多活。

验证系统有没有真正用起来,不看登录次数,看一个指标:日记录连续录入天数。连续三十天不断,说明流程跑通了。断一天,就要问为什么。常见原因是那天太忙忘了,或者手机没信号。如果是忘了,加个每天下午五点的提醒;如果是信号问题,允许离线暂存,有网再同步。

最后一个技巧:把批次看板做成大屏,挂在办公室墙上。场长每天路过看一眼,存栏、采食、产蛋、死淘率一目了然。数据不准他自己就会来找你。这比任何培训都管用。我见过太多系统,功能做了一堆,最后没人看,就是因为数据没展示在最显眼的地方。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进