wwh пре 1 месец
родитељ
комит
7c374d05aa

+ 24 - 0
baqing-admin/src/main/java/com/ruoyi/web/modules/screen/domain/dto/HomeScreenAppointmentStatDto.java

@@ -5,12 +5,36 @@ package com.ruoyi.web.modules.screen.domain.dto;
5
  */
5
  */
6
 public class HomeScreenAppointmentStatDto
6
 public class HomeScreenAppointmentStatDto
7
 {
7
 {
8
+    private Integer statYear;
9
+
10
+    private Integer statMonth;
11
+
8
     private Integer month;
12
     private Integer month;
9
 
13
 
10
     private Integer providerType;
14
     private Integer providerType;
11
 
15
 
12
     private Integer count;
16
     private Integer count;
13
 
17
 
18
+    public Integer getStatYear()
19
+    {
20
+        return statYear;
21
+    }
22
+
23
+    public void setStatYear(Integer statYear)
24
+    {
25
+        this.statYear = statYear;
26
+    }
27
+
28
+    public Integer getStatMonth()
29
+    {
30
+        return statMonth;
31
+    }
32
+
33
+    public void setStatMonth(Integer statMonth)
34
+    {
35
+        this.statMonth = statMonth;
36
+    }
37
+
14
     public Integer getMonth()
38
     public Integer getMonth()
15
     {
39
     {
16
         return month;
40
         return month;

+ 24 - 0
baqing-admin/src/main/java/com/ruoyi/web/modules/screen/domain/dto/HomeScreenTrainingMonthDto.java

@@ -5,12 +5,36 @@ package com.ruoyi.web.modules.screen.domain.dto;
5
  */
5
  */
6
 public class HomeScreenTrainingMonthDto
6
 public class HomeScreenTrainingMonthDto
7
 {
7
 {
8
+    private Integer statYear;
9
+
10
+    private Integer statMonth;
11
+
8
     private Integer month;
12
     private Integer month;
9
 
13
 
10
     private Integer activityCount;
14
     private Integer activityCount;
11
 
15
 
12
     private Integer enrolledCount;
16
     private Integer enrolledCount;
13
 
17
 
18
+    public Integer getStatYear()
19
+    {
20
+        return statYear;
21
+    }
22
+
23
+    public void setStatYear(Integer statYear)
24
+    {
25
+        this.statYear = statYear;
26
+    }
27
+
28
+    public Integer getStatMonth()
29
+    {
30
+        return statMonth;
31
+    }
32
+
33
+    public void setStatMonth(Integer statMonth)
34
+    {
35
+        this.statMonth = statMonth;
36
+    }
37
+
14
     public Integer getMonth()
38
     public Integer getMonth()
15
     {
39
     {
16
         return month;
40
         return month;

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

@@ -4,7 +4,10 @@ package com.ruoyi.web.modules.screen.domain.vo;
4
  */
4
  */
5
 public class HomeScreenAppointmentMonthVo
5
 public class HomeScreenAppointmentMonthVo
6
 {
6
 {
7
-    /** 月份 1~12 */
7
+    /** 所属年份(自然年) */
8
+    private int statYear;
9
+
10
+    /** 所属月份 1~12 */
8
     private int month;
11
     private int month;
9
     /** 兽医预约次数 */
12
     /** 兽医预约次数 */
10
     private int vetCount;
13
     private int vetCount;
@@ -13,6 +16,16 @@ public class HomeScreenAppointmentMonthVo
13
     /** 机构预约次数 */
16
     /** 机构预约次数 */
14
     private int orgCount;
17
     private int orgCount;
15
 
18
 
19
+    public int getStatYear()
20
+    {
21
+        return statYear;
22
+    }
23
+
24
+    public void setStatYear(int statYear)
25
+    {
26
+        this.statYear = statYear;
27
+    }
28
+
16
     public int getMonth()
29
     public int getMonth()
17
     {
30
     {
18
         return month;
31
         return month;

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

@@ -4,13 +4,26 @@ package com.ruoyi.web.modules.screen.domain.vo;
4
  */
4
  */
5
 public class HomeScreenTrainingMonthVo
5
 public class HomeScreenTrainingMonthVo
6
 {
6
 {
7
-    /** 月份 1~12 */
7
+    /** 所属年份(自然年) */
8
+    private int statYear;
9
+
10
+    /** 所属月份 1~12 */
8
     private int month;
11
     private int month;
9
     /** 培训活动场次 */
12
     /** 培训活动场次 */
10
     private int activityCount;
13
     private int activityCount;
11
     /** 报名人次 */
14
     /** 报名人次 */
12
     private int enrolledCount;
15
     private int enrolledCount;
13
 
16
 
17
+    public int getStatYear()
18
+    {
19
+        return statYear;
20
+    }
21
+
22
+    public void setStatYear(int statYear)
23
+    {
24
+        this.statYear = statYear;
25
+    }
26
+
14
     public int getMonth()
27
     public int getMonth()
15
     {
28
     {
16
         return month;
29
         return month;

+ 12 - 0
baqing-admin/src/main/java/com/ruoyi/web/modules/screen/mapper/HomeScreenStatsMapper.java

@@ -42,6 +42,10 @@ public interface HomeScreenStatsMapper
42
     /** 当年预约按月+provider_type 分组 */
42
     /** 当年预约按月+provider_type 分组 */
43
     List<HomeScreenAppointmentStatDto> selectAppointmentMonthlyByYear(@Param("statYear") int statYear);
43
     List<HomeScreenAppointmentStatDto> selectAppointmentMonthlyByYear(@Param("statYear") int statYear);
44
 
44
 
45
+    /** 自然年月区间内预约按月+provider_type 分组({@code startYm}/{@code endYm} 为 yyyyMM) */
46
+    List<HomeScreenAppointmentStatDto> selectAppointmentMonthlyByYearMonthRange(
47
+            @Param("startYm") int startYm, @Param("endYm") int endYm);
48
+
45
     /** 惠农补贴按类型汇总金额(元) */
49
     /** 惠农补贴按类型汇总金额(元) */
46
     List<HomeScreenSubsidyTypeSumDto> sumSubsidyAmountByTypeForYear(
50
     List<HomeScreenSubsidyTypeSumDto> sumSubsidyAmountByTypeForYear(
47
             @Param("yearStart") Date yearStart,
51
             @Param("yearStart") Date yearStart,
@@ -57,8 +61,16 @@ public interface HomeScreenStatsMapper
57
     /** 培训趋势:按培训举办时间所在月统计 */
61
     /** 培训趋势:按培训举办时间所在月统计 */
58
     List<HomeScreenTrainingMonthDto> selectTrainingTrendByTrainingYear(@Param("statYear") int statYear);
62
     List<HomeScreenTrainingMonthDto> selectTrainingTrendByTrainingYear(@Param("statYear") int statYear);
59
 
63
 
64
+    /** 自然年月区间内培训按月统计({@code startYm}/{@code endYm} 为 yyyyMM) */
65
+    List<HomeScreenTrainingMonthDto> selectTrainingTrendByTrainingYearMonthRange(
66
+            @Param("startYm") int startYm, @Param("endYm") int endYm);
67
+
60
     int countPublishedTrainingsWithTrainingTimeInYear(@Param("statYear") int statYear);
68
     int countPublishedTrainingsWithTrainingTimeInYear(@Param("statYear") int statYear);
61
 
69
 
70
+    /** 自然年月区间内已发布且 training_time 落在区间内的培训条数 */
71
+    int countPublishedTrainingsWithTrainingTimeInYearMonthRange(
72
+            @Param("startYm") int startYm, @Param("endYm") int endYm);
73
+
62
     List<Integer> selectDistinctOutboundYears();
74
     List<Integer> selectDistinctOutboundYears();
63
 
75
 
64
     List<Integer> selectDistinctAppointmentYears();
76
     List<Integer> selectDistinctAppointmentYears();

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

@@ -114,22 +114,22 @@ public class HomeScreenServiceImpl implements IHomeScreenService
114
                 breedingIndustryStatsMapper.countAvailableGrassland(),
114
                 breedingIndustryStatsMapper.countAvailableGrassland(),
115
                 breedingIndustryStatsMapper.countInUseGrassland(toDate(statDate)),
115
                 breedingIndustryStatsMapper.countInUseGrassland(toDate(statDate)),
116
                 breedingIndustryStatsMapper.selectDegradationGroupCounts()));
116
                 breedingIndustryStatsMapper.selectDegradationGroupCounts()));
117
-        // 预约(兽医/专家/机构)、惠农补贴、农技课堂、培训趋势
118
         vo.setAppointment(HomeScreenSupport.buildAppointment(
117
         vo.setAppointment(HomeScreenSupport.buildAppointment(
119
                 homeScreenStatsMapper.selectAppointmentAnnualByYear(year),
118
                 homeScreenStatsMapper.selectAppointmentAnnualByYear(year),
120
-                homeScreenStatsMapper.selectAppointmentMonthlyByYear(year),
121
-                statDate));
119
+                homeScreenStatsMapper.selectAppointmentMonthlyByYearMonthRange(startYm, endYm),
120
+                anchor));
122
         vo.setSubsidy(HomeScreenSupport.buildSubsidy(
121
         vo.setSubsidy(HomeScreenSupport.buildSubsidy(
123
                 homeScreenStatsMapper.sumSubsidyAmountByTypeForYear(yearStart, yearEnd),
122
                 homeScreenStatsMapper.sumSubsidyAmountByTypeForYear(yearStart, yearEnd),
124
                 homeScreenStatsMapper.countSubsidyGrantInYear(yearStart, yearEnd) > 0));
123
                 homeScreenStatsMapper.countSubsidyGrantInYear(yearStart, yearEnd) > 0));
125
         int videoCount = homeScreenStatsMapper.countVideoCoursesPublishedInYear(yearStart, yearEnd);
124
         int videoCount = homeScreenStatsMapper.countVideoCoursesPublishedInYear(yearStart, yearEnd);
126
         int trainingPublishCount = homeScreenStatsMapper.countTrainingsPublishedInYear(yearStart, yearEnd);
125
         int trainingPublishCount = homeScreenStatsMapper.countTrainingsPublishedInYear(yearStart, yearEnd);
127
         vo.setAgriClassroom(HomeScreenSupport.buildAgriClassroom(videoCount, trainingPublishCount));
126
         vo.setAgriClassroom(HomeScreenSupport.buildAgriClassroom(videoCount, trainingPublishCount));
128
-        boolean hasTraining = homeScreenStatsMapper.countPublishedTrainingsWithTrainingTimeInYear(year) > 0;
127
+        boolean hasTraining = homeScreenStatsMapper.countPublishedTrainingsWithTrainingTimeInYearMonthRange(
128
+                startYm, endYm) > 0;
129
         vo.setTrainingTrend(HomeScreenSupport.buildTrainingTrend(
129
         vo.setTrainingTrend(HomeScreenSupport.buildTrainingTrend(
130
-                homeScreenStatsMapper.selectTrainingTrendByTrainingYear(year),
130
+                homeScreenStatsMapper.selectTrainingTrendByTrainingYearMonthRange(startYm, endYm),
131
                 hasTraining,
131
                 hasTraining,
132
-                statDate));
132
+                anchor));
133
         vo.setAvailableYears(HomeScreenSupport.buildAvailableYears(
133
         vo.setAvailableYears(HomeScreenSupport.buildAvailableYears(
134
                 homeScreenStatsMapper.selectDistinctOutboundYears(),
134
                 homeScreenStatsMapper.selectDistinctOutboundYears(),
135
                 homeScreenStatsMapper.selectDistinctAppointmentYears(),
135
                 homeScreenStatsMapper.selectDistinctAppointmentYears(),

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

@@ -284,6 +284,75 @@ public final class HomeScreenSupport
284
         return list;
284
         return list;
285
     }
285
     }
286
 
286
 
287
+    /**
288
+     * 线下服务预约:最近 12 个月堆叠柱(按时间升序,含 {@code statYear}、{@code month})。
289
+     */
290
+    public static List<HomeScreenAppointmentMonthVo> buildRolling12MonthAppointmentStacked(
291
+            List<HomeScreenAppointmentStatDto> rows, YearMonth anchor)
292
+    {
293
+        Map<Integer, Map<Integer, Integer>> byYmAndType = new HashMap<>();
294
+        if (rows != null)
295
+        {
296
+            for (HomeScreenAppointmentStatDto row : rows)
297
+            {
298
+                if (row.getStatYear() != null && row.getStatMonth() != null && row.getProviderType() != null)
299
+                {
300
+                    int key = yearMonthKey(YearMonth.of(row.getStatYear(), row.getStatMonth()));
301
+                    byYmAndType.computeIfAbsent(key, k -> new HashMap<>())
302
+                            .put(row.getProviderType(), row.getCount() != null ? row.getCount() : 0);
303
+                }
304
+            }
305
+        }
306
+        List<HomeScreenAppointmentMonthVo> stacked = new ArrayList<>();
307
+        YearMonth start = anchor.minusMonths(11);
308
+        for (int slot = 0; slot < 12; slot++)
309
+        {
310
+            YearMonth ym = start.plusMonths(slot);
311
+            Map<Integer, Integer> types = byYmAndType.getOrDefault(yearMonthKey(ym), Collections.emptyMap());
312
+            HomeScreenAppointmentMonthVo point = new HomeScreenAppointmentMonthVo();
313
+            point.setStatYear(ym.getYear());
314
+            point.setMonth(ym.getMonthValue());
315
+            point.setVetCount(types.getOrDefault(PROVIDER_VET, 0));
316
+            point.setExpertCount(types.getOrDefault(PROVIDER_EXPERT, 0));
317
+            point.setOrgCount(types.getOrDefault(PROVIDER_ORG, 0));
318
+            stacked.add(point);
319
+        }
320
+        return stacked;
321
+    }
322
+
323
+    /**
324
+     * 实战培训报名:最近 12 个月活动数/报名人数(按时间升序,含 {@code statYear}、{@code month})。
325
+     */
326
+    public static List<HomeScreenTrainingMonthVo> buildRolling12MonthTraining(
327
+            List<HomeScreenTrainingMonthDto> rows, YearMonth anchor)
328
+    {
329
+        Map<Integer, HomeScreenTrainingMonthDto> byYm = new HashMap<>();
330
+        if (rows != null)
331
+        {
332
+            for (HomeScreenTrainingMonthDto row : rows)
333
+            {
334
+                if (row.getStatYear() != null && row.getStatMonth() != null)
335
+                {
336
+                    byYm.put(yearMonthKey(YearMonth.of(row.getStatYear(), row.getStatMonth())), row);
337
+                }
338
+            }
339
+        }
340
+        List<HomeScreenTrainingMonthVo> monthly = new ArrayList<>();
341
+        YearMonth start = anchor.minusMonths(11);
342
+        for (int slot = 0; slot < 12; slot++)
343
+        {
344
+            YearMonth ym = start.plusMonths(slot);
345
+            HomeScreenTrainingMonthDto src = byYm.get(yearMonthKey(ym));
346
+            HomeScreenTrainingMonthVo point = new HomeScreenTrainingMonthVo();
347
+            point.setStatYear(ym.getYear());
348
+            point.setMonth(ym.getMonthValue());
349
+            point.setActivityCount(src != null && src.getActivityCount() != null ? src.getActivityCount() : 0);
350
+            point.setEnrolledCount(src != null && src.getEnrolledCount() != null ? src.getEnrolledCount() : 0);
351
+            monthly.add(point);
352
+        }
353
+        return monthly;
354
+    }
355
+
287
     /**
356
     /**
288
      * 公母牛月度存栏趋势(存栏填报表 {@code bull_total}/{@code cow_total} 全县按月合计);当前年未来月份填 0。
357
      * 公母牛月度存栏趋势(存栏填报表 {@code bull_total}/{@code cow_total} 全县按月合计);当前年未来月份填 0。
289
      */
358
      */
@@ -457,8 +526,8 @@ public final class HomeScreenSupport
457
 
526
 
458
     public static HomeScreenAppointmentVo buildAppointment(
527
     public static HomeScreenAppointmentVo buildAppointment(
459
             List<HomeScreenAppointmentStatDto> annualRows,
528
             List<HomeScreenAppointmentStatDto> annualRows,
460
-            List<HomeScreenAppointmentStatDto> monthlyRows,
461
-            LocalDate statDate)
529
+            List<HomeScreenAppointmentStatDto> rollingMonthlyRows,
530
+            YearMonth anchor)
462
     {
531
     {
463
         int vet = 0;
532
         int vet = 0;
464
         int expert = 0;
533
         int expert = 0;
@@ -485,45 +554,11 @@ public final class HomeScreenSupport
485
                 }
554
                 }
486
             }
555
             }
487
         }
556
         }
488
-        Map<String, Integer> monthlyMap = new HashMap<>();
489
-        if (monthlyRows != null)
490
-        {
491
-            for (HomeScreenAppointmentStatDto row : monthlyRows)
492
-            {
493
-                if (row.getMonth() == null || row.getProviderType() == null || row.getCount() == null)
494
-                {
495
-                    continue;
496
-                }
497
-                monthlyMap.put(row.getMonth() + ":" + row.getProviderType(), row.getCount());
498
-            }
499
-        }
500
-        int futureAfter = statDate.getMonthValue();
501
-        boolean currentYear = statDate.getYear()
502
-                == LocalDate.now(ZoneId.of(BreedingIndustryRules.ZONE_SHANGHAI)).getYear();
503
-        List<HomeScreenAppointmentMonthVo> stacked = new ArrayList<>();
504
-        for (int month = 1; month <= 12; month++)
505
-        {
506
-            HomeScreenAppointmentMonthVo point = new HomeScreenAppointmentMonthVo();
507
-            point.setMonth(month);
508
-            if (currentYear && month > futureAfter)
509
-            {
510
-                point.setVetCount(0);
511
-                point.setExpertCount(0);
512
-                point.setOrgCount(0);
513
-            }
514
-            else
515
-            {
516
-                point.setVetCount(monthlyMap.getOrDefault(month + ":" + PROVIDER_VET, 0));
517
-                point.setExpertCount(monthlyMap.getOrDefault(month + ":" + PROVIDER_EXPERT, 0));
518
-                point.setOrgCount(monthlyMap.getOrDefault(month + ":" + PROVIDER_ORG, 0));
519
-            }
520
-            stacked.add(point);
521
-        }
522
         HomeScreenAppointmentVo vo = new HomeScreenAppointmentVo();
557
         HomeScreenAppointmentVo vo = new HomeScreenAppointmentVo();
523
         vo.setAnnualVetCount(vet);
558
         vo.setAnnualVetCount(vet);
524
         vo.setAnnualExpertCount(expert);
559
         vo.setAnnualExpertCount(expert);
525
         vo.setAnnualOrgCount(org);
560
         vo.setAnnualOrgCount(org);
526
-        vo.setMonthlyStacked(stacked);
561
+        vo.setMonthlyStacked(buildRolling12MonthAppointmentStacked(rollingMonthlyRows, anchor));
527
         return vo;
562
         return vo;
528
     }
563
     }
529
 
564
 
@@ -571,44 +606,16 @@ public final class HomeScreenSupport
571
     }
606
     }
572
 
607
 
573
     public static HomeScreenTrainingTrendVo buildTrainingTrend(
608
     public static HomeScreenTrainingTrendVo buildTrainingTrend(
574
-            List<HomeScreenTrainingMonthDto> rows, boolean hasTrainingData, LocalDate statDate)
609
+            List<HomeScreenTrainingMonthDto> rollingRows, boolean hasTrainingData, YearMonth anchor)
575
     {
610
     {
576
-        Map<Integer, HomeScreenTrainingMonthDto> byMonth = new HashMap<>();
577
-        if (rows != null)
611
+        List<HomeScreenTrainingMonthVo> monthly = buildRolling12MonthTraining(rollingRows, anchor);
612
+        if (!hasTrainingData)
578
         {
613
         {
579
-            for (HomeScreenTrainingMonthDto row : rows)
580
-            {
581
-                if (row.getMonth() != null)
582
-                {
583
-                    byMonth.put(row.getMonth(), row);
584
-                }
585
-            }
586
-        }
587
-        int futureAfter = statDate.getMonthValue();
588
-        boolean currentYear = statDate.getYear()
589
-                == LocalDate.now(ZoneId.of(BreedingIndustryRules.ZONE_SHANGHAI)).getYear();
590
-        List<HomeScreenTrainingMonthVo> monthly = new ArrayList<>();
591
-        for (int month = 1; month <= 12; month++)
592
-        {
593
-            HomeScreenTrainingMonthVo point = new HomeScreenTrainingMonthVo();
594
-            point.setMonth(month);
595
-            if (!hasTrainingData)
614
+            for (HomeScreenTrainingMonthVo point : monthly)
596
             {
615
             {
597
                 point.setActivityCount(0);
616
                 point.setActivityCount(0);
598
                 point.setEnrolledCount(0);
617
                 point.setEnrolledCount(0);
599
             }
618
             }
600
-            else if (currentYear && month > futureAfter)
601
-            {
602
-                point.setActivityCount(0);
603
-                point.setEnrolledCount(0);
604
-            }
605
-            else
606
-            {
607
-                HomeScreenTrainingMonthDto src = byMonth.get(month);
608
-                point.setActivityCount(src != null && src.getActivityCount() != null ? src.getActivityCount() : 0);
609
-                point.setEnrolledCount(src != null && src.getEnrolledCount() != null ? src.getEnrolledCount() : 0);
610
-            }
611
-            monthly.add(point);
612
         }
619
         }
613
         HomeScreenTrainingTrendVo vo = new HomeScreenTrainingTrendVo();
620
         HomeScreenTrainingTrendVo vo = new HomeScreenTrainingTrendVo();
614
         vo.setHasTrainingData(hasTrainingData);
621
         vo.setHasTrainingData(hasTrainingData);

+ 33 - 0
baqing-admin/src/main/resources/mapper/screen/HomeScreenStatsMapper.xml

@@ -54,6 +54,19 @@
54
         group by month(appoint_date), provider_type
54
         group by month(appoint_date), provider_type
55
     </select>
55
     </select>
56
 
56
 
57
+    <select id="selectAppointmentMonthlyByYearMonthRange"
58
+            resultType="com.ruoyi.web.modules.screen.domain.dto.HomeScreenAppointmentStatDto">
59
+        select year(appoint_date) as statYear,
60
+               month(appoint_date) as statMonth,
61
+               provider_type as providerType,
62
+               count(*) as count
63
+        from biz_service_appointment
64
+        where year(appoint_date) * 100 + month(appoint_date) &gt;= #{startYm}
65
+          and year(appoint_date) * 100 + month(appoint_date) &lt;= #{endYm}
66
+          and status not in (2, 3)
67
+        group by year(appoint_date), month(appoint_date), provider_type
68
+    </select>
69
+
57
     <select id="sumSubsidyAmountByTypeForYear"
70
     <select id="sumSubsidyAmountByTypeForYear"
58
             resultType="com.ruoyi.web.modules.screen.domain.dto.HomeScreenSubsidyTypeSumDto">
71
             resultType="com.ruoyi.web.modules.screen.domain.dto.HomeScreenSubsidyTypeSumDto">
59
         select subsidy_type as subsidyType, coalesce(sum(subsidy_amount), 0) as amountSum
72
         select subsidy_type as subsidyType, coalesce(sum(subsidy_amount), 0) as amountSum
@@ -94,6 +107,26 @@
94
         group by month(training_time)
107
         group by month(training_time)
95
     </select>
108
     </select>
96
 
109
 
110
+    <select id="selectTrainingTrendByTrainingYearMonthRange"
111
+            resultType="com.ruoyi.web.modules.screen.domain.dto.HomeScreenTrainingMonthDto">
112
+        select year(training_time) as statYear,
113
+               month(training_time) as statMonth,
114
+               count(*) as activityCount,
115
+               coalesce(sum(actual_enrolled_count), 0) as enrolledCount
116
+        from biz_practical_training
117
+        where del_flag = '0' and publish_status = 1
118
+          and year(training_time) * 100 + month(training_time) &gt;= #{startYm}
119
+          and year(training_time) * 100 + month(training_time) &lt;= #{endYm}
120
+        group by year(training_time), month(training_time)
121
+    </select>
122
+
123
+    <select id="countPublishedTrainingsWithTrainingTimeInYearMonthRange" resultType="int">
124
+        select count(*) from biz_practical_training
125
+        where del_flag = '0' and publish_status = 1
126
+          and year(training_time) * 100 + month(training_time) &gt;= #{startYm}
127
+          and year(training_time) * 100 + month(training_time) &lt;= #{endYm}
128
+    </select>
129
+
97
     <select id="countPublishedTrainingsWithTrainingTimeInYear" resultType="int">
130
     <select id="countPublishedTrainingsWithTrainingTimeInYear" resultType="int">
98
         select count(*) from biz_practical_training
131
         select count(*) from biz_practical_training
99
         where del_flag = '0' and publish_status = 1
132
         where del_flag = '0' and publish_status = 1

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

@@ -197,16 +197,17 @@ class HomeScreenServiceImplTest
197
         when(homeScreenStatsMapper.countTradeMarketForScreen()).thenReturn(0);
197
         when(homeScreenStatsMapper.countTradeMarketForScreen()).thenReturn(0);
198
         when(homeScreenStatsMapper.selectAppointmentAnnualByYear(any(Integer.class)))
198
         when(homeScreenStatsMapper.selectAppointmentAnnualByYear(any(Integer.class)))
199
                 .thenReturn(Collections.emptyList());
199
                 .thenReturn(Collections.emptyList());
200
-        when(homeScreenStatsMapper.selectAppointmentMonthlyByYear(any(Integer.class)))
200
+        when(homeScreenStatsMapper.selectAppointmentMonthlyByYearMonthRange(any(Integer.class), any(Integer.class)))
201
                 .thenReturn(Collections.emptyList());
201
                 .thenReturn(Collections.emptyList());
202
         when(homeScreenStatsMapper.sumSubsidyAmountByTypeForYear(any(), any()))
202
         when(homeScreenStatsMapper.sumSubsidyAmountByTypeForYear(any(), any()))
203
                 .thenReturn(Collections.emptyList());
203
                 .thenReturn(Collections.emptyList());
204
         when(homeScreenStatsMapper.countSubsidyGrantInYear(any(), any())).thenReturn(0);
204
         when(homeScreenStatsMapper.countSubsidyGrantInYear(any(), any())).thenReturn(0);
205
         when(homeScreenStatsMapper.countVideoCoursesPublishedInYear(any(), any())).thenReturn(0);
205
         when(homeScreenStatsMapper.countVideoCoursesPublishedInYear(any(), any())).thenReturn(0);
206
         when(homeScreenStatsMapper.countTrainingsPublishedInYear(any(), any())).thenReturn(0);
206
         when(homeScreenStatsMapper.countTrainingsPublishedInYear(any(), any())).thenReturn(0);
207
-        when(homeScreenStatsMapper.countPublishedTrainingsWithTrainingTimeInYear(any(Integer.class))).thenReturn(0);
208
-        when(homeScreenStatsMapper.selectTrainingTrendByTrainingYear(any(Integer.class)))
209
-                .thenReturn(Collections.emptyList());
207
+        when(homeScreenStatsMapper.countPublishedTrainingsWithTrainingTimeInYearMonthRange(
208
+                any(Integer.class), any(Integer.class))).thenReturn(0);
209
+        when(homeScreenStatsMapper.selectTrainingTrendByTrainingYearMonthRange(
210
+                any(Integer.class), any(Integer.class))).thenReturn(Collections.emptyList());
210
         when(homeScreenStatsMapper.selectDistinctOutboundYears()).thenReturn(Collections.emptyList());
211
         when(homeScreenStatsMapper.selectDistinctOutboundYears()).thenReturn(Collections.emptyList());
211
         when(homeScreenStatsMapper.selectDistinctAppointmentYears()).thenReturn(Collections.emptyList());
212
         when(homeScreenStatsMapper.selectDistinctAppointmentYears()).thenReturn(Collections.emptyList());
212
         when(homeScreenStatsMapper.selectDistinctSubsidyYears()).thenReturn(Collections.emptyList());
213
         when(homeScreenStatsMapper.selectDistinctSubsidyYears()).thenReturn(Collections.emptyList());

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

@@ -15,7 +15,9 @@ import org.junit.jupiter.api.Test;
15
 import com.ruoyi.web.modules.industryservice.domain.dto.BreedingIndustryYakSnapshot;
15
 import com.ruoyi.web.modules.industryservice.domain.dto.BreedingIndustryYakSnapshot;
16
 import com.ruoyi.web.modules.industryservice.support.BreedingIndustryRules;
16
 import com.ruoyi.web.modules.industryservice.support.BreedingIndustryRules;
17
 import com.ruoyi.web.modules.screen.domain.dto.HomeScreenAppointmentStatDto;
17
 import com.ruoyi.web.modules.screen.domain.dto.HomeScreenAppointmentStatDto;
18
+import com.ruoyi.web.modules.screen.domain.dto.HomeScreenAppointmentStatDto;
18
 import com.ruoyi.web.modules.screen.domain.dto.HomeScreenMonthCountDto;
19
 import com.ruoyi.web.modules.screen.domain.dto.HomeScreenMonthCountDto;
20
+import com.ruoyi.web.modules.screen.domain.dto.HomeScreenTrainingMonthDto;
19
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenAgriClassroomVo;
21
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenAgriClassroomVo;
20
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenAppointmentVo;
22
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenAppointmentVo;
21
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenAgeBandVo;
23
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenAgeBandVo;
@@ -141,12 +143,30 @@ class HomeScreenSupportTest
141
         HomeScreenAppointmentStatDto vet = appt(null, 1, 2);
143
         HomeScreenAppointmentStatDto vet = appt(null, 1, 2);
142
         HomeScreenAppointmentStatDto expert = appt(null, 3, 1);
144
         HomeScreenAppointmentStatDto expert = appt(null, 3, 1);
143
         HomeScreenAppointmentVo vo = HomeScreenSupport.buildAppointment(
145
         HomeScreenAppointmentVo vo = HomeScreenSupport.buildAppointment(
144
-                Arrays.asList(vet, expert), Collections.emptyList(), LocalDate.of(2026, 5, 20));
146
+                Arrays.asList(vet, expert), Collections.emptyList(), YearMonth.of(2026, 7));
145
         assertEquals(2, vo.getAnnualVetCount());
147
         assertEquals(2, vo.getAnnualVetCount());
146
         assertEquals(1, vo.getAnnualExpertCount());
148
         assertEquals(1, vo.getAnnualExpertCount());
147
         assertEquals(12, vo.getMonthlyStacked().size());
149
         assertEquals(12, vo.getMonthlyStacked().size());
148
     }
150
     }
149
 
151
 
152
+    @Test
153
+    @DisplayName("DP-SY-UT-011b 预约滚动 12 月堆叠")
154
+    void ut011b_rolling12MonthAppointment()
155
+    {
156
+        YearMonth anchor = YearMonth.of(2026, 7);
157
+        HomeScreenAppointmentStatDto row = new HomeScreenAppointmentStatDto();
158
+        row.setStatYear(2026);
159
+        row.setStatMonth(5);
160
+        row.setProviderType(1);
161
+        row.setCount(3);
162
+        HomeScreenAppointmentVo vo = HomeScreenSupport.buildAppointment(
163
+                Collections.emptyList(), Collections.singletonList(row), anchor);
164
+        assertEquals(12, vo.getMonthlyStacked().size());
165
+        assertEquals(2026, vo.getMonthlyStacked().get(9).getStatYear());
166
+        assertEquals(5, vo.getMonthlyStacked().get(9).getMonth());
167
+        assertEquals(3, vo.getMonthlyStacked().get(9).getVetCount());
168
+    }
169
+
150
     @Test
170
     @Test
151
     @DisplayName("DP-SY-UT-013 补贴万元换算")
171
     @DisplayName("DP-SY-UT-013 补贴万元换算")
152
     void ut013_subsidyWan()
172
     void ut013_subsidyWan()
@@ -181,12 +201,31 @@ class HomeScreenSupportTest
181
         assertFalse(vo.isHasAgriData());
201
         assertFalse(vo.isHasAgriData());
182
     }
202
     }
183
 
203
 
204
+    @Test
205
+    @DisplayName("DP-SY-UT-017b 培训滚动 12 月")
206
+    void ut017b_rolling12MonthTraining()
207
+    {
208
+        YearMonth anchor = YearMonth.of(2026, 7);
209
+        HomeScreenTrainingMonthDto row = new HomeScreenTrainingMonthDto();
210
+        row.setStatYear(2026);
211
+        row.setStatMonth(3);
212
+        row.setActivityCount(1);
213
+        row.setEnrolledCount(20);
214
+        HomeScreenTrainingTrendVo vo = HomeScreenSupport.buildTrainingTrend(
215
+                Collections.singletonList(row), true, anchor);
216
+        assertEquals(12, vo.getMonthly().size());
217
+        assertEquals(2026, vo.getMonthly().get(7).getStatYear());
218
+        assertEquals(3, vo.getMonthly().get(7).getMonth());
219
+        assertEquals(1, vo.getMonthly().get(7).getActivityCount());
220
+        assertEquals(20, vo.getMonthly().get(7).getEnrolledCount());
221
+    }
222
+
184
     @Test
223
     @Test
185
     @DisplayName("DP-SY-UT-018 无培训空态")
224
     @DisplayName("DP-SY-UT-018 无培训空态")
186
     void ut018_noTraining()
225
     void ut018_noTraining()
187
     {
226
     {
188
         HomeScreenTrainingTrendVo vo = HomeScreenSupport.buildTrainingTrend(
227
         HomeScreenTrainingTrendVo vo = HomeScreenSupport.buildTrainingTrend(
189
-                Collections.emptyList(), false, LocalDate.of(2026, 5, 20));
228
+                Collections.emptyList(), false, YearMonth.of(2026, 5));
190
         assertFalse(vo.isHasTrainingData());
229
         assertFalse(vo.isHasTrainingData());
191
         assertEquals(12, vo.getMonthly().size());
230
         assertEquals(12, vo.getMonthly().size());
192
     }
231
     }

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

@@ -39,7 +39,7 @@
39
 | 类型 | 适用区块 | 横轴规则 |
39
 | 类型 | 适用区块 | 横轴规则 |
40
 | --- | --- | --- |
40
 | --- | --- | --- |
41
 | **统计年 Y 内 12 月** | 线下服务预约堆叠柱、实战培训报名趋势等 | 固定 **1 月~12 月** 共 12 个刻度,数据范围限定在 **Y 年** |
41
 | **统计年 Y 内 12 月** | 线下服务预约堆叠柱、实战培训报名趋势等 | 固定 **1 月~12 月** 共 12 个刻度,数据范围限定在 **Y 年** |
42
-| **滚动最近 12 个月** | **牦牛存栏变动**、**牦牛出栏统计** | 以服务器当前年月为锚点,取**含当月在内向前连续 12 个自然月**;**不**随页面「统计年份 **Y**」切换 |
42
+| **滚动最近 12 个月** | **牦牛存栏变动**、**牦牛出栏统计**、**线下服务预约**(堆叠柱)、**实战培训报名** | 以服务器当前年月为锚点,取**含当月在内向前连续 12 个自然月**;**不**随页面「统计年份 **Y**」切换 |
43
 
43
 
44
 **统计年 Y 内 12 月(预约、培训等)**
44
 **统计年 Y 内 12 月(预约、培训等)**
45
 
45
 
@@ -344,7 +344,8 @@ flowchart TB
344
 | 项 | 规则 |
344
 | 项 | 规则 |
345
 | --- | --- |
345
 | --- | --- |
346
 | 左侧 | 三类**年度预约合计**卡片:兽医人员、专家人员、诊疗机构(**§2.7**) |
346
 | 左侧 | 三类**年度预约合计**卡片:兽医人员、专家人员、诊疗机构(**§2.7**) |
347
-| 右侧 | **堆叠柱状图**:横轴 12 月;系列为兽医/专家/机构预约人次 |
347
+| 右侧 | **堆叠柱状图**:**§2.3 滚动最近 12 月**;系列为兽医/专家/机构预约人次 |
348
+| 横轴 / 悬停 | 横轴**仅月份**;悬停显示 **YYYY年M月** 及三类人次 |
348
 | 图例 | 与三类名称一致,颜色区分 |
349
 | 图例 | 与三类名称一致,颜色区分 |
349
 
350
 
350
 ---
351
 ---
@@ -387,8 +388,9 @@ flowchart TB
387
 | --- | --- |
388
 | --- | --- |
388
 | 图表类型 | 双系列**曲线/面积图**(以原型为准) |
389
 | 图表类型 | 双系列**曲线/面积图**(以原型为准) |
389
 | 系列 | **活动数**(个)、**报名人数**(人) |
390
 | 系列 | **活动数**(个)、**报名人数**(人) |
390
-| 横轴 | **§2.3** 1~12 月 |
391
-| 口径 | **§2.11** |
391
+| 横轴 | **§2.3 滚动最近 12 月** |
392
+| 横轴 / 悬停 | 横轴**仅月份**;悬停显示 **YYYY年M月** 及活动数、报名人数 |
393
+| 口径 | **§2.11**(窗口内 `training_time`) |
392
 
394
 
393
 ---
395
 ---
394
 
396
 

+ 14 - 11
doc/大屏/首页/大屏首页技术方案.md

@@ -22,8 +22,8 @@
22
 | 数据权限 | **全县**聚合:牦牛、牧场取全部 `biz_status=1` 且 `del_flag=0` 的牧场 ID(与疫病风险大屏一致,**不按**登录用户牧场过滤);草场全县 `del_flag=0` |
22
 | 数据权限 | **全县**聚合:牦牛、牧场取全部 `biz_status=1` 且 `del_flag=0` 的牧场 ID(与疫病风险大屏一致,**不按**登录用户牧场过滤);草场全县 `del_flag=0` |
23
 | 统计日 | `Y = 当前年` → `statDate = 今日`;`Y` 为历史年 → `statDate = Y-12-31`(时点、月末还原、年出栏上界均据此) |
23
 | 统计日 | `Y = 当前年` → `statDate = 今日`;`Y` 为历史年 → `statDate = Y-12-31`(时点、月末还原、年出栏上界均据此) |
24
 | 主数据三项 | 供应商 / 承销商 / 交易市场**实时快照**,**不**随 `statYear` 变 |
24
 | 主数据三项 | 供应商 / 承销商 / 交易市场**实时快照**,**不**随 `statYear` 变 |
25
-| 按月图表(预约、培训等) | 横轴固定 **12** 月(**Y** 年内);缺月、当前年未来月均为 **0** |
26
-| 滚动 12 月图表(存栏变动、出栏统计) | 锚点 = 服务器当前 `YearMonth`(`Asia/Shanghai`);窗口 = 锚点向前 11 月~锚点;固定 **12** 条;**不**随 `statYear` 变 |
25
+| 滚动 12 月图表 | 存栏变动、出栏统计、预约堆叠、培训趋势;锚点 = 当前 `YearMonth`;**不**随 `statYear` 变 |
26
+| 按月图表(Y 年内) | 已无(预约/培训已并入滚动 12 月) |
27
 | 性能 | 默认实时聚合;可选 **§2.3** 缓存表 |
27
 | 性能 | 默认实时聚合;可选 **§2.3** 缓存表 |
28
 
28
 
29
 **依赖表(只读)**
29
 **依赖表(只读)**
@@ -285,7 +285,9 @@
285
 | 字段 | 说明 |
285
 | 字段 | 说明 |
286
 | --- | --- |
286
 | --- | --- |
287
 | `annualVetCount` / `annualExpertCount` / `annualOrgCount` | 三类年度合计 |
287
 | `annualVetCount` / `annualExpertCount` / `annualOrgCount` | 三类年度合计 |
288
-| `monthlyStacked[]` | 12 条:`month`、`vetCount`、`expertCount`、`orgCount` |
288
+| `monthlyStacked[]` | 固定 **12** 条;滚动最近 12 自然月;**不**随 `statYear` 变 |
289
+
290
+`monthlyStacked[]`:`statYear`、`month`、`vetCount`、`expertCount`、`orgCount`(缺月各计数为 **0**)。
289
 
291
 
290
 #### 3.1.7 `subsidy`
292
 #### 3.1.7 `subsidy`
291
 
293
 
@@ -307,10 +309,10 @@
307
 
309
 
308
 | 字段 | 说明 |
310
 | 字段 | 说明 |
309
 | --- | --- |
311
 | --- | --- |
310
-| `hasTrainingData` | **Y** 年是否存在已发布培训 |
311
-| `monthly[]` | 12 条:`month`、`activityCount`、`enrolledCount` |
312
+| `hasTrainingData` | 滚动 12 月窗口内是否存在已发布且 `training_time` 落在窗口内的培训 |
313
+| `monthly[]` | 固定 **12** 条;`statYear`、`month`、`activityCount`、`enrolledCount` |
312
 
314
 
313
-`hasTrainingData=false` 时 `monthly` 可为 `[]` 或全 0(前端以 `hasTrainingData` 走空态)。
315
+`hasTrainingData=false` 时 `monthly` 仍为 12 条全 0(前端以 `hasTrainingData` 走空态)。
314
 
316
 
315
 #### 响应示例(节选)
317
 #### 响应示例(节选)
316
 
318
 
@@ -450,15 +452,15 @@
450
 
452
 
451
 **工程**:`ruoyi-screen/src/views/home/`(`index.vue`、`chartOptions.js`)。
453
 **工程**:`ruoyi-screen/src/views/home/`(`index.vue`、`chartOptions.js`)。
452
 
454
 
453
-### 4.1 存栏变动 / 出栏统计图表
455
+### 4.1 滚动 12 月图表(存栏 / 出栏 / 预约 / 培训)
454
 
456
 
455
 | 项 | 实现 |
457
 | 项 | 实现 |
456
 | --- | --- |
458
 | --- | --- |
457
-| 数据字段 | `inventoryChange.monthlyTrend[]`、`outboundMonthly[]`;按 `statYear`、`month` 升序 |
458
-| 横轴 | **仅月份**标签(`8月`);`buildInventoryTrendOption` / `buildOutboundOption` |
459
+| 数据字段 | `monthlyTrend[]`、`outboundMonthly[]`、`appointment.monthlyStacked[]`、`trainingTrend.monthly[]` |
460
+| 排序 | 按 `statYear`、`month` 升序 |
461
+| 横轴 | **仅月份**(`8月`);`chartOptions.js` |
459
 | Tooltip | **完整年月**(`2025年8月`)+ 数值 |
462
 | Tooltip | **完整年月**(`2025年8月`)+ 数值 |
460
-| 出栏图 | **纯柱状图**,无趋势折线 |
461
-| 与 `statYear` | 切换年份**不**改变上述两图数据窗口 |
463
+| 与 `statYear` | 切换年份**不**改变上述图表数据窗口 |
462
 
464
 
463
 ---
465
 ---
464
 
466
 
@@ -487,5 +489,6 @@ SQL 示例:`sql/big_screen_home_perm.sql`(挂载「大屏」父菜单下)
487
 
489
 
488
 | 版本 | 日期 | 说明 |
490
 | 版本 | 日期 | 说明 |
489
 | --- | --- | --- |
491
 | --- | --- | --- |
492
+| 1.2 | 2026-07-04 | 预约堆叠、培训趋势改滚动 12 月 + statYear;前端横轴/Tooltip 与存栏出栏一致 |
490
 | 1.1 | 2026-07-04 | 存栏/出栏改填报表滚动 12 月;VO 增 `statYear`;年龄四档;地图/总览最近填报年月;§4 前端横轴/Tooltip |
493
 | 1.1 | 2026-07-04 | 存栏/出栏改填报表滚动 12 月;VO 增 `statYear`;年龄四档;地图/总览最近填报年月;§4 前端横轴/Tooltip |
491
 | 1.0 | 2026-05-20 | 初稿:无新表;双接口;对齐 `大屏首页功能需求.md` v1.0 |
494
 | 1.0 | 2026-05-20 | 初稿:无新表;双接口;对齐 `大屏首页功能需求.md` v1.0 |

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

@@ -11,7 +11,7 @@
11
 | --- | --- |
11
 | --- | --- |
12
 | 统计年 **Y** | 切换后全页一致;默认当前自然年 |
12
 | 统计年 **Y** | 切换后全页一致;默认当前自然年 |
13
 | 统计日 | `Y`=当前年→今日;历史年→`Y-12-31` |
13
 | 统计日 | `Y`=当前年→今日;历史年→`Y-12-31` |
14
-| 按月图表(预约、培训) | 固定 12 月(**Y** 年内);缺月、当前年未来月=**0** |
14
+| 按月图表(预约、培训) | 已改为**滚动 12 月**(与存栏/出栏一致);含 `statYear`+`month`;**不**随 **Y** |
15
 | 滚动 12 月(存栏变动、出栏统计) | 锚点=当前年月;含 `statYear`+`month`;**不**随 **Y**;缺月=**0** |
15
 | 滚动 12 月(存栏变动、出栏统计) | 锚点=当前年月;含 `statYear`+`month`;**不**随 **Y**;缺月=**0** |
16
 | 牦牛填报 | `biz_yak_herd_inventory` / `biz_yak_outbound_report` 全县合计;最近填报年月 |
16
 | 牦牛填报 | `biz_yak_herd_inventory` / `biz_yak_outbound_report` 全县合计;最近填报年月 |
17
 | 流通三项 | 供应商/承销商/交易市场**实时快照**,**不随 Y** |
17
 | 流通三项 | 供应商/承销商/交易市场**实时快照**,**不随 Y** |
@@ -143,10 +143,10 @@
143
 | DP-SY-UI-008 | 图表 | 出栏柱状 12 月 | UI 测试 | Playwright+Chrome | SY-06 | `OUT-R12` | 查看出栏统计+悬停 | 12 柱、**无趋势折线**;横轴仅月;悬停显年月+头数 |
143
 | DP-SY-UI-008 | 图表 | 出栏柱状 12 月 | UI 测试 | Playwright+Chrome | SY-06 | `OUT-R12` | 查看出栏统计+悬停 | 12 柱、**无趋势折线**;横轴仅月;悬停显年月+头数 |
144
 | DP-SY-UI-009 | 图表 | 草场可用/使用中 | UI 测试 | Playwright+Chrome | SY-07 | `GRASS-MIX` | 查看草场区上部 | 两数字区分「可用」「使用中」;不与退化表简单相加为总量 |
144
 | DP-SY-UI-009 | 图表 | 草场可用/使用中 | UI 测试 | Playwright+Chrome | SY-07 | `GRASS-MIX` | 查看草场区上部 | 两数字区分「可用」「使用中」;不与退化表简单相加为总量 |
145
 | DP-SY-UI-010 | 图表 | 退化等级占比 | UI 测试 | Playwright+Chrome | SY-07 | 五档均有 | 查看退化表/环图 | 五档名称与 API 一致;占比为 % |
145
 | DP-SY-UI-010 | 图表 | 退化等级占比 | UI 测试 | Playwright+Chrome | SY-07 | 五档均有 | 查看退化表/环图 | 五档名称与 API 一致;占比为 % |
146
-| DP-SY-UI-011 | 图表 | 预约堆叠柱 | UI 测试 | Playwright+Chrome | SY-08 | `APT-Y` | 查看预约区 | 左侧三类年度卡片;右侧 12 月堆叠;图例兽医/专家/机构 |
146
+| DP-SY-UI-011 | 图表 | 预约堆叠柱 | UI 测试 | Playwright+Chrome | SY-08 | `APT-Y` | 查看预约区+悬停 | 左侧三类年度卡片;右侧滚动 12 月堆叠;横轴仅月;悬停显年月 |
147
 | DP-SY-UI-012 | 图表 | 补贴三类柱图 | UI 测试 | Playwright+Chrome | SY-09 | `SUB-Y` | 查看惠农补贴 | 横轴三类;纵轴万元;与 API 一致 |
147
 | DP-SY-UI-012 | 图表 | 补贴三类柱图 | UI 测试 | Playwright+Chrome | SY-09 | `SUB-Y` | 查看惠农补贴 | 横轴三类;纵轴万元;与 API 一致 |
148
 | DP-SY-UI-013 | 指标 | 农技课堂 | UI 测试 | Playwright+Chrome | SY-11 | `VIDEO-Y`+培训 | 查看农技课堂 | 视频数、培训场次、培训内容合计正确 |
148
 | DP-SY-UI-013 | 指标 | 农技课堂 | UI 测试 | Playwright+Chrome | SY-11 | `VIDEO-Y`+培训 | 查看农技课堂 | 视频数、培训场次、培训内容合计正确 |
149
-| DP-SY-UI-014 | 图表 | 培训双曲线 | UI 测试 | Playwright+Chrome | SY-12 | `TRAIN-PUB` | 查看培训报名趋势 | 活动数、报名人数两系列;12 月 |
149
+| DP-SY-UI-014 | 图表 | 培训双曲线 | UI 测试 | Playwright+Chrome | SY-12 | `TRAIN-PUB` | 查看培训报名趋势+悬停 | 活动数、报名人数两系列;横轴仅月;悬停显年月 |
150
 | DP-SY-UI-015 | 空态 | 无出栏 | UI 测试 | Playwright+Chrome | §7.1 | 选无出栏年 | 切换该年 | 出栏图空态或全 0;年出栏卡片为 0 |
150
 | DP-SY-UI-015 | 空态 | 无出栏 | UI 测试 | Playwright+Chrome | §7.1 | 选无出栏年 | 切换该年 | 出栏图空态或全 0;年出栏卡片为 0 |
151
 | DP-SY-UI-016 | 空态 | 无存栏趋势 | UI 测试 | Playwright+Chrome | §7.1 | `HERD-EMPTY` | 打开页 | 趋势图空态或全 0 折线;年龄结构空态 |
151
 | DP-SY-UI-016 | 空态 | 无存栏趋势 | UI 测试 | Playwright+Chrome | §7.1 | `HERD-EMPTY` | 打开页 | 趋势图空态或全 0 折线;年龄结构空态 |
152
 | DP-SY-UI-017 | 空态 | 无预约 | UI 测试 | Playwright+Chrome | §7.1 | 无预约年 | 切换该年 | 预约区空态;三类年度卡片为 0 |
152
 | DP-SY-UI-017 | 空态 | 无预约 | UI 测试 | Playwright+Chrome | §7.1 | 无预约年 | 切换该年 | 预约区空态;三类年度卡片为 0 |