殖系統(tǒng):從數(shù)據(jù)對不上到全鏈路跑通)
簡介這是一套基于SpringBoot與Vue實現(xiàn)的家禽養(yǎng)殖管理系統(tǒng)源碼面向計算機專業(yè)學生、課程設計開發(fā)者及需要企業(yè)級管理后臺參考的Java學習者。系統(tǒng)圍繞養(yǎng)殖場日常運營展開劃分管理員、普通員工與物資管理員三類角色管理員負責賬戶創(chuàng)建、權(quán)限分配與注冊申請審核普通員工完成家禽信息、飼養(yǎng)記錄、生長健康狀態(tài)等日數(shù)據(jù)導入并執(zhí)行喂養(yǎng)與環(huán)境清潔任務物資管理員則管理飼料、藥品、醫(yī)療器械的采購與庫存監(jiān)控維護供應商列表以保障物資質(zhì)量。資源包共449個文件以72個Java后端源碼、21個Vue組件、22個JavaScript腳本及22個XML配置為主另含SQL建表腳本、YAML配置與少量圖片資源壓縮包約11.03MB前后端結(jié)構(gòu)完整。目前已有176人學習下載適合作為畢業(yè)設計、課程作業(yè)或全棧練手項目幫助讀者快速理解角色權(quán)限設計與養(yǎng)殖業(yè)務模塊的實現(xiàn)思路。1. 家禽養(yǎng)殖系統(tǒng)為什么總在“數(shù)據(jù)對不上”這一步翻車養(yǎng)了三年雞的老張最近很煩。他那個存欄兩萬只的養(yǎng)殖場每天喂了多少料、死了多少只、產(chǎn)了多少蛋全靠三本手寫臺賬。月底一算賬飼料消耗比理論值高了百分之八死淘率對不上獸醫(yī)記錄產(chǎn)蛋數(shù)跟收購單差了兩百多枚。問題出在哪不是他不會養(yǎng)雞是數(shù)據(jù)在雞舍、倉庫、財務三個環(huán)節(jié)各記各的中間全靠人腦和微信語音傳遞?;赟pringBootVue的家禽養(yǎng)殖系統(tǒng)要解決的就是這個“數(shù)據(jù)對不上”的問題。它把進雛、飼喂、免疫、用藥、產(chǎn)蛋、死淘、出欄、庫存、成本核算串成一條線讓每個環(huán)節(jié)的數(shù)據(jù)從哪來到哪去都有跡可循。適合誰用中小型養(yǎng)殖場的場長、技術員以及想用一套完整項目練手全棧開發(fā)的學生或初級工程師。這篇文章不講空泛的架構(gòu)圖只講怎么把這條數(shù)據(jù)鏈跑通、參數(shù)怎么設、哪里容易翻車。2. 從雞舍到數(shù)據(jù)庫家禽養(yǎng)殖系統(tǒng)的領域建模與表結(jié)構(gòu)設計2.1 先搞清楚“一批雞”在系統(tǒng)里怎么表示家禽養(yǎng)殖的核心對象是“批次”。一批雞從進雛到出欄所有生產(chǎn)數(shù)據(jù)都掛在批次下面。常見做法是建三張主表批次表、日記錄表、事件表。批次表存進雛日期、品種、初始數(shù)量、當前存欄、雞舍編號日記錄表存每天的采食量、飲水量、產(chǎn)蛋數(shù)、死淘數(shù)、平均體重事件表存免疫、用藥、消毒、轉(zhuǎn)群這些非每日發(fā)生但必須留痕的操作。為什么不用一張大寬表因為日記錄是高頻寫入事件是低頻但字段差異大。分開之后日記錄表可以按批次和日期做聯(lián)合索引查詢“某批次最近七天采食趨勢”時不會掃全表。事件表用類型字段區(qū)分免疫記錄存疫苗名稱和劑量用藥記錄存藥品名稱和休藥期字段用JSON擴展列存避免頻繁改表結(jié)構(gòu)。-- 批次表一批雞的身份證 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 進雛數(shù)量, current_count INT NOT NULL COMMENT 當前存欄, start_date DATE NOT NULL COMMENT 進雛日期, status TINYINT DEFAULT 1 COMMENT 1在養(yǎng) 2已出欄 3已清欄, INDEX idx_start_date (start_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 日記錄表每天一條高頻寫入 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 產(chǎn)蛋數(shù), dead_count INT DEFAULT 0 COMMENT 死淘數(shù), avg_weight_g INT COMMENT 平均體重克, UNIQUE KEY uk_batch_date (batch_id, record_date), INDEX idx_batch (batch_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;參數(shù)說明batch_no用業(yè)務編號而不是自增ID對外方便場里人溝通。current_count不通過觸發(fā)器自動減而是由服務層在死淘事件里更新因為死淘可能補錄觸發(fā)器容易算錯。uk_batch_date唯一索引防止同一天重復錄入這是血淚經(jīng)驗——不加上這個約束月底對賬時你會發(fā)現(xiàn)同一天有三條記錄分別是技術員、場長、財務各錄了一遍。2.2 存欄數(shù)為什么不能只靠“初始減死淘”新手最容易踩的坑以為當前存欄 初始數(shù)量 - 累計死淘。實際養(yǎng)殖中還有淘汰、轉(zhuǎn)群、出售、調(diào)撥。比如一批雞養(yǎng)到中期把兩千只轉(zhuǎn)到另一個雞舍這批的存欄要減那批的要加。如果只記死淘轉(zhuǎn)群之后兩邊數(shù)據(jù)全亂。常見做法是引入“存欄變動流水表”每次數(shù)量變化都插一條記錄字段包括批次ID、變動類型死淘/轉(zhuǎn)群出/轉(zhuǎn)群入/出售/淘汰、變動數(shù)量、變動后存欄、操作時間。當前存欄直接讀批次表的冗余字段但每天凌晨跑一次對賬任務用流水表重算存欄和冗余字段比對不一致就告警。這樣既保證查詢快又保證數(shù)據(jù)可追溯。// 存欄變動服務每次變動都寫流水并更新冗余 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用正負表示增減轉(zhuǎn)群出傳負數(shù)轉(zhuǎn)群入傳正數(shù)。newCount 0校驗必須做否則一次誤操作就能把存欄搞成負數(shù)后面所有報表全廢。對賬任務用SELECT batch_id, SUM(delta) FROM stock_flow GROUP BY batch_id和批次表比對差異超過閾值就發(fā)通知。2.3 免疫和用藥記錄怎么和休藥期聯(lián)動家禽養(yǎng)殖繞不開用藥但用藥之后有休藥期休藥期內(nèi)產(chǎn)的蛋不能賣。系統(tǒng)如果不做聯(lián)動全靠人記遲早出事。設計上用藥事件表加兩個字段drug_name和withdraw_days。當錄入一條用藥記錄時自動計算withdraw_end_date 用藥日期 withdraw_days并寫入批次表的lock_until字段。產(chǎn)蛋記錄提交時如果當前日期小于lock_until彈窗提示“該批次處于休藥期產(chǎn)蛋不可銷售”但允許記錄為“非銷售蛋”。這個邏輯不復雜但參數(shù)要留活口。不同藥品休藥期不同同一藥品不同給藥方式休藥期也不同。常見做法是建一個藥品字典表存藥品名、默認休藥期、給藥方式錄入時帶出默認值允許手動改。改的時候要填原因留審計痕跡。3. 用SpringBoot把生產(chǎn)數(shù)據(jù)接口跑通從錄入到校驗的完整鏈路3.1 日記錄錄入接口的防重與校驗日記錄是系統(tǒng)里寫入最頻繁的接口一天可能幾百條。設計不好要么重復錄入要么并發(fā)寫沖突。常見做法是用batch_id record_date做唯一鍵插入時用INSERT ... ON DUPLICATE KEY UPDATE但這樣會覆蓋之前的數(shù)據(jù)不適合需要留痕的場景。更穩(wěn)的做法是先查后插查的時候加行鎖。PostMapping(/daily) public Result saveDaily(RequestBody Valid DailyRecordDTO dto) { // 1. 校驗批次是否存在且在養(yǎng) Batch batch batchMapper.selectById(dto.getBatchId()); if (batch null || batch.getStatus() ! 1) { return Result.fail(批次不存在或已結(jié)束); } // 2. 校驗日期不能晚于今天 if (dto.getRecordDate().isAfter(LocalDate.now())) { return Result.fail(不能錄入未來日期); } // 3. 查重加鎖防并發(fā) DailyRecord exist dailyMapper.selectForUpdate(dto.getBatchId(), dto.getRecordDate()); if (exist ! null) { return Result.fail(該批次當日記錄已存在請勿重復錄入); } // 4. 死淘數(shù)不能超過當前存欄 if (dto.getDeadCount() batch.getCurrentCount()) { return Result.fail(死淘數(shù)超過當前存欄); } // 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(); }參數(shù)說明selectForUpdate在Mapper里寫SELECT * FROM daily_record WHERE batch_id? AND record_date? FOR UPDATE利用InnoDB行鎖防止兩個請求同時查到空然后都插入。deadCount校驗用而不是因為死淘等于存欄是允許的意味著清欄。死淘更新存欄走StockService保證流水和冗余一致。3.2 產(chǎn)蛋數(shù)據(jù)的異常值過濾產(chǎn)蛋數(shù)受品種、日齡、季節(jié)、光照影響波動正常但突然翻倍或減半大概率是錄入錯誤。系統(tǒng)里加一層軟校驗根據(jù)批次品種和日齡查標準產(chǎn)蛋曲線實際值偏離標準值超過百分之三十時標記為“待確認”不直接拒絕但推送給技術員復核。// 產(chǎn)蛋異常檢測偏離標準曲線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是產(chǎn)蛋率從標準曲線表讀比如海蘭褐在200日齡產(chǎn)蛋率約0.92。expected是理論產(chǎn)蛋數(shù)。偏離超過百分之三十標記異常前端顯示黃色感嘆號技術員點確認后才進入統(tǒng)計。這個閾值可以配置不同品種、不同季節(jié)可以調(diào)。注意不要用硬編碼否則換品種就得改代碼。3.3 批次成本核算的定時任務養(yǎng)殖場最關心的是這批雞到底賺沒賺錢。成本包括雞雛、飼料、藥品、疫苗、人工、水電、折舊。前四項能從系統(tǒng)數(shù)據(jù)算后三項通常按批次分攤。常見做法是每天凌晨跑一個定時任務匯總當天各批次的飼料消耗、藥品消耗寫入成本日匯總表。出欄時再算總成本。Scheduled(cron 0 30 1 * * ?) // 每天凌晨1:30執(zhí)行 public void calcDailyCost() { ListBatch 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); } }參數(shù)說明cron表達式按服務器時區(qū)如果服務器在UTC要改成對應時間。feedPriceService.getPrice按品種和日期查因為飼料價格會波動不能用一個固定值算全程。sumDrugCost在事件表里按類型過濾只算用藥和疫苗不算消毒。這個任務要加失敗重試某天沒跑成功第二天要補算否則成本缺一天。4. Vue前端怎么把“雞舍現(xiàn)場”搬進瀏覽器4.1 批次看板一屏看清所有在養(yǎng)批次場長最??吹捻撁媸桥慰窗濉P枨蠛芎唵蚊總€在養(yǎng)批次一張卡片顯示批次號、品種、日齡、當前存欄、今日采食、今日產(chǎn)蛋、死淘率。數(shù)據(jù)從/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), // 死淘率累計死淘/初始數(shù)量 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算日齡注意時區(qū)問題如果服務器返回的是UTC日期要轉(zhuǎn)成本地再算。deadRate用累計死淘除以初始數(shù)量不是除以當前存欄否則分母越來越小死淘率會虛高。定時刷新用setInterval組件銷毀時要clearInterval否則內(nèi)存泄漏。這個頁面在手機瀏覽器上也要能看卡片布局用flex換行不要寫死寬度。4.2 日記錄表單的聯(lián)動校驗錄入日記錄時前端要做幾項即時校驗日期不能選未來、死淘數(shù)不能超過當前存欄、采食量不能為負。這些校驗后端也會做但前端先攔一道減少無效請求。用Element UI的el-form規(guī)則加上自定義校驗函數(shù)。// 日記錄表單校驗規(guī)則 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: 請輸入死淘數(shù), trigger: blur }, { validator: (rule, value, callback) { if (value this.currentBatch.currentCount) { callback(new Error(死淘數(shù)不能超過當前存欄 this.currentBatch.currentCount)); } else { callback(); } }, trigger: blur } ], feedKg: [ { type: number, min: 0, message: 采食量不能為負, trigger: blur } ] }參數(shù)說明dayjs(value).isAfter(dayjs(), day)比較到天同一天允許。this.currentBatch從父組件傳入切換批次時要重置表單。feedKg用type: number而不是type: integer因為采食量可能有小數(shù)。校驗觸發(fā)用blur而不是change避免輸入過程中頻繁報錯。4.3 用ECharts畫產(chǎn)蛋曲線和采食趨勢技術員需要看趨勢判斷雞群是否正常。產(chǎn)蛋曲線和采食趨勢用ECharts折線圖數(shù)據(jù)從/api/report/trend?batchIdxdays30拉。關鍵點是橫軸日期要連續(xù)不能因為某天沒記錄就斷掉。// 產(chǎn)蛋趨勢圖配置 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: 產(chǎn)蛋數(shù) }, series: [ { name: 實際產(chǎn)蛋, type: line, data: eggCounts, connectNulls: false }, { name: 標準曲線, type: line, data: expectedCounts, lineStyle: { type: dashed } } ], tooltip: { trigger: axis } }); }邏輯說明connectNulls: false讓缺失日期斷開一眼能看出哪天沒錄。標準曲線用虛線實際用實線偏離大時技術員能直接看到。expectedCount從后端算好返回前端不做業(yè)務計算。圖表容器要設固定高度否則ECharts在flex布局里可能高度為0。5. 避坑與排查家禽養(yǎng)殖系統(tǒng)落地時最容易踩的五個坑5.1 坑一存欄數(shù)越用越亂最后沒人信系統(tǒng)現(xiàn)象用了兩個月場里人發(fā)現(xiàn)系統(tǒng)顯示的存欄和實際對不上差幾十只于是又回去用手寫臺賬。原因死淘錄入不及時或者轉(zhuǎn)群沒錄或者補錄時只改了日記錄沒走存欄變動。解決第一日記錄提交時死淘必須走StockService不允許直接改批次表第二每天凌晨對賬任務跑一遍差異超過千分之五就推送告警給場長第三轉(zhuǎn)群操作做成獨立功能必須選轉(zhuǎn)出批次和轉(zhuǎn)入批次兩邊同時更新。關鍵是讓場里人知道系統(tǒng)數(shù)據(jù)比手寫準他們才會用。5.2 坑二休藥期沒鎖住產(chǎn)蛋記錄照樣能提交現(xiàn)象技術員錄了用藥記錄但產(chǎn)蛋錄入時沒提示休藥期雞蛋照樣標記為可銷售。原因用藥事件和產(chǎn)蛋記錄之間沒有聯(lián)動或者lock_until字段沒寫進批次表。解決用藥保存時同步更新批次表的lock_until產(chǎn)蛋提交時查這個字段在休藥期內(nèi)強制標記為“非銷售蛋”并且前端用紅色橫幅提示。如果業(yè)務允許手動解鎖必須填解鎖原因并記錄操作人。5.3 坑三日記錄重復錄入月底對賬發(fā)現(xiàn)同一天三條現(xiàn)象技術員錄了一次場長又錄了一次財務補錄一次同一天三條記錄統(tǒng)計時不知道用哪條。原因沒有唯一約束或者前端沒做防重提交。解決數(shù)據(jù)庫加uk_batch_date唯一索引接口層先查后插加行鎖前端提交按鈕點擊后置灰三秒。如果確實需要修改走編輯接口保留修改日志。不要用INSERT ON DUPLICATE KEY UPDATE那樣會靜默覆蓋出了事查不到。5.4 坑四成本核算漏算飼料價格波動算出來全是錯的現(xiàn)象出欄一算每只雞成本比實際低了兩塊場長覺得系統(tǒng)在瞎算。原因飼料價格用了固定值但實際飼料價格每月都在變。解決建飼料價格表按品種和生效日期存價格成本核算時按日記錄日期查對應價格。如果價格表沒維護成本任務要告警而不是用默認值硬算。另外人工水電這些分攤項要允許手動錄入不能全指望自動算。5.5 坑五前端圖表在手機上一片空白現(xiàn)象場長用手機看產(chǎn)蛋趨勢圖表區(qū)域白的什么都沒有。原因ECharts容器高度用了百分比父元素沒有固定高度算出來是0。解決圖表容器設固定高度比如height: 300px或者用vh單位。另外移動端要監(jiān)聽resize事件調(diào)chart.resize()否則橫豎屏切換后圖表變形。如果還不行檢查數(shù)據(jù)是不是空數(shù)組空數(shù)據(jù)時ECharts不報錯但也不顯示。6. 把系統(tǒng)真正用起來從演示到日常的最后一公里很多養(yǎng)殖系統(tǒng)死在演示之后。演示時數(shù)據(jù)漂亮真用起來場里人嫌麻煩錄兩天就不錄了。我自己的習慣是上線第一周只開一個功能日記錄錄入。其他功能全隱藏。等日記錄錄順了再開產(chǎn)蛋和死淘。再一周開免疫用藥。再一周開報表。每次只加一個功能場里人不會覺得一下子多了很多活。驗證系統(tǒng)有沒有真正用起來不看登錄次數(shù)看一個指標日記錄連續(xù)錄入天數(shù)。連續(xù)三十天不斷說明流程跑通了。斷一天就要問為什么。常見原因是那天太忙忘了或者手機沒信號。如果是忘了加個每天下午五點的提醒如果是信號問題允許離線暫存有網(wǎng)再同步。最后一個技巧把批次看板做成大屏掛在辦公室墻上。場長每天路過看一眼存欄、采食、產(chǎn)蛋、死淘率一目了然。數(shù)據(jù)不準他自己就會來找你。這比任何培訓都管用。我見過太多系統(tǒng)功能做了一堆最后沒人看就是因為數(shù)據(jù)沒展示在最顯眼的地方。希望幫到你。本文還有配套的精品資源點擊獲取