wwh il y a 1 semaine
Parent
commit
6203a828bb

+ 1 - 1
baqing-admin/src/main/java/com/ruoyi/web/modules/screen/domain/vo/HomeScreenInventoryChangeVo.java

@@ -12,7 +12,7 @@ public class HomeScreenInventoryChangeVo
12
     private int cowCount;
12
     private int cowCount;
13
     /** 性别未知头数(存栏 - 公 - 母) */
13
     /** 性别未知头数(存栏 - 公 - 母) */
14
     private int unknownGenderCount;
14
     private int unknownGenderCount;
15
-    /** 公母牛月度存栏(12 点) */
15
+    /** 公母牛月度存栏(1~数据最大月;区间内缺月补 0) */
16
     private List<HomeScreenInventoryMonthVo> monthlyTrend;
16
     private List<HomeScreenInventoryMonthVo> monthlyTrend;
17
 
17
 
18
     public int getBullCount()
18
     public int getBullCount()

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

@@ -65,7 +65,7 @@ import com.ruoyi.web.modules.screen.support.HomeScreenSupport;
65
  * <p>
65
  * <p>
66
  * 牦牛存栏/出栏趋势取自 {@code biz_yak_herd_inventory}、{@code biz_yak_outbound_report} 所选年各月全县村级合计;
66
  * 牦牛存栏/出栏趋势取自 {@code biz_yak_herd_inventory}、{@code biz_yak_outbound_report} 所选年各月全县村级合计;
67
  * 产业总览填报类指标取所选年<strong>最大填报月</strong>合计或记录数;
67
  * 产业总览填报类指标取所选年<strong>最大填报月</strong>合计或记录数;
68
- * 存栏/出栏图为所选年 1~12 月(当年未到月份补零)。
68
+ * 存栏公母牛趋势、出栏月度趋势:均按所选年填报最大月份 N 输出 1~N 月(缺月补零)。
69
  */
69
  */
70
 @Service
70
 @Service
71
 public class HomeScreenServiceImpl implements IHomeScreenService
71
 public class HomeScreenServiceImpl implements IHomeScreenService
@@ -154,7 +154,7 @@ public class HomeScreenServiceImpl implements IHomeScreenService
154
         overview.setWithLivestockHouseholdCount(countAtPeriod(yearOwnership,
154
         overview.setWithLivestockHouseholdCount(countAtPeriod(yearOwnership,
155
                 homeScreenStatsMapper::sumWithLivestockHouseholdCountByStatYearMonth));
155
                 homeScreenStatsMapper::sumWithLivestockHouseholdCountByStatYearMonth));
156
 
156
 
157
-        // 存栏变动当前公母:所选年库中最大月份;趋势/出栏:所选年 1~12 
157
+        // 存栏变动当前公母:所选年库中最大月份;存栏趋势/出栏趋势:均为 1~填报最大
158
         HomeScreenInventoryChangeVo inventoryChange = new HomeScreenInventoryChangeVo();
158
         HomeScreenInventoryChangeVo inventoryChange = new HomeScreenInventoryChangeVo();
159
         if (yearHerd == null)
159
         if (yearHerd == null)
160
         {
160
         {

+ 64 - 36
baqing-admin/src/main/java/com/ruoyi/web/modules/screen/support/HomeScreenSupport.java

@@ -550,43 +550,55 @@ public final class HomeScreenSupport
550
     }
550
     }
551
 
551
 
552
     /**
552
     /**
553
-     * 公母牛月度存栏趋势(存栏填报表 {@code bull_total}/{@code cow_total} 全县按月合计);
554
-     * 固定输出所选年 1~12 月,缺月补 0;当前年未来月份填 0。
553
+     * 公母牛月度存栏趋势(存栏填报表 {@code bull_total}/{@code cow_total} 全县按月合计)。
554
+     * <p>
555
+     * 以所选年填报数据中的<strong>最大月份 N</strong>为准,输出 1~N 月(按月升序);
556
+     * 区间内缺月补 0;无任何填报月则返回空列表。
557
+     * </p>
558
+     *
559
+     * @param rows     按月合计行
560
+     * @param statDate 所选年统计截止日(取年份写入 {@code statYear})
555
      */
561
      */
556
     public static List<HomeScreenInventoryMonthVo> buildBullCowMonthlyTrendFromHerdReport(
562
     public static List<HomeScreenInventoryMonthVo> buildBullCowMonthlyTrendFromHerdReport(
557
             List<HomeScreenHerdGenderMonthDto> rows, LocalDate statDate)
563
             List<HomeScreenHerdGenderMonthDto> rows, LocalDate statDate)
558
     {
564
     {
559
         Map<Integer, HomeScreenHerdGenderMonthDto> byMonth = new HashMap<>();
565
         Map<Integer, HomeScreenHerdGenderMonthDto> byMonth = new HashMap<>();
566
+        int maxMonth = 0;
560
         if (rows != null)
567
         if (rows != null)
561
         {
568
         {
562
             for (HomeScreenHerdGenderMonthDto row : rows)
569
             for (HomeScreenHerdGenderMonthDto row : rows)
563
             {
570
             {
564
-                if (row.getMonth() != null)
571
+                if (row == null || row.getMonth() == null)
572
+                {
573
+                    continue;
574
+                }
575
+                int month = row.getMonth();
576
+                if (month < 1 || month > 12)
577
+                {
578
+                    continue;
579
+                }
580
+                byMonth.put(month, row);
581
+                if (month > maxMonth)
565
                 {
582
                 {
566
-                    byMonth.put(row.getMonth(), row);
583
+                    maxMonth = month;
567
                 }
584
                 }
568
             }
585
             }
569
         }
586
         }
570
-        int statYear = statDate.getYear();
571
-        int futureAfter = statDate.getMonthValue();
572
-        boolean currentYear = statYear == LocalDate.now(ZoneId.of(BreedingIndustryRules.ZONE_SHANGHAI)).getYear();
573
-        List<HomeScreenInventoryMonthVo> points = new ArrayList<>();
574
-        for (int month = 1; month <= 12; month++)
587
+        if (maxMonth <= 0)
588
+        {
589
+            return Collections.emptyList();
590
+        }
591
+        int statYear = statDate != null ? statDate.getYear()
592
+                : LocalDate.now(ZoneId.of(BreedingIndustryRules.ZONE_SHANGHAI)).getYear();
593
+        List<HomeScreenInventoryMonthVo> points = new ArrayList<>(maxMonth);
594
+        for (int month = 1; month <= maxMonth; month++)
575
         {
595
         {
576
             HomeScreenInventoryMonthVo point = new HomeScreenInventoryMonthVo();
596
             HomeScreenInventoryMonthVo point = new HomeScreenInventoryMonthVo();
577
             point.setStatYear(statYear);
597
             point.setStatYear(statYear);
578
             point.setMonth(month);
598
             point.setMonth(month);
579
-            if (currentYear && month > futureAfter)
580
-            {
581
-                point.setBullCount(0);
582
-                point.setCowCount(0);
583
-            }
584
-            else
585
-            {
586
-                HomeScreenHerdGenderMonthDto src = byMonth.get(month);
587
-                point.setBullCount(src != null && src.getBullCount() != null ? src.getBullCount() : 0);
588
-                point.setCowCount(src != null && src.getCowCount() != null ? src.getCowCount() : 0);
589
-            }
599
+            HomeScreenHerdGenderMonthDto src = byMonth.get(month);
600
+            point.setBullCount(src != null && src.getBullCount() != null ? src.getBullCount() : 0);
601
+            point.setCowCount(src != null && src.getCowCount() != null ? src.getCowCount() : 0);
590
             points.add(point);
602
             points.add(point);
591
         }
603
         }
592
         return points;
604
         return points;
@@ -1406,38 +1418,54 @@ public final class HomeScreenSupport
1406
         return result;
1418
         return result;
1407
     }
1419
     }
1408
 
1420
 
1421
+    /**
1422
+     * 牦牛月度出栏趋势(出栏填报表 {@code yak_outbound_count} 全县按月合计)。
1423
+     * <p>
1424
+     * 以所选年填报数据中的<strong>最大月份 N</strong>为准,输出 1~N 月(按月升序);
1425
+     * 区间内缺月补 0;无任何填报月则返回空列表。
1426
+     * </p>
1427
+     *
1428
+     * @param rows     按月合计行
1429
+     * @param statDate 所选年统计截止日(取年份写入 {@code statYear})
1430
+     */
1409
     public static List<HomeScreenOutboundMonthVo> buildOutboundMonthly(
1431
     public static List<HomeScreenOutboundMonthVo> buildOutboundMonthly(
1410
             List<HomeScreenMonthCountDto> rows, LocalDate statDate)
1432
             List<HomeScreenMonthCountDto> rows, LocalDate statDate)
1411
     {
1433
     {
1412
         Map<Integer, Integer> byMonth = new HashMap<>();
1434
         Map<Integer, Integer> byMonth = new HashMap<>();
1435
+        int maxMonth = 0;
1413
         if (rows != null)
1436
         if (rows != null)
1414
         {
1437
         {
1415
             for (HomeScreenMonthCountDto row : rows)
1438
             for (HomeScreenMonthCountDto row : rows)
1416
             {
1439
             {
1417
-                if (row.getMonth() != null && row.getCount() != null)
1440
+                if (row == null || row.getMonth() == null)
1441
+                {
1442
+                    continue;
1443
+                }
1444
+                int month = row.getMonth();
1445
+                if (month < 1 || month > 12)
1446
+                {
1447
+                    continue;
1448
+                }
1449
+                byMonth.put(month, row.getCount() != null ? row.getCount() : 0);
1450
+                if (month > maxMonth)
1418
                 {
1451
                 {
1419
-                    byMonth.put(row.getMonth(), row.getCount());
1452
+                    maxMonth = month;
1420
                 }
1453
                 }
1421
             }
1454
             }
1422
         }
1455
         }
1423
-        int statYear = statDate.getYear();
1424
-        int futureAfter = statDate.getMonthValue();
1425
-        boolean currentYear = statYear
1426
-                == LocalDate.now(ZoneId.of(BreedingIndustryRules.ZONE_SHANGHAI)).getYear();
1427
-        List<HomeScreenOutboundMonthVo> list = new ArrayList<>();
1428
-        for (int month = 1; month <= 12; month++)
1456
+        if (maxMonth <= 0)
1457
+        {
1458
+            return Collections.emptyList();
1459
+        }
1460
+        int statYear = statDate != null ? statDate.getYear()
1461
+                : LocalDate.now(ZoneId.of(BreedingIndustryRules.ZONE_SHANGHAI)).getYear();
1462
+        List<HomeScreenOutboundMonthVo> list = new ArrayList<>(maxMonth);
1463
+        for (int month = 1; month <= maxMonth; month++)
1429
         {
1464
         {
1430
             HomeScreenOutboundMonthVo vo = new HomeScreenOutboundMonthVo();
1465
             HomeScreenOutboundMonthVo vo = new HomeScreenOutboundMonthVo();
1431
             vo.setStatYear(statYear);
1466
             vo.setStatYear(statYear);
1432
             vo.setMonth(month);
1467
             vo.setMonth(month);
1433
-            if (currentYear && month > futureAfter)
1434
-            {
1435
-                vo.setOutboundCount(0);
1436
-            }
1437
-            else
1438
-            {
1439
-                vo.setOutboundCount(byMonth.getOrDefault(month, 0));
1440
-            }
1468
+            vo.setOutboundCount(byMonth.getOrDefault(month, 0));
1441
             list.add(vo);
1469
             list.add(vo);
1442
         }
1470
         }
1443
         return list;
1471
         return list;

+ 3 - 2
baqing-admin/src/test/java/com/ruoyi/web/modules/screen/controller/HomeScreenControllerApiTest.java

@@ -93,7 +93,7 @@ class HomeScreenControllerApiTest
93
                 .andExpect(jsonPath("$.code").value(200))
93
                 .andExpect(jsonPath("$.code").value(200))
94
                 .andExpect(jsonPath("$.data.statYear").value(2026))
94
                 .andExpect(jsonPath("$.data.statYear").value(2026))
95
                 .andExpect(jsonPath("$.data.industryOverview.inventoryTotal").value(10))
95
                 .andExpect(jsonPath("$.data.industryOverview.inventoryTotal").value(10))
96
-                .andExpect(jsonPath("$.data.inventoryChange.monthlyTrend.length()").value(12));
96
+                .andExpect(jsonPath("$.data.inventoryChange.monthlyTrend.length()").value(5));
97
     }
97
     }
98
 
98
 
99
     @Test
99
     @Test
@@ -517,9 +517,10 @@ class HomeScreenControllerApiTest
517
         inv.setCowCount(5);
517
         inv.setCowCount(5);
518
         inv.setUnknownGenderCount(1);
518
         inv.setUnknownGenderCount(1);
519
         List<HomeScreenInventoryMonthVo> trend = new ArrayList<>();
519
         List<HomeScreenInventoryMonthVo> trend = new ArrayList<>();
520
-        for (int m = 1; m <= 12; m++)
520
+        for (int m = 1; m <= 5; m++)
521
         {
521
         {
522
             HomeScreenInventoryMonthVo p = new HomeScreenInventoryMonthVo();
522
             HomeScreenInventoryMonthVo p = new HomeScreenInventoryMonthVo();
523
+            p.setStatYear(2026);
523
             p.setMonth(m);
524
             p.setMonth(m);
524
             p.setBullCount(1);
525
             p.setBullCount(1);
525
             p.setCowCount(1);
526
             p.setCowCount(1);

+ 20 - 6
baqing-admin/src/test/java/com/ruoyi/web/modules/screen/service/impl/HomeScreenServiceImplTest.java

@@ -140,8 +140,8 @@ class HomeScreenServiceImplTest
140
         assertNotNull(vo.getStatDate());
140
         assertNotNull(vo.getStatDate());
141
         assertNotNull(vo.getIndustryOverview());
141
         assertNotNull(vo.getIndustryOverview());
142
         assertNotNull(vo.getInventoryChange());
142
         assertNotNull(vo.getInventoryChange());
143
-        assertEquals(12, vo.getOutboundMonthly().size());
144
-        assertEquals(12, vo.getInventoryChange().getMonthlyTrend().size());
143
+        assertEquals(0, vo.getOutboundMonthly().size());
144
+        assertEquals(0, vo.getInventoryChange().getMonthlyTrend().size());
145
         assertEquals(0, vo.getAgeStructure().size());
145
         assertEquals(0, vo.getAgeStructure().size());
146
         assertNotNull(vo.getGrassland());
146
         assertNotNull(vo.getGrassland());
147
         assertNotNull(vo.getAppointment());
147
         assertNotNull(vo.getAppointment());
@@ -643,7 +643,7 @@ class HomeScreenServiceImplTest
643
     }
643
     }
644
 
644
 
645
     @Test
645
     @Test
646
-    @DisplayName("DP-SY-UT-031 存栏变动当前公母取所选年最大填报月,趋势/出栏为所选年1~12月")
646
+    @DisplayName("DP-SY-UT-031 存栏变动当前公母取所选年最大填报月,趋势为 1~填报最大月")
647
     void ut031_inventoryChangeCurrentGenderFromYearMaxMonth()
647
     void ut031_inventoryChangeCurrentGenderFromYearMaxMonth()
648
     {
648
     {
649
         stubGrasslandAndReport();
649
         stubGrasslandAndReport();
@@ -654,16 +654,30 @@ class HomeScreenServiceImplTest
654
         gender.setCowTotal(200);
654
         gender.setCowTotal(200);
655
         gender.setYakTotal(310);
655
         gender.setYakTotal(310);
656
         when(homeScreenStatsMapper.sumHerdInventoryGenderByStatYearMonth(2024, 9)).thenReturn(gender);
656
         when(homeScreenStatsMapper.sumHerdInventoryGenderByStatYearMonth(2024, 9)).thenReturn(gender);
657
+        com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderMonthDto m3 =
658
+                new com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderMonthDto();
659
+        m3.setMonth(3);
660
+        m3.setBullCount(40);
661
+        m3.setCowCount(50);
662
+        com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderMonthDto m9 =
663
+                new com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderMonthDto();
664
+        m9.setMonth(9);
665
+        m9.setBullCount(100);
666
+        m9.setCowCount(200);
667
+        when(homeScreenStatsMapper.selectHerdInventoryBullCowByMonth(2024))
668
+                .thenReturn(java.util.Arrays.asList(m3, m9));
657
 
669
 
658
         HomeScreenDashboardVo vo = service.loadDashboard("2024");
670
         HomeScreenDashboardVo vo = service.loadDashboard("2024");
659
 
671
 
660
         assertEquals(100, vo.getInventoryChange().getBullCount());
672
         assertEquals(100, vo.getInventoryChange().getBullCount());
661
         assertEquals(200, vo.getInventoryChange().getCowCount());
673
         assertEquals(200, vo.getInventoryChange().getCowCount());
662
         assertEquals(10, vo.getInventoryChange().getUnknownGenderCount());
674
         assertEquals(10, vo.getInventoryChange().getUnknownGenderCount());
663
-        assertEquals(12, vo.getInventoryChange().getMonthlyTrend().size());
664
-        assertEquals(12, vo.getOutboundMonthly().size());
675
+        assertEquals(9, vo.getInventoryChange().getMonthlyTrend().size());
676
+        assertEquals(0, vo.getOutboundMonthly().size());
665
         assertEquals(1, vo.getInventoryChange().getMonthlyTrend().get(0).getMonth());
677
         assertEquals(1, vo.getInventoryChange().getMonthlyTrend().get(0).getMonth());
666
-        assertEquals(12, vo.getInventoryChange().getMonthlyTrend().get(11).getMonth());
678
+        assertEquals(9, vo.getInventoryChange().getMonthlyTrend().get(8).getMonth());
679
+        assertEquals(0, vo.getInventoryChange().getMonthlyTrend().get(0).getBullCount());
680
+        assertEquals(40, vo.getInventoryChange().getMonthlyTrend().get(2).getBullCount());
667
     }
681
     }
668
 
682
 
669
     @Test
683
     @Test

+ 39 - 8
baqing-admin/src/test/java/com/ruoyi/web/modules/screen/support/HomeScreenSupportTest.java

@@ -89,22 +89,41 @@ class HomeScreenSupportTest
89
     }
89
     }
90
 
90
 
91
     @Test
91
     @Test
92
-    @DisplayName("DP-SY-UT-005b 存栏填报公母牛趋势 12 月补零")
93
-    void ut005b_herdReportBullCowMonthlyTwelve()
92
+    @DisplayName("DP-SY-UT-005b 存栏填报公母牛趋势按最大月输出 1~N")
93
+    void ut005b_herdReportBullCowMonthlyToMaxMonth()
94
     {
94
     {
95
         com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderMonthDto m5 =
95
         com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderMonthDto m5 =
96
                 new com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderMonthDto();
96
                 new com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderMonthDto();
97
         m5.setMonth(5);
97
         m5.setMonth(5);
98
         m5.setBullCount(10);
98
         m5.setBullCount(10);
99
         m5.setCowCount(20);
99
         m5.setCowCount(20);
100
+        com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderMonthDto m3 =
101
+                new com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderMonthDto();
102
+        m3.setMonth(3);
103
+        m3.setBullCount(7);
104
+        m3.setCowCount(8);
100
         List<HomeScreenInventoryMonthVo> trend = HomeScreenSupport.buildBullCowMonthlyTrendFromHerdReport(
105
         List<HomeScreenInventoryMonthVo> trend = HomeScreenSupport.buildBullCowMonthlyTrendFromHerdReport(
101
-                Collections.singletonList(m5), LocalDate.of(2024, 12, 31));
102
-        assertEquals(12, trend.size());
106
+                Arrays.asList(m5, m3), LocalDate.of(2024, 12, 31));
107
+        assertEquals(5, trend.size());
108
+        assertEquals(1, trend.get(0).getMonth());
109
+        assertEquals(0, trend.get(0).getBullCount());
110
+        assertEquals(3, trend.get(2).getMonth());
111
+        assertEquals(7, trend.get(2).getBullCount());
112
+        assertEquals(5, trend.get(4).getMonth());
103
         assertEquals(10, trend.get(4).getBullCount());
113
         assertEquals(10, trend.get(4).getBullCount());
104
-        assertEquals(20, trend.get(4).getCowCount());
105
         assertEquals(2024, trend.get(4).getStatYear());
114
         assertEquals(2024, trend.get(4).getStatYear());
106
     }
115
     }
107
 
116
 
117
+    @Test
118
+    @DisplayName("DP-SY-UT-005d 存栏填报公母牛趋势无数据返回空列表")
119
+    void ut005d_herdReportBullCowMonthlyEmpty()
120
+    {
121
+        assertEquals(0, HomeScreenSupport.buildBullCowMonthlyTrendFromHerdReport(
122
+                Collections.emptyList(), LocalDate.of(2024, 12, 31)).size());
123
+        assertEquals(0, HomeScreenSupport.buildBullCowMonthlyTrendFromHerdReport(
124
+                null, LocalDate.of(2024, 12, 31)).size());
125
+    }
126
+
108
     @Test
127
     @Test
109
     @DisplayName("DP-SY-UT-006 母牛分龄两档")
128
     @DisplayName("DP-SY-UT-006 母牛分龄两档")
110
     void ut006_cowAgeStructure()
129
     void ut006_cowAgeStructure()
@@ -478,19 +497,31 @@ class HomeScreenSupportTest
478
     }
497
     }
479
 
498
 
480
     @Test
499
     @Test
481
-    @DisplayName("DP-SY-UT-007 出栏按月缺月补零")
500
+    @DisplayName("DP-SY-UT-007 出栏按月按最大月输出 1~N")
482
     void ut007_outboundMonthly()
501
     void ut007_outboundMonthly()
483
     {
502
     {
484
         HomeScreenMonthCountDto m3 = monthCount(3, 1);
503
         HomeScreenMonthCountDto m3 = monthCount(3, 1);
485
         HomeScreenMonthCountDto m8 = monthCount(8, 2);
504
         HomeScreenMonthCountDto m8 = monthCount(8, 2);
486
         List<HomeScreenOutboundMonthVo> list = HomeScreenSupport.buildOutboundMonthly(
505
         List<HomeScreenOutboundMonthVo> list = HomeScreenSupport.buildOutboundMonthly(
487
                 Arrays.asList(m3, m8), LocalDate.of(2024, 12, 31));
506
                 Arrays.asList(m3, m8), LocalDate.of(2024, 12, 31));
488
-        assertEquals(12, list.size());
489
-        assertEquals(1, list.get(2).getOutboundCount());
507
+        assertEquals(8, list.size());
508
+        assertEquals(1, list.get(0).getMonth());
490
         assertEquals(0, list.get(0).getOutboundCount());
509
         assertEquals(0, list.get(0).getOutboundCount());
510
+        assertEquals(1, list.get(2).getOutboundCount());
511
+        assertEquals(2, list.get(7).getOutboundCount());
491
         assertEquals(2024, list.get(2).getStatYear());
512
         assertEquals(2024, list.get(2).getStatYear());
492
     }
513
     }
493
 
514
 
515
+    @Test
516
+    @DisplayName("DP-SY-UT-007c 出栏按月无数据返回空列表")
517
+    void ut007c_outboundMonthlyEmpty()
518
+    {
519
+        assertEquals(0, HomeScreenSupport.buildOutboundMonthly(
520
+                Collections.emptyList(), LocalDate.of(2024, 12, 31)).size());
521
+        assertEquals(0, HomeScreenSupport.buildOutboundMonthly(
522
+                null, LocalDate.of(2024, 12, 31)).size());
523
+    }
524
+
494
     @Test
525
     @Test
495
     @DisplayName("DP-SY-UT-011 预约排除拒绝取消")
526
     @DisplayName("DP-SY-UT-011 预约排除拒绝取消")
496
     void ut011_appointmentAnnualMapping()
527
     void ut011_appointmentAnnualMapping()

+ 4 - 2
doc/大屏/首页/大屏首页功能需求.md

@@ -78,7 +78,7 @@
78
 | **产业总览·有畜户数** | 所选年 **Y** 内 `biz_livestock_ownership_household` 最大填报月全县村级 `with_livestock_household_count` 合计;接口字段 **`withLivestockHouseholdCount`** |
78
 | **产业总览·有畜户数** | 所选年 **Y** 内 `biz_livestock_ownership_household` 最大填报月全县村级 `with_livestock_household_count` 合计;接口字段 **`withLivestockHouseholdCount`** |
79
 | **产业总览·草场面积** | 所选年 **Y** 内 `biz_farmer_household` 最大填报月全县村级 `pasture_contract_area` 合计;接口字段 **`grasslandAreaMu`** |
79
 | **产业总览·草场面积** | 所选年 **Y** 内 `biz_farmer_household` 最大填报月全县村级 `pasture_contract_area` 合计;接口字段 **`grasslandAreaMu`** |
80
 | **存栏变动·当前公母** | 存栏表**最近填报年月**全县 `bull_total`、`cow_total`;`unknownGenderCount = yak_total − bull − cow`(不足为 0) |
80
 | **存栏变动·当前公母** | 存栏表**最近填报年月**全县 `bull_total`、`cow_total`;`unknownGenderCount = yak_total − bull − cow`(不足为 0) |
81
-| **存栏变动·趋势** | 所选年 **Y** 的 1~12 月;各月全县村级 `bull_total`、`cow_total` 合计;缺月/当年未来月补 0;接口字段 **`inventoryChange.monthlyTrend`** |
81
+| **存栏变动·趋势** | 所选年 **Y** 填报数据中**最大月份为 N** 则输出 1~N 月;各月全县村级 `bull_total`、`cow_total` 合计;区间内缺月补 0;无填报则空数组;接口字段 **`inventoryChange.monthlyTrend`** |
82
 | **年龄结构** | **本期首页不展示**;接口 `ageStructure` 返回空列表 |
82
 | **年龄结构** | **本期首页不展示**;接口 `ageStructure` 返回空列表 |
83
 | **畜群存栏结构** | 所选年 **Y** 内存栏表最大填报月全县 `yak_total`、`dairy_total`、`sheep_total`;饼图展示占比与头数(单位:头);接口字段 **`herdInventoryStructure`** |
83
 | **畜群存栏结构** | 所选年 **Y** 内存栏表最大填报月全县 `yak_total`、`dairy_total`、`sheep_total`;饼图展示占比与头数(单位:头);接口字段 **`herdInventoryStructure`** |
84
 | **牦牛存栏结构** | 所选年 **Y** 内存栏表最大填报月全县种公牛/公牛阉畜/幼畜/成畜/保险;双层饼图(内环公牛·母牛·保险牛,外环细分);单位:头;接口字段 **`yakInventoryStructure`** |
84
 | **牦牛存栏结构** | 所选年 **Y** 内存栏表最大填报月全县种公牛/公牛阉畜/幼畜/成畜/保险;双层饼图(内环公牛·母牛·保险牛,外环细分);单位:头;接口字段 **`yakInventoryStructure`** |
@@ -103,7 +103,7 @@
103
 | **乡镇列表** | 独立接口返回全县填报乡镇(`parent_id=100` 且 `order_num>12`);**不按**登录人数据权限收窄 |
103
 | **乡镇列表** | 独立接口返回全县填报乡镇(`parent_id=100` 且 `order_num>12`);**不按**登录人数据权限收窄 |
104
 | **村树结构** | 独立接口返回县 → 乡镇 → 村树(`TreeSelect`);全县口径 |
104
 | **村树结构** | 独立接口返回县 → 乡镇 → 村树(`TreeSelect`);全县口径 |
105
 | **草畜失衡预警明细** | 独立接口分页查询 `biz_grass_livestock_imbalance_warning`;条件:年/月/乡镇/村;按**预警时间倒序**;返回年、月、乡镇、村、主体名称、主体类型、核定载畜量、年底存栏、是否草畜平衡 |
105
 | **草畜失衡预警明细** | 独立接口分页查询 `biz_grass_livestock_imbalance_warning`;条件:年/月/乡镇/村;按**预警时间倒序**;返回年、月、乡镇、村、主体名称、主体类型、核定载畜量、年底存栏、是否草畜平衡 |
106
-| **出栏统计** | 所选年 **Y** 的 1~12 月;各月全县村级 `yak_outbound_count` 合计;纯柱状图无趋势线;接口字段 **`outboundMonthly`** |
106
+| **出栏统计** | 所选年 **Y** 填报数据中**最大月份为 N** 则输出 1~N 月;各月全县村级 `yak_outbound_count` 合计;区间内缺月补 0;无填报则空数组;纯柱状图无趋势线;接口字段 **`outboundMonthly`** |
107
 | **地图标注** | 所选年 **Y** 内存栏/出栏表各自**最大填报月**,按乡镇汇总各村 `yak_total` / `yak_outbound_count`;**随 Y 切换** |
107
 | **地图标注** | 所选年 **Y** 内存栏/出栏表各自**最大填报月**,按乡镇汇总各村 `yak_total` / `yak_outbound_count`;**随 Y 切换** |
108
 | **牧场 / 草场** | 同《养殖产业数据》**§2.7** 等(档案/台账口径) |
108
 | **牧场 / 草场** | 同《养殖产业数据》**§2.7** 等(档案/台账口径) |
109
 
109
 
@@ -503,6 +503,8 @@ flowchart TB
503
 
503
 
504
 | 版本 | 日期 | 说明 |
504
 | 版本 | 日期 | 说明 |
505
 | --- | --- | --- |
505
 | --- | --- | --- |
506
+| 1.18 | 2026-07-24 | 牦牛月度出栏趋势改为按填报最大月 N 输出 1~N 月(区间缺月补 0) |
507
+| 1.17 | 2026-07-23 | 公母牛月度存栏趋势改为按填报最大月 N 输出 1~N 月(区间缺月补 0) |
506
 | 1.16 | 2026-07-23 | 新增大屏乡镇列表、村树接口(全县口径) |
508
 | 1.16 | 2026-07-23 | 新增大屏乡镇列表、村树接口(全县口径) |
507
 | 1.15 | 2026-07-23 | 新增养殖主体数量明细分页接口(农牧户主表左关联合作社/家庭牧场/养殖大户) |
509
 | 1.15 | 2026-07-23 | 新增养殖主体数量明细分页接口(农牧户主表左关联合作社/家庭牧场/养殖大户) |
508
 | 1.14 | 2026-07-23 | 牦牛存出栏明细:年月未传不过滤(跨年全量翻页);SQL 改为存栏主表+UNION ALL 补仅出栏 |
510
 | 1.14 | 2026-07-23 | 牦牛存出栏明细:年月未传不过滤(跨年全量翻页);SQL 改为存栏主表+UNION ALL 补仅出栏 |

+ 5 - 3
doc/大屏/首页/大屏首页技术方案.md

@@ -265,7 +265,7 @@
265
 | 字段 | 说明 |
265
 | 字段 | 说明 |
266
 | --- | --- |
266
 | --- | --- |
267
 | `bullCount` / `cowCount` / `unknownGenderCount` | 存栏表**最近填报年月**全县公/母/`yak_total` 合计 |
267
 | `bullCount` / `cowCount` / `unknownGenderCount` | 存栏表**最近填报年月**全县公/母/`yak_total` 合计 |
268
-| `monthlyTrend` | 固定 **12** 条;**滚动最近 12 自然月**;**不**随 `statYear` 变 |
268
+| `monthlyTrend` | 所选年填报**最大月 N** → 输出 **N** 条(1~N);区间内缺月补 0;无填报为空 |
269
 
269
 
270
 `monthlyTrend[]`:
270
 `monthlyTrend[]`:
271
 
271
 
@@ -291,9 +291,9 @@
291
 | --- | --- |
291
 | --- | --- |
292
 | `statYear` | 所属自然年 |
292
 | `statYear` | 所属自然年 |
293
 | `month` | 所属月份 1~12 |
293
 | `month` | 所属月份 1~12 |
294
-| `outboundCount` | 该月全县出栏头数;无数据为 **0** |
294
+| `outboundCount` | 该月全县出栏头数;区间内无填报为 **0** |
295
 
295
 
296
-固定 **12** 条;滚动窗口规则同 **§3.1.2** `monthlyTrend`。
296
+所选年填报**最大月 N** → 输出 **N** 条(1~N);区间内缺月补 0;无填报为空。规则同 **§3.1.2** `monthlyTrend`。
297
 
297
 
298
 #### 3.1.5 `grassland`
298
 #### 3.1.5 `grassland`
299
 
299
 
@@ -663,6 +663,8 @@ SQL 示例:`sql/big_screen_home_perm.sql`(挂载「大屏」父菜单下)
663
 
663
 
664
 | 版本 | 日期 | 说明 |
664
 | 版本 | 日期 | 说明 |
665
 | --- | --- | --- |
665
 | --- | --- | --- |
666
+| 1.19 | 2026-07-24 | 出栏 `outboundMonthly`:按填报最大月 N 输出 1~N |
667
+| 1.18 | 2026-07-23 | 公母牛 `monthlyTrend`:按填报最大月 N 输出 1~N |
666
 | 1.17 | 2026-07-23 | 新增大屏 `/townOptions`、`/villageTree`(全县口径) |
668
 | 1.17 | 2026-07-23 | 新增大屏 `/townOptions`、`/villageTree`(全县口径) |
667
 | 1.16 | 2026-07-23 | 新增养殖主体数量明细分页接口 `/breedingEntityQuantities` |
669
 | 1.16 | 2026-07-23 | 新增养殖主体数量明细分页接口 `/breedingEntityQuantities` |
668
 | 1.15 | 2026-07-23 | `/yakInventoryOutbounds`:年月未传不过滤;SQL 改为存栏主表 LEFT JOIN + UNION ALL 补仅出栏 |
670
 | 1.15 | 2026-07-23 | `/yakInventoryOutbounds`:年月未传不过滤;SQL 改为存栏主表 LEFT JOIN + UNION ALL 补仅出栏 |

+ 3 - 3
doc/大屏/首页/大屏首页测试用例.md

@@ -55,10 +55,10 @@
55
 | DP-SY-UT-002 | 参数校验 | statYear 非法 | 单元测试 | JUnit5 | 异常入参 | 无 | 空、`26`、`abcd`、`1899` | 抛 `ServiceException` 或等价失败 |
55
 | DP-SY-UT-002 | 参数校验 | statYear 非法 | 单元测试 | JUnit5 | 异常入参 | 无 | 空、`26`、`abcd`、`1899` | 抛 `ServiceException` 或等价失败 |
56
 | DP-SY-UT-003 | 统计日 | 当前年 statDate | 单元测试 | JUnit5 | §2.2、SY-01 | Mock 当前日 2026-05-20 | `resolveStatDate(2026)` | 返回 `2026-05-20` |
56
 | DP-SY-UT-003 | 统计日 | 当前年 statDate | 单元测试 | JUnit5 | §2.2、SY-01 | Mock 当前日 2026-05-20 | `resolveStatDate(2026)` | 返回 `2026-05-20` |
57
 | DP-SY-UT-004 | 统计日 | 历史年 statDate | 单元测试 | JUnit5 | §2.2 | 无 | `resolveStatDate(2024)` | 返回 `2024-12-31` |
57
 | DP-SY-UT-004 | 统计日 | 历史年 statDate | 单元测试 | JUnit5 | §2.2 | 无 | `resolveStatDate(2024)` | 返回 `2024-12-31` |
58
-| DP-SY-UT-005 | 存栏趋势 | 滚动 12 月补零 | 单元测试 | JUnit5 | §2.3、SY-04 | Mock 仅锚点月有公/母 | `buildRolling12MonthBullCowTrend(..., 2026-07)` | `length=12`;末条 `statYear=2026,month=7`;缺月 `bullCount=cowCount=0` |
58
+| DP-SY-UT-005b | 存栏趋势 | 按最大月 1~N | 单元测试 | JUnit5 | §2.5 | Mock 仅 3、5 月有公/母 | `buildBullCowMonthlyTrendFromHerdReport` | `length=5`;1~2/4 补 0;3、5 有值 |
59
 | DP-SY-UT-005b | 存栏趋势 | 窗口起止年月 | 单元测试 | JUnit5 | §2.3 | anchor=2026-07 | 检查首条 | `statYear=2025,month=8` |
59
 | DP-SY-UT-005b | 存栏趋势 | 窗口起止年月 | 单元测试 | JUnit5 | §2.3 | anchor=2026-07 | 检查首条 | `statYear=2025,month=8` |
60
 | DP-SY-UT-006 | 存栏趋势 | 不随 statYear | 单元测试 | JUnit5 | 技术§3.1 | `loadDashboard(2025)` 与 `2026` | 比较 `monthlyTrend` | 两次 `monthlyTrend` 相同(Mock 填报一致时) |
60
 | DP-SY-UT-006 | 存栏趋势 | 不随 statYear | 单元测试 | JUnit5 | 技术§3.1 | `loadDashboard(2025)` 与 `2026` | 比较 `monthlyTrend` | 两次 `monthlyTrend` 相同(Mock 填报一致时) |
61
-| DP-SY-UT-007 | 出栏按月 | 滚动 12 月补零 | 单元测试 | JUnit5 | SY-06 | `OUT-R12` | `buildRolling12MonthOutbound(..., 2026-07)` | 12 条;2026-05 对应槽位 `outboundCount=88`;其余 0 |
61
+| DP-SY-UT-007 | 出栏按月 | 按最大月 1~N | 单元测试 | JUnit5 | SY-06 | Mock 仅 3、8 月有出栏 | `buildOutboundMonthly` | `length=8`;1~2/4~7 补 0;3、8 有值 |
62
 | DP-SY-UT-007b | 出栏按月 | statYear+month | 单元测试 | JUnit5 | §3.1.4 | 同上 | 检查含 `statYear`、`month` | 每条均含年月 |
62
 | DP-SY-UT-007b | 出栏按月 | statYear+month | 单元测试 | JUnit5 | §3.1.4 | 同上 | 检查含 `statYear`、`month` | 每条均含年月 |
63
 | DP-SY-UT-008 | 年龄结构 | 两档占比之和 | 单元测试 | JUnit5 | SY-05 | 两档头数 10/40 | `buildCowAgeStructureFromHerdReport` | 两档固定返回;`sum(ratio)≈100` |
63
 | DP-SY-UT-008 | 年龄结构 | 两档占比之和 | 单元测试 | JUnit5 | SY-05 | 两档头数 10/40 | `buildCowAgeStructureFromHerdReport` | 两档固定返回;`sum(ratio)≈100` |
64
 | DP-SY-UT-009 | 年龄结构 | 无数据 | 单元测试 | JUnit5 | §7.1 | `HERD-EMPTY` | `buildCowAgeStructureFromHerdReport(null)` | 两档 `count=0`、`ratio=0` |
64
 | DP-SY-UT-009 | 年龄结构 | 无数据 | 单元测试 | JUnit5 | §7.1 | `HERD-EMPTY` | `buildCowAgeStructureFromHerdReport(null)` | 两档 `count=0`、`ratio=0` |
@@ -108,7 +108,7 @@
108
 | DP-SY-API-004 | 看板 | 历史年 statDate | 接口测试 | Postman | §2.2 | 已登录 | `statYear=2024` | `statDate=2024-12-31`;预约/补贴等按 2024;滚动 12 月趋势与 **Y** 无关 |
108
 | DP-SY-API-004 | 看板 | 历史年 statDate | 接口测试 | Postman | §2.2 | 已登录 | `statYear=2024` | `statDate=2024-12-31`;预约/补贴等按 2024;滚动 12 月趋势与 **Y** 无关 |
109
 | DP-SY-API-005 | 看板 | 存栏趋势滚动 12 月 | 接口测试 | Postman | SY-04 | `HERD-R12` | `GET dashboard` | `monthlyTrend.length=12`;含 `statYear`、`month`、`bullCount`、`cowCount`;缺月补 0 |
109
 | DP-SY-API-005 | 看板 | 存栏趋势滚动 12 月 | 接口测试 | Postman | SY-04 | `HERD-R12` | `GET dashboard` | `monthlyTrend.length=12`;含 `statYear`、`month`、`bullCount`、`cowCount`;缺月补 0 |
110
 | DP-SY-API-006 | 看板 | 年龄结构两档 | 接口测试 | Postman | SY-05 | 有存栏填报 | `GET dashboard` | `ageStructure.length=2`;有数据时 `sum(ratio)≈100` |
110
 | DP-SY-API-006 | 看板 | 年龄结构两档 | 接口测试 | Postman | SY-05 | 有存栏填报 | `GET dashboard` | `ageStructure.length=2`;有数据时 `sum(ratio)≈100` |
111
-| DP-SY-API-007 | 看板 | 出栏滚动 12 月 | 接口测试 | Postman | SY-06 | `OUT-R12` | `GET dashboard` | `outboundMonthly.length=12`;含 `statYear`、`month`;2026-05=88,其余 0 |
111
+| DP-SY-API-007 | 看板 | 出栏按最大月 1~N | 接口测试 | Postman | SY-06 | 出栏填报有样本 | `GET dashboard` | `outboundMonthly.length`=所选年最大填报月;含 `statYear`、`month`;区间缺月为 0 |
112
 | DP-SY-API-008 | 看板 | 草场退化五档 | 接口测试 | Postman | SY-07 | `GRASS-MIX` | `GET dashboard` | `degradationStats.length=5`;`sum(count)=grasslandCount` |
112
 | DP-SY-API-008 | 看板 | 草场退化五档 | 接口测试 | Postman | SY-07 | `GRASS-MIX` | `GET dashboard` | `degradationStats.length=5`;`sum(count)=grasslandCount` |
113
 | DP-SY-API-009 | 看板 | 预约三类年度+堆叠 | 接口测试 | Postman | SY-08 | `APT-Y`+`APT-EXCL` | `GET dashboard` | 年度合计不含拒绝/取消;`monthlyStacked` 12 条 |
113
 | DP-SY-API-009 | 看板 | 预约三类年度+堆叠 | 接口测试 | Postman | SY-08 | `APT-Y`+`APT-EXCL` | `GET dashboard` | 年度合计不含拒绝/取消;`monthlyStacked` 12 条 |
114
 | DP-SY-API-010 | 看板 | 补贴万元 | 接口测试 | Postman | SY-09 | `SUB-Y` | `GET dashboard` | `typeStats` 3 项;金额分别为 1.00/2.00/3.00 万;`hasSubsidyData=true` |
114
 | DP-SY-API-010 | 看板 | 补贴万元 | 接口测试 | Postman | SY-09 | `SUB-Y` | `GET dashboard` | `typeStats` 3 项;金额分别为 1.00/2.00/3.00 万;`hasSubsidyData=true` |