wwh 1 週間 前
コミット
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 12
     private int cowCount;
13 13
     /** 性别未知头数(存栏 - 公 - 母) */
14 14
     private int unknownGenderCount;
15
-    /** 公母牛月度存栏(12 点) */
15
+    /** 公母牛月度存栏(1~数据最大月;区间内缺月补 0) */
16 16
     private List<HomeScreenInventoryMonthVo> monthlyTrend;
17 17
 
18 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 65
  * <p>
66 66
  * 牦牛存栏/出栏趋势取自 {@code biz_yak_herd_inventory}、{@code biz_yak_outbound_report} 所选年各月全县村级合计;
67 67
  * 产业总览填报类指标取所选年<strong>最大填报月</strong>合计或记录数;
68
- * 存栏/出栏图为所选年 1~12 月(当年未到月份补零)。
68
+ * 存栏公母牛趋势、出栏月度趋势:均按所选年填报最大月份 N 输出 1~N 月(缺月补零)。
69 69
  */
70 70
 @Service
71 71
 public class HomeScreenServiceImpl implements IHomeScreenService
@@ -154,7 +154,7 @@ public class HomeScreenServiceImpl implements IHomeScreenService
154 154
         overview.setWithLivestockHouseholdCount(countAtPeriod(yearOwnership,
155 155
                 homeScreenStatsMapper::sumWithLivestockHouseholdCountByStatYearMonth));
156 156
 
157
-        // 存栏变动当前公母:所选年库中最大月份;趋势/出栏:所选年 1~12 
157
+        // 存栏变动当前公母:所选年库中最大月份;存栏趋势/出栏趋势:均为 1~填报最大
158 158
         HomeScreenInventoryChangeVo inventoryChange = new HomeScreenInventoryChangeVo();
159 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 562
     public static List<HomeScreenInventoryMonthVo> buildBullCowMonthlyTrendFromHerdReport(
557 563
             List<HomeScreenHerdGenderMonthDto> rows, LocalDate statDate)
558 564
     {
559 565
         Map<Integer, HomeScreenHerdGenderMonthDto> byMonth = new HashMap<>();
566
+        int maxMonth = 0;
560 567
         if (rows != null)
561 568
         {
562 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 596
             HomeScreenInventoryMonthVo point = new HomeScreenInventoryMonthVo();
577 597
             point.setStatYear(statYear);
578 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 602
             points.add(point);
591 603
         }
592 604
         return points;
@@ -1406,38 +1418,54 @@ public final class HomeScreenSupport
1406 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 1431
     public static List<HomeScreenOutboundMonthVo> buildOutboundMonthly(
1410 1432
             List<HomeScreenMonthCountDto> rows, LocalDate statDate)
1411 1433
     {
1412 1434
         Map<Integer, Integer> byMonth = new HashMap<>();
1435
+        int maxMonth = 0;
1413 1436
         if (rows != null)
1414 1437
         {
1415 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 1465
             HomeScreenOutboundMonthVo vo = new HomeScreenOutboundMonthVo();
1431 1466
             vo.setStatYear(statYear);
1432 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 1469
             list.add(vo);
1442 1470
         }
1443 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 93
                 .andExpect(jsonPath("$.code").value(200))
94 94
                 .andExpect(jsonPath("$.data.statYear").value(2026))
95 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 99
     @Test
@@ -517,9 +517,10 @@ class HomeScreenControllerApiTest
517 517
         inv.setCowCount(5);
518 518
         inv.setUnknownGenderCount(1);
519 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 522
             HomeScreenInventoryMonthVo p = new HomeScreenInventoryMonthVo();
523
+            p.setStatYear(2026);
523 524
             p.setMonth(m);
524 525
             p.setBullCount(1);
525 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 140
         assertNotNull(vo.getStatDate());
141 141
         assertNotNull(vo.getIndustryOverview());
142 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 145
         assertEquals(0, vo.getAgeStructure().size());
146 146
         assertNotNull(vo.getGrassland());
147 147
         assertNotNull(vo.getAppointment());
@@ -643,7 +643,7 @@ class HomeScreenServiceImplTest
643 643
     }
644 644
 
645 645
     @Test
646
-    @DisplayName("DP-SY-UT-031 存栏变动当前公母取所选年最大填报月,趋势/出栏为所选年1~12月")
646
+    @DisplayName("DP-SY-UT-031 存栏变动当前公母取所选年最大填报月,趋势为 1~填报最大月")
647 647
     void ut031_inventoryChangeCurrentGenderFromYearMaxMonth()
648 648
     {
649 649
         stubGrasslandAndReport();
@@ -654,16 +654,30 @@ class HomeScreenServiceImplTest
654 654
         gender.setCowTotal(200);
655 655
         gender.setYakTotal(310);
656 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 670
         HomeScreenDashboardVo vo = service.loadDashboard("2024");
659 671
 
660 672
         assertEquals(100, vo.getInventoryChange().getBullCount());
661 673
         assertEquals(200, vo.getInventoryChange().getCowCount());
662 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 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 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 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 95
         com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderMonthDto m5 =
96 96
                 new com.ruoyi.web.modules.screen.domain.dto.HomeScreenHerdGenderMonthDto();
97 97
         m5.setMonth(5);
98 98
         m5.setBullCount(10);
99 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 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 113
         assertEquals(10, trend.get(4).getBullCount());
104
-        assertEquals(20, trend.get(4).getCowCount());
105 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 127
     @Test
109 128
     @DisplayName("DP-SY-UT-006 母牛分龄两档")
110 129
     void ut006_cowAgeStructure()
@@ -478,19 +497,31 @@ class HomeScreenSupportTest
478 497
     }
479 498
 
480 499
     @Test
481
-    @DisplayName("DP-SY-UT-007 出栏按月缺月补零")
500
+    @DisplayName("DP-SY-UT-007 出栏按月按最大月输出 1~N")
482 501
     void ut007_outboundMonthly()
483 502
     {
484 503
         HomeScreenMonthCountDto m3 = monthCount(3, 1);
485 504
         HomeScreenMonthCountDto m8 = monthCount(8, 2);
486 505
         List<HomeScreenOutboundMonthVo> list = HomeScreenSupport.buildOutboundMonthly(
487 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 509
         assertEquals(0, list.get(0).getOutboundCount());
510
+        assertEquals(1, list.get(2).getOutboundCount());
511
+        assertEquals(2, list.get(7).getOutboundCount());
491 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 525
     @Test
495 526
     @DisplayName("DP-SY-UT-011 预约排除拒绝取消")
496 527
     void ut011_appointmentAnnualMapping()

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

@@ -78,7 +78,7 @@
78 78
 | **产业总览·有畜户数** | 所选年 **Y** 内 `biz_livestock_ownership_household` 最大填报月全县村级 `with_livestock_household_count` 合计;接口字段 **`withLivestockHouseholdCount`** |
79 79
 | **产业总览·草场面积** | 所选年 **Y** 内 `biz_farmer_household` 最大填报月全县村级 `pasture_contract_area` 合计;接口字段 **`grasslandAreaMu`** |
80 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 82
 | **年龄结构** | **本期首页不展示**;接口 `ageStructure` 返回空列表 |
83 83
 | **畜群存栏结构** | 所选年 **Y** 内存栏表最大填报月全县 `yak_total`、`dairy_total`、`sheep_total`;饼图展示占比与头数(单位:头);接口字段 **`herdInventoryStructure`** |
84 84
 | **牦牛存栏结构** | 所选年 **Y** 内存栏表最大填报月全县种公牛/公牛阉畜/幼畜/成畜/保险;双层饼图(内环公牛·母牛·保险牛,外环细分);单位:头;接口字段 **`yakInventoryStructure`** |
@@ -103,7 +103,7 @@
103 103
 | **乡镇列表** | 独立接口返回全县填报乡镇(`parent_id=100` 且 `order_num>12`);**不按**登录人数据权限收窄 |
104 104
 | **村树结构** | 独立接口返回县 → 乡镇 → 村树(`TreeSelect`);全县口径 |
105 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 107
 | **地图标注** | 所选年 **Y** 内存栏/出栏表各自**最大填报月**,按乡镇汇总各村 `yak_total` / `yak_outbound_count`;**随 Y 切换** |
108 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 508
 | 1.16 | 2026-07-23 | 新增大屏乡镇列表、村树接口(全县口径) |
507 509
 | 1.15 | 2026-07-23 | 新增养殖主体数量明细分页接口(农牧户主表左关联合作社/家庭牧场/养殖大户) |
508 510
 | 1.14 | 2026-07-23 | 牦牛存出栏明细:年月未传不过滤(跨年全量翻页);SQL 改为存栏主表+UNION ALL 补仅出栏 |

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

@@ -265,7 +265,7 @@
265 265
 | 字段 | 说明 |
266 266
 | --- | --- |
267 267
 | `bullCount` / `cowCount` / `unknownGenderCount` | 存栏表**最近填报年月**全县公/母/`yak_total` 合计 |
268
-| `monthlyTrend` | 固定 **12** 条;**滚动最近 12 自然月**;**不**随 `statYear` 变 |
268
+| `monthlyTrend` | 所选年填报**最大月 N** → 输出 **N** 条(1~N);区间内缺月补 0;无填报为空 |
269 269
 
270 270
 `monthlyTrend[]`:
271 271
 
@@ -291,9 +291,9 @@
291 291
 | --- | --- |
292 292
 | `statYear` | 所属自然年 |
293 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 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 668
 | 1.17 | 2026-07-23 | 新增大屏 `/townOptions`、`/villageTree`(全县口径) |
667 669
 | 1.16 | 2026-07-23 | 新增养殖主体数量明细分页接口 `/breedingEntityQuantities` |
668 670
 | 1.15 | 2026-07-23 | `/yakInventoryOutbounds`:年月未传不过滤;SQL 改为存栏主表 LEFT JOIN + UNION ALL 补仅出栏 |

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

@@ -55,10 +55,10 @@
55 55
 | DP-SY-UT-002 | 参数校验 | statYear 非法 | 单元测试 | JUnit5 | 异常入参 | 无 | 空、`26`、`abcd`、`1899` | 抛 `ServiceException` 或等价失败 |
56 56
 | DP-SY-UT-003 | 统计日 | 当前年 statDate | 单元测试 | JUnit5 | §2.2、SY-01 | Mock 当前日 2026-05-20 | `resolveStatDate(2026)` | 返回 `2026-05-20` |
57 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 59
 | DP-SY-UT-005b | 存栏趋势 | 窗口起止年月 | 单元测试 | JUnit5 | §2.3 | anchor=2026-07 | 检查首条 | `statYear=2025,month=8` |
60 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 62
 | DP-SY-UT-007b | 出栏按月 | statYear+month | 单元测试 | JUnit5 | §3.1.4 | 同上 | 检查含 `statYear`、`month` | 每条均含年月 |
63 63
 | DP-SY-UT-008 | 年龄结构 | 两档占比之和 | 单元测试 | JUnit5 | SY-05 | 两档头数 10/40 | `buildCowAgeStructureFromHerdReport` | 两档固定返回;`sum(ratio)≈100` |
64 64
 | DP-SY-UT-009 | 年龄结构 | 无数据 | 单元测试 | JUnit5 | §7.1 | `HERD-EMPTY` | `buildCowAgeStructureFromHerdReport(null)` | 两档 `count=0`、`ratio=0` |
@@ -108,7 +108,7 @@
108 108
 | DP-SY-API-004 | 看板 | 历史年 statDate | 接口测试 | Postman | §2.2 | 已登录 | `statYear=2024` | `statDate=2024-12-31`;预约/补贴等按 2024;滚动 12 月趋势与 **Y** 无关 |
109 109
 | DP-SY-API-005 | 看板 | 存栏趋势滚动 12 月 | 接口测试 | Postman | SY-04 | `HERD-R12` | `GET dashboard` | `monthlyTrend.length=12`;含 `statYear`、`month`、`bullCount`、`cowCount`;缺月补 0 |
110 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 112
 | DP-SY-API-008 | 看板 | 草场退化五档 | 接口测试 | Postman | SY-07 | `GRASS-MIX` | `GET dashboard` | `degradationStats.length=5`;`sum(count)=grasslandCount` |
113 113
 | DP-SY-API-009 | 看板 | 预约三类年度+堆叠 | 接口测试 | Postman | SY-08 | `APT-Y`+`APT-EXCL` | `GET dashboard` | 年度合计不含拒绝/取消;`monthlyStacked` 12 条 |
114 114
 | DP-SY-API-010 | 看板 | 补贴万元 | 接口测试 | Postman | SY-09 | `SUB-Y` | `GET dashboard` | `typeStats` 3 项;金额分别为 1.00/2.00/3.00 万;`hasSubsidyData=true` |