소스 검색

牦牛资产档案

wwh 1 개월 전
부모
커밋
d68c819a5c

+ 36 - 5
baqing-admin/src/main/java/com/ruoyi/web/modules/industryservice/domain/vo/BreedingIndustryMonthlyTrendPointVo.java

@@ -1,10 +1,31 @@
1 1
 package com.ruoyi.web.modules.industryservice.domain.vo;
2 2
 
3
+/**
4
+ * 存栏变动月度趋势点(与大屏 {@code monthlyTrend[]} 字段一致)。
5
+ */
3 6
 public class BreedingIndustryMonthlyTrendPointVo
4 7
 {
8
+    /** 所属年份(自然年) */
9
+    private int statYear;
10
+
11
+    /** 所属月份 1~12 */
5 12
     private int month;
6 13
 
7
-    private int inventoryCount;
14
+    /** 公牛存栏(头) */
15
+    private int bullCount;
16
+
17
+    /** 母牛存栏(头) */
18
+    private int cowCount;
19
+
20
+    public int getStatYear()
21
+    {
22
+        return statYear;
23
+    }
24
+
25
+    public void setStatYear(int statYear)
26
+    {
27
+        this.statYear = statYear;
28
+    }
8 29
 
9 30
     public int getMonth()
10 31
     {
@@ -16,13 +37,23 @@ public class BreedingIndustryMonthlyTrendPointVo
16 37
         this.month = month;
17 38
     }
18 39
 
19
-    public int getInventoryCount()
40
+    public int getBullCount()
41
+    {
42
+        return bullCount;
43
+    }
44
+
45
+    public void setBullCount(int bullCount)
46
+    {
47
+        this.bullCount = bullCount;
48
+    }
49
+
50
+    public int getCowCount()
20 51
     {
21
-        return inventoryCount;
52
+        return cowCount;
22 53
     }
23 54
 
24
-    public void setInventoryCount(int inventoryCount)
55
+    public void setCowCount(int cowCount)
25 56
     {
26
-        this.inventoryCount = inventoryCount;
57
+        this.cowCount = cowCount;
27 58
     }
28 59
 }

+ 78 - 57
baqing-admin/src/main/java/com/ruoyi/web/modules/industryservice/service/impl/BreedingIndustryDashboardServiceImpl.java

@@ -3,10 +3,10 @@ package com.ruoyi.web.modules.industryservice.service.impl;
3 3
 import java.math.BigDecimal;
4 4
 import java.math.RoundingMode;
5 5
 import java.time.LocalDate;
6
+import java.time.YearMonth;
6 7
 import java.time.ZoneId;
7 8
 import java.time.format.DateTimeFormatter;
8 9
 import java.util.ArrayList;
9
-import java.util.Collections;
10 10
 import java.util.Date;
11 11
 import java.util.HashMap;
12 12
 import java.util.List;
@@ -14,7 +14,6 @@ import java.util.Map;
14 14
 import org.springframework.beans.factory.annotation.Autowired;
15 15
 import org.springframework.stereotype.Service;
16 16
 import com.ruoyi.common.exception.ServiceException;
17
-import com.ruoyi.web.modules.industryservice.domain.dto.BreedingIndustryYakSnapshot;
18 17
 import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryAgeBandVo;
19 18
 import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryDashboardVo;
20 19
 import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryDegradationStatVo;
@@ -24,10 +23,15 @@ import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryOverviewV
24 23
 import com.ruoyi.web.modules.industryservice.mapper.BreedingIndustryStatsMapper;
25 24
 import com.ruoyi.web.modules.industryservice.service.IBreedingIndustryDashboardService;
26 25
 import com.ruoyi.web.modules.industryservice.support.BreedingIndustryCalculator;
26
+import com.ruoyi.web.modules.industryservice.support.BreedingIndustryDashboardSupport;
27 27
 import com.ruoyi.web.modules.industryservice.support.BreedingIndustryPastureScope;
28 28
 import com.ruoyi.web.modules.industryservice.support.BreedingIndustryRules;
29 29
 import com.ruoyi.web.modules.industryservice.support.GrasslandRules;
30
+import com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdCowAgeSumDto;
31
+import com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderSumDto;
32
+import com.ruoyi.web.modules.screen.domain.dto.HomeScreenStatPeriodDto;
30 33
 import com.ruoyi.web.modules.screen.mapper.HomeScreenStatsMapper;
34
+import com.ruoyi.web.modules.screen.support.HomeScreenSupport;
31 35
 
32 36
 @Service
33 37
 public class BreedingIndustryDashboardServiceImpl implements IBreedingIndustryDashboardService
@@ -60,72 +64,55 @@ public class BreedingIndustryDashboardServiceImpl implements IBreedingIndustryDa
60 64
 
61 65
             BreedingIndustryOverviewVo overview = new BreedingIndustryOverviewVo();
62 66
             fillGrasslandOverview(overview);
67
+            fillOverviewFromHerdReports(overview);
63 68
             dashboard.setOverview(overview);
64 69
 
65
-            int currentCalendarYear = statDate.getYear();
66
-            // 产业总览三项:填报合计填入既有字段(与大屏首页一致);原快照口径见下方暂保留调用
67
-            overview.setInventoryTotal(
68
-                    homeScreenStatsMapper.sumHerdInventoryYakTotalByStatYear(currentCalendarYear - 1));
69
-            overview.setAnnualOutbound(
70
-                    homeScreenStatsMapper.sumOutboundReportYakCountByStatYear(currentCalendarYear));
71
-            overview.setPastureCount(
72
-                    homeScreenStatsMapper.sumOutboundReportFarmerHouseholdCountByStatYear(currentCalendarYear));
70
+            HomeScreenStatPeriodDto latestHerd = homeScreenStatsMapper.selectLatestHerdInventoryStatPeriod();
71
+            YearMonth anchor = HomeScreenSupport.currentYearMonth();
72
+            int startYm = HomeScreenSupport.yearMonthKey(anchor.minusMonths(11));
73
+            int endYm = HomeScreenSupport.yearMonthKey(anchor);
73 74
 
74 75
             BreedingIndustryInventoryChangeVo inventoryChange = new BreedingIndustryInventoryChangeVo();
75
-            List<BreedingIndustryAgeBandVo> ageStructure = buildEmptyAgeStructure();
76
-            if (allowedPastureIds.isEmpty())
76
+            List<BreedingIndustryAgeBandVo> ageStructure;
77
+            if (latestHerd == null)
77 78
             {
78
-                overview.setWarningYakCount(0);
79 79
                 inventoryChange.setBullCount(0);
80 80
                 inventoryChange.setCowCount(0);
81 81
                 inventoryChange.setUnknownGenderCount(0);
82
-                inventoryChange.setMonthlyTrend(Collections.emptyList());
82
+                ageStructure = BreedingIndustryDashboardSupport.toAgeStructure(
83
+                        HomeScreenSupport.buildCowAgeStructureFromHerdReport(null));
84
+            }
85
+            else
86
+            {
87
+                HomeScreenHerdGenderSumDto genderSum = homeScreenStatsMapper.sumHerdInventoryGenderByStatYearMonth(
88
+                        latestHerd.getStatYear(), latestHerd.getStatMonth());
89
+                inventoryChange.setBullCount(
90
+                        genderSum != null && genderSum.getBullTotal() != null ? genderSum.getBullTotal() : 0);
91
+                inventoryChange.setCowCount(
92
+                        genderSum != null && genderSum.getCowTotal() != null ? genderSum.getCowTotal() : 0);
93
+                inventoryChange.setUnknownGenderCount(0);
94
+                HomeScreenHerdCowAgeSumDto cowAge = homeScreenStatsMapper.sumHerdInventoryCowAgeByStatYearMonth(
95
+                        latestHerd.getStatYear(), latestHerd.getStatMonth());
96
+                ageStructure = BreedingIndustryDashboardSupport.toAgeStructure(
97
+                        HomeScreenSupport.buildCowAgeStructureFromHerdReport(cowAge));
98
+            }
99
+            inventoryChange.setMonthlyTrend(BreedingIndustryDashboardSupport.toMonthlyTrend(
100
+                    HomeScreenSupport.buildRolling12MonthBullCowTrend(
101
+                            homeScreenStatsMapper.selectHerdInventoryBullCowByYearMonthRange(startYm, endYm),
102
+                            anchor)));
103
+
104
+            if (allowedPastureIds.isEmpty())
105
+            {
106
+                overview.setWarningYakCount(0);
83 107
             }
84 108
             else
85 109
             {
86 110
                 Date yearStart = toDate(statDate.withDayOfYear(1));
87 111
                 Date statDateEnd = toDateEndOfDay(statDate);
88
-
89
-                // 原牧场/存栏/出栏口径(暂保留调用)
90
-                breedingIndustryStatsMapper.countPastureInScope(allowedPastureIds);
91
-                int legacyInventoryTotal = breedingIndustryStatsMapper.countInventory(allowedPastureIds);
92
-                breedingIndustryStatsMapper.countAnnualOutbound(allowedPastureIds, yearStart, statDateEnd);
93
-
94 112
                 overview.setWarningYakCount(
95 113
                         breedingIndustryStatsMapper.countWarningYak(allowedPastureIds, yearStart, statDateEnd));
96
-
97
-                List<BreedingIndustryYakSnapshot> snapshots =
98
-                        breedingIndustryStatsMapper.selectYakSnapshotsInScope(allowedPastureIds);
99
-                if (snapshots == null)
100
-                {
101
-                    snapshots = Collections.emptyList();
102
-                }
103
-                int bull = 0;
104
-                int cow = 0;
105
-                for (BreedingIndustryYakSnapshot snapshot : snapshots)
106
-                {
107
-                    if (!BreedingIndustryCalculator.countsAsCurrentInventory(snapshot))
108
-                    {
109
-                        continue;
110
-                    }
111
-                    String gender = BreedingIndustryCalculator.normalizeGender(snapshot.getGender());
112
-                    if (BreedingIndustryRules.GENDER_BULL.equals(gender))
113
-                    {
114
-                        bull++;
115
-                    }
116
-                    else if (BreedingIndustryRules.GENDER_COW.equals(gender))
117
-                    {
118
-                        cow++;
119
-                    }
120
-                }
121
-                inventoryChange.setBullCount(bull);
122
-                inventoryChange.setCowCount(cow);
123
-                inventoryChange.setUnknownGenderCount(Math.max(0, legacyInventoryTotal - bull - cow));
124
-                inventoryChange.setMonthlyTrend(BreedingIndustryCalculator.buildMonthlyTrend(snapshots, statDate));
125
-
126
-                Map<String, Integer> bandCounts = BreedingIndustryCalculator.countAgeBands(snapshots, statDate);
127
-                ageStructure = BreedingIndustryCalculator.buildAgeStructure(bandCounts, legacyInventoryTotal);
128 114
             }
115
+
129 116
             dashboard.setInventoryChange(inventoryChange);
130 117
             dashboard.setAgeStructure(ageStructure);
131 118
             dashboard.setGrassland(buildGrasslandVo(overview.getGrasslandCount()));
@@ -137,6 +124,45 @@ public class BreedingIndustryDashboardServiceImpl implements IBreedingIndustryDa
137 124
         }
138 125
     }
139 126
 
127
+    private void fillOverviewFromHerdReports(BreedingIndustryOverviewVo overview)
128
+    {
129
+        HomeScreenStatPeriodDto latestHerd = homeScreenStatsMapper.selectLatestHerdInventoryStatPeriod();
130
+        HomeScreenStatPeriodDto latestOutbound = homeScreenStatsMapper.selectLatestOutboundReportStatPeriod();
131
+        overview.setInventoryTotal(sumHerdYakAtPeriod(latestHerd));
132
+        overview.setAnnualOutbound(sumOutboundYakAtPeriod(latestOutbound));
133
+        overview.setPastureCount(sumFarmerHouseholdAtPeriod(latestOutbound));
134
+    }
135
+
136
+    private int sumHerdYakAtPeriod(HomeScreenStatPeriodDto period)
137
+    {
138
+        if (period == null || period.getStatYear() == null || period.getStatMonth() == null)
139
+        {
140
+            return 0;
141
+        }
142
+        return homeScreenStatsMapper.sumHerdInventoryYakTotalByStatYearMonth(
143
+                period.getStatYear(), period.getStatMonth());
144
+    }
145
+
146
+    private int sumOutboundYakAtPeriod(HomeScreenStatPeriodDto period)
147
+    {
148
+        if (period == null || period.getStatYear() == null || period.getStatMonth() == null)
149
+        {
150
+            return 0;
151
+        }
152
+        return homeScreenStatsMapper.sumOutboundReportYakCountByStatYearMonth(
153
+                period.getStatYear(), period.getStatMonth());
154
+    }
155
+
156
+    private int sumFarmerHouseholdAtPeriod(HomeScreenStatPeriodDto period)
157
+    {
158
+        if (period == null || period.getStatYear() == null || period.getStatMonth() == null)
159
+        {
160
+            return 0;
161
+        }
162
+        return homeScreenStatsMapper.sumOutboundReportFarmerHouseholdCountByStatYearMonth(
163
+                period.getStatYear(), period.getStatMonth());
164
+    }
165
+
140 166
     private void fillGrasslandOverview(BreedingIndustryOverviewVo overview)
141 167
     {
142 168
         int grasslandCount = breedingIndustryStatsMapper.countGrassland();
@@ -180,11 +206,6 @@ public class BreedingIndustryDashboardServiceImpl implements IBreedingIndustryDa
180 206
         return result;
181 207
     }
182 208
 
183
-    private static List<BreedingIndustryAgeBandVo> buildEmptyAgeStructure()
184
-    {
185
-        return BreedingIndustryCalculator.buildAgeStructure(Collections.emptyMap(), 0);
186
-    }
187
-
188 209
     private static Date toDate(LocalDate localDate)
189 210
     {
190 211
         return Date.from(localDate.atStartOfDay(ZoneId.of(BreedingIndustryRules.ZONE_SHANGHAI)).toInstant());

+ 0 - 25
baqing-admin/src/main/java/com/ruoyi/web/modules/industryservice/support/BreedingIndustryCalculator.java

@@ -12,7 +12,6 @@ import java.util.List;
12 12
 import java.util.Map;
13 13
 import com.ruoyi.web.modules.industryservice.domain.dto.BreedingIndustryYakSnapshot;
14 14
 import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryAgeBandVo;
15
-import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryMonthlyTrendPointVo;
16 15
 
17 16
 /**
18 17
  * 养殖产业看板纯计算(月龄分档、月末存栏、性别、占比)。
@@ -121,30 +120,6 @@ public final class BreedingIndustryCalculator
121 120
                 .setScale(BreedingIndustryRules.RATIO_SCALE, RoundingMode.HALF_UP);
122 121
     }
123 122
 
124
-    public static List<BreedingIndustryMonthlyTrendPointVo> buildMonthlyTrend(
125
-            List<BreedingIndustryYakSnapshot> assets, LocalDate statDate)
126
-    {
127
-        List<BreedingIndustryMonthlyTrendPointVo> points = new ArrayList<>();
128
-        int currentMonth = statDate.getMonthValue();
129
-        for (int month = 1; month <= currentMonth; month++)
130
-        {
131
-            LocalDate monthEnd = resolveMonthEnd(statDate.getYear(), month, statDate);
132
-            int count = 0;
133
-            for (BreedingIndustryYakSnapshot asset : assets)
134
-            {
135
-                if (countsAtMonthEnd(asset, monthEnd))
136
-                {
137
-                    count++;
138
-                }
139
-            }
140
-            BreedingIndustryMonthlyTrendPointVo point = new BreedingIndustryMonthlyTrendPointVo();
141
-            point.setMonth(month);
142
-            point.setInventoryCount(count);
143
-            points.add(point);
144
-        }
145
-        return points;
146
-    }
147
-
148 123
     public static Map<String, Integer> countAgeBands(List<BreedingIndustryYakSnapshot> assets, LocalDate statDate)
149 124
     {
150 125
         Map<String, Integer> counts = new HashMap<>();

+ 57 - 0
baqing-admin/src/main/java/com/ruoyi/web/modules/industryservice/support/BreedingIndustryDashboardSupport.java

@@ -0,0 +1,57 @@
1
+package com.ruoyi.web.modules.industryservice.support;
2
+
3
+import java.util.ArrayList;
4
+import java.util.Collections;
5
+import java.util.List;
6
+import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryAgeBandVo;
7
+import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryMonthlyTrendPointVo;
8
+import com.ruoyi.web.modules.screen.domain.vo.HomeScreenAgeBandVo;
9
+import com.ruoyi.web.modules.screen.domain.vo.HomeScreenInventoryMonthVo;
10
+
11
+/**
12
+ * 养殖产业看板与填报表 / 大屏口径之间的 VO 转换。
13
+ */
14
+public final class BreedingIndustryDashboardSupport
15
+{
16
+    private BreedingIndustryDashboardSupport()
17
+    {
18
+    }
19
+
20
+    public static List<BreedingIndustryMonthlyTrendPointVo> toMonthlyTrend(List<HomeScreenInventoryMonthVo> from)
21
+    {
22
+        if (from == null || from.isEmpty())
23
+        {
24
+            return Collections.emptyList();
25
+        }
26
+        List<BreedingIndustryMonthlyTrendPointVo> list = new ArrayList<>(from.size());
27
+        for (HomeScreenInventoryMonthVo src : from)
28
+        {
29
+            BreedingIndustryMonthlyTrendPointVo point = new BreedingIndustryMonthlyTrendPointVo();
30
+            point.setStatYear(src.getStatYear());
31
+            point.setMonth(src.getMonth());
32
+            point.setBullCount(src.getBullCount());
33
+            point.setCowCount(src.getCowCount());
34
+            list.add(point);
35
+        }
36
+        return list;
37
+    }
38
+
39
+    public static List<BreedingIndustryAgeBandVo> toAgeStructure(List<HomeScreenAgeBandVo> from)
40
+    {
41
+        if (from == null || from.isEmpty())
42
+        {
43
+            return Collections.emptyList();
44
+        }
45
+        List<BreedingIndustryAgeBandVo> list = new ArrayList<>(from.size());
46
+        for (HomeScreenAgeBandVo src : from)
47
+        {
48
+            BreedingIndustryAgeBandVo band = new BreedingIndustryAgeBandVo();
49
+            band.setBandCode(src.getBandCode());
50
+            band.setBandLabel(src.getBandLabel());
51
+            band.setCount(src.getCount());
52
+            band.setRatio(src.getRatio());
53
+            list.add(band);
54
+        }
55
+        return list;
56
+    }
57
+}

+ 7 - 5
baqing-admin/src/test/java/com/ruoyi/web/modules/industryservice/controller/BreedingIndustryDashboardControllerApiTest.java

@@ -79,7 +79,7 @@ class BreedingIndustryDashboardControllerApiTest
79 79
                 .andExpect(jsonPath("$.data.statDate").value("2026-05-20"))
80 80
                 .andExpect(jsonPath("$.data.overview.inventoryTotal").value(120))
81 81
                 .andExpect(jsonPath("$.data.inventoryChange.bullCount").value(45))
82
-                .andExpect(jsonPath("$.data.ageStructure[0].bandCode").value("M1_6"))
82
+                .andExpect(jsonPath("$.data.ageStructure[0].bandCode").value("COW_U1"))
83 83
                 .andExpect(jsonPath("$.data.grassland.availableCount").value(50));
84 84
     }
85 85
 
@@ -143,15 +143,17 @@ class BreedingIndustryDashboardControllerApiTest
143 143
         BreedingIndustryInventoryChangeVo inv = new BreedingIndustryInventoryChangeVo();
144 144
         inv.setBullCount(45);
145 145
         inv.setCowCount(60);
146
-        inv.setUnknownGenderCount(15);
146
+        inv.setUnknownGenderCount(0);
147 147
         BreedingIndustryMonthlyTrendPointVo point = new BreedingIndustryMonthlyTrendPointVo();
148
+        point.setStatYear(2026);
148 149
         point.setMonth(5);
149
-        point.setInventoryCount(120);
150
+        point.setBullCount(40);
151
+        point.setCowCount(55);
150 152
         inv.setMonthlyTrend(Collections.singletonList(point));
151 153
         vo.setInventoryChange(inv);
152 154
         BreedingIndustryAgeBandVo band = new BreedingIndustryAgeBandVo();
153
-        band.setBandCode("M1_6");
154
-        band.setBandLabel("1-6月龄");
155
+        band.setBandCode("COW_U1");
156
+        band.setBandLabel("1岁以下");
155 157
         band.setCount(30);
156 158
         band.setRatio(new BigDecimal("25.0"));
157 159
         vo.setAgeStructure(Collections.singletonList(band));

+ 93 - 50
baqing-admin/src/test/java/com/ruoyi/web/modules/industryservice/service/impl/BreedingIndustryDashboardServiceImplTest.java

@@ -2,15 +2,13 @@ package com.ruoyi.web.modules.industryservice.service.impl;
2 2
 
3 3
 import static org.junit.jupiter.api.Assertions.assertEquals;
4 4
 import static org.junit.jupiter.api.Assertions.assertThrows;
5
-import static org.mockito.ArgumentMatchers.eq;
6 5
 import static org.mockito.ArgumentMatchers.any;
7
-import static org.mockito.ArgumentMatchers.anyList;
6
+import static org.mockito.ArgumentMatchers.anyInt;
7
+import static org.mockito.ArgumentMatchers.eq;
8 8
 import static org.mockito.Mockito.lenient;
9 9
 import static org.mockito.Mockito.when;
10 10
 
11 11
 import java.math.BigDecimal;
12
-import java.time.Year;
13
-import java.util.Arrays;
14 12
 import java.util.Collections;
15 13
 import org.junit.jupiter.api.DisplayName;
16 14
 import org.junit.jupiter.api.Test;
@@ -19,11 +17,14 @@ import org.mockito.InjectMocks;
19 17
 import org.mockito.Mock;
20 18
 import org.mockito.junit.jupiter.MockitoExtension;
21 19
 import com.ruoyi.common.exception.ServiceException;
22
-import com.ruoyi.web.modules.industryservice.BreedingIndustryTestSamples;
23 20
 import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryDashboardVo;
24 21
 import com.ruoyi.web.modules.industryservice.mapper.BreedingIndustryStatsMapper;
25 22
 import com.ruoyi.web.modules.industryservice.support.BreedingIndustryPastureScope;
26 23
 import com.ruoyi.web.modules.industryservice.support.BreedingIndustryRules;
24
+import com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdCowAgeSumDto;
25
+import com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderSumDto;
26
+import com.ruoyi.web.modules.screen.domain.dto.HomeScreenStatPeriodDto;
27
+import com.ruoyi.web.modules.screen.domain.dto.HomeScreenYearMonthBullCowDto;
27 28
 import com.ruoyi.web.modules.screen.mapper.HomeScreenStatsMapper;
28 29
 
29 30
 @ExtendWith(MockitoExtension.class)
@@ -43,65 +44,81 @@ class BreedingIndustryDashboardServiceImplTest
43 44
     private BreedingIndustryDashboardServiceImpl service;
44 45
 
45 46
     @Test
46
-    @DisplayName("ZCZX-YZCYSJ-UT-002 可见牧场为空")
47
+    @DisplayName("ZCZX-YZCYSJ-UT-002 可见牧场为空仍返回填报趋势")
47 48
     void ut002_emptyScope()
48 49
     {
49
-        int currentYear = Year.now().getValue();
50 50
         stubGrassland();
51
-        stubReportOverviewSums(currentYear, 100, 50, 20);
51
+        stubNoHerdReport();
52 52
         when(breedingIndustryPastureScope.resolveAllowedPastureIds()).thenReturn(Collections.emptyList());
53 53
         BreedingIndustryDashboardVo vo = service.loadDashboard(false);
54
-        assertEquals(100, vo.getOverview().getInventoryTotal());
55
-        assertEquals(50, vo.getOverview().getAnnualOutbound());
56
-        assertEquals(20, vo.getOverview().getPastureCount());
57
-        assertEquals(3, vo.getOverview().getGrasslandCount());
58
-        assertEquals(0, vo.getInventoryChange().getMonthlyTrend().size());
54
+        assertEquals(0, vo.getOverview().getInventoryTotal());
55
+        assertEquals(0, vo.getInventoryChange().getBullCount());
56
+        assertEquals(0, vo.getInventoryChange().getCowCount());
57
+        assertEquals(0, vo.getInventoryChange().getUnknownGenderCount());
58
+        assertEquals(12, vo.getInventoryChange().getMonthlyTrend().size());
59
+        assertEquals(4, vo.getAgeStructure().size());
59 60
     }
60 61
 
61 62
     @Test
62
-    @DisplayName("ZCZX-YZCYSJ-UT-028 产业总览三项与大屏填报口径一致")
63
-    void ut028_overviewFromReportTables()
63
+    @DisplayName("ZCZX-YZCYSJ-UT-028 产业总览与存栏取自最近填报年月")
64
+    void ut028_overviewFromLatestReport()
64 65
     {
65
-        int currentYear = Year.now().getValue();
66 66
         stubGrassland();
67
-        stubReportOverviewSums(currentYear, 50000, 3200, 8988);
67
+        HomeScreenStatPeriodDto herd = period(2025, 12);
68
+        HomeScreenStatPeriodDto outbound = period(2026, 5);
69
+        when(homeScreenStatsMapper.selectLatestHerdInventoryStatPeriod()).thenReturn(herd);
70
+        when(homeScreenStatsMapper.selectLatestOutboundReportStatPeriod()).thenReturn(outbound);
71
+        when(homeScreenStatsMapper.sumHerdInventoryYakTotalByStatYearMonth(2025, 12)).thenReturn(50000);
72
+        when(homeScreenStatsMapper.sumOutboundReportYakCountByStatYearMonth(2026, 5)).thenReturn(3200);
73
+        when(homeScreenStatsMapper.sumOutboundReportFarmerHouseholdCountByStatYearMonth(2026, 5)).thenReturn(8988);
74
+        stubHerdInventoryBlock(herd, 100, 200);
68 75
         when(breedingIndustryPastureScope.resolveAllowedPastureIds())
69
-                .thenReturn(Collections.singletonList(BreedingIndustryTestSamples.PASTURE_P1));
70
-        when(breedingIndustryStatsMapper.countPastureInScope(anyList())).thenReturn(12);
71
-        when(breedingIndustryStatsMapper.countInventory(anyList())).thenReturn(3);
72
-        when(breedingIndustryStatsMapper.countAnnualOutbound(anyList(), any(), any())).thenReturn(8);
73
-        when(breedingIndustryStatsMapper.countWarningYak(anyList(), any(), any())).thenReturn(0);
74
-        when(breedingIndustryStatsMapper.selectYakSnapshotsInScope(anyList()))
75
-                .thenReturn(Collections.singletonList(BreedingIndustryTestSamples.normalBull()));
76
+                .thenReturn(Collections.singletonList(1L));
77
+        when(breedingIndustryStatsMapper.countWarningYak(any(), any(), any())).thenReturn(0);
76 78
 
77 79
         BreedingIndustryDashboardVo vo = service.loadDashboard(false);
78 80
 
79 81
         assertEquals(50000, vo.getOverview().getInventoryTotal());
80 82
         assertEquals(3200, vo.getOverview().getAnnualOutbound());
81 83
         assertEquals(8988, vo.getOverview().getPastureCount());
84
+        assertEquals(100, vo.getInventoryChange().getBullCount());
85
+        assertEquals(200, vo.getInventoryChange().getCowCount());
86
+        assertEquals(0, vo.getInventoryChange().getUnknownGenderCount());
87
+        assertEquals(12, vo.getInventoryChange().getMonthlyTrend().size());
88
+        assertEquals(4, vo.getAgeStructure().size());
82 89
     }
83 90
 
84 91
     @Test
85
-    @DisplayName("ZCZX-YZCYSJ-UT-009 公母未标=原存栏时点总量")
86
-    void ut009_genderSum()
92
+    @DisplayName("ZCZX-YZCYSJ-UT-009 公母牛取自填报无未标注")
93
+    void ut009_genderFromReport()
87 94
     {
88
-        int currentYear = Year.now().getValue();
89 95
         stubGrassland();
90
-        stubReportOverviewSums(currentYear, 0, 0, 0);
96
+        HomeScreenStatPeriodDto herd = period(2026, 7);
97
+        when(homeScreenStatsMapper.selectLatestHerdInventoryStatPeriod()).thenReturn(herd);
98
+        when(homeScreenStatsMapper.selectLatestOutboundReportStatPeriod()).thenReturn(null);
99
+        when(homeScreenStatsMapper.sumHerdInventoryYakTotalByStatYearMonth(2026, 7)).thenReturn(350);
100
+        HomeScreenHerdGenderSumDto gender = new HomeScreenHerdGenderSumDto();
101
+        gender.setBullTotal(120);
102
+        gender.setCowTotal(230);
103
+        gender.setYakTotal(350);
104
+        when(homeScreenStatsMapper.sumHerdInventoryGenderByStatYearMonth(2026, 7)).thenReturn(gender);
105
+        HomeScreenHerdCowAgeSumDto age = new HomeScreenHerdCowAgeSumDto();
106
+        age.setCowUnder1Count(10);
107
+        age.setCow13Count(20);
108
+        age.setCow312Count(30);
109
+        age.setCowOver12Count(40);
110
+        when(homeScreenStatsMapper.sumHerdInventoryCowAgeByStatYearMonth(2026, 7)).thenReturn(age);
111
+        when(homeScreenStatsMapper.selectHerdInventoryBullCowByYearMonthRange(anyInt(), anyInt()))
112
+                .thenReturn(Collections.emptyList());
91 113
         when(breedingIndustryPastureScope.resolveAllowedPastureIds())
92
-                .thenReturn(Collections.singletonList(BreedingIndustryTestSamples.PASTURE_P1));
93
-        when(breedingIndustryStatsMapper.countPastureInScope(anyList())).thenReturn(1);
94
-        when(breedingIndustryStatsMapper.countInventory(anyList())).thenReturn(3);
95
-        when(breedingIndustryStatsMapper.countAnnualOutbound(anyList(), any(), any())).thenReturn(0);
96
-        when(breedingIndustryStatsMapper.countWarningYak(anyList(), any(), any())).thenReturn(0);
97
-        when(breedingIndustryStatsMapper.selectYakSnapshotsInScope(anyList())).thenReturn(Arrays.asList(
98
-                BreedingIndustryTestSamples.normalBull(),
99
-                BreedingIndustryTestSamples.normalCow(),
100
-                BreedingIndustryTestSamples.unknownGender()));
114
+                .thenReturn(Collections.singletonList(1L));
115
+        when(breedingIndustryStatsMapper.countWarningYak(any(), any(), any())).thenReturn(0);
116
+
101 117
         BreedingIndustryDashboardVo vo = service.loadDashboard(false);
102
-        assertEquals(1, vo.getInventoryChange().getBullCount());
103
-        assertEquals(1, vo.getInventoryChange().getCowCount());
104
-        assertEquals(1, vo.getInventoryChange().getUnknownGenderCount());
118
+
119
+        assertEquals(120, vo.getInventoryChange().getBullCount());
120
+        assertEquals(230, vo.getInventoryChange().getCowCount());
121
+        assertEquals(0, vo.getInventoryChange().getUnknownGenderCount());
105 122
     }
106 123
 
107 124
     @Test
@@ -114,6 +131,42 @@ class BreedingIndustryDashboardServiceImplTest
114 131
         assertEquals(BreedingIndustryRules.MSG_LOAD_FAILED, ex.getMessage());
115 132
     }
116 133
 
134
+    private static HomeScreenStatPeriodDto period(int year, int month)
135
+    {
136
+        HomeScreenStatPeriodDto dto = new HomeScreenStatPeriodDto();
137
+        dto.setStatYear(year);
138
+        dto.setStatMonth(month);
139
+        return dto;
140
+    }
141
+
142
+    private void stubNoHerdReport()
143
+    {
144
+        when(homeScreenStatsMapper.selectLatestHerdInventoryStatPeriod()).thenReturn(null);
145
+        when(homeScreenStatsMapper.selectLatestOutboundReportStatPeriod()).thenReturn(null);
146
+        when(homeScreenStatsMapper.selectHerdInventoryBullCowByYearMonthRange(anyInt(), anyInt()))
147
+                .thenReturn(Collections.emptyList());
148
+    }
149
+
150
+    private void stubHerdInventoryBlock(HomeScreenStatPeriodDto herd, int bull, int cow)
151
+    {
152
+        HomeScreenHerdGenderSumDto gender = new HomeScreenHerdGenderSumDto();
153
+        gender.setBullTotal(bull);
154
+        gender.setCowTotal(cow);
155
+        gender.setYakTotal(bull + cow);
156
+        when(homeScreenStatsMapper.sumHerdInventoryGenderByStatYearMonth(
157
+                eq(herd.getStatYear()), eq(herd.getStatMonth()))).thenReturn(gender);
158
+        HomeScreenHerdCowAgeSumDto age = new HomeScreenHerdCowAgeSumDto();
159
+        when(homeScreenStatsMapper.sumHerdInventoryCowAgeByStatYearMonth(
160
+                eq(herd.getStatYear()), eq(herd.getStatMonth()))).thenReturn(age);
161
+        HomeScreenYearMonthBullCowDto row = new HomeScreenYearMonthBullCowDto();
162
+        row.setStatYear(herd.getStatYear());
163
+        row.setStatMonth(herd.getStatMonth());
164
+        row.setBullCount(bull);
165
+        row.setCowCount(cow);
166
+        when(homeScreenStatsMapper.selectHerdInventoryBullCowByYearMonthRange(anyInt(), anyInt()))
167
+                .thenReturn(Collections.singletonList(row));
168
+    }
169
+
117 170
     private void stubGrassland()
118 171
     {
119 172
         when(breedingIndustryStatsMapper.countGrassland()).thenReturn(3);
@@ -122,14 +175,4 @@ class BreedingIndustryDashboardServiceImplTest
122 175
         when(breedingIndustryStatsMapper.countInUseGrassland(any())).thenReturn(1);
123 176
         when(breedingIndustryStatsMapper.selectDegradationGroupCounts()).thenReturn(Collections.emptyList());
124 177
     }
125
-
126
-    private void stubReportOverviewSums(int currentYear, int inventory, int outbound, int farmerHousehold)
127
-    {
128
-        lenient().when(homeScreenStatsMapper.sumHerdInventoryYakTotalByStatYear(eq(currentYear - 1)))
129
-                .thenReturn(inventory);
130
-        lenient().when(homeScreenStatsMapper.sumOutboundReportYakCountByStatYear(eq(currentYear)))
131
-                .thenReturn(outbound);
132
-        lenient().when(homeScreenStatsMapper.sumOutboundReportFarmerHouseholdCountByStatYear(eq(currentYear)))
133
-                .thenReturn(farmerHousehold);
134
-    }
135 178
 }

+ 0 - 32
baqing-admin/src/test/java/com/ruoyi/web/modules/industryservice/support/BreedingIndustryCalculatorTest.java

@@ -13,7 +13,6 @@ import org.junit.jupiter.api.DisplayName;
13 13
 import org.junit.jupiter.api.Test;
14 14
 import com.ruoyi.web.modules.industryservice.BreedingIndustryTestSamples;
15 15
 import com.ruoyi.web.modules.industryservice.domain.dto.BreedingIndustryYakSnapshot;
16
-import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryMonthlyTrendPointVo;
17 16
 
18 17
 @DisplayName("BreedingIndustryCalculator")
19 18
 class BreedingIndustryCalculatorTest
@@ -74,37 +73,6 @@ class BreedingIndustryCalculatorTest
74 73
                 BreedingIndustryTestSamples.deadWithoutChangeDate(), any));
75 74
     }
76 75
 
77
-    @Test
78
-    @DisplayName("ZCZX-YZCYSJ-UT-015 当前月=统计日")
79
-    void ut015_currentMonthTrend()
80
-    {
81
-        List<BreedingIndustryYakSnapshot> assets = Arrays.asList(
82
-                BreedingIndustryTestSamples.normalBull(),
83
-                BreedingIndustryTestSamples.normalCow());
84
-        List<BreedingIndustryMonthlyTrendPointVo> trend =
85
-                BreedingIndustryCalculator.buildMonthlyTrend(assets, STAT_DATE);
86
-        int may = trend.get(trend.size() - 1).getInventoryCount();
87
-        int inventory = 0;
88
-        for (BreedingIndustryYakSnapshot asset : assets)
89
-        {
90
-            if (BreedingIndustryCalculator.countsAsCurrentInventory(asset))
91
-            {
92
-                inventory++;
93
-            }
94
-        }
95
-        assertEquals(inventory, may);
96
-    }
97
-
98
-    @Test
99
-    @DisplayName("ZCZX-YZCYSJ-UT-022 不生成未来月")
100
-    void ut022_noFutureMonths()
101
-    {
102
-        List<BreedingIndustryMonthlyTrendPointVo> trend = BreedingIndustryCalculator.buildMonthlyTrend(
103
-                Collections.singletonList(BreedingIndustryTestSamples.normalBull()), STAT_DATE);
104
-        assertEquals(5, trend.size());
105
-        assertEquals(5, trend.get(4).getMonth());
106
-    }
107
-
108 76
     @Test
109 77
     @DisplayName("ZCZX-YZCYSJ-UT-004 pasture_id 空")
110 78
     void ut004_noPasture()

+ 5 - 3
doc/产业数据模型及服务/养殖产业数据/养殖产业数据功能需求-草稿.md

@@ -1,9 +1,11 @@
1 1
 做一个养殖产业数据模块:
2 2
 
3 3
 1. 养殖产业数据包括:产业总览、牦牛存栏变动、牦牛年龄结构、草场类型占比。
4
-2. 产业总览包括:牦牛存栏总量(头)、牦牛年出栏量(头)、预警牦牛数量(头)、牧场数量(个)、草场数量(个)、草场面积(亩);
5
-3. 牦牛存栏变动:包括公牛数量(头)、母牛数量(头)两个维度,按月统计牦牛存栏数量的变动趋势
6
-4. 牦牛年龄结构:统计1-6月龄、7-12月龄、12月龄以上3个阶段的占比
4
+2. 产业总览包括:牦牛存栏总量(头)、牦牛年出栏量(头)、预警牦牛数量(头)、农牧户(户)、草场数量(个)、草场面积(亩);存栏/出栏/农牧户取填报表**最近年月**全县合计。
5
+3. 牦牛存栏变动:公牛、母牛两维度(最近填报年月);趋势为滚动最近 **12** 自然月双折线,横轴仅月、悬停显年月;**不展示**未标注性别
6
+4. 牦牛年龄结构:母牛四档(1岁以下、1~3岁、3~12岁、12岁以上),与大屏一致
7 7
 5. 草场类型占比:1、统计可用、使用中的草场数量;2、通过未退化、轻度、中度、重度、极重度等维度统计草场的数量和占比。
8 8
 6. 异常返回:统一格式 code、msg、data。
9 9
 7. 技术栈:RuoYi v3.9.2-springboot2 分支 + MySQL-5.7.39。
10
+
11
+> 正式需求见同目录 `养殖产业数据功能需求.md`(v1.2+)。

+ 49 - 38
doc/产业数据模型及服务/养殖产业数据/养殖产业数据功能需求.md

@@ -118,6 +118,20 @@
118 118
 
119 119
 **状态变更日期为空**且当前为非正常状态:该个体在**所有历史月末**均**不计入**存栏趋势。
120 120
 
121
+> **看板说明(v1.2)**:**§5.3 存栏变动**、**§5.4 年龄结构**及总览存栏/出栏/农牧户三项已改由 **§2.11 存栏出栏填报** 统计,**不再**使用本节档案月末还原。本节保留供档案类指标(如预警关联档案)参考。
122
+
123
+### 2.11 存栏出栏填报(看板主口径)
124
+
125
+与大屏首页一致,数据来自乡镇填报表:
126
+
127
+| 指标/图表 | 数据源 | 规则 |
128
+| --- | --- | --- |
129
+| 总览存栏 | `biz_yak_herd_inventory.yak_total` | 全县合计;**最近填报年月**(`stat_year`+`stat_month` 最大) |
130
+| 总览出栏、农牧户 | `biz_yak_outbound_report` | 同上最近填报年月;字段 `yak_outbound_count`、`farmer_household_count` |
131
+| 公/母牛数量 | `biz_yak_herd_inventory` | 最近填报年月全县 `bull_total`、`cow_total` |
132
+| 存栏趋势 | `biz_yak_herd_inventory` | 距当前 **12** 个自然月;按月全县公/母合计;缺月 **0** |
133
+| 年龄结构 | `biz_yak_herd_inventory` | 最近填报年月母牛四龄档合计 |
134
+
121 135
 ---
122 136
 
123 137
 ## 3. 功能范围
@@ -128,7 +142,7 @@
128 142
 | --- | --- |
129 143
 | 产业总览 | 六项指标卡片展示,见 **§5.2** |
130 144
 | 牦牛存栏变动 | 当前公、母牛数量 + 按月存栏趋势图,见 **§5.3** |
131
-| 牦牛年龄结构 | 三档月龄占比图,见 **§5.4** |
145
+| 牦牛年龄结构 | 母牛四档年龄占比图,见 **§5.4** |
132 146
 | 草场类型占比 | 可用/使用中数量 + 退化等级数量与占比,见 **§5.5** |
133 147
 | 刷新 | 重新拉取并计算全页指标,见 **§5.1** |
134 148
 
@@ -210,10 +224,10 @@ flowchart LR
210 224
 
211 225
 | 序号 | 指标名称 | 单位 | 统计口径 |
212 226
 | --- | --- | --- | --- |
213
-| 1 | 牦牛存栏总量 | 头 | `biz_yak_herd_inventory.yak_total` 全县乡镇合计;统计年 = **当前自然年 − 1**;接口字段 **`inventoryTotal`**(与大屏一致) |
214
-| 2 | 牦牛年出栏量 | 头 | `biz_yak_outbound_report.yak_outbound_count` 全县乡镇合计;统计年 = **当前自然年**;接口字段 **`annualOutbound`** |
215
-| 3 | 预警牦牛数量 | 头 | **§2.5**(仍为档案时点口径) |
216
-| 4 | 农牧户 | 户 | `biz_yak_outbound_report.farmer_household_count` 全县乡镇合计;统计年 = **当前自然年**;接口字段沿用 **`pastureCount`**(与大屏一致) |
227
+| 1 | 牦牛存栏总量 | 头 | `biz_yak_herd_inventory.yak_total` 全县乡镇合计;**最近填报年月**;接口字段 **`inventoryTotal`** |
228
+| 2 | 牦牛年出栏量 | 头 | `biz_yak_outbound_report.yak_outbound_count` 全县乡镇合计;**最近填报年月**;接口字段 **`annualOutbound`** |
229
+| 3 | 预警牦牛数量 | 头 | **§2.5**(档案时点口径) |
230
+| 4 | 农牧户 | 户 | `biz_yak_outbound_report.farmer_household_count` 全县乡镇合计;**最近填报年月**;接口字段 **`pastureCount`** |
217 231
 | 5 | 草场数量 | 个 | **§2.6** |
218 232
 | 6 | 草场面积 | 亩 | **§2.6** |
219 233
 
@@ -227,45 +241,34 @@ flowchart LR
227 241
 
228 242
 #### 5.3.1 当前性别结构(数字)
229 243
 
230
-在趋势图上方或侧栏展示两个指标(与原型一致):
244
+在趋势图上方展示两个指标(与原型一致):
231 245
 
232 246
 | 指标 | 单位 | 口径 |
233 247
 | --- | --- | --- |
234
-| 公牛数量 | 头 | 当前存栏中性别为「公」的头数 |
235
-| 母牛数量 | 头 | 当前存栏中性别为「母」的头数 |
248
+| 公牛数量 | 头 | `biz_yak_herd_inventory.bull_total` 全县乡镇合计;取**最近填报年月**(`stat_year` + `stat_month` 最大) |
249
+| 母牛数量 | 头 | `biz_yak_herd_inventory.cow_total` 全县乡镇合计;同上 |
236 250
 
237
-**数量关系:** 公牛数量 + 母牛数量 + 未标注性别数量 = **§2.3** 当前存栏时点总量(原档案口径,**不等同于** **§5.2** 第 1 项填报合计)。当未标注性别数量 > 0 时,在公、母牛数量旁展示 **「未标注性别:N 头」**(N 为整数)
251
+**不展示**「未标注性别」;与大屏首页存栏变动 header 口径一致
238 252
 
239 253
 #### 5.3.2 存栏趋势(折线图)
240 254
 
241 255
 | 项 | 要求 |
242 256
 | --- | --- |
243
-| 图表类型 | 折线图 |
244
-| 横轴 | 当前自然年 1 月~12 月(标签:1月…12月) |
257
+| 图表类型 | 折线图(**公牛**、**母牛**两系列) |
258
+| 数据范围 | 距当前时间**最近 12 个自然月**(与大屏「牦牛存栏变动」一致) |
259
+| 横轴 | 12 个刻度,**仅显示月份**;悬停展示 **YYYY年M月** |
245 260
 | 纵轴 | 存栏数量,单位 **头** |
246
-| 序列 | **存栏总量**一条折线 |
247
-| 各月数值 | 按 **§2.10** 计算;仅绘制 **1 月~当前月** 有定义的月份点,**未来月份不绘制** |
248
-| 当前月 | 以 **统计日** 作为时点代入 **§2.10**(代替月末日) |
249
-| 图例 | 「存栏数量」(或与原型一致) |
250
-
251
-**与总览联动:** 当前月折线点与 **§2.10** 月末还原存栏一致;**§5.2** 第 1 项已改为全县填报合计,**不要求**与折线当前月点数值相同。
261
+| 接口字段 | `monthlyTrend[]` 含 `statYear`、`month`、`bullCount`、`cowCount`;缺月补 **0** |
252 262
 
253 263
 ### 5.4 牦牛年龄结构
254 264
 
255 265
 | 项 | 要求 |
256 266
 | --- | --- |
257 267
 | 图表类型 | 饼图(或环形图,与原型一致) |
258
-| 统计对象 | **§2.3** 当前存栏个体 |
259
-| 分档 | 三档,互斥且穷尽(月龄按 **§2.3**): |
260
-
261
-| 档位 | 月龄区间 |
262
-| --- | --- |
263
-| 1-6月龄 | 0 ≤ 月龄 ≤ 6 |
264
-| 7-12月龄 | 7 ≤ 月龄 ≤ 12(**含**整 12 月龄) |
265
-| 12月龄以上 | 月龄 ≥ 13 |
266
-
267
-| 展示 | 每档展示**占比(%)**;图例或标签含档位名称与百分比;各档占比之和为 **100%**(四舍五入导致 ±1% 须在界面容忍) |
268
-| 数量为 0 | 该档占比 0%,饼图仍保留图例项 |
268
+| 统计对象 | 存栏填报**最近年月**全县母牛分龄合计 |
269
+| 分档 | 四档(与大屏一致):**1岁以下**、**1~3岁**、**3~12岁**、**12岁以上** |
270
+| 编码 | `COW_U1` / `COW_1_3` / `COW_3_12` / `COW_O12` |
271
+| 展示 | 每档展示**头数**与**占比(%)**;各档占比之和为 **100%**(四舍五入 ±1% 可接受) |
269 272
 
270 273
 ### 5.5 草场类型占比
271 274
 
@@ -296,7 +299,9 @@ flowchart LR
296 299
 | --- | --- |
297 300
 | 牧场管理 | 提供牧场数量;经营状态、逻辑删除规则影响是否计入 |
298 301
 | 草场管理 | 提供草场数量、面积、退化等级、利用类型、禁牧日期;驱动可用/使用中及退化占比 |
299
-| 牦牛资产档案管理 | 提供存栏、性别、月龄、出栏量;资产状态与变更日期为核心字段 |
302
+| 牦牛资产档案管理 | 预警等仍关联档案;存栏变动/年龄已改填报表 **§2.11** |
303
+| 牦牛存栏数据填报 | 驱动总览存栏、公母数量、趋势、年龄结构 |
304
+| 牦牛出栏数据填报 | 驱动总览出栏、农牧户 |
300 305
 | 牦牛疾病预警 | 提供预警牦牛数量;周期与去重规则见 **§2.5**、该模块 **§5.6** |
301 306
 | 草畜平衡计算 | 本期看板**不展示**平衡指数;若后续纳入须修订本文档 |
302 307
 
@@ -349,13 +354,13 @@ flowchart LR
349 354
 | --- | --- |
350 355
 | 缺页面结构、刷新与加载 | **§5.1**、**§4.2** |
351 356
 | 指标无统计口径 | **§2** 分节定义;**§5.2~§5.5** 引用 |
352
-| 存栏趋势「按月」未说明横轴与序列 | **§5.3.2**、**§2.10** 月末还原与仅画至当前月 |
353
-| 年龄分档边界 | **§5.4**(12 月龄归入 7~12 档) |
357
+| 存栏趋势「按月」未说明横轴与序列 | **§5.3.2**、**§2.11** 滚动 12 月;横轴仅月、悬停显年月 |
358
+| 年龄分档边界 | **§5.4** 母牛四档(与大屏一致) |
354 359
 | 草场可用/使用中含义模糊 | **§2.7** 与草场管理字段挂钩 |
355 360
 | 退化占比分母 | **§2.8** 有效草场总条数 |
356 361
 | 技术栈、code/msg/data | 已移除 |
357 362
 | 未写数据来源 | **§4.1**、**§6** |
358
-| 预警/出栏/性别等待定口径 | **§2.4~§2.5**、**§2.9**、**§5.3.1** 已固化为唯一约定 |
363
+| 预警/出栏/性别等待定口径 | **§2.4~§2.5**、**§2.11**、**§5.3.1** 已固化为填报表 + 预警档案口径 |
359 364
 
360 365
 ---
361 366
 
@@ -363,11 +368,17 @@ flowchart LR
363 368
 
364 369
 | 能力 | 章节 |
365 370
 | --- | --- |
366
-| 产业总览六项 | §5.2、§2.3~§2.6 |
367
-| 公母牛数量 + 月趋势 | §5.3 |
368
-| 年龄档占比 | §5.4 |
371
+| 产业总览六项 | §5.2、§2.3~§2.6、§2.11 |
372
+| 公母牛数量 + 月趋势 | §5.3、§2.11 |
373
+| 年龄档占比 | §5.4 |
369 374
 | 草场可用/使用中 + 退化占比 | §5.5、§2.7、§2.8 |
370 375
 | 只读看板 | §3、§5.1 |
371
-| 模块依赖 | §4.1、§6 |
372
-| 数据权限 | §2.9 |
373
-| 月趋势还原 | §2.10 |
376
+
377
+---
378
+
379
+## 11. 修订记录
380
+
381
+| 版本 | 日期 | 说明 |
382
+| --- | --- | --- |
383
+| 1.2 | 2026-07-04 | 存栏变动/年龄/总览三项改填报表;滚动 12 月;母牛四档;与大屏对齐 |
384
+| 1.1 | (既有) | 总览存栏/出栏/农牧户改填报合计 |

+ 38 - 26
doc/产业数据模型及服务/养殖产业数据/养殖产业数据技术方案.md

@@ -21,8 +21,8 @@
21 21
 | --- | --- |
22 22
 | 看板加载/刷新 | 一次请求返回四块数据;`statDate` 取服务器当前日期 |
23 23
 | 数据权限 | Service 注入用户可见 `pastureId` 列表;牦牛、预警按 **§2.2** 过滤;草场全县台账不按牧场过滤 |
24
-| 月末存栏趋势 | 应用层按 1~当前月循环,每月按 **§2.2 月末存栏判定** 及需求 **§2.10** 计数(**非** `asset_status=1` 简单过滤) |
25
-| 公/母牛 | 仅统计**当前存栏**(`asset_status=1`),与趋势图口径分离 |
24
+| 月末存栏趋势(档案) | 历史能力保留于 `BreedingIndustryCalculator`;**看板趋势已改填报表**,见 **§2.11** |
25
+| 存栏变动/年龄/趋势 | 复用 `HomeScreenStatsMapper` + `HomeScreenSupport` 滚动 12 月逻辑,与大屏一致 |
26 26
 | 性能 | 数据量大时启用 **§2.3** 可选快照表 + 短 TTL 缓存;本期可先实时聚合;月末 12 点目标 **≤3s**(1 万级档案,评审可调) |
27 27
 
28 28
 **依赖模块(只读)**
@@ -33,6 +33,8 @@
33 33
 | `biz_grassland` | 草场管理 |
34 34
 | `biz_yak_asset` | 牦牛资产档案 |
35 35
 | `biz_yak_disease_warning` | 牦牛疾病预警 |
36
+| `biz_yak_herd_inventory` | 牦牛存栏数据填报(存栏变动、年龄结构、总览存栏) |
37
+| `biz_yak_outbound_report` | 牦牛出栏数据填报(总览出栏、农牧户) |
36 38
 
37 39
 ---
38 40
 
@@ -218,10 +220,10 @@ return success(dashboardService.loadDashboard(allowed, forceRefresh));
218 220
 
219 221
 | 字段 | 类型 | 说明 |
220 222
 | --- | --- | --- |
221
-| `inventoryTotal` | int | 牦牛存栏总量(头);`biz_yak_herd_inventory` 全县合计,年份 = 当前自然年 **− 1**(与大屏 `inventoryTotal` 一致) |
222
-| `annualOutbound` | int | 牦牛年出栏量(头);`biz_yak_outbound_report` 全县 `yak_outbound_count` 合计,年份 = 当前自然年 |
223
+| `inventoryTotal` | int | 牦牛存栏总量(头);`biz_yak_herd_inventory` **最近填报年月**全县 `yak_total` 合计 |
224
+| `annualOutbound` | int | 牦牛年出栏量(头);`biz_yak_outbound_report` **最近填报年月**全县 `yak_outbound_count` 合计 |
223 225
 | `warningYakCount` | int | 预警牦牛数量(头);**§2.5** 档案口径 |
224
-| `pastureCount` | int | 农牧户(户);`biz_yak_outbound_report` 全县 `farmer_household_count` 合计,年份 = 当前自然年(与大屏 `pastureCount` 一致) |
226
+| `pastureCount` | int | 农牧户(户);`biz_yak_outbound_report` **最近填报年月**全县 `farmer_household_count` 合计 |
225 227
 | `grasslandCount` | int | 草场数量(个) |
226 228
 | `grasslandAreaMu` | decimal | 草场面积(亩) |
227 229
 
@@ -229,28 +231,30 @@ return success(dashboardService.loadDashboard(allowed, forceRefresh));
229 231
 
230 232
 | 字段 | 类型 | 说明 |
231 233
 | --- | --- | --- |
232
-| `bullCount` | int | 公牛数量(头) |
233
-| `cowCount` | int | 母牛数量(头) |
234
-| `unknownGenderCount` | int | 未标注性别(头;为 0 时前端可不展示 |
235
-| `monthlyTrend` | array | 按月存栏,仅含 1~当前月 |
234
+| `bullCount` | int | 公牛数量(头);最近填报年月全县 `bull_total` 合计 |
235
+| `cowCount` | int | 母牛数量(头);最近填报年月全县 `cow_total` 合计 |
236
+| `unknownGenderCount` | int | 固定 **0**(不展示未标注性别) |
237
+| `monthlyTrend` | array | 滚动最近 **12** 自然月;固定 **12** 条 |
236 238
 
237 239
 `monthlyTrend[]` 元素:
238 240
 
239 241
 | 字段 | 类型 | 说明 |
240 242
 | --- | --- | --- |
241
-| `month` | int | 1~12 |
242
-| `inventoryCount` | int | 该月还原存栏总量(头) |
243
+| `statYear` | int | 所属自然年 |
244
+| `month` | int | 所属月份 1~12 |
245
+| `bullCount` | int | 该月全县公牛合计;无数据为 **0** |
246
+| `cowCount` | int | 该月全县母牛合计;无数据为 **0** |
243 247
 
244 248
 #### 3.1.5 `ageStructure[]`
245 249
 
246 250
 | 字段 | 类型 | 说明 |
247 251
 | --- | --- | --- |
248
-| `bandCode` | string | `M1_6` / `M7_12` / `M13_PLUS` |
249
-| `bandLabel` | string | 展示名:`1-6月龄` 等 |
252
+| `bandCode` | string | `COW_U1` / `COW_1_3` / `COW_3_12` / `COW_O12` |
253
+| `bandLabel` | string | `1岁以下` / `1~3岁` / `3~12岁` / `12岁以上` |
250 254
 | `count` | int | 头数 |
251
-| `ratio` | decimal | 占比 0~1 或前端转 %(约定返回 **0~100** 数值,保留 1 位小数,如 `37.5` 表示 37.5%) |
255
+| `ratio` | decimal | 占比 0~100 数值,保留 1 位小数 |
252 256
 
253
-三档固定返回(count 为 0 也返回)
257
+四档固定返回(count 为 0 也返回);与大屏年龄结构一致
254 258
 
255 259
 #### 3.1.6 `grassland`
256 260
 
@@ -291,14 +295,14 @@ return success(dashboardService.loadDashboard(allowed, forceRefresh));
291 295
     "inventoryChange": {
292 296
       "bullCount": 45,
293 297
       "cowCount": 60,
294
-      "unknownGenderCount": 15,
298
+      "unknownGenderCount": 0,
295 299
       "monthlyTrend": [
296
-        { "month": 1, "inventoryCount": 110 },
297
-        { "month": 5, "inventoryCount": 120 }
300
+        { "statYear": 2025, "month": 8, "bullCount": 40, "cowCount": 55 },
301
+        { "statYear": 2026, "month": 5, "bullCount": 45, "cowCount": 60 }
298 302
       ]
299 303
     },
300 304
     "ageStructure": [
301
-      { "bandCode": "M1_6", "bandLabel": "1-6月龄", "count": 30, "ratio": 25.0 }
305
+      { "bandCode": "COW_U1", "bandLabel": "1岁以下", "count": 30, "ratio": 25.0 }
302 306
     ],
303 307
     "grassland": {
304 308
       "availableCount": 50,
@@ -328,9 +332,9 @@ return success(dashboardService.loadDashboard(allowed, forceRefresh));
328 332
 | --- | --- |
329 333
 | **月龄** | `COALESCE(age_months, TIMESTAMPDIFF(MONTH, birth_date, statDate))`,`birth_date` 空则 0 |
330 334
 | **性别** | 入库值 `trim` 后精确匹配 `公`/`母`;第三方若写「公牛」「母牛」等,Service 层归一后再统计 |
331
-| **公母 vs 趋势** | `bullCount`/`cowCount`/`unknownGenderCount`/`ageStructure` 用 **§2.3** 原档案时点口径;`monthlyTrend` 用 **§2.10** 月末判定 |
332
-| **总览填报三项** | `overview.inventoryTotal` / `annualOutbound` / `pastureCount` 复用大屏 `HomeScreenStatsMapper` 填报合计 SQL;原 `countInventory` / `countAnnualOutbound` / `countPastureInScope` 在 Service 内暂保留调用 |
333
-| **一致性** | `monthlyTrend` 当前月点与 **§2.3** 时点存栏一致;**不要求**与 `overview.inventoryTotal`(填报合计)相同 |
335
+| **存栏变动/年龄/趋势** | `HomeScreenStatsMapper` + `HomeScreenSupport.buildRolling12MonthBullCowTrend` / `buildCowAgeStructureFromHerdReport`;`BreedingIndustryDashboardSupport` 做 VO 转换 |
336
+| **总览填报三项** | `overview.inventoryTotal` / `annualOutbound` / `pastureCount` 取各表**最近填报年月**全县合计 |
337
+| **预警** | 仍用 `biz_yak_asset` + `biz_yak_disease_warning`,按 **§2.9** 可见牧场过滤 |
334 338
 | **比率** | `ratio = count * 100.0 / NULLIF(total, 0)`,分母为 0 时 ratio=0 |
335 339
 | **月末性能** | 优先 1 条 SQL + `CASE WHEN` 按月末分组,或 Java 循环 12 次;超过 3s 启用 **§2.3** 缓存 |
336 340
 | **日志** | 看板接口可 `@Log(title="养殖产业数据", businessType=OTHER)` 或仅记录慢查询 |
@@ -365,10 +369,18 @@ VALUES ('养殖产业数据', {parentId}, 10, 'breedingIndustry', 'livestockIndu
365 369
 | 功能需求 | 技术落点 |
366 370
 | --- | --- |
367 371
 | §5.2 产业总览 | `overview` + **§2.2** SQL |
368
-| §5.3 存栏变动 | `inventoryChange` + **§2.2** 月末判定 |
369
-| §5.4 年龄结构 | `ageStructure` |
372
+| §5.3 存栏变动 | `inventoryChange` + 填报表滚动 12 月(与大屏一致) |
373
+| §5.4 年龄结构 | `ageStructure` 母牛四档(填报最近年月) |
370 374
 | §5.5 草场占比 | `grassland` |
371 375
 | §5.1 刷新 | **3.1** `forceRefresh` |
372
-| §2.9 数据权限 | **§2.6** `BreedingIndustryPastureScope` |
373
-| §2.10 月末趋势 | **§2.2** 月末判定 + `monthlyTrend` |
376
+| §2.9 数据权限 | **§2.6** `BreedingIndustryPastureScope`(仅预警等档案类指标) |
374 377
 | §2.5 预警 | **§2.2** warning SQL |
378
+
379
+---
380
+
381
+## 7. 修订记录
382
+
383
+| 版本 | 日期 | 说明 |
384
+| --- | --- | --- |
385
+| 1.2 | 2026-07-04 | 存栏变动/年龄/趋势改 `biz_yak_herd_inventory` 填报表;滚动 12 月;与大屏字段对齐 |
386
+| 1.1 | (既有) | 总览三项改填报合计 |

+ 38 - 35
doc/产业数据模型及服务/养殖产业数据/养殖产业数据测试用例.md

@@ -3,7 +3,7 @@
3 3
 > 依据:`养殖产业数据功能需求.md`、`养殖产业数据技术方案.md`、`养殖产业数据前端技术方案.md`  
4 4
 > **接口 Base Path**:`/dataModel/breedingIndustry`(含网关/context-path 时须补齐前缀)。鉴权:若依 Cookie / `Authorization` Token。
5 5
 
6
-**通用前置**:已执行 `sql/biz_pasture.sql`、`sql/biz_grassland.sql`、`sql/biz_yak_asset.sql`、`sql/biz_yak_disease_warning.sql`;测试账号具备 `dataModel:breedingIndustry:query` 及菜单「养殖产业数据」。
6
+**通用前置**:已执行 `sql/biz_pasture.sql`、`sql/biz_grassland.sql`、`sql/biz_yak_asset.sql`、`sql/biz_yak_disease_warning.sql`、`sql/biz_yak_herd_inventory.sql`、`sql/biz_yak_outbound_report.sql`;测试账号具备 `dataModel:breedingIndustry:query` 及菜单「养殖产业数据」。
7 7
 
8 8
 **UI 工具**:**Playwright** + **Chromium**,`channel: 'chrome'`(本机 **Google Chrome**)。组件路径:`livestockIndustry/breedingIndustry/index`。
9 9
 
@@ -30,7 +30,10 @@
30 30
 | **W-out** | `alert_time` 为上年或统计日次日 |
31 31
 | **G** | 草场 `del_flag=0`;退化 1~5、禁牧/轮牧/全年各造 1 条 |
32 32
 
33
-**联调统计日**:固定 **2026-05-20**(`monthlyTrend` 含 1~5 月共 5 点;不含 6~12 月)。
33
+| **HERD-L** | `biz_yak_herd_inventory` 最近填报年月全县 bull/cow/yak 合计 |
34
+| **HERD-M** | 某历史月填报记录,用于滚动 12 月趋势 |
35
+
36
+**联调统计日**:固定 **2026-05-20**(`monthlyTrend` 含 **12** 点:2025-06~2026-05;每项含 `statYear`、`month`、`bullCount`、`cowCount`)。
34 37
 
35 38
 ---
36 39
 
@@ -40,7 +43,7 @@
40 43
 | --- | --- |
41 44
 | **正常流程** | 菜单进入自动加载、刷新全页、四块指标与图表展示(§4.2、§5.1~§5.5) |
42 45
 | **异常流程** | 未登录、无权限、聚合失败、空库、接口不存在维护能力(§3、§7.2、技术§3.1.8) |
43
-| **业务约束** | §2.1~§2.10 口径;§2.9 权限三态;§3 本期不实现项(无 CRUD/筛选/导出) |
46
+| **业务约束** | §2.5~§2.11 口径;§2.9 权限三态;§3 本期不实现项(无 CRUD/筛选/导出) |
44 47
 
45 48
 ---
46 49
 
@@ -60,17 +63,17 @@
60 63
 | ZCZX-YZCYSJ-UT-010 | 预警 | 周期下界 1/1 0:00 | 单元测试 | JUnit5 | §2.5 | `alert_time=当年1/1 00:00:00` | 统计 | 计入 |
61 64
 | ZCZX-YZCYSJ-UT-011 | 预警 | 周期外排除 | 单元测试 | JUnit5 | §2.5 | W-out | 统计 | 0 |
62 65
 | ZCZX-YZCYSJ-UT-012 | 预警 | 须关联纳入档案 | 单元测试 | JUnit5 | §2.9 | 预警牛仅 P2、范围 P1 | `countWarning` | 0 |
63
-| ZCZX-YZCYSJ-UT-013 | 性别 | 公+母+未标=原时点总量 | 单元测试 | JUnit5 | §5.3.1 | Y-N 公母 + Y-U | 分项求和 | 等于原 `countInventory` 口径,**不等同** `overview.inventoryTotal` |
64
-| ZCZX-YZCYSJ-UT-028 | 产业总览 | 填报三项与大屏一致 | 单元测试 | JUnit5 | §5.2 | Mock 填报合计 | `loadDashboard` | `inventoryTotal`/`annualOutbound`/`pastureCount` 与 `HomeScreenStatsMapper` 合计一致 |
65
-| ZCZX-YZCYSJ-UT-014 | 性别 | trim 后匹配 | 单元测试 | JUnit5 | 技术§4 | `gender=' 母 '` | `normalizeGender` | 计母牛 |
66
-| ZCZX-YZCYSJ-UT-015 | 月龄 | birth_date 回退 | 单元测试 | JUnit5 | §2.3 | `age_months` 空、有生日 | `resolveAgeMonths(statDate)` | 与 `TIMESTAMPDIFF` 一致;无生日为 0 |
67
-| ZCZX-YZCYSJ-UT-016 | 年龄分档 | 边界 0/6/7/12/13 | 单元测试 | JUnit5 | §5.4 | 各月龄一头 | `bandOf` | M1_6、M7_12(含12)、M13_PLUS |
68
-| ZCZX-YZCYSJ-UT-017 | 年龄分档 | 占比与零分母 | 单元测试 | JUnit5 | §5.4 | 存栏 0 | `calcRatio` | 三档 count=0、ratio=0 仍返回 |
69
-| ZCZX-YZCYSJ-UT-018 | 月末趋势 | 变更日晚于月末仍计入 | 单元测试 | JUnit5 | §2.10 | Y-H,M=2 月末 | `countAtMonthEnd(2)` | 计入 |
70
-| ZCZX-YZCYSJ-UT-019 | 月末趋势 | 变更日≤月末排除 | 单元测试 | JUnit5 | §2.10 | Y-L,M=2 月末 | `countAtMonthEnd(2)` | 不计 |
71
-| ZCZX-YZCYSJ-UT-020 | 月末趋势 | 非正常且变更日空 | 单元测试 | JUnit5 | §2.10 | status=2、date 空 | 各月 | 恒 0 |
72
-| ZCZX-YZCYSJ-UT-021 | 月末趋势 | 当前月用统计日 | 单元测试 | JUnit5 | §2.10、技术§4 | statDate=5-20 | 5 月点 vs `countInventory` | 相等 |
73
-| ZCZX-YZCYSJ-UT-022 | 月末趋势 | 不生成未来月 | 单元测试 | JUnit5 | §2.2、§5.3.2 | 5 月环境 | `buildMonthlyTrend` | 仅 month 1~5 |
66
+| ZCZX-YZCYSJ-UT-013 | 存栏变动 | 公母来自最近填报 | 单元测试 | JUnit5 | §5.3.1、§2.11 | HERD-L | `loadDashboard` | `bullCount`/`cowCount` 与 HERD-L 全县合计一致;`unknownGenderCount=0` |
67
+| ZCZX-YZCYSJ-UT-028 | 产业总览 | 填报三项最近年月 | 单元测试 | JUnit5 | §5.2、§2.11 | Mock 填报合计 | `loadDashboard` | `inventoryTotal`/`annualOutbound`/`pastureCount` 与 `HomeScreenStatsMapper` 最近年月合计一致 |
68
+| ZCZX-YZCYSJ-UT-014 | 性别 | trim 后匹配 | 单元测试 | JUnit5 | 技术§4 | `gender=' 母 '` | `normalizeGender` | 计母牛(档案类,预警用) |
69
+| ZCZX-YZCYSJ-UT-015 | 年龄结构 | 四档编码 | 单元测试 | JUnit5 | §5.4 | Mock 母牛四龄填报 | `toAgeStructure` | `bandCode` 为 COW_U1/COW_1_3/COW_3_12/COW_O12 |
70
+| ZCZX-YZCYSJ-UT-016 | 年龄分档 | 占比与零分母 | 单元测试 | JUnit5 | §5.4 | 母牛合计 0 | `buildCowAgeStructureFromHerdReport` | 四档 count=0、ratio=0 仍返回 |
71
+| ZCZX-YZCYSJ-UT-017 | 年龄分档 | 占比之和 | 单元测试 | JUnit5 | §5.4 | 有母牛数据 | sum(ratio) | 99.9~100.1 |
72
+| ZCZX-YZCYSJ-UT-018 | 存栏趋势 | 滚动 12 月长度 | 单元测试 | JUnit5 | §5.3.2、§2.11 | 5 月环境 | `buildRolling12MonthBullCowTrend` | 固定 **12** 条;锚点含 statYear+month |
73
+| ZCZX-YZCYSJ-UT-019 | 存栏趋势 | 缺月补零 | 单元测试 | JUnit5 | §2.11 | 仅 HERD-M 有数 | 趋势某月 | `bullCount`/`cowCount` 无填报为 **0** |
74
+| ZCZX-YZCYSJ-UT-020 | 存栏趋势 | 最近月与 header 一致 | 单元测试 | JUnit5 | §5.3.1 | HERD-L | 趋势最后一点 vs header | 公/母分别相等 |
75
+| ZCZX-YZCYSJ-UT-021 | 存栏趋势 | 锚点当前月 | 单元测试 | JUnit5 | §2.11 | statDate=5-20 | 最后一点 month | 等于 **5**,statYear=**2026** |
76
+| ZCZX-YZCYSJ-UT-022 | 存栏趋势 | 不依赖档案月末 | 单元测试 | JUnit5 | §2.11 | 档案与填报不一致 | 趋势 | 以填报表为准 |
74 77
 | ZCZX-YZCYSJ-UT-023 | 牧场 | 原口径暂保留 | 单元测试 | JUnit5 | §2.6 | P-停、注销 | `countPastureInScope` 仍调用 | 结果**不**写入 `pastureCount` |
75 78
 | ZCZX-YZCYSJ-UT-024 | 草场 | 面积空不参与求和 | 单元测试 | JUnit5 | §2.6 | 一条 `area_mu` null | `sumArea` | 不计该条 |
76 79
 | ZCZX-YZCYSJ-UT-025 | 草场 | 可用=未退化+轻度 | 单元测试 | JUnit5 | §2.7 | 退化 1、2、3 各 1 | `availableCount` | 2 |
@@ -88,10 +91,10 @@
88 91
 | ZCZX-YZCYSJ-API-001 | 看板 | 首次加载全量结构 | 接口测试 | Postman/curl | §5.1 正常流程 | UA 登录;造数完整 | `GET /dataModel/breedingIndustry/dashboard` | `code=200`;`data` 含 `statDate`、`statYear`、`overview`、`inventoryChange`、`ageStructure`、`grassland` |
89 92
 | ZCZX-YZCYSJ-API-002 | 看板 | 统计日与自然年 | 接口测试 | Postman | §2.2 | 已登录 | 同上 | `statDate`=`yyyy-MM-dd` 服务器当日;`statYear` 为公历年 |
90 93
 | ZCZX-YZCYSJ-API-003 | 产业总览 | 六项指标非负 | 接口测试 | Postman | §5.2 | 已知样本 | 读 `overview` 六字段 | 与 UT-004~012、023~024 手工值一致;`grasslandAreaMu`≥0 |
91
-| ZCZX-YZCYSJ-API-004 | 存栏变动 | 公母未标关系 | 接口测试 | Postman | §5.3.1 | 含 Y-U | `bullCount+cowCount+unknownGenderCount` | 等于原档案时点存栏,**不要求**等于 `overview.inventoryTotal` |
92
-| ZCZX-YZCYSJ-API-005 | 存栏变动 | 月趋势仅至当前月 | 接口测试 | Postman | §5.3.2 | 统计日 5 月 | `monthlyTrend` | 长度=5;`month`∈{1..5};无 6~12 |
93
-| ZCZX-YZCYSJ-API-006 | 存栏变动 | 当前月=总览存栏 | 接口测试 | Postman | §5.3.2、技术§4 | 已登录 | 比 `monthlyTrend` 最后项与 `inventoryTotal` | 相等 |
94
-| ZCZX-YZCYSJ-API-007 | 年龄结构 | 固定三档 | 接口测试 | Postman | §5.4 | 已登录 | `ageStructure` | 长度=3;`bandCode` 为 M1_6/M7_12/M13_PLUS;count=0 也返回 |
94
+| ZCZX-YZCYSJ-API-004 | 存栏变动 | 公母与未标注 | 接口测试 | Postman | §5.3.1 | HERD-L | 读 `inventoryChange` | `unknownGenderCount=0`;公/母与 HERD-L 一致 |
95
+| ZCZX-YZCYSJ-API-005 | 存栏变动 | 滚动 12 月趋势 | 接口测试 | Postman | §5.3.2 | 统计日 5 月 | `monthlyTrend` | 长度=**12**;含 `statYear`/`month`/`bullCount`/`cowCount`;末项 2026-05 |
96
+| ZCZX-YZCYSJ-API-006 | 存栏变动 | 最近月与 header | 接口测试 | Postman | §5.3.1、§2.11 | HERD-L | 比 `monthlyTrend` 最后一项与 header | 公/母分别相等 |
97
+| ZCZX-YZCYSJ-API-007 | 年龄结构 | 固定四档 | 接口测试 | Postman | §5.4 | 已登录 | `ageStructure` | 长度=**4**;`bandCode` 为 COW_U1/COW_1_3/COW_3_12/COW_O12;count=0 也返回 |
95 98
 | ZCZX-YZCYSJ-API-008 | 年龄结构 | 占比之和容忍 | 接口测试 | Postman | §5.4 | 存栏>0 | sum(ratio) | 99.9~100.1(四舍五入容忍) |
96 99
 | ZCZX-YZCYSJ-API-009 | 草场占比 | 可用/使用中 | 接口测试 | Postman | §5.5.1 | 已登录 | `grassland.availableCount/inUseCount` | 与 UT-025~026 一致 |
97 100
 | ZCZX-YZCYSJ-API-010 | 草场占比 | 退化五档与合计 | 接口测试 | Postman | §5.5.2 | 已登录 | `degradationStats` | 长度=5;sum(count)=`grasslandCount` |
@@ -104,7 +107,7 @@
104 107
 | ZCZX-YZCYSJ-API-017 | 鉴权 | 未登录 | 接口测试 | curl | §7.2 异常 | 无 Token | `GET /dashboard` | 401 或统一未登录 |
105 108
 | ZCZX-YZCYSJ-API-018 | 权限 | 无 query 权限 | 接口测试 | Postman | §7.1 | 角色无 `breedingIndustry:query` | `GET /dashboard` | 403 或 `msg` 无权限 |
106 109
 | ZCZX-YZCYSJ-API-019 | 异常 | 聚合失败 | 接口测试 | Postman/Mock | §3.1.8 | Mock Mapper 抛错 | `GET /dashboard` | 失败;`data` 空;`msg` 含「加载失败」类文案 |
107
-| ZCZX-YZCYSJ-API-020 | 异常 | 空库零值 | 接口测试 | Postman | §5.1 空态 | 新库无业务数据 | `GET /dashboard` | `code=200`;数值 0;`ageStructure` 长 3、`degradationStats` 长 5 |
110
+| ZCZX-YZCYSJ-API-020 | 异常 | 空库零值 | 接口测试 | Postman | §5.1 空态 | 新库无业务数据 | `GET /dashboard` | `code=200`;数值 0;`ageStructure` 长 **4**、`degradationStats` 长 5 |
108 111
 | ZCZX-YZCYSJ-API-021 | 范围 | 无维护/同步接口 | 接口测试 | Postman | §3 本期不实现 | 已登录 | `POST/PUT/DELETE /dataModel/breedingIndustry/**` | 404/405 或无权限 |
109 112
 | ZCZX-YZCYSJ-API-022 | 范围 | 无筛选参数 | 接口测试 | Postman | §3、§8 | 已登录 | 传 `pastureId`/`year` 等非法 Query | 忽略或 400;口径仍按 §2 默认 |
110 113
 | ZCZX-YZCYSJ-API-023 | 出栏 | 无变更日出栏 | 接口测试 | Postman | §2.4 | 仅 Y-O0 | `annualOutbound` | 0 |
@@ -120,12 +123,11 @@
120 123
 | --- | --- | --- | --- | --- | --- | --- | --- | --- |
121 124
 | ZCZX-YZCYSJ-UI-001 | 正常流程 | 菜单进入自动加载 | UI | Playwright+Chrome | §4.2、§5.1 | UA 有菜单;后端正常 | 登录→点击「养殖产业数据」 | 出现四块标题;`v-loading` 结束后六项卡片有数;无报错弹窗 |
122 125
 | ZCZX-YZCYSJ-UI-002 | 产业总览 | 六项单位与只读 | UI | Playwright+Chrome | §5.2 | 有数据 | 检查首屏卡片 | 名称含存栏/出栏/预警/农牧户/草场/面积;单位头/户/个/亩;卡片不可点击跳转 |
123
-| ZCZX-YZCYSJ-UI-003 | 存栏变动 | 公母与未标注文案 | UI | Playwright+Chrome | §5.3.1 | `unknownGenderCount>0` | 查看存栏区文案 | 显示公牛、母牛数量;显示「未标注性别:N 头」 |
124
-| ZCZX-YZCYSJ-UI-004 | 存栏变动 | 未标注为 0 隐藏 | UI | Playwright+Chrome | §5.3.1、前端§4 | 造数全公母 | 打开页 | 不展示「未标注性别」文案 |
125
-| ZCZX-YZCYSJ-UI-005 | 存栏变动 | 折线止于当前月 | UI | Playwright+Chrome | §5.3.2 | 5 月环境 | 检查 ECharts 折线 X 轴与数据点 | 1~5 月有值;6~12 月无数据点/折线不延伸 |
126
-| ZCZX-YZCYSJ-UI-006 | 存栏变动 | 当前月与总览联动 | UI | Playwright+Chrome | §5.3.2 | 有数据 | 比对折线最后一点与「牦牛存栏总量」卡片 | 数值一致 |
127
-| ZCZX-YZCYSJ-UI-007 | 年龄结构 | 三档饼图与占比 | UI | Playwright+Chrome | §5.4 | 有数据 | 查看年龄结构图例/标签 | 含 1-6、7-12、12月龄以上(或等价文案);显示 % |
128
-| ZCZX-YZCYSJ-UI-008 | 草场占比 | 可用/使用中与退化 | UI | Playwright+Chrome | §5.5 | 有 G | 查看草场区 | 展示可用、使用中个数;退化列表/图五档;列表占比与图一致 |
126
+| ZCZX-YZCYSJ-UI-003 | 存栏变动 | 公母文案 | UI | Playwright+Chrome | §5.3.1 | 有 HERD-L | 查看存栏区文案 | 显示公牛、母牛数量;**无**「未标注性别」 |
127
+| ZCZX-YZCYSJ-UI-004 | 存栏变动 | 双折线 12 月 | UI | Playwright+Chrome | §5.3.2、前端§4 | 5 月环境 | 检查 ECharts 折线 | **12** 个刻度;横轴仅「N月」;悬停显「YYYY年N月」 |
128
+| ZCZX-YZCYSJ-UI-005 | 存栏变动 | 最近月与 header | UI | Playwright+Chrome | §5.3.1 | 有 HERD-L | 比对折线最后一点与上方公/母数 | 分别一致 |
129
+| ZCZX-YZCYSJ-UI-006 | 年龄结构 | 四档饼图与占比 | UI | Playwright+Chrome | §5.4 | 有数据 | 查看年龄结构图例/标签 | 含 1岁以下、1~3岁、3~12岁、12岁以上;显示 % |
130
+| ZCZX-YZCYSJ-UI-007 | 草场占比 | 可用/使用中与退化 | UI | Playwright+Chrome | §5.5 | 有 G | 查看草场区 | 展示可用、使用中个数;退化列表/图五档;列表占比与图一致 |
129 131
 | ZCZX-YZCYSJ-UI-009 | 刷新 | 成功更新全页 | UI | Playwright+Chrome | §5.1 | 已打开页 | 点击「刷新」 | 按钮 loading/disabled;完成后四块数据更新;可选轻提示「数据已更新」 |
130 132
 | ZCZX-YZCYSJ-UI-010 | 刷新 | 防重复点击 | UI | Playwright+Chrome | §5.1、前端§4 | 已打开页 | 刷新未完成时连点 2 次 | 仅 1 次有效请求或第二次按钮不可用 |
131 133
 | ZCZX-YZCYSJ-UI-011 | 异常 | 加载失败提示 | UI | Playwright+Chrome | §5.1、§7.2 | Mock `/dashboard` 返回 500 | 进入或点刷新 | 提示「加载失败,请稍后重试」类;页面不展示上一用户/会话缓存假数据 |
@@ -133,7 +135,7 @@
133 135
 | ZCZX-YZCYSJ-UI-013 | 权限 | 无菜单不可进 | UI | Playwright+Chrome | §7.1 | 无 `breedingIndustry:query` | 直接访问 `#/livestockIndustry/breedingIndustry/index` | 403/无权限页/重定向;不见四块看板 |
134 136
 | ZCZX-YZCYSJ-UI-014 | 范围 | 无维护按钮 | UI | Playwright+Chrome | §3、§5.1 | UA 有 query | 扫描工具栏与页面 | 无新增/编辑/删除/同步/导出;仅有刷新(及返回) |
135 137
 | ZCZX-YZCYSJ-UI-015 | 布局 | 纵向四块可滚动 | UI | Playwright+Chrome | §5.1 | 小屏或缩窗口 | 滚动页面 | 四块标题均可达;统计日/年可选展示 |
136
-| ZCZX-YZCYSJ-UI-016 | 权限 | UB 数据隔离展示 | UI | Playwright+Chrome | §2.9 | UB 登录;P1、P2 均有牛 | 打开看板记存栏总量;换 UA 对比 | UB 与 UA 存栏总量不同且符合各自牧场范围 |
138
+| ZCZX-YZCYSJ-UI-016 | 权限 | UB 预警隔离 | UI | Playwright+Chrome | §2.9 | UB 登录;P1、P2 均有预警 | 打开看板记 `warningYakCount`;换 UA 对比 | UB 与 UA 预警数不同;填报类指标(存栏/出栏/农牧户)全县一致 |
137 139
 
138 140
 ---
139 141
 
@@ -142,20 +144,21 @@
142 144
 | 功能需求 | 单元 | 接口 | 界面 |
143 145
 | --- | --- | --- | --- |
144 146
 | §2.1 只读看板 | — | API-021 | UI-014 |
145
-| §2.2 统计日/趋势月份 | UT-021~022 | API-002、005 | UI-005 |
146
-| §2.3 存栏认定 | UT-004~006 | API-003、026 | UI-006 |
147
+| §2.2 统计日/趋势月份 | UT-018~022 | API-002、005 | UI-004 |
148
+| §2.3 存栏认定(档案) | UT-004~006 | API-003、026 | — |
149
+| §2.11 填报存栏趋势 | UT-013、018~022 | API-004~006 | UI-003~005 |
147 150
 | §2.4 年出栏 | UT-007~008 | API-023 | API-003 |
148 151
 | §2.5 预警 | UT-009~012 | API-024~025 | API-003 |
149 152
 | §2.6 牧场草场总览 | UT-023~024 | API-003、010 | UI-002、008 |
150 153
 | §2.7 可用/使用中 | UT-025~026 | API-009 | UI-008 |
151 154
 | §2.8 退化占比 | UT-027 | API-010 | UI-008 |
152 155
 | §2.9 数据权限 | UT-001~003 | API-013~016 | UI-016 |
153
-| §2.10 月末趋势 | UT-018~022 | API-005~006 | UI-005~006 |
156
+| §2.10 月末趋势(档案) | — | — | — |
154 157
 | §5.1 加载/刷新/空态 | UT-028~029 | API-001、011~012、019~020 | UI-001、009~012 |
155
-| §5.2 产业总览 | UT-023~024 | API-003 | UI-002 |
156
-| §5.3 存栏变动 | UT-013~014、018~022 | API-004~006 | UI-003~006 |
157
-| §5.4 年龄结构 | UT-015~017 | API-007~008 | UI-007 |
158
-| §5.5 草场占比 | UT-025~027 | API-009~010 | UI-008 |
158
+| §5.2 产业总览 | UT-028 | API-003 | UI-002 |
159
+| §5.3 存栏变动 | UT-013、018~022 | API-004~006 | UI-003~005 |
160
+| §5.4 年龄结构 | UT-015~017 | API-007~008 | UI-006 |
161
+| §5.5 草场占比 | UT-025~027 | API-009~010 | UI-007 |
159 162
 | §7 权限提示 | — | API-017~018 | UI-013 |
160 163
 | §3 本期不实现 | — | API-021~022 | UI-014 |
161 164
 
@@ -171,6 +174,6 @@ use: { channel: 'chrome', locale: 'zh-CN', baseURL: process.env.RUOYI_BASE_URL }
171 174
 1. `beforeEach`:UI 用例先 `page.goto('/login')` 登录 UA/UB/U0/ADMIN。  
172 175
 2. 菜单路径以环境为准:`page.getByText('养殖产业数据').click()` 或路由 `#/livestockIndustry/breedingIndustry/index`。  
173 176
 3. 接口 Mock:UI-011 可用 `page.route('**/dataModel/breedingIndustry/dashboard', route => route.fulfill({ status: 500 }))`。  
174
-4. 图表断言:对 ECharts 容器 `.breeding-trend-chart` 等(以实现 `data-testid` 为准)检查 DOM 或拦截 XHR 校验 `monthlyTrend` 长度。
177
+4. 图表断言:对 ECharts 容器检查 DOM 或拦截 XHR 校验 `monthlyTrend` 长度 **=12**
175 178
 
176
-**用例统计**:单元 **29**、接口 **26**、界面 **16**,合计 **71** 条。
179
+**用例统计**:单元 **29**、接口 **26**、界面 **15**,合计 **70** 条。