wwh 1 неделя назад
Родитель
Сommit
f447b4a729

+ 4 - 2
baqing-admin/src/main/java/com/ruoyi/web/modules/industryservice/controller/BreedingIndustryDashboardController.java

@@ -22,8 +22,10 @@ public class BreedingIndustryDashboardController extends BaseController
22 22
 
23 23
     @PreAuthorize("@ss.hasPermi('dataModel:breedingIndustry:query')")
24 24
     @GetMapping("/dashboard")
25
-    public AjaxResult dashboard(@RequestParam(value = "forceRefresh", defaultValue = "false") boolean forceRefresh)
25
+    public AjaxResult dashboard(
26
+            @RequestParam(value = "forceRefresh", defaultValue = "false") boolean forceRefresh,
27
+            @RequestParam(value = "statYear", required = false) String statYear)
26 28
     {
27
-        return success(breedingIndustryDashboardService.loadDashboard(forceRefresh));
29
+        return success(breedingIndustryDashboardService.loadDashboard(forceRefresh, statYear));
28 30
     }
29 31
 }

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

@@ -2,6 +2,7 @@ package com.ruoyi.web.modules.industryservice.domain.vo;
2 2
 
3 3
 import java.util.ArrayList;
4 4
 import java.util.List;
5
+import com.ruoyi.web.modules.screen.domain.vo.HomeScreenYakStructureCategoryVo;
5 6
 
6 7
 public class BreedingIndustryDashboardVo
7 8
 {
@@ -13,7 +14,8 @@ public class BreedingIndustryDashboardVo
13 14
 
14 15
     private BreedingIndustryInventoryChangeVo inventoryChange = new BreedingIndustryInventoryChangeVo();
15 16
 
16
-    private List<BreedingIndustryAgeBandVo> ageStructure = new ArrayList<>();
17
+    /** 牦牛存栏结构(与大屏 yakInventoryStructure 口径一致) */
18
+    private List<HomeScreenYakStructureCategoryVo> yakInventoryStructure = new ArrayList<>();
17 19
 
18 20
     private BreedingIndustryGrasslandVo grassland = new BreedingIndustryGrasslandVo();
19 21
 
@@ -57,14 +59,14 @@ public class BreedingIndustryDashboardVo
57 59
         this.inventoryChange = inventoryChange;
58 60
     }
59 61
 
60
-    public List<BreedingIndustryAgeBandVo> getAgeStructure()
62
+    public List<HomeScreenYakStructureCategoryVo> getYakInventoryStructure()
61 63
     {
62
-        return ageStructure;
64
+        return yakInventoryStructure;
63 65
     }
64 66
 
65
-    public void setAgeStructure(List<BreedingIndustryAgeBandVo> ageStructure)
67
+    public void setYakInventoryStructure(List<HomeScreenYakStructureCategoryVo> yakInventoryStructure)
66 68
     {
67
-        this.ageStructure = ageStructure;
69
+        this.yakInventoryStructure = yakInventoryStructure;
68 70
     }
69 71
 
70 72
     public BreedingIndustryGrasslandVo getGrassland()

+ 9 - 1
baqing-admin/src/main/java/com/ruoyi/web/modules/industryservice/service/IBreedingIndustryDashboardService.java

@@ -8,9 +8,17 @@ import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryDashboard
8 8
 public interface IBreedingIndustryDashboardService
9 9
 {
10 10
     /**
11
-     * 加载看板全量数据。
11
+     * 加载看板全量数据(统计年默认当前自然年)
12 12
      *
13 13
      * @param forceRefresh 是否跳过可选缓存(本期实时聚合,参数预留)
14 14
      */
15 15
     BreedingIndustryDashboardVo loadDashboard(boolean forceRefresh);
16
+
17
+    /**
18
+     * 加载看板全量数据。
19
+     *
20
+     * @param forceRefresh 是否跳过可选缓存(本期实时聚合,参数预留)
21
+     * @param statYear     统计年份;空则当前自然年
22
+     */
23
+    BreedingIndustryDashboardVo loadDashboard(boolean forceRefresh, String statYear);
16 24
 }

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

@@ -3,7 +3,6 @@ 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;
7 6
 import java.time.ZoneId;
8 7
 import java.time.format.DateTimeFormatter;
9 8
 import java.util.ArrayList;
@@ -11,10 +10,11 @@ import java.util.Date;
11 10
 import java.util.HashMap;
12 11
 import java.util.List;
13 12
 import java.util.Map;
13
+
14
+import com.ruoyi.web.modules.industryservice.support.*;
14 15
 import org.springframework.beans.factory.annotation.Autowired;
15 16
 import org.springframework.stereotype.Service;
16 17
 import com.ruoyi.common.exception.ServiceException;
17
-import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryAgeBandVo;
18 18
 import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryDashboardVo;
19 19
 import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryDegradationStatVo;
20 20
 import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryGrasslandVo;
@@ -22,17 +22,17 @@ import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryInventory
22 22
 import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryOverviewVo;
23 23
 import com.ruoyi.web.modules.industryservice.mapper.BreedingIndustryStatsMapper;
24 24
 import com.ruoyi.web.modules.industryservice.service.IBreedingIndustryDashboardService;
25
-import com.ruoyi.web.modules.industryservice.support.BreedingIndustryCalculator;
26
-import com.ruoyi.web.modules.industryservice.support.BreedingIndustryDashboardSupport;
27
-import com.ruoyi.web.modules.industryservice.support.BreedingIndustryPastureScope;
28
-import com.ruoyi.web.modules.industryservice.support.BreedingIndustryRules;
29
-import com.ruoyi.web.modules.industryservice.support.GrasslandRules;
30
-import com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdCowAgeSumDto;
25
+import com.ruoyi.web.modules.prosperity.support.AchievementReportValidation;
31 26
 import com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderSumDto;
32 27
 import com.ruoyi.web.modules.screen.domain.dto.HomeScreenStatPeriodDto;
28
+import com.ruoyi.web.modules.screen.domain.vo.HomeScreenYakStructureCategoryVo;
33 29
 import com.ruoyi.web.modules.screen.mapper.HomeScreenStatsMapper;
34 30
 import com.ruoyi.web.modules.screen.support.HomeScreenSupport;
35 31
 
32
+/**
33
+ * 养殖产业看板:产业总量(存栏/出栏/农牧户/草场面积)、存栏变动、牦牛存栏结构与大屏首页口径一致;
34
+ * 默认所选年为当前自然年。
35
+ */
36 36
 @Service
37 37
 public class BreedingIndustryDashboardServiceImpl implements IBreedingIndustryDashboardService
38 38
 {
@@ -53,84 +53,101 @@ public class BreedingIndustryDashboardServiceImpl implements IBreedingIndustryDa
53 53
 
54 54
     @Override
55 55
     public BreedingIndustryDashboardVo loadDashboard(boolean forceRefresh)
56
+    {
57
+        return loadDashboard(forceRefresh, null);
58
+    }
59
+
60
+    @Override
61
+    public BreedingIndustryDashboardVo loadDashboard(boolean forceRefresh, String statYear)
56 62
     {
57 63
         try
58 64
         {
59
-            LocalDate statDate = LocalDate.now(ZoneId.of(BreedingIndustryRules.ZONE_SHANGHAI));
65
+            int year = resolveStatYear(statYear);
66
+            LocalDate statDate = HomeScreenSupport.resolveStatDate(year);
60 67
             List<Long> allowedPastureIds = breedingIndustryPastureScope.resolveAllowedPastureIds();
61 68
             BreedingIndustryDashboardVo dashboard = new BreedingIndustryDashboardVo();
62 69
             dashboard.setStatDate(statDate.format(STAT_DATE_FMT));
63
-            dashboard.setStatYear(statDate.getYear());
70
+            dashboard.setStatYear(year);
71
+
72
+            HomeScreenStatPeriodDto yearHerd =
73
+                    homeScreenStatsMapper.selectLatestHerdInventoryStatPeriodByYear(year);
74
+            HomeScreenStatPeriodDto yearOutbound =
75
+                    homeScreenStatsMapper.selectLatestOutboundReportStatPeriodByYear(year);
76
+            HomeScreenStatPeriodDto yearFarmerHousehold =
77
+                    homeScreenStatsMapper.selectLatestFarmerHouseholdStatPeriodByYear(year);
64 78
 
65 79
             BreedingIndustryOverviewVo overview = new BreedingIndustryOverviewVo();
66
-            fillGrasslandOverview(overview);
67
-            fillOverviewFromHerdReports(overview);
80
+            overview.setGrasslandCount(breedingIndustryStatsMapper.countGrassland());
81
+            overview.setInventoryTotal(sumHerdYakAtPeriod(yearHerd));
82
+            overview.setAnnualOutbound(sumOutboundYakAtPeriod(yearOutbound));
83
+            overview.setPastureCount(sumFarmerHouseholdAtPeriod(yearFarmerHousehold));
84
+            overview.setGrasslandAreaMu(sumFarmerHouseholdPastureAreaAtPeriod(yearFarmerHousehold));
85
+            fillWarningYakCount(overview, allowedPastureIds, statDate);
68 86
             dashboard.setOverview(overview);
69 87
 
70
-            HomeScreenStatPeriodDto latestHerd = homeScreenStatsMapper.selectLatestHerdInventoryStatPeriod();
71
-            YearMonth anchor = HomeScreenSupport.currentYearMonth();
72
-            int startYm = HomeScreenSupport.yearMonthKey(anchor.minusMonths(11));
73
-            int endYm = HomeScreenSupport.yearMonthKey(anchor);
74
-
75 88
             BreedingIndustryInventoryChangeVo inventoryChange = new BreedingIndustryInventoryChangeVo();
76
-            List<BreedingIndustryAgeBandVo> ageStructure;
77
-            if (latestHerd == null)
89
+            List<HomeScreenYakStructureCategoryVo> yakInventoryStructure;
90
+            if (yearHerd == null)
78 91
             {
79 92
                 inventoryChange.setBullCount(0);
80 93
                 inventoryChange.setCowCount(0);
81 94
                 inventoryChange.setUnknownGenderCount(0);
82
-                ageStructure = BreedingIndustryDashboardSupport.toAgeStructure(
83
-                        HomeScreenSupport.buildCowAgeStructureFromHerdReport(null));
95
+                yakInventoryStructure = HomeScreenSupport.buildYakInventoryStructure(null);
84 96
             }
85 97
             else
86 98
             {
87 99
                 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));
100
+                        yearHerd.getStatYear(), yearHerd.getStatMonth());
101
+                int bull = genderSum != null && genderSum.getBullTotal() != null ? genderSum.getBullTotal() : 0;
102
+                int cow = genderSum != null && genderSum.getCowTotal() != null ? genderSum.getCowTotal() : 0;
103
+                int yakTotal = genderSum != null && genderSum.getYakTotal() != null ? genderSum.getYakTotal() : 0;
104
+                inventoryChange.setBullCount(bull);
105
+                inventoryChange.setCowCount(cow);
106
+                inventoryChange.setUnknownGenderCount(Math.max(0, yakTotal - bull - cow));
107
+                yakInventoryStructure = HomeScreenSupport.buildYakInventoryStructure(
108
+                        homeScreenStatsMapper.sumHerdInventoryYakStructureByStatYearMonth(
109
+                                yearHerd.getStatYear(), yearHerd.getStatMonth()));
98 110
             }
99 111
             inventoryChange.setMonthlyTrend(BreedingIndustryDashboardSupport.toMonthlyTrend(
100
-                    HomeScreenSupport.buildRolling12MonthBullCowTrend(
101
-                            homeScreenStatsMapper.selectHerdInventoryBullCowByYearMonthRange(startYm, endYm),
102
-                            anchor)));
103
-
104
-            if (allowedPastureIds.isEmpty())
105
-            {
106
-                overview.setWarningYakCount(0);
107
-            }
108
-            else
109
-            {
110
-                Date yearStart = toDate(statDate.withDayOfYear(1));
111
-                Date statDateEnd = toDateEndOfDay(statDate);
112
-                overview.setWarningYakCount(
113
-                        breedingIndustryStatsMapper.countWarningYak(allowedPastureIds, yearStart, statDateEnd));
114
-            }
112
+                    HomeScreenSupport.buildBullCowMonthlyTrendFromHerdReport(
113
+                            homeScreenStatsMapper.selectHerdInventoryBullCowByMonth(year), statDate)));
115 114
 
116 115
             dashboard.setInventoryChange(inventoryChange);
117
-            dashboard.setAgeStructure(ageStructure);
118
-            dashboard.setGrassland(buildGrasslandVo(overview.getGrasslandCount()));
116
+            dashboard.setYakInventoryStructure(yakInventoryStructure);
117
+            dashboard.setGrassland(buildGrasslandVo(overview.getGrasslandCount(), statDate));
119 118
             return dashboard;
120 119
         }
120
+        catch (ServiceException ex)
121
+        {
122
+            throw ex;
123
+        }
121 124
         catch (Exception ex)
122 125
         {
123 126
             throw new ServiceException(BreedingIndustryRules.MSG_LOAD_FAILED);
124 127
         }
125 128
     }
126 129
 
127
-    private void fillOverviewFromHerdReports(BreedingIndustryOverviewVo overview)
130
+    private static int resolveStatYear(String statYear)
128 131
     {
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));
132
+        if (statYear == null || statYear.trim().isEmpty())
133
+        {
134
+            return HomeScreenSupport.currentYearMonth().getYear();
135
+        }
136
+        return AchievementReportValidation.parseStatYear(statYear);
137
+    }
138
+
139
+    private void fillWarningYakCount(BreedingIndustryOverviewVo overview, List<Long> allowedPastureIds,
140
+            LocalDate statDate)
141
+    {
142
+        if (allowedPastureIds == null || allowedPastureIds.isEmpty())
143
+        {
144
+            overview.setWarningYakCount(0);
145
+            return;
146
+        }
147
+        Date yearStart = toDate(statDate.withDayOfYear(1));
148
+        Date statDateEnd = toDateEndOfDay(statDate);
149
+        overview.setWarningYakCount(
150
+                breedingIndustryStatsMapper.countWarningYak(allowedPastureIds, yearStart, statDateEnd));
134 151
     }
135 152
 
136 153
     private int sumHerdYakAtPeriod(HomeScreenStatPeriodDto period)
@@ -159,21 +176,23 @@ public class BreedingIndustryDashboardServiceImpl implements IBreedingIndustryDa
159 176
         {
160 177
             return 0;
161 178
         }
162
-        return homeScreenStatsMapper.sumOutboundReportFarmerHouseholdCountByStatYearMonth(
179
+        return homeScreenStatsMapper.sumFarmerHouseholdCountByStatYearMonth(
163 180
                 period.getStatYear(), period.getStatMonth());
164 181
     }
165 182
 
166
-    private void fillGrasslandOverview(BreedingIndustryOverviewVo overview)
183
+    private BigDecimal sumFarmerHouseholdPastureAreaAtPeriod(HomeScreenStatPeriodDto period)
167 184
     {
168
-        int grasslandCount = breedingIndustryStatsMapper.countGrassland();
169
-        overview.setGrasslandCount(grasslandCount);
170
-        BigDecimal area = breedingIndustryStatsMapper.sumGrasslandAreaMu();
171
-        overview.setGrasslandAreaMu(area != null ? area.setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
185
+        if (period == null || period.getStatYear() == null || period.getStatMonth() == null)
186
+        {
187
+            return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
188
+        }
189
+        BigDecimal area = homeScreenStatsMapper.sumFarmerHouseholdPastureContractAreaByStatYearMonth(
190
+                period.getStatYear(), period.getStatMonth());
191
+        return area != null ? area.setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
172 192
     }
173 193
 
174
-    private BreedingIndustryGrasslandVo buildGrasslandVo(int grasslandTotal)
194
+    private BreedingIndustryGrasslandVo buildGrasslandVo(int grasslandTotal, LocalDate statDate)
175 195
     {
176
-        LocalDate statDate = LocalDate.now(ZoneId.of(BreedingIndustryRules.ZONE_SHANGHAI));
177 196
         BreedingIndustryGrasslandVo vo = new BreedingIndustryGrasslandVo();
178 197
         vo.setAvailableCount(breedingIndustryStatsMapper.countAvailableGrassland());
179 198
         vo.setInUseCount(breedingIndustryStatsMapper.countInUseGrassland(toDate(statDate)));

+ 4 - 3
baqing-admin/src/main/java/com/ruoyi/web/modules/screen/service/impl/HomeScreenServiceImpl.java

@@ -177,7 +177,6 @@ public class HomeScreenServiceImpl implements IHomeScreenService
177 177
                 homeScreenStatsMapper.selectHerdInventoryBullCowByMonth(year), statDate));
178 178
         vo.setOutboundMonthly(HomeScreenSupport.buildOutboundMonthly(
179 179
                 homeScreenStatsMapper.selectOutboundReportYakCountByMonth(year), statDate));
180
-        vo.setAgeStructure(Collections.emptyList());
181 180
         // 牲畜出栏 / 自食:所选年出栏表最大填报月
182 181
         if (yearOutbound == null)
183 182
         {
@@ -194,12 +193,12 @@ public class HomeScreenServiceImpl implements IHomeScreenService
194 193
                     yearOutbound,
195 194
                     homeScreenStatsMapper.sumOutboundReportSelfConsumptionByStatYearMonth(outYear, outMonth)));
196 195
         }
197
-        // 畜群存栏结构:所选年库中最大填报月,牦牛/奶牛/羊头数与占比
198
-        // 牦牛存栏结构:同月公牛/母牛/保险及细分(双层饼图)
196
+        // 畜群存栏结构 / 牦牛存栏结构 / 年龄结构:所选年库中最大填报月
199 197
         if (yearHerd == null)
200 198
         {
201 199
             vo.setHerdInventoryStructure(HomeScreenSupport.buildHerdInventoryStructure(null));
202 200
             vo.setYakInventoryStructure(HomeScreenSupport.buildYakInventoryStructure(null));
201
+            vo.setAgeStructure(HomeScreenSupport.buildCowAgeStructureFromHerdReport(null));
203 202
         }
204 203
         else
205 204
         {
@@ -209,6 +208,8 @@ public class HomeScreenServiceImpl implements IHomeScreenService
209 208
                     homeScreenStatsMapper.sumHerdInventorySpeciesByStatYearMonth(herdYear, herdMonth)));
210 209
             vo.setYakInventoryStructure(HomeScreenSupport.buildYakInventoryStructure(
211 210
                     homeScreenStatsMapper.sumHerdInventoryYakStructureByStatYearMonth(herdYear, herdMonth)));
211
+            vo.setAgeStructure(HomeScreenSupport.buildCowAgeStructureFromHerdReport(
212
+                    homeScreenStatsMapper.sumHerdInventoryCowAgeByStatYearMonth(herdYear, herdMonth)));
212 213
         }
213 214
         // 企业年度收入与补助:家庭牧场 / 养殖大户,所选年最大填报月各项合计(万元)
214 215
         if (yearFamilyRanch == null)

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

@@ -1,6 +1,9 @@
1 1
 package com.ruoyi.web.modules.industryservice.controller;
2 2
 
3
+import static org.mockito.ArgumentMatchers.any;
3 4
 import static org.mockito.ArgumentMatchers.anyBoolean;
5
+import static org.mockito.ArgumentMatchers.eq;
6
+import static org.mockito.ArgumentMatchers.isNull;
4 7
 import static org.mockito.Mockito.when;
5 8
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
6 9
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -30,7 +33,6 @@ import com.ruoyi.common.core.domain.entity.SysUser;
30 33
 import com.ruoyi.common.core.domain.model.LoginUser;
31 34
 import com.ruoyi.common.exception.ServiceException;
32 35
 import com.ruoyi.framework.web.exception.GlobalExceptionHandler;
33
-import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryAgeBandVo;
34 36
 import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryDashboardVo;
35 37
 import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryDegradationStatVo;
36 38
 import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryGrasslandVo;
@@ -39,6 +41,8 @@ import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryMonthlyTr
39 41
 import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryOverviewVo;
40 42
 import com.ruoyi.web.modules.industryservice.service.IBreedingIndustryDashboardService;
41 43
 import com.ruoyi.web.modules.industryservice.support.BreedingIndustryRules;
44
+import com.ruoyi.web.modules.screen.domain.vo.HomeScreenYakStructureCategoryVo;
45
+import com.ruoyi.web.modules.screen.domain.vo.HomeScreenYakStructureDetailVo;
42 46
 
43 47
 @ExtendWith(MockitoExtension.class)
44 48
 @MockitoSettings(strictness = Strictness.LENIENT)
@@ -72,14 +76,14 @@ class BreedingIndustryDashboardControllerApiTest
72 76
     @DisplayName("ZCZX-YZCYSJ-API-001 GET /dashboard")
73 77
     void api001_dashboard() throws Exception
74 78
     {
75
-        when(breedingIndustryDashboardService.loadDashboard(false)).thenReturn(sampleDashboard());
79
+        when(breedingIndustryDashboardService.loadDashboard(eq(false), isNull())).thenReturn(sampleDashboard());
76 80
         mockMvc.perform(get("/dataModel/breedingIndustry/dashboard"))
77 81
                 .andExpect(status().isOk())
78 82
                 .andExpect(jsonPath("$.code").value(200))
79 83
                 .andExpect(jsonPath("$.data.statDate").value("2026-05-20"))
80 84
                 .andExpect(jsonPath("$.data.overview.inventoryTotal").value(120))
81 85
                 .andExpect(jsonPath("$.data.inventoryChange.bullCount").value(45))
82
-                .andExpect(jsonPath("$.data.ageStructure[0].bandCode").value("COW_YOUNG"))
86
+                .andExpect(jsonPath("$.data.yakInventoryStructure[0].categoryCode").value("BULL"))
83 87
                 .andExpect(jsonPath("$.data.grassland.availableCount").value(50));
84 88
     }
85 89
 
@@ -87,17 +91,28 @@ class BreedingIndustryDashboardControllerApiTest
87 91
     @DisplayName("ZCZX-YZCYSJ-API-010 forceRefresh")
88 92
     void api010_forceRefresh() throws Exception
89 93
     {
90
-        when(breedingIndustryDashboardService.loadDashboard(true)).thenReturn(sampleDashboard());
94
+        when(breedingIndustryDashboardService.loadDashboard(eq(true), isNull())).thenReturn(sampleDashboard());
91 95
         mockMvc.perform(get("/dataModel/breedingIndustry/dashboard").param("forceRefresh", "true"))
92 96
                 .andExpect(status().isOk())
93 97
                 .andExpect(jsonPath("$.code").value(200));
94 98
     }
95 99
 
100
+    @Test
101
+    @DisplayName("ZCZX-YZCYSJ-API-011 指定统计年")
102
+    void api011_statYear() throws Exception
103
+    {
104
+        when(breedingIndustryDashboardService.loadDashboard(eq(false), eq("2025"))).thenReturn(sampleDashboard());
105
+        mockMvc.perform(get("/dataModel/breedingIndustry/dashboard").param("statYear", "2025"))
106
+                .andExpect(status().isOk())
107
+                .andExpect(jsonPath("$.code").value(200))
108
+                .andExpect(jsonPath("$.data.statYear").value(2026));
109
+    }
110
+
96 111
     @Test
97 112
     @DisplayName("ZCZX-YZCYSJ-API-017 聚合失败")
98 113
     void api017_loadFailed() throws Exception
99 114
     {
100
-        when(breedingIndustryDashboardService.loadDashboard(anyBoolean()))
115
+        when(breedingIndustryDashboardService.loadDashboard(anyBoolean(), any()))
101 116
                 .thenThrow(new ServiceException(BreedingIndustryRules.MSG_LOAD_FAILED));
102 117
         mockMvc.perform(get("/dataModel/breedingIndustry/dashboard"))
103 118
                 .andExpect(status().isOk())
@@ -151,12 +166,18 @@ class BreedingIndustryDashboardControllerApiTest
151 166
         point.setCowCount(55);
152 167
         inv.setMonthlyTrend(Collections.singletonList(point));
153 168
         vo.setInventoryChange(inv);
154
-        BreedingIndustryAgeBandVo band = new BreedingIndustryAgeBandVo();
155
-        band.setBandCode("COW_YOUNG");
156
-        band.setBandLabel("0~1.5岁幼畜");
157
-        band.setCount(30);
158
-        band.setRatio(new BigDecimal("25.0"));
159
-        vo.setAgeStructure(Collections.singletonList(band));
169
+        HomeScreenYakStructureCategoryVo cat = new HomeScreenYakStructureCategoryVo();
170
+        cat.setCategoryCode("BULL");
171
+        cat.setCategoryLabel("公牛");
172
+        cat.setCount(45);
173
+        cat.setRatio(new BigDecimal("37.5"));
174
+        HomeScreenYakStructureDetailVo detail = new HomeScreenYakStructureDetailVo();
175
+        detail.setDetailCode("BREEDING_BULL");
176
+        detail.setDetailLabel("种公牛");
177
+        detail.setCount(20);
178
+        detail.setRatio(new BigDecimal("16.7"));
179
+        cat.setDetails(Collections.singletonList(detail));
180
+        vo.setYakInventoryStructure(Collections.singletonList(cat));
160 181
         BreedingIndustryGrasslandVo grass = new BreedingIndustryGrasslandVo();
161 182
         grass.setAvailableCount(50);
162 183
         grass.setInUseCount(40);

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

@@ -5,7 +5,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
5 5
 import static org.mockito.ArgumentMatchers.any;
6 6
 import static org.mockito.ArgumentMatchers.anyInt;
7 7
 import static org.mockito.ArgumentMatchers.eq;
8
-import static org.mockito.Mockito.lenient;
9 8
 import static org.mockito.Mockito.when;
10 9
 
11 10
 import java.math.BigDecimal;
@@ -21,11 +20,12 @@ import com.ruoyi.web.modules.industryservice.domain.vo.BreedingIndustryDashboard
21 20
 import com.ruoyi.web.modules.industryservice.mapper.BreedingIndustryStatsMapper;
22 21
 import com.ruoyi.web.modules.industryservice.support.BreedingIndustryPastureScope;
23 22
 import com.ruoyi.web.modules.industryservice.support.BreedingIndustryRules;
24
-import com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdCowAgeSumDto;
23
+import com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderMonthDto;
25 24
 import com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderSumDto;
25
+import com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdYakStructureSumDto;
26 26
 import com.ruoyi.web.modules.screen.domain.dto.HomeScreenStatPeriodDto;
27
-import com.ruoyi.web.modules.screen.domain.dto.HomeScreenYearMonthBullCowDto;
28 27
 import com.ruoyi.web.modules.screen.mapper.HomeScreenStatsMapper;
28
+import com.ruoyi.web.modules.screen.support.HomeScreenSupport;
29 29
 
30 30
 @ExtendWith(MockitoExtension.class)
31 31
 @DisplayName("BreedingIndustryDashboardServiceImpl")
@@ -44,79 +44,98 @@ class BreedingIndustryDashboardServiceImplTest
44 44
     private BreedingIndustryDashboardServiceImpl service;
45 45
 
46 46
     @Test
47
-    @DisplayName("ZCZX-YZCYSJ-UT-002 可见牧场为空仍返回填报趋势")
47
+    @DisplayName("ZCZX-YZCYSJ-UT-002 可见牧场为空仍返回填报口径")
48 48
     void ut002_emptyScope()
49 49
     {
50 50
         stubGrassland();
51
-        stubNoHerdReport();
51
+        stubNoHerdReport(2026);
52 52
         when(breedingIndustryPastureScope.resolveAllowedPastureIds()).thenReturn(Collections.emptyList());
53
-        BreedingIndustryDashboardVo vo = service.loadDashboard(false);
53
+        BreedingIndustryDashboardVo vo = service.loadDashboard(false, "2026");
54
+        assertEquals(2026, vo.getStatYear());
54 55
         assertEquals(0, vo.getOverview().getInventoryTotal());
55 56
         assertEquals(0, vo.getInventoryChange().getBullCount());
56 57
         assertEquals(0, vo.getInventoryChange().getCowCount());
57 58
         assertEquals(0, vo.getInventoryChange().getUnknownGenderCount());
58
-        assertEquals(12, vo.getInventoryChange().getMonthlyTrend().size());
59
-        assertEquals(2, vo.getAgeStructure().size());
59
+        assertEquals(0, vo.getInventoryChange().getMonthlyTrend().size());
60
+        assertEquals(3, vo.getYakInventoryStructure().size());
61
+        assertEquals(HomeScreenSupport.YAK_CAT_BULL, vo.getYakInventoryStructure().get(0).getCategoryCode());
60 62
     }
61 63
 
62 64
     @Test
63
-    @DisplayName("ZCZX-YZCYSJ-UT-028 产业总览与存栏取自最近填报年月")
64
-    void ut028_overviewFromLatestReport()
65
+    @DisplayName("ZCZX-YZCYSJ-UT-028 产业总览与存栏取自所选年最大填报月")
66
+    void ut028_overviewFromYearMaxMonth()
65 67
     {
66 68
         stubGrassland();
67
-        HomeScreenStatPeriodDto herd = period(2025, 12);
69
+        HomeScreenStatPeriodDto herd = period(2026, 7);
68 70
         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);
71
+        HomeScreenStatPeriodDto farmer = period(2026, 6);
72
+        when(homeScreenStatsMapper.selectLatestHerdInventoryStatPeriodByYear(2026)).thenReturn(herd);
73
+        when(homeScreenStatsMapper.selectLatestOutboundReportStatPeriodByYear(2026)).thenReturn(outbound);
74
+        when(homeScreenStatsMapper.selectLatestFarmerHouseholdStatPeriodByYear(2026)).thenReturn(farmer);
75
+        when(homeScreenStatsMapper.sumHerdInventoryYakTotalByStatYearMonth(2026, 7)).thenReturn(50000);
72 76
         when(homeScreenStatsMapper.sumOutboundReportYakCountByStatYearMonth(2026, 5)).thenReturn(3200);
73
-        when(homeScreenStatsMapper.sumOutboundReportFarmerHouseholdCountByStatYearMonth(2026, 5)).thenReturn(8988);
77
+        when(homeScreenStatsMapper.sumFarmerHouseholdCountByStatYearMonth(2026, 6)).thenReturn(8988);
78
+        when(homeScreenStatsMapper.sumFarmerHouseholdPastureContractAreaByStatYearMonth(2026, 6))
79
+                .thenReturn(new BigDecimal("12500.50"));
74 80
         stubHerdInventoryBlock(herd, 100, 200);
75 81
         when(breedingIndustryPastureScope.resolveAllowedPastureIds())
76 82
                 .thenReturn(Collections.singletonList(1L));
77 83
         when(breedingIndustryStatsMapper.countWarningYak(any(), any(), any())).thenReturn(0);
78 84
 
79
-        BreedingIndustryDashboardVo vo = service.loadDashboard(false);
85
+        BreedingIndustryDashboardVo vo = service.loadDashboard(false, "2026");
80 86
 
81 87
         assertEquals(50000, vo.getOverview().getInventoryTotal());
82 88
         assertEquals(3200, vo.getOverview().getAnnualOutbound());
83 89
         assertEquals(8988, vo.getOverview().getPastureCount());
90
+        assertEquals(new BigDecimal("12500.50"), vo.getOverview().getGrasslandAreaMu());
84 91
         assertEquals(100, vo.getInventoryChange().getBullCount());
85 92
         assertEquals(200, vo.getInventoryChange().getCowCount());
86 93
         assertEquals(0, vo.getInventoryChange().getUnknownGenderCount());
87
-        assertEquals(12, vo.getInventoryChange().getMonthlyTrend().size());
88
-        assertEquals(2, vo.getAgeStructure().size());
94
+        assertEquals(7, vo.getInventoryChange().getMonthlyTrend().size());
95
+        assertEquals(3, vo.getYakInventoryStructure().size());
89 96
     }
90 97
 
91 98
     @Test
92
-    @DisplayName("ZCZX-YZCYSJ-UT-009 公母牛取自填报无未标注")
93
-    void ut009_genderFromReport()
99
+    @DisplayName("ZCZX-YZCYSJ-UT-009 公母牛取自填报;存栏结构与大屏一致")
100
+    void ut009_genderAndYakStructureFromReport()
94 101
     {
95 102
         stubGrassland();
96 103
         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);
104
+        when(homeScreenStatsMapper.selectLatestHerdInventoryStatPeriodByYear(2026)).thenReturn(herd);
105
+        when(homeScreenStatsMapper.selectLatestOutboundReportStatPeriodByYear(2026)).thenReturn(null);
106
+        when(homeScreenStatsMapper.selectLatestFarmerHouseholdStatPeriodByYear(2026)).thenReturn(null);
107
+        when(homeScreenStatsMapper.sumHerdInventoryYakTotalByStatYearMonth(2026, 7)).thenReturn(360);
100 108
         HomeScreenHerdGenderSumDto gender = new HomeScreenHerdGenderSumDto();
101 109
         gender.setBullTotal(120);
102 110
         gender.setCowTotal(230);
103
-        gender.setYakTotal(350);
111
+        gender.setYakTotal(360);
104 112
         when(homeScreenStatsMapper.sumHerdInventoryGenderByStatYearMonth(2026, 7)).thenReturn(gender);
105
-        HomeScreenHerdCowAgeSumDto age = new HomeScreenHerdCowAgeSumDto();
106
-        age.setCowYoungCount(40);
107
-        age.setCowAdultCount(60);
108
-        when(homeScreenStatsMapper.sumHerdInventoryCowAgeByStatYearMonth(2026, 7)).thenReturn(age);
109
-        when(homeScreenStatsMapper.selectHerdInventoryBullCowByYearMonthRange(anyInt(), anyInt()))
113
+        HomeScreenHerdYakStructureSumDto structure = new HomeScreenHerdYakStructureSumDto();
114
+        structure.setBreedingBullCount(50);
115
+        structure.setBullCastratedCount(70);
116
+        structure.setCowYoungCount(80);
117
+        structure.setCowAdultCount(150);
118
+        structure.setInsuredYakCount(10);
119
+        when(homeScreenStatsMapper.sumHerdInventoryYakStructureByStatYearMonth(2026, 7)).thenReturn(structure);
120
+        when(homeScreenStatsMapper.selectHerdInventoryBullCowByMonth(2026))
110 121
                 .thenReturn(Collections.emptyList());
111 122
         when(breedingIndustryPastureScope.resolveAllowedPastureIds())
112 123
                 .thenReturn(Collections.singletonList(1L));
113 124
         when(breedingIndustryStatsMapper.countWarningYak(any(), any(), any())).thenReturn(0);
114 125
 
115
-        BreedingIndustryDashboardVo vo = service.loadDashboard(false);
126
+        BreedingIndustryDashboardVo vo = service.loadDashboard(false, "2026");
116 127
 
117 128
         assertEquals(120, vo.getInventoryChange().getBullCount());
118 129
         assertEquals(230, vo.getInventoryChange().getCowCount());
119
-        assertEquals(0, vo.getInventoryChange().getUnknownGenderCount());
130
+        assertEquals(10, vo.getInventoryChange().getUnknownGenderCount());
131
+        assertEquals(0, vo.getInventoryChange().getMonthlyTrend().size());
132
+        assertEquals(HomeScreenSupport.YAK_CAT_BULL, vo.getYakInventoryStructure().get(0).getCategoryCode());
133
+        assertEquals(120, vo.getYakInventoryStructure().get(0).getCount());
134
+        assertEquals(HomeScreenSupport.YAK_CAT_COW, vo.getYakInventoryStructure().get(1).getCategoryCode());
135
+        assertEquals(230, vo.getYakInventoryStructure().get(1).getCount());
136
+        assertEquals(10, vo.getYakInventoryStructure().get(2).getCount());
137
+        assertEquals("种公牛", vo.getYakInventoryStructure().get(0).getDetails().get(0).getDetailLabel());
138
+        assertEquals(50, vo.getYakInventoryStructure().get(0).getDetails().get(0).getCount());
120 139
     }
121 140
 
122 141
     @Test
@@ -125,7 +144,7 @@ class BreedingIndustryDashboardServiceImplTest
125 144
     {
126 145
         when(breedingIndustryPastureScope.resolveAllowedPastureIds())
127 146
                 .thenThrow(new RuntimeException("db"));
128
-        ServiceException ex = assertThrows(ServiceException.class, () -> service.loadDashboard(false));
147
+        ServiceException ex = assertThrows(ServiceException.class, () -> service.loadDashboard(false, "2026"));
129 148
         assertEquals(BreedingIndustryRules.MSG_LOAD_FAILED, ex.getMessage());
130 149
     }
131 150
 
@@ -137,11 +156,12 @@ class BreedingIndustryDashboardServiceImplTest
137 156
         return dto;
138 157
     }
139 158
 
140
-    private void stubNoHerdReport()
159
+    private void stubNoHerdReport(int year)
141 160
     {
142
-        when(homeScreenStatsMapper.selectLatestHerdInventoryStatPeriod()).thenReturn(null);
143
-        when(homeScreenStatsMapper.selectLatestOutboundReportStatPeriod()).thenReturn(null);
144
-        when(homeScreenStatsMapper.selectHerdInventoryBullCowByYearMonthRange(anyInt(), anyInt()))
161
+        when(homeScreenStatsMapper.selectLatestHerdInventoryStatPeriodByYear(year)).thenReturn(null);
162
+        when(homeScreenStatsMapper.selectLatestOutboundReportStatPeriodByYear(year)).thenReturn(null);
163
+        when(homeScreenStatsMapper.selectLatestFarmerHouseholdStatPeriodByYear(year)).thenReturn(null);
164
+        when(homeScreenStatsMapper.selectHerdInventoryBullCowByMonth(year))
145 165
                 .thenReturn(Collections.emptyList());
146 166
     }
147 167
 
@@ -153,22 +173,20 @@ class BreedingIndustryDashboardServiceImplTest
153 173
         gender.setYakTotal(bull + cow);
154 174
         when(homeScreenStatsMapper.sumHerdInventoryGenderByStatYearMonth(
155 175
                 eq(herd.getStatYear()), eq(herd.getStatMonth()))).thenReturn(gender);
156
-        HomeScreenHerdCowAgeSumDto age = new HomeScreenHerdCowAgeSumDto();
157
-        when(homeScreenStatsMapper.sumHerdInventoryCowAgeByStatYearMonth(
158
-                eq(herd.getStatYear()), eq(herd.getStatMonth()))).thenReturn(age);
159
-        HomeScreenYearMonthBullCowDto row = new HomeScreenYearMonthBullCowDto();
160
-        row.setStatYear(herd.getStatYear());
161
-        row.setStatMonth(herd.getStatMonth());
176
+        when(homeScreenStatsMapper.sumHerdInventoryYakStructureByStatYearMonth(
177
+                eq(herd.getStatYear()), eq(herd.getStatMonth())))
178
+                .thenReturn(new HomeScreenHerdYakStructureSumDto());
179
+        HomeScreenHerdGenderMonthDto row = new HomeScreenHerdGenderMonthDto();
180
+        row.setMonth(herd.getStatMonth());
162 181
         row.setBullCount(bull);
163 182
         row.setCowCount(cow);
164
-        when(homeScreenStatsMapper.selectHerdInventoryBullCowByYearMonthRange(anyInt(), anyInt()))
183
+        when(homeScreenStatsMapper.selectHerdInventoryBullCowByMonth(anyInt()))
165 184
                 .thenReturn(Collections.singletonList(row));
166 185
     }
167 186
 
168 187
     private void stubGrassland()
169 188
     {
170 189
         when(breedingIndustryStatsMapper.countGrassland()).thenReturn(3);
171
-        when(breedingIndustryStatsMapper.sumGrasslandAreaMu()).thenReturn(new BigDecimal("100.00"));
172 190
         when(breedingIndustryStatsMapper.countAvailableGrassland()).thenReturn(2);
173 191
         when(breedingIndustryStatsMapper.countInUseGrassland(any())).thenReturn(1);
174 192
         when(breedingIndustryStatsMapper.selectDegradationGroupCounts()).thenReturn(Collections.emptyList());

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

@@ -26,7 +26,7 @@
26 26
 
27 27
 | 方法 | HTTP | 说明 |
28 28
 | --- | --- | --- |
29
-| `getBreedingDashboard` | GET | `/dashboard`,Query 可选 `forceRefresh=true` |
29
+| `getBreedingDashboard` | GET | `/dashboard`,Query:`statYear`(默认当前年)、可选 `forceRefresh=true` |
30 30
 
31 31
 ---
32 32
 
@@ -36,10 +36,10 @@
36 36
 
37 37
 | 区块 | 组件建议 | 数据节点 |
38 38
 | --- | --- | --- |
39
-| 工具栏 | 右上角「刷新」`el-button`,`v-hasPermi` query | — |
39
+| 工具栏 | 年份 `el-select`(默认当前年)+「刷新」`el-button`,`v-hasPermi` query | `statYear` |
40 40
 | 产业总览 | 6 个 `el-card` 或指标条 | `data.overview` |
41 41
 | 牦牛存栏变动 | 公/母数字 + ECharts 折线 | `data.inventoryChange` |
42
-| 牦牛年龄结构 | ECharts 饼图或条形 | `data.ageStructure` |
42
+| 牦牛存栏结构 | ECharts 双层饼图 | `data.yakInventoryStructure` |
43 43
 | 草场类型占比 | 可用/使用中数字 + 退化占比图 | `data.grassland` |
44 44
 
45 45
 页脚或总览区展示 **统计日** `statDate`、**统计年** `statYear`(可选小字)。

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

@@ -126,11 +126,14 @@
126 126
 
127 127
 | 指标/图表 | 数据源 | 规则 |
128 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` | 最近填报年月母牛两龄档合计(0~1.5 / 1.5+) |
129
+| 总览存栏 | `biz_yak_herd_inventory.yak_total` | 全县合计;**所选年**内最大填报月 |
130
+| 总览出栏 | `biz_yak_outbound_report.yak_outbound_count` | 同上;所选年最大填报月 |
131
+| 农牧户、草场面积 | `biz_farmer_household` | 所选年最大填报月;`household_count`、`pasture_contract_area`(与大屏一致) |
132
+| 公/母牛数量 | `biz_yak_herd_inventory` | 所选年最大填报月全县 `bull_total`、`cow_total`;未标注=`yak−公−母` |
133
+| 存栏趋势 | `biz_yak_herd_inventory` | 所选年 1~填报最大月;缺月补 **0**;无填报为空数组 |
134
+| 年龄结构 | `biz_yak_herd_inventory` | **改为牦牛存栏结构**(与大屏 `yakInventoryStructure` 一致):所选年最大填报月公牛/母牛/保险及细分 |
135
+
136
+**默认年份**:进入看板时所选年为**当前自然年**(可切换)。
134 137
 
135 138
 ---
136 139
 
@@ -142,7 +145,7 @@
142 145
 | --- | --- |
143 146
 | 产业总览 | 六项指标卡片展示,见 **§5.2** |
144 147
 | 牦牛存栏变动 | 当前公、母牛数量 + 按月存栏趋势图,见 **§5.3** |
145
-| 牦牛年龄结构 | 母牛两档年龄占比图,见 **§5.4** |
148
+| 牦牛存栏结构 | 双层饼图(内环公牛·母牛·保险牛,外环细分),与大屏一致,见 **§5.4** |
146 149
 | 草场类型占比 | 可用/使用中数量 + 退化等级数量与占比,见 **§5.5** |
147 150
 | 刷新 | 重新拉取并计算全页指标,见 **§5.1** |
148 151
 
@@ -212,6 +215,7 @@ flowchart LR
212 215
 | 项 | 要求 |
213 216
 | --- | --- |
214 217
 | 布局 | 单页纵向滚动;四块标题与原型一致:产业总览、牦牛存栏变动、牦牛年龄结构、草场类型占比 |
218
+| 年份 | 工具栏提供年份选择;**默认当前自然年**;切换后按所选年重算总览填报项、存栏变动、年龄结构(与大屏一致) |
215 219
 | 加载 | 首次进入全页加载;失败时给出明确提示,**不得**展示上一用户缓存数据冒充成功 |
216 220
 | 刷新 | 页面提供「刷新」按钮(位置与全局规范一致,如右上角工具栏);刷新中防重复点击;成功后更新全部指标与图表 |
217 221
 | 空数据 | 某模块无数据时,数字显示 **0** 或「—」,图表展示空态说明,不报错崩溃 |
@@ -224,12 +228,12 @@ flowchart LR
224 228
 
225 229
 | 序号 | 指标名称 | 单位 | 统计口径 |
226 230
 | --- | --- | --- | --- |
227
-| 1 | 牦牛存栏总量 | 头 | `biz_yak_herd_inventory.yak_total` 全县乡镇合计;**最近填报年月**;接口字段 **`inventoryTotal`** |
228
-| 2 | 牦牛年出栏量 | 头 | `biz_yak_outbound_report.yak_outbound_count` 全县乡镇合计;**最近填报年月**;接口字段 **`annualOutbound`** |
231
+| 1 | 牦牛存栏总量 | 头 | `biz_yak_herd_inventory.yak_total` 全县合计;**所选年最大填报月**;接口字段 **`inventoryTotal`** |
232
+| 2 | 牦牛年出栏量 | 头 | `biz_yak_outbound_report.yak_outbound_count` 全县合计;**所选年最大填报月**;接口字段 **`annualOutbound`** |
229 233
 | 3 | 预警牦牛数量 | 头 | **§2.5**(档案时点口径) |
230
-| 4 | 农牧户 | 户 | `biz_yak_outbound_report.farmer_household_count` 全县乡镇合计;**最近填报年月**;接口字段 **`pastureCount`** |
231
-| 5 | 草场数量 | 个 | **§2.6** |
232
-| 6 | 草场面积 | 亩 | **§2.6** |
234
+| 4 | 农牧户 | 户 | `biz_farmer_household.household_count` 全县合计;**所选年最大填报月**;接口字段 **`pastureCount`** |
235
+| 5 | 草场数量 | 个 | **§2.6**(台账块数) |
236
+| 6 | 草场面积 | 亩 | `biz_farmer_household.pasture_contract_area` 全县合计;**所选年最大填报月**;接口字段 **`grasslandAreaMu`** |
233 237
 
234 238
 **展示规则:**
235 239
 
@@ -245,30 +249,32 @@ flowchart LR
245 249
 
246 250
 | 指标 | 单位 | 口径 |
247 251
 | --- | --- | --- |
248
-| 公牛数量 | 头 | `biz_yak_herd_inventory.bull_total` 全县乡镇合计;取**最近填报年月**(`stat_year` + `stat_month` 最大) |
249
-| 母牛数量 | 头 | `biz_yak_herd_inventory.cow_total` 全县乡镇合计;同上 |
252
+| 公牛数量 | 头 | `biz_yak_herd_inventory.bull_total` 全县合计;取**所选年最大填报月** |
253
+| 母牛数量 | 头 | `biz_yak_herd_inventory.cow_total` 全县合计;同上 |
250 254
 
251
-**不展示**「未标注性别」;与大屏首页存栏变动 header 口径一致
255
+接口另回传 `unknownGenderCount = max(0, yak−bull−cow)`(与大屏一致;页面可不展示)
252 256
 
253 257
 #### 5.3.2 存栏趋势(折线图)
254 258
 
255 259
 | 项 | 要求 |
256 260
 | --- | --- |
257 261
 | 图表类型 | 折线图(**公牛**、**母牛**两系列) |
258
-| 数据范围 | 距当前时间**最近 12 个自然月**(与大屏「牦牛存栏变动」一致) |
259
-| 横轴 | 12 个刻度,**仅显示月份**;悬停展示 **YYYY年M月** |
262
+| 数据范围 | **所选年** 1~填报最大月(与大屏「牦牛存栏变动」一致);无填报为空 |
263
+| 横轴 | 按月刻度,**仅显示月份**;悬停展示 **YYYY年M月** |
260 264
 | 纵轴 | 存栏数量,单位 **头** |
261
-| 接口字段 | `monthlyTrend[]` 含 `statYear`、`month`、`bullCount`、`cowCount`;缺月补 **0** |
265
+| 接口字段 | `monthlyTrend[]` 含 `statYear`、`month`、`bullCount`、`cowCount`;区间内缺月补 **0** |
262 266
 
263
-### 5.4 牦牛年龄结构
267
+### 5.4 牦牛存栏结构
264 268
 
265 269
 | 项 | 要求 |
266 270
 | --- | --- |
267
-| 图表类型 | 饼图(或环形图,与原型一致) |
268
-| 统计对象 | 存栏填报**最近年月**全县母牛分龄合计 |
269
-| 分档 | 两档(与大屏一致):**0~1.5岁幼畜**、**1.5岁以上成畜** |
270
-| 编码 | `COW_YOUNG` / `COW_ADULT` |
271
-| 展示 | 每档展示**头数**与**占比(%)**;各档占比之和为 **100%**(四舍五入 ±1% 可接受) |
271
+| 图表类型 | 双层饼图(内环大类、外环细分;与大屏「牦牛存栏结构」一致) |
272
+| 统计对象 | 存栏填报**所选年最大填报月**全县合计 |
273
+| 内环 | 公牛 / 母牛 / 保险牛 |
274
+| 外环 | 种公牛、公牛/阉畜、0~1.5岁、1.5岁以上、保险 |
275
+| 接口字段 | `yakInventoryStructure[]`(与大屏同结构) |
276
+| 默认年份 | **当前自然年** |
277
+| 展示 | 内环/外环均展示头数与占比(%);占比按牦牛总量(公牛+母牛+保险)计算 |
272 278
 
273 279
 ### 5.5 草场类型占比
274 280
 
@@ -380,6 +386,8 @@ flowchart LR
380 386
 
381 387
 | 版本 | 日期 | 说明 |
382 388
 | --- | --- | --- |
389
+| 1.5 | 2026-07-24 | 原「年龄结构」改为「牦牛存栏结构」,与大屏 `yakInventoryStructure` 双层饼口径一致;默认当前年 |
390
+| 1.4 | 2026-07-24 | 产业总量/存栏变动/年龄与大屏按所选年对齐;默认当前年;农牧户与草场面积改农牧户填报 |
383 391
 | 1.3 | 2026-07-17 | 年龄结构改为母牛两档(0~1.5 / 1.5+),与存栏填报、大屏对齐 |
384 392
 | 1.2 | 2026-07-04 | 存栏变动/年龄/总览三项改填报表;滚动 12 月;母牛四档;与大屏对齐 |
385 393
 | 1.1 | (既有) | 总览存栏/出栏/农牧户改填报合计 |

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

@@ -22,7 +22,7 @@
22 22
 | 看板加载/刷新 | 一次请求返回四块数据;`statDate` 取服务器当前日期 |
23 23
 | 数据权限 | Service 注入用户可见 `pastureId` 列表;牦牛、预警按 **§2.2** 过滤;草场全县台账不按牧场过滤 |
24 24
 | 月末存栏趋势(档案) | 历史能力保留于 `BreedingIndustryCalculator`;**看板趋势已改填报表**,见 **§2.11** |
25
-| 存栏变动/年龄/趋势 | 复用 `HomeScreenStatsMapper` + `HomeScreenSupport` 滚动 12 月逻辑,与大屏一致 |
25
+| 存栏变动/年龄/趋势 | 复用 `HomeScreenStatsMapper` + `HomeScreenSupport`;按**所选年**最大填报月与 1~N 月趋势,与大屏首页一致 |
26 26
 | 性能 | 数据量大时启用 **§2.3** 可选快照表 + 短 TTL 缓存;本期可先实时聚合;月末 12 点目标 **≤3s**(1 万级档案,评审可调) |
27 27
 
28 28
 **依赖模块(只读)**
@@ -34,7 +34,8 @@
34 34
 | `biz_yak_asset` | 牦牛资产档案 |
35 35
 | `biz_yak_disease_warning` | 牦牛疾病预警 |
36 36
 | `biz_yak_herd_inventory` | 牦牛存栏数据填报(存栏变动、年龄结构、总览存栏) |
37
-| `biz_yak_outbound_report` | 牦牛出栏数据填报(总览出栏、农牧户) |
37
+| `biz_yak_outbound_report` | 牦牛出栏数据填报(总览出栏) |
38
+| `biz_farmer_household` | 农牧户填报(总览农牧户、草场面积) |
38 39
 
39 40
 ---
40 41
 
@@ -204,37 +205,38 @@ return success(dashboardService.loadDashboard(allowed, forceRefresh));
204 205
 | 参数 | 类型 | 必填 | 说明 |
205 206
 | --- | --- | --- | --- |
206 207
 | `forceRefresh` | boolean | N | 默认 `false`;`true` 时跳过 **§2.3** 缓存 |
208
+| `statYear` | string | N | 四位年份;空则**当前自然年** |
207 209
 
208 210
 #### 3.1.2 响应 `data` 结构
209 211
 
210 212
 | 节点 | 类型 | 说明 |
211 213
 | --- | --- | --- |
212
-| `statDate` | string | 统计日 `yyyy-MM-dd` |
213
-| `statYear` | int | 当前自然年,如 `2026` |
214
+| `statDate` | string | 统计日 `yyyy-MM-dd`(所选年为当年则当日,历史年为 12-31) |
215
+| `statYear` | int | 回显所选年 |
214 216
 | `overview` | object | 产业总览 **§3.1.3** |
215 217
 | `inventoryChange` | object | 牦牛存栏变动 **§3.1.4** |
216
-| `ageStructure` | array | 年龄结构 **§3.1.5** |
218
+| `yakInventoryStructure` | array | 牦牛存栏结构 **§3.1.5**(与大屏一致) |
217 219
 | `grassland` | object | 草场类型占比 **§3.1.6** |
218 220
 
219 221
 #### 3.1.3 `overview`
220 222
 
221 223
 | 字段 | 类型 | 说明 |
222 224
 | --- | --- | --- |
223
-| `inventoryTotal` | int | 牦牛存栏总量(头);`biz_yak_herd_inventory` **最近填报年月**全县 `yak_total` 合计 |
224
-| `annualOutbound` | int | 牦牛年出栏量(头);`biz_yak_outbound_report` **最近填报年月**全县 `yak_outbound_count` 合计 |
225
+| `inventoryTotal` | int | 牦牛存栏总量(头);所选年最大填报月全县 `yak_total` |
226
+| `annualOutbound` | int | 牦牛年出栏量(头);所选年最大填报月全县 `yak_outbound_count` |
225 227
 | `warningYakCount` | int | 预警牦牛数量(头);**§2.5** 档案口径 |
226
-| `pastureCount` | int | 农牧户(户);`biz_yak_outbound_report` **最近填报年月**全县 `farmer_household_count` 合计 |
227
-| `grasslandCount` | int | 草场数量(个) |
228
-| `grasslandAreaMu` | decimal | 草场面积(亩) |
228
+| `pastureCount` | int | 农牧户(户);所选年最大填报月 `biz_farmer_household.household_count` |
229
+| `grasslandCount` | int | 草场数量(个);台账块数 |
230
+| `grasslandAreaMu` | decimal | 草场面积(亩);所选年最大填报月 `pasture_contract_area` |
229 231
 
230 232
 #### 3.1.4 `inventoryChange`
231 233
 
232 234
 | 字段 | 类型 | 说明 |
233 235
 | --- | --- | --- |
234
-| `bullCount` | int | 公牛数量(头);最近填报年月全县 `bull_total` 合计 |
235
-| `cowCount` | int | 母牛数量(头);最近填报年月全县 `cow_total` 合计 |
236
-| `unknownGenderCount` | int | 固定 **0**(不展示未标注性别) |
237
-| `monthlyTrend` | array | 滚动最近 **12** 自然月;固定 **12** 条 |
236
+| `bullCount` | int | 公牛数量(头);所选年最大填报月全县 `bull_total` |
237
+| `cowCount` | int | 母牛数量(头);所选年最大填报月全县 `cow_total` |
238
+| `unknownGenderCount` | int | `max(0, yak−bull−cow)` |
239
+| `monthlyTrend` | array | 所选年 1~填报最大月;无填报为空 |
238 240
 
239 241
 `monthlyTrend[]` 元素:
240 242
 
@@ -245,16 +247,12 @@ return success(dashboardService.loadDashboard(allowed, forceRefresh));
245 247
 | `bullCount` | int | 该月全县公牛合计;无数据为 **0** |
246 248
 | `cowCount` | int | 该月全县母牛合计;无数据为 **0** |
247 249
 
248
-#### 3.1.5 `ageStructure[]`
250
+#### 3.1.5 `yakInventoryStructure[]`
249 251
 
250
-| 字段 | 类型 | 说明 |
251
-| --- | --- | --- |
252
-| `bandCode` | string | `COW_YOUNG` / `COW_ADULT` |
253
-| `bandLabel` | string | `0~1.5岁幼畜` / `1.5岁以上成畜` |
254
-| `count` | int | 头数 |
255
-| `ratio` | decimal | 占比 0~100 数值,保留 1 位小数 |
252
+与大屏首页 `yakInventoryStructure` 完全一致:内环三类(`BULL`/`COW`/`INSURED`),外环细分(种公牛、公牛/阉畜、0~1.5岁、1.5岁以上、保险);所选年最大填报月全县合计;count=0 也返回三类。
256 253
 
257
-两档固定返回(count 为 0 也返回);与大屏年龄结构一致;数据源为存栏填报 `cow_young_count` / `cow_adult_count`。
254
+大类字段:`categoryCode`、`categoryLabel`、`count`、`ratio`、`details[]`。  
255
+明细字段:`detailCode`、`detailLabel`、`count`、`ratio`。
258 256
 
259 257
 #### 3.1.6 `grassland`
260 258
 
@@ -271,7 +269,7 @@ return success(dashboardService.loadDashboard(allowed, forceRefresh));
271 269
 | `degradationLevel` | int | 1~5 |
272 270
 | `degradationLabel` | string | 未退化、轻度… |
273 271
 | `count` | int | 个数 |
274
-| `ratio` | decimal | 占比 %(同 `ageStructure`) |
272
+| `ratio` | decimal | 占比 % |
275 273
 
276 274
 五档固定返回;`count` 之和应等于 `overview.grasslandCount`。
277 275
 
@@ -301,8 +299,17 @@ return success(dashboardService.loadDashboard(allowed, forceRefresh));
301 299
         { "statYear": 2026, "month": 5, "bullCount": 45, "cowCount": 60 }
302 300
       ]
303 301
     },
304
-    "ageStructure": [
305
-      { "bandCode": "COW_U1", "bandLabel": "1岁以下", "count": 30, "ratio": 25.0 }
302
+    "yakInventoryStructure": [
303
+      {
304
+        "categoryCode": "BULL",
305
+        "categoryLabel": "公牛",
306
+        "count": 45,
307
+        "ratio": 37.5,
308
+        "details": [
309
+          { "detailCode": "BREEDING_BULL", "detailLabel": "种公牛", "count": 20, "ratio": 16.7 },
310
+          { "detailCode": "BULL_CASTRATED", "detailLabel": "公牛/阉畜", "count": 25, "ratio": 20.8 }
311
+        ]
312
+      }
306 313
     ],
307 314
     "grassland": {
308 315
       "availableCount": 50,
@@ -369,8 +376,8 @@ VALUES ('养殖产业数据', {parentId}, 10, 'breedingIndustry', 'livestockIndu
369 376
 | 功能需求 | 技术落点 |
370 377
 | --- | --- |
371 378
 | §5.2 产业总览 | `overview` + **§2.2** SQL |
372
-| §5.3 存栏变动 | `inventoryChange` + 填报表滚动 12 月(与大屏一致) |
373
-| §5.4 年龄结构 | `ageStructure` 母牛两档(填报最近年月) |
379
+| §5.3 存栏变动 | `inventoryChange` + 所选年 1~N 月趋势(与大屏一致) |
380
+| §5.4 牦牛存栏结构 | `yakInventoryStructure` 双层饼(与大屏一致;默认当前年) |
374 381
 | §5.5 草场占比 | `grassland` |
375 382
 | §5.1 刷新 | **3.1** `forceRefresh` |
376 383
 | §2.9 数据权限 | **§2.6** `BreedingIndustryPastureScope`(仅预警等档案类指标) |
@@ -382,6 +389,8 @@ VALUES ('养殖产业数据', {parentId}, 10, 'breedingIndustry', 'livestockIndu
382 389
 
383 390
 | 版本 | 日期 | 说明 |
384 391
 | --- | --- | --- |
392
+| 1.5 | 2026-07-24 | 看板「年龄结构」改为 `yakInventoryStructure`,与大屏牦牛存栏结构一致 |
393
+| 1.4 | 2026-07-24 | 产业总量/存栏变动/年龄与大屏按所选年对齐;默认当前年;农牧户与草场面积改农牧户填报 |
385 394
 | 1.3 | 2026-07-17 | 年龄结构两档 `COW_YOUNG`/`COW_ADULT` |
386 395
 | 1.2 | 2026-07-04 | 存栏变动/年龄/趋势改 `biz_yak_herd_inventory` 填报表;滚动 12 月;与大屏字段对齐 |
387 396
 | 1.1 | (既有) | 总览三项改填报合计 |

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

@@ -63,16 +63,16 @@
63 63
 | ZCZX-YZCYSJ-UT-010 | 预警 | 周期下界 1/1 0:00 | 单元测试 | JUnit5 | §2.5 | `alert_time=当年1/1 00:00:00` | 统计 | 计入 |
64 64
 | ZCZX-YZCYSJ-UT-011 | 预警 | 周期外排除 | 单元测试 | JUnit5 | §2.5 | W-out | 统计 | 0 |
65 65
 | ZCZX-YZCYSJ-UT-012 | 预警 | 须关联纳入档案 | 单元测试 | JUnit5 | §2.9 | 预警牛仅 P2、范围 P1 | `countWarning` | 0 |
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` 最近年月合计一致 |
66
+| ZCZX-YZCYSJ-UT-013 | 存栏变动 | 公母来自所选年最大月 | 单元测试 | JUnit5 | §5.3.1、§2.11 | HERD-L | `loadDashboard` | `bullCount`/`cowCount` 与所选年最大月一致;`unknownGenderCount=yak−公−母` |
67
+| ZCZX-YZCYSJ-UT-028 | 产业总览 | 填报三项所选年最大月 | 单元测试 | JUnit5 | §5.2、§2.11 | Mock 填报合计 | `loadDashboard` | `inventoryTotal`/`annualOutbound`/`pastureCount`/`grasslandAreaMu` 与大屏所选年口径一致 |
68 68
 | ZCZX-YZCYSJ-UT-014 | 性别 | trim 后匹配 | 单元测试 | JUnit5 | 技术§4 | `gender=' 母 '` | `normalizeGender` | 计母牛(档案类,预警用) |
69
-| ZCZX-YZCYSJ-UT-015 | 年龄结构 | 两档编码 | 单元测试 | JUnit5 | §5.4 | Mock 母牛两龄填报 | `toAgeStructure` | `bandCode` 为 COW_YOUNG/COW_ADULT |
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 |
69
+| ZCZX-YZCYSJ-UT-015 | 存栏结构 | 三类编码 | 单元测试 | JUnit5 | §5.4 | Mock 填报结构 | `buildYakInventoryStructure` | `categoryCode` 为 BULL/COW/INSURED |
70
+| ZCZX-YZCYSJ-UT-016 | 存栏结构 | 占比与零分母 | 单元测试 | JUnit5 | §5.4 | 合计 0 | `buildYakInventoryStructure` | 三类 count=0、ratio=0 仍返回 |
71
+| ZCZX-YZCYSJ-UT-017 | 存栏结构 | 细分明细 | 单元测试 | JUnit5 | §5.4 | 有细分数据 | details | 含种公牛/阉畜/幼畜/成畜/保险 |
72
+| ZCZX-YZCYSJ-UT-018 | 存栏趋势 | 所选年 1~N 长度 | 单元测试 | JUnit5 | §5.3.2、§2.11 | 最大填报月=7 | `buildBullCowMonthlyTrendFromHerdReport` | 长度=**7**;含 statYear+month |
73 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** |
74
+| ZCZX-YZCYSJ-UT-020 | 存栏趋势 | 最月与 header 一致 | 单元测试 | JUnit5 | §5.3.1 | HERD-L | 趋势最后一点 vs header | 公/母分别相等 |
75
+| ZCZX-YZCYSJ-UT-021 | 存栏趋势 | 无填报为空 | 单元测试 | JUnit5 | §2.11 | 所选年无存栏 | 趋势 | 空数组 |
76 76
 | ZCZX-YZCYSJ-UT-022 | 存栏趋势 | 不依赖档案月末 | 单元测试 | JUnit5 | §2.11 | 档案与填报不一致 | 趋势 | 以填报表为准 |
77 77
 | ZCZX-YZCYSJ-UT-023 | 牧场 | 原口径暂保留 | 单元测试 | JUnit5 | §2.6 | P-停、注销 | `countPastureInScope` 仍调用 | 结果**不**写入 `pastureCount` |
78 78
 | ZCZX-YZCYSJ-UT-024 | 草场 | 面积空不参与求和 | 单元测试 | JUnit5 | §2.6 | 一条 `area_mu` null | `sumArea` | 不计该条 |
@@ -88,17 +88,17 @@
88 88
 
89 89
 | 用例编号 | 测试模块 | 测试项 | 测试类型 | 测试工具 | 测试目的 | 前置条件 | 测试步骤 | 预期结果 |
90 90
 | --- | --- | --- | --- | --- | --- | --- | --- | --- |
91
-| ZCZX-YZCYSJ-API-001 | 看板 | 首次加载全量结构 | 接口测试 | Postman/curl | §5.1 正常流程 | UA 登录;造数完整 | `GET /dataModel/breedingIndustry/dashboard` | `code=200`;`data` 含 `statDate`、`statYear`、`overview`、`inventoryChange`、`ageStructure`、`grassland` |
92
-| ZCZX-YZCYSJ-API-002 | 看板 | 统计日与自然年 | 接口测试 | Postman | §2.2 | 已登录 | 同上 | `statDate`=`yyyy-MM-dd` 服务器当日;`statYear` 为公历年 |
93
-| ZCZX-YZCYSJ-API-003 | 产业总览 | 六项指标非负 | 接口测试 | Postman | §5.2 | 已知样本 | 读 `overview` 六字段 | 与 UT-004~012、023~024 手工值一致;`grasslandAreaMu`≥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` | 长度=**2**;`bandCode` 为 COW_YOUNG/COW_ADULT;count=0 也返回 |
98
-| ZCZX-YZCYSJ-API-008 | 年龄结构 | 占比之和容忍 | 接口测试 | Postman | §5.4 | 存栏>0 | sum(ratio) | 99.9~100.1(四舍五入容忍) |
91
+| ZCZX-YZCYSJ-API-001 | 看板 | 首次加载全量结构 | 接口测试 | Postman/curl | §5.1 正常流程 | UA 登录;造数完整 | `GET /dataModel/breedingIndustry/dashboard` | `code=200`;`data` 含 `statDate`、`statYear`、`overview`、`inventoryChange`、`yakInventoryStructure`、`grassland` |
92
+| ZCZX-YZCYSJ-API-002 | 看板 | 默认当前年 | 接口测试 | Postman | §2.11 | 已登录;不传 `statYear` | `GET .../dashboard` | `statYear`=服务器当前公历年 |
93
+| ZCZX-YZCYSJ-API-003 | 产业总览 | 六项指标非负 | 接口测试 | Postman | §5.2 | 已知样本 | 读 `overview` 六字段 | 与 UT 手工值一致;`grasslandAreaMu`≥0 |
94
+| ZCZX-YZCYSJ-API-004 | 存栏变动 | 公母与未标注 | 接口测试 | Postman | §5.3.1 | HERD-L | 读 `inventoryChange` | 公/母与所选年最大月一致;`unknownGenderCount=yak−公−母` |
95
+| ZCZX-YZCYSJ-API-005 | 存栏变动 | 所选年 1~N 趋势 | 接口测试 | Postman | §5.3.2 | 最大填报月=5 | `monthlyTrend` | 长度=**5**;含 `statYear`/`month`/`bullCount`/`cowCount`;末项 month=5 |
96
+| ZCZX-YZCYSJ-API-006 | 存栏变动 | 最月与 header | 接口测试 | Postman | §5.3.1、§2.11 | HERD-L | 比 `monthlyTrend` 最后一项与 header | 公/母分别相等 |
97
+| ZCZX-YZCYSJ-API-007 | 存栏结构 | 固定三类 | 接口测试 | Postman | §5.4 | 已登录 | `yakInventoryStructure` | 长度=**3**;`categoryCode` 为 BULL/COW/INSURED;含 details |
98
+| ZCZX-YZCYSJ-API-008 | 存栏结构 | 占比之和容忍 | 接口测试 | Postman | §5.4 | 存栏>0 | sum(大类 ratio) | 99.9~100.1(四舍五入容忍) |
99 99
 | ZCZX-YZCYSJ-API-009 | 草场占比 | 可用/使用中 | 接口测试 | Postman | §5.5.1 | 已登录 | `grassland.availableCount/inUseCount` | 与 UT-025~026 一致 |
100 100
 | ZCZX-YZCYSJ-API-010 | 草场占比 | 退化五档与合计 | 接口测试 | Postman | §5.5.2 | 已登录 | `degradationStats` | 长度=5;sum(count)=`grasslandCount` |
101
-| ZCZX-YZCYSJ-API-011 | 刷新 | forceRefresh | 接口测试 | Postman | §5.1 | 已登录 | `GET .../dashboard?forceRefresh=true` | `code=200`;全节点更新;`statDate` 不变(同日) |
101
+| ZCZX-YZCYSJ-API-011 | 刷新 | forceRefresh + 指定年 | 接口测试 | Postman | §5.1 | 已登录 | `GET .../dashboard?statYear=2025&forceRefresh=true` | `code=200`;`statYear` 回显 2025 |
102 102
 | ZCZX-YZCYSJ-API-012 | 刷新 | 同日再次加载一致性 | 接口测试 | Postman | 技术§4 一致性 | 后台数据未变 | 连续两次 `GET`(第二次 `forceRefresh=true`) | 指标一致(除非源数据变) |
103 103
 | ZCZX-YZCYSJ-API-013 | 数据权限 | UA 隔离 P2 | 接口测试 | Postman | §2.9 | P2 牦牛+W2;UA 登录 | `GET /dashboard` | 不含 P2 牦牛;`warningYakCount` 不含 W2 |
104 104
 | ZCZX-YZCYSJ-API-014 | 数据权限 | UB 仅 P2 | 接口测试 | Postman | §2.9 | UB 登录 | `GET /dashboard` | 仅 P2 牦牛;`pastureCount` 含 P2 不含 P1 |
@@ -126,7 +126,7 @@
126 126
 | ZCZX-YZCYSJ-UI-003 | 存栏变动 | 公母文案 | UI | Playwright+Chrome | §5.3.1 | 有 HERD-L | 查看存栏区文案 | 显示公牛、母牛数量;**无**「未标注性别」 |
127 127
 | ZCZX-YZCYSJ-UI-004 | 存栏变动 | 双折线 12 月 | UI | Playwright+Chrome | §5.3.2、前端§4 | 5 月环境 | 检查 ECharts 折线 | **12** 个刻度;横轴仅「N月」;悬停显「YYYY年N月」 |
128 128
 | ZCZX-YZCYSJ-UI-005 | 存栏变动 | 最近月与 header | UI | Playwright+Chrome | §5.3.1 | 有 HERD-L | 比对折线最后一点与上方公/母数 | 分别一致 |
129
-| ZCZX-YZCYSJ-UI-006 | 年龄结构 | 两档饼图与占比 | UI | Playwright+Chrome | §5.4 | 有数据 | 查看年龄结构图例/标签 | 含 0~1.5岁幼畜、1.5岁以上成畜;显示 % |
129
+| ZCZX-YZCYSJ-UI-006 | 存栏结构 | 双层饼图 | UI | Playwright+Chrome | §5.4 | 有数据 | 查看存栏结构图例/标签 | 含公牛/母牛/保险及细分;显示 % |
130 130
 | ZCZX-YZCYSJ-UI-007 | 草场占比 | 可用/使用中与退化 | UI | Playwright+Chrome | §5.5 | 有 G | 查看草场区 | 展示可用、使用中个数;退化列表/图五档;列表占比与图一致 |
131 131
 | ZCZX-YZCYSJ-UI-009 | 刷新 | 成功更新全页 | UI | Playwright+Chrome | §5.1 | 已打开页 | 点击「刷新」 | 按钮 loading/disabled;完成后四块数据更新;可选轻提示「数据已更新」 |
132 132
 | ZCZX-YZCYSJ-UI-010 | 刷新 | 防重复点击 | UI | Playwright+Chrome | §5.1、前端§4 | 已打开页 | 刷新未完成时连点 2 次 | 仅 1 次有效请求或第二次按钮不可用 |