소스 검색

共同富裕

wwh 1 주 전
부모
커밋
0aa152af99

+ 178 - 61
baqing-admin/src/main/java/com/ruoyi/web/modules/prosperity/support/AchievementReportAggregator.java

@@ -13,7 +13,11 @@ import com.ruoyi.web.modules.prosperity.domain.vo.AchievementAnnualSummaryVo;
13 13
 import com.ruoyi.web.modules.prosperity.domain.vo.AchievementMonthlyVo;
14 14
 
15 15
 /**
16
- * 年度报告数据组装与汇总计算(纯逻辑,便于单测)。
16
+ * 年度报告 / 大屏共同富裕成果组装(口径一致):
17
+ * <ul>
18
+ *   <li>按月序列:所选年 1~库中最大统计月,缺月补 0</li>
19
+ *   <li>总览:最大统计月的单月指标</li>
20
+ * </ul>
17 21
  */
18 22
 public final class AchievementReportAggregator
19 23
 {
@@ -41,42 +45,65 @@ public final class AchievementReportAggregator
41 45
         }
42 46
     }
43 47
 
48
+    /**
49
+     * 所选年内台账的最大统计月;无有效行返回 0。
50
+     */
51
+    public static int resolveMaxMonth(int statYear, List<BizCommonProsperityAchievement> rows)
52
+    {
53
+        int maxMonth = 0;
54
+        if (rows == null)
55
+        {
56
+            return 0;
57
+        }
58
+        String yearPrefix = String.valueOf(statYear) + "-";
59
+        for (BizCommonProsperityAchievement row : rows)
60
+        {
61
+            if (row == null || StringUtils.isEmpty(row.getStatPeriod())
62
+                    || !row.getStatPeriod().startsWith(yearPrefix))
63
+            {
64
+                continue;
65
+            }
66
+            int month = parseMonthFromStatPeriod(row.getStatPeriod());
67
+            if (month >= 1 && month <= 12 && month > maxMonth)
68
+            {
69
+                maxMonth = month;
70
+            }
71
+        }
72
+        return maxMonth;
73
+    }
74
+
44 75
     public static AchievementAnnualReportVo buildReport(int statYear, List<BizCommonProsperityAchievement> rows)
45 76
     {
46 77
         AchievementAnnualReportVo report = new AchievementAnnualReportVo();
47 78
         report.setStatYear(statYear);
48
-        boolean hasData = rows != null && !rows.isEmpty();
79
+        boolean hasData = rows != null && !rows.isEmpty() && resolveMaxMonth(statYear, rows) > 0;
49 80
         report.setHasData(hasData);
50 81
         List<AchievementMonthlyVo> monthly = buildMonthly(statYear, rows);
51 82
         report.setMonthly(monthly);
52
-        report.setSummary(hasData ? calcSummary(monthly) : null);
83
+        report.setSummary(hasData ? calcSummaryFromMaxMonth(statYear, rows) : null);
53 84
         return report;
54 85
     }
55 86
 
87
+    /**
88
+     * 所选年 1~库中最大月;无数据返回空列表。
89
+     */
56 90
     public static List<AchievementMonthlyVo> buildMonthly(int statYear, List<BizCommonProsperityAchievement> rows)
57 91
     {
58
-        Map<Integer, BizCommonProsperityAchievement> byMonth = new HashMap<>();
59
-        if (rows != null)
92
+        Map<Integer, BizCommonProsperityAchievement> byMonth = indexByMonth(statYear, rows);
93
+        int maxMonth = 0;
94
+        for (Integer m : byMonth.keySet())
60 95
         {
61
-            for (BizCommonProsperityAchievement row : rows)
96
+            if (m != null && m > maxMonth)
62 97
             {
63
-                if (row == null || StringUtils.isEmpty(row.getStatPeriod()))
64
-                {
65
-                    continue;
66
-                }
67
-                if (!row.getStatPeriod().startsWith(String.valueOf(statYear) + "-"))
68
-                {
69
-                    continue;
70
-                }
71
-                int month = parseMonthFromStatPeriod(row.getStatPeriod());
72
-                if (month >= 1 && month <= 12)
73
-                {
74
-                    byMonth.put(month, row);
75
-                }
98
+                maxMonth = m;
76 99
             }
77 100
         }
78
-        List<AchievementMonthlyVo> monthly = new ArrayList<>(MONTHS_IN_YEAR);
79
-        for (int m = 1; m <= MONTHS_IN_YEAR; m++)
101
+        if (maxMonth <= 0)
102
+        {
103
+            return new ArrayList<>();
104
+        }
105
+        List<AchievementMonthlyVo> monthly = new ArrayList<>(maxMonth);
106
+        for (int m = 1; m <= maxMonth; m++)
80 107
         {
81 108
             BizCommonProsperityAchievement src = byMonth.get(m);
82 109
             if (src != null)
@@ -141,52 +168,142 @@ public final class AchievementReportAggregator
141 168
         return monthly;
142 169
     }
143 170
 
144
-    public static AchievementAnnualSummaryVo calcSummary(List<AchievementMonthlyVo> monthly)
171
+    /**
172
+     * 取所选年内台账最大统计月的单月指标;无有效台账返回 {@code null}。
173
+     */
174
+    public static AchievementAnnualSummaryVo calcSummaryFromMaxMonth(
175
+            int statYear, List<BizCommonProsperityAchievement> rows)
145 176
     {
177
+        BizCommonProsperityAchievement latest = findMaxMonthRow(statYear, rows);
178
+        if (latest == null)
179
+        {
180
+            return null;
181
+        }
146 182
         AchievementAnnualSummaryVo summary = new AchievementAnnualSummaryVo();
147
-        BigDecimal incomeSum = BigDecimal.ZERO;
148
-        int employmentSum = 0;
149
-        int jobsSum = 0;
150
-        int envSum = 0;
151
-        BigDecimal greenSum = BigDecimal.ZERO;
152
-        int heritageSum = 0;
153
-        int integrationSum = 0;
154
-        int unitySum = 0;
155
-        BigDecimal digitalSum = BigDecimal.ZERO;
156
-        int brandCountSum = 0;
157
-        BigDecimal premiumSum = BigDecimal.ZERO;
158
-
159
-        for (AchievementMonthlyVo item : monthly)
160
-        {
161
-            incomeSum = incomeSum.add(nvl(item.getCollectiveEconomyIncome()));
162
-            employmentSum += nvlInt(item.getEmploymentDrivenCount());
163
-            jobsSum += nvlInt(item.getNewJobPositions());
164
-            envSum += nvlInt(item.getEnvProjectCount());
165
-            greenSum = greenSum.add(nvl(item.getNewGreenArea()));
166
-            heritageSum += nvlInt(item.getCulturalHeritageCount());
167
-            integrationSum += nvlInt(item.getEthnicIntegrationProjectCount());
168
-            unitySum += nvlInt(item.getEthnicUnityActivityCount());
169
-            digitalSum = digitalSum.add(nvl(item.getDigitalPlatformUsageRate()));
170
-            brandCountSum += nvlInt(item.getRegionalPublicBrandCount());
171
-            premiumSum = premiumSum.add(nvl(item.getBrandPremiumRate()));
172
-        }
173
-
174
-        summary.setCollectiveEconomyIncome(incomeSum.setScale(2, RoundingMode.HALF_UP));
175
-        summary.setEmploymentDrivenCount(employmentSum);
176
-        summary.setNewJobPositions(jobsSum);
177
-        summary.setEnvProjectCount(envSum);
178
-        summary.setNewGreenArea(greenSum.setScale(2, RoundingMode.HALF_UP));
179
-        summary.setCulturalHeritageCount(heritageSum);
180
-        summary.setEthnicIntegrationProjectCount(integrationSum);
181
-        summary.setEthnicUnityActivityCount(unitySum);
183
+        summary.setCollectiveEconomyIncome(nvl(latest.getCollectiveEconomyIncome()).setScale(2, RoundingMode.HALF_UP));
184
+        summary.setEmploymentDrivenCount(nvlInt(latest.getEmploymentDrivenCount()));
185
+        summary.setNewJobPositions(nvlInt(latest.getNewJobPositions()));
186
+        summary.setEnvProjectCount(nvlInt(latest.getEnvProjectCount()));
187
+        summary.setNewGreenArea(nvl(latest.getNewGreenArea()).setScale(2, RoundingMode.HALF_UP));
188
+        summary.setCulturalHeritageCount(nvlInt(latest.getCulturalHeritageCount()));
189
+        summary.setEthnicIntegrationProjectCount(nvlInt(latest.getEthnicIntegrationProjectCount()));
190
+        summary.setEthnicUnityActivityCount(nvlInt(latest.getEthnicUnityActivityCount()));
182 191
         summary.setDigitalPlatformUsageRate(
183
-                digitalSum.divide(BigDecimal.valueOf(MONTHS_IN_YEAR), RATE_SCALE, RoundingMode.HALF_UP));
184
-        summary.setRegionalPublicBrandCount(brandCountSum);
185
-        summary.setBrandPremiumRate(
186
-                premiumSum.divide(BigDecimal.valueOf(MONTHS_IN_YEAR), RATE_SCALE, RoundingMode.HALF_UP));
192
+                nvl(latest.getDigitalPlatformUsageRate()).setScale(RATE_SCALE, RoundingMode.HALF_UP));
193
+        summary.setRegionalPublicBrandCount(nvlInt(latest.getRegionalPublicBrandCount()));
194
+        summary.setBrandPremiumRate(nvl(latest.getBrandPremiumRate()).setScale(RATE_SCALE, RoundingMode.HALF_UP));
187 195
         return summary;
188 196
     }
189 197
 
198
+    /**
199
+     * @deprecated 使用 {@link #calcSummaryFromMaxMonth(int, List)}
200
+     */
201
+    @Deprecated
202
+    public static AchievementAnnualSummaryVo calcSummaryFromMaxMonth(List<BizCommonProsperityAchievement> rows)
203
+    {
204
+        if (rows == null || rows.isEmpty())
205
+        {
206
+            return null;
207
+        }
208
+        for (BizCommonProsperityAchievement row : rows)
209
+        {
210
+            if (row != null && StringUtils.isNotEmpty(row.getStatPeriod()) && row.getStatPeriod().length() >= 4)
211
+            {
212
+                try
213
+                {
214
+                    int year = Integer.parseInt(row.getStatPeriod().substring(0, 4));
215
+                    return calcSummaryFromMaxMonth(year, rows);
216
+                }
217
+                catch (NumberFormatException ignored)
218
+                {
219
+                    // try next
220
+                }
221
+            }
222
+        }
223
+        return null;
224
+    }
225
+
226
+    /**
227
+     * 在所选年台账行中取最大统计月的一条。
228
+     */
229
+    public static BizCommonProsperityAchievement findMaxMonthRow(
230
+            int statYear, List<BizCommonProsperityAchievement> rows)
231
+    {
232
+        BizCommonProsperityAchievement latest = null;
233
+        int maxMonth = -1;
234
+        if (rows == null)
235
+        {
236
+            return null;
237
+        }
238
+        String yearPrefix = String.valueOf(statYear) + "-";
239
+        for (BizCommonProsperityAchievement row : rows)
240
+        {
241
+            if (row == null || StringUtils.isEmpty(row.getStatPeriod())
242
+                    || !row.getStatPeriod().startsWith(yearPrefix))
243
+            {
244
+                continue;
245
+            }
246
+            int month = parseMonthFromStatPeriod(row.getStatPeriod());
247
+            if (month >= 1 && month <= 12 && month >= maxMonth)
248
+            {
249
+                maxMonth = month;
250
+                latest = row;
251
+            }
252
+        }
253
+        return latest;
254
+    }
255
+
256
+    /** @deprecated 使用 {@link #findMaxMonthRow(int, List)} */
257
+    @Deprecated
258
+    public static BizCommonProsperityAchievement findMaxMonthRow(List<BizCommonProsperityAchievement> rows)
259
+    {
260
+        if (rows == null || rows.isEmpty())
261
+        {
262
+            return null;
263
+        }
264
+        for (BizCommonProsperityAchievement row : rows)
265
+        {
266
+            if (row != null && StringUtils.isNotEmpty(row.getStatPeriod()) && row.getStatPeriod().length() >= 4)
267
+            {
268
+                try
269
+                {
270
+                    int year = Integer.parseInt(row.getStatPeriod().substring(0, 4));
271
+                    return findMaxMonthRow(year, rows);
272
+                }
273
+                catch (NumberFormatException ignored)
274
+                {
275
+                    // try next
276
+                }
277
+            }
278
+        }
279
+        return null;
280
+    }
281
+
282
+    private static Map<Integer, BizCommonProsperityAchievement> indexByMonth(
283
+            int statYear, List<BizCommonProsperityAchievement> rows)
284
+    {
285
+        Map<Integer, BizCommonProsperityAchievement> byMonth = new HashMap<>();
286
+        if (rows == null)
287
+        {
288
+            return byMonth;
289
+        }
290
+        String yearPrefix = String.valueOf(statYear) + "-";
291
+        for (BizCommonProsperityAchievement row : rows)
292
+        {
293
+            if (row == null || StringUtils.isEmpty(row.getStatPeriod())
294
+                    || !row.getStatPeriod().startsWith(yearPrefix))
295
+            {
296
+                continue;
297
+            }
298
+            int month = parseMonthFromStatPeriod(row.getStatPeriod());
299
+            if (month >= 1 && month <= 12)
300
+            {
301
+                byMonth.put(month, row);
302
+            }
303
+        }
304
+        return byMonth;
305
+    }
306
+
190 307
     private static AchievementMonthlyVo fromEntity(int month, BizCommonProsperityAchievement src)
191 308
     {
192 309
         AchievementMonthlyVo vo = new AchievementMonthlyVo();

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

@@ -5,7 +5,6 @@ import java.util.Collections;
5 5
 import java.util.List;
6 6
 import java.util.Map;
7 7
 import java.util.stream.Collectors;
8
-import java.time.YearMonth;
9 8
 import org.springframework.beans.factory.annotation.Autowired;
10 9
 import org.springframework.stereotype.Service;
11 10
 import com.ruoyi.web.modules.prosperity.domain.BizCommonProsperityAchievement;
@@ -14,27 +13,22 @@ import com.ruoyi.web.modules.prosperity.domain.BizCommonProsperityProjectOpImage
14 13
 import com.ruoyi.web.modules.prosperity.mapper.BizCommonProsperityAchievementMapper;
15 14
 import com.ruoyi.web.modules.prosperity.mapper.BizCommonProsperityProjectMapper;
16 15
 import com.ruoyi.web.modules.prosperity.mapper.BizCommonProsperityProjectOpImageMapper;
17
-import com.ruoyi.web.modules.prosperity.service.IAchievementReportService;
18 16
 import com.ruoyi.web.modules.prosperity.support.AchievementReportAggregator;
19 17
 import com.ruoyi.web.modules.prosperity.support.AchievementReportValidation;
20
-import com.ruoyi.web.modules.prosperity.domain.vo.AchievementAnnualReportVo;
21 18
 import com.ruoyi.web.modules.screen.domain.vo.CommonProsperityScreenDashboardVo;
22 19
 import com.ruoyi.web.modules.screen.domain.vo.ScreenProjectListItemVo;
23 20
 import com.ruoyi.web.modules.screen.service.ICommonProsperityScreenService;
24 21
 import com.ruoyi.web.modules.screen.support.CommonProsperityScreenSupport;
25
-import com.ruoyi.web.modules.screen.support.HomeScreenSupport;
26 22
 
27 23
 /**
28 24
  * 大屏共同富裕统计服务实现。
29 25
  * <p>
30
- * 看板复用 {@link IAchievementReportService} 年度成果;项目列表仅含已发布且实施周期与统计年有交集的记录。
26
+ * 成果口径与管理端年度报告一致:总览为所选年<strong>最大统计月</strong>单月值;
27
+ * 按月图表为该年 1~最大月(缺月补 0);项目列表仅含已发布且实施周期与统计年有交集的记录。
31 28
  */
32 29
 @Service
33 30
 public class CommonProsperityScreenServiceImpl implements ICommonProsperityScreenService
34 31
 {
35
-    @Autowired
36
-    private IAchievementReportService achievementReportService;
37
-
38 32
     @Autowired
39 33
     private BizCommonProsperityAchievementMapper achievementMapper;
40 34
 
@@ -48,14 +42,10 @@ public class CommonProsperityScreenServiceImpl implements ICommonProsperityScree
48 42
     public CommonProsperityScreenDashboardVo loadDashboard(String statYear)
49 43
     {
50 44
         int year = AchievementReportValidation.parseStatYear(statYear);
51
-        YearMonth rollingAnchor = HomeScreenSupport.currentYearMonth();
52 45
         CommonProsperityScreenDashboardVo vo = new CommonProsperityScreenDashboardVo();
53 46
         vo.setStatYear(year);
54
-        AchievementAnnualReportVo achievement = achievementReportService.getAnnualReport(String.valueOf(year));
55
-        List<BizCommonProsperityAchievement> rollingRows = safeAchievementsBetween(
56
-                rollingAnchor.minusMonths(11), rollingAnchor);
57
-        achievement.setMonthly(AchievementReportAggregator.buildRollingMonthly(rollingAnchor, rollingRows));
58
-        vo.setAchievement(achievement);
47
+        List<BizCommonProsperityAchievement> rows = safeAchievementsByYear(year);
48
+        vo.setAchievement(AchievementReportAggregator.buildReport(year, rows));
59 49
         List<BizCommonProsperityProject> projects = safePublishedForScreen(year);
60 50
         vo.setHasProjectData(!projects.isEmpty());
61 51
         vo.setProjectTypeStats(CommonProsperityScreenSupport.buildProjectTypeStats(projects));
@@ -87,11 +77,9 @@ public class CommonProsperityScreenServiceImpl implements ICommonProsperityScree
87 77
         return list != null ? list : new ArrayList<>();
88 78
     }
89 79
 
90
-    private List<BizCommonProsperityAchievement> safeAchievementsBetween(YearMonth start, YearMonth end)
80
+    private List<BizCommonProsperityAchievement> safeAchievementsByYear(int statYear)
91 81
     {
92
-        List<BizCommonProsperityAchievement> list = achievementMapper.selectByStatPeriodBetween(
93
-                HomeScreenSupport.formatStatPeriod(start),
94
-                HomeScreenSupport.formatStatPeriod(end));
82
+        List<BizCommonProsperityAchievement> list = achievementMapper.selectByStatYear(String.valueOf(statYear));
95 83
         return list != null ? list : new ArrayList<>();
96 84
     }
97 85
 }

+ 1 - 1
baqing-admin/src/test/java/com/ruoyi/web/modules/prosperity/controller/BizCommonProsperityAchievementReportControllerApiTest.java

@@ -95,7 +95,7 @@ class BizCommonProsperityAchievementReportControllerApiTest
95 95
                 .andExpect(status().isOk())
96 96
                 .andExpect(jsonPath("$.code").value(200))
97 97
                 .andExpect(jsonPath("$.data.hasData").value(true))
98
-                .andExpect(jsonPath("$.data.monthly.length()").value(12))
98
+                .andExpect(jsonPath("$.data.monthly.length()").value(5))
99 99
                 .andExpect(jsonPath("$.data.summary.collectiveEconomyIncome").exists());
100 100
     }
101 101
 

+ 35 - 31
baqing-admin/src/test/java/com/ruoyi/web/modules/prosperity/support/AchievementReportAggregatorTest.java

@@ -17,22 +17,22 @@ import com.ruoyi.web.modules.prosperity.domain.vo.AchievementAnnualSummaryVo;
17 17
 import com.ruoyi.web.modules.prosperity.domain.vo.AchievementMonthlyVo;
18 18
 
19 19
 /**
20
- * GTFY-NB-UT-003~014
20
+ * GTFY-NB-UT-003~014;口径:1~最大月 + 最大月总览
21 21
  */
22 22
 @DisplayName("AchievementReportAggregator")
23 23
 class AchievementReportAggregatorTest
24 24
 {
25 25
 
26 26
     @Test
27
-    @DisplayName("GTFY-NB-UT-003 仅3月有数据缺月补零")
27
+    @DisplayName("GTFY-NB-UT-003 仅3月有数据:序列为1~3,缺月补零")
28 28
     void ut003_partialMonth()
29 29
     {
30 30
         List<AchievementMonthlyVo> monthly = AchievementReportAggregator.buildMonthly(2026,
31 31
                 Collections.singletonList(row("2026-03", "10", 30)));
32
-        assertEquals(12, monthly.size());
32
+        assertEquals(3, monthly.size());
33 33
         assertEquals(0, monthly.get(0).getEmploymentDrivenCount().intValue());
34 34
         assertEquals(30, monthly.get(2).getEmploymentDrivenCount().intValue());
35
-        assertNull(monthly.get(0).getStatPeriod());
35
+        assertEquals("2026-01", monthly.get(0).getStatPeriod());
36 36
         assertEquals("2026-03", monthly.get(2).getStatPeriod());
37 37
     }
38 38
 
@@ -51,40 +51,41 @@ class AchievementReportAggregatorTest
51 51
     }
52 52
 
53 53
     @Test
54
-    @DisplayName("GTFY-NB-UT-005 加总类 SUM")
55
-    void ut005_sumMetrics()
54
+    @DisplayName("GTFY-NB-UT-005 总览取最大月单月值")
55
+    void ut005_summaryMaxMonth()
56 56
     {
57
-        List<AchievementMonthlyVo> monthly = AchievementReportAggregator.buildMonthly(2026,
57
+        AchievementAnnualReportVo report = AchievementReportAggregator.buildReport(2026,
58 58
                 Collections.singletonList(row("2026-03", "10", 30)));
59
-        AchievementAnnualSummaryVo summary = AchievementReportAggregator.calcSummary(monthly);
59
+        AchievementAnnualSummaryVo summary = report.getSummary();
60 60
         assertEquals(new BigDecimal("10.00"), summary.getCollectiveEconomyIncome());
61 61
         assertEquals(30, summary.getEmploymentDrivenCount().intValue());
62 62
     }
63 63
 
64 64
     @Test
65
-    @DisplayName("GTFY-NB-UT-006 比率类月均")
66
-    void ut006_rateAverage()
65
+    @DisplayName("GTFY-NB-UT-006 比率取最大月原值")
66
+    void ut006_rateFromMaxMonth()
67 67
     {
68 68
         BizCommonProsperityAchievement r = row("2026-01", "0", 0);
69 69
         r.setDigitalPlatformUsageRate(new BigDecimal("0.6"));
70 70
         r.setBrandPremiumRate(BigDecimal.ZERO);
71
-        List<AchievementMonthlyVo> monthly = AchievementReportAggregator.buildMonthly(2026,
72
-                Collections.singletonList(r));
73
-        AchievementAnnualSummaryVo summary = AchievementReportAggregator.calcSummary(monthly);
74
-        assertEquals(new BigDecimal("0.0500"), summary.getDigitalPlatformUsageRate());
71
+        AchievementAnnualSummaryVo summary = AchievementReportAggregator.buildReport(2026,
72
+                Collections.singletonList(r)).getSummary();
73
+        assertEquals(new BigDecimal("0.6000"), summary.getDigitalPlatformUsageRate());
75 74
     }
76 75
 
77 76
     @Test
78
-    @DisplayName("GTFY-NB-UT-007 多月比率")
79
-    void ut007_multiMonthRate()
77
+    @DisplayName("GTFY-NB-UT-007 多月时总览取最大月")
78
+    void ut007_multiMonthMax()
80 79
     {
81
-        BizCommonProsperityAchievement m3 = row("2026-03", "0", 0);
80
+        BizCommonProsperityAchievement m3 = row("2026-03", "10", 10);
82 81
         m3.setDigitalPlatformUsageRate(new BigDecimal("0.6"));
83
-        BizCommonProsperityAchievement m6 = row("2026-06", "0", 0);
84
-        m6.setDigitalPlatformUsageRate(new BigDecimal("0.6"));
85
-        List<AchievementMonthlyVo> monthly = AchievementReportAggregator.buildMonthly(2026, Arrays.asList(m3, m6));
86
-        AchievementAnnualSummaryVo summary = AchievementReportAggregator.calcSummary(monthly);
87
-        assertEquals(new BigDecimal("0.1000"), summary.getDigitalPlatformUsageRate());
82
+        BizCommonProsperityAchievement m6 = row("2026-06", "88", 50);
83
+        m6.setDigitalPlatformUsageRate(new BigDecimal("0.75"));
84
+        AchievementAnnualReportVo report =
85
+                AchievementReportAggregator.buildReport(2026, Arrays.asList(m3, m6));
86
+        assertEquals(6, report.getMonthly().size());
87
+        assertEquals(new BigDecimal("88.00"), report.getSummary().getCollectiveEconomyIncome());
88
+        assertEquals(new BigDecimal("0.7500"), report.getSummary().getDigitalPlatformUsageRate());
88 89
     }
89 90
 
90 91
     @Test
@@ -94,7 +95,7 @@ class AchievementReportAggregatorTest
94 95
         AchievementAnnualReportVo report = AchievementReportAggregator.buildReport(2026, Collections.emptyList());
95 96
         assertFalse(report.getHasData());
96 97
         assertNull(report.getSummary());
97
-        assertEquals(12, report.getMonthly().size());
98
+        assertEquals(0, report.getMonthly().size());
98 99
     }
99 100
 
100 101
     @Test
@@ -105,6 +106,7 @@ class AchievementReportAggregatorTest
105 106
                 Collections.singletonList(row("2026-05", "1", 1)));
106 107
         assertTrue(report.getHasData());
107 108
         assertTrue(report.getSummary() != null);
109
+        assertEquals(5, report.getMonthly().size());
108 110
     }
109 111
 
110 112
     @Test
@@ -114,8 +116,8 @@ class AchievementReportAggregatorTest
114 116
         List<AchievementMonthlyVo> monthly = AchievementReportAggregator.buildMonthly(2026, Arrays.asList(
115 117
                 row("2025-12", "99", 99),
116 118
                 row("2026-01", "1", 1)));
119
+        assertEquals(1, monthly.size());
117 120
         assertEquals(new BigDecimal("1"), monthly.get(0).getCollectiveEconomyIncome());
118
-        assertEquals(0, monthly.get(11).getEmploymentDrivenCount().intValue());
119 121
     }
120 122
 
121 123
     @Test
@@ -123,30 +125,32 @@ class AchievementReportAggregatorTest
123 125
     void ut011_parseMonth()
124 126
     {
125 127
         assertEquals(9, AchievementReportAggregator.parseMonthFromStatPeriod("2026-09"));
128
+        assertEquals(8, AchievementReportAggregator.resolveMaxMonth(2026,
129
+                Collections.singletonList(row("2026-08", "1", 1))));
126 130
     }
127 131
 
128 132
     @Test
129
-    @DisplayName("GTFY-NB-UT-012 monthly 月份顺序")
133
+    @DisplayName("GTFY-NB-UT-012 monthly 月份顺序至最大月")
130 134
     void ut012_monthOrder()
131 135
     {
132 136
         List<AchievementMonthlyVo> monthly = AchievementReportAggregator.buildMonthly(2026, Arrays.asList(
133
-                row("2026-12", "12", 12),
137
+                row("2026-08", "8", 8),
134 138
                 row("2026-03", "3", 3),
135 139
                 row("2026-01", "1", 1)));
136
-        for (int i = 0; i < 12; i++)
140
+        assertEquals(8, monthly.size());
141
+        for (int i = 0; i < monthly.size(); i++)
137 142
         {
138 143
             assertEquals(i + 1, monthly.get(i).getMonth().intValue());
139 144
         }
140 145
     }
141 146
 
142 147
     @Test
143
-    @DisplayName("GTFY-NB-UT-014 单月收入为0仍参与汇总")
148
+    @DisplayName("GTFY-NB-UT-014 最大月收入为0仍展示")
144 149
     void ut014_zeroIncome()
145 150
     {
146 151
         BizCommonProsperityAchievement r = row("2026-01", "0", 5);
147
-        List<AchievementMonthlyVo> monthly = AchievementReportAggregator.buildMonthly(2026,
148
-                Collections.singletonList(r));
149
-        AchievementAnnualSummaryVo summary = AchievementReportAggregator.calcSummary(monthly);
152
+        AchievementAnnualSummaryVo summary = AchievementReportAggregator.buildReport(2026,
153
+                Collections.singletonList(r)).getSummary();
150 154
         assertEquals(new BigDecimal("0.00"), summary.getCollectiveEconomyIncome());
151 155
         assertEquals(5, summary.getEmploymentDrivenCount().intValue());
152 156
     }

+ 49 - 24
baqing-admin/src/test/java/com/ruoyi/web/modules/screen/service/impl/CommonProsperityScreenServiceImplTest.java

@@ -10,6 +10,7 @@ import static org.mockito.ArgumentMatchers.eq;
10 10
 import static org.mockito.Mockito.verify;
11 11
 import static org.mockito.Mockito.when;
12 12
 
13
+import java.math.BigDecimal;
13 14
 import java.util.Arrays;
14 15
 import java.util.Collections;
15 16
 import org.junit.jupiter.api.DisplayName;
@@ -18,13 +19,11 @@ import org.junit.jupiter.api.extension.ExtendWith;
18 19
 import org.mockito.InjectMocks;
19 20
 import org.mockito.Mock;
20 21
 import org.mockito.junit.jupiter.MockitoExtension;
22
+import com.ruoyi.web.modules.prosperity.domain.BizCommonProsperityAchievement;
21 23
 import com.ruoyi.web.modules.prosperity.domain.BizCommonProsperityProject;
22
-import com.ruoyi.web.modules.prosperity.domain.vo.AchievementAnnualReportVo;
23 24
 import com.ruoyi.web.modules.prosperity.mapper.BizCommonProsperityAchievementMapper;
24 25
 import com.ruoyi.web.modules.prosperity.mapper.BizCommonProsperityProjectMapper;
25 26
 import com.ruoyi.web.modules.prosperity.mapper.BizCommonProsperityProjectOpImageMapper;
26
-import com.ruoyi.web.modules.prosperity.service.IAchievementReportService;
27
-import com.ruoyi.web.modules.prosperity.support.AchievementReportAggregator;
28 27
 import com.ruoyi.web.modules.screen.domain.vo.CommonProsperityScreenDashboardVo;
29 28
 
30 29
 /**
@@ -35,9 +34,6 @@ import com.ruoyi.web.modules.screen.domain.vo.CommonProsperityScreenDashboardVo;
35 34
 class CommonProsperityScreenServiceImplTest
36 35
 {
37 36
 
38
-    @Mock
39
-    private IAchievementReportService achievementReportService;
40
-
41 37
     @Mock
42 38
     private BizCommonProsperityAchievementMapper achievementMapper;
43 39
 
@@ -54,8 +50,7 @@ class CommonProsperityScreenServiceImplTest
54 50
     @DisplayName("DP-GTFY-UT-014 仅项目无成果")
55 51
     void ut014_projectsOnly()
56 52
     {
57
-        AchievementAnnualReportVo report = AchievementReportAggregator.buildReport(2026, Collections.emptyList());
58
-        when(achievementReportService.getAnnualReport("2026")).thenReturn(report);
53
+        when(achievementMapper.selectByStatYear("2026")).thenReturn(Collections.emptyList());
59 54
         BizCommonProsperityProject p = new BizCommonProsperityProject();
60 55
         p.setId(1L);
61 56
         p.setProjectType(1);
@@ -75,9 +70,8 @@ class CommonProsperityScreenServiceImplTest
75 70
     @DisplayName("DP-GTFY-UT-015 仅成果无项目")
76 71
     void ut015_achievementOnly()
77 72
     {
78
-        AchievementAnnualReportVo report = AchievementReportAggregator.buildReport(2026,
79
-                Collections.singletonList(sampleAchievementRow()));
80
-        when(achievementReportService.getAnnualReport("2026")).thenReturn(report);
73
+        when(achievementMapper.selectByStatYear("2026"))
74
+                .thenReturn(Collections.singletonList(sampleAchievementRow()));
81 75
         when(projectMapper.selectPublishedForScreen(2026)).thenReturn(Collections.emptyList());
82 76
         when(achievementMapper.selectDistinctStatYears()).thenReturn(Collections.singletonList("2026"));
83 77
         when(projectMapper.selectDistinctStatYearsFromPublished()).thenReturn(Collections.emptyList());
@@ -89,20 +83,47 @@ class CommonProsperityScreenServiceImplTest
89 83
     }
90 84
 
91 85
     @Test
92
-    @DisplayName("复用年度报告服务")
93
-    void reusesAchievementReport()
86
+    @DisplayName("按所选年查询成果台账")
87
+    void loadsAchievementsByStatYear()
94 88
     {
95
-        when(achievementReportService.getAnnualReport("2026"))
96
-                .thenReturn(AchievementReportAggregator.buildReport(2026, Collections.emptyList()));
89
+        when(achievementMapper.selectByStatYear("2026")).thenReturn(Collections.emptyList());
97 90
         when(projectMapper.selectPublishedForScreen(2026)).thenReturn(Collections.emptyList());
98 91
         when(achievementMapper.selectDistinctStatYears()).thenReturn(Collections.emptyList());
99 92
         when(projectMapper.selectDistinctStatYearsFromPublished()).thenReturn(Collections.emptyList());
100 93
 
101 94
         service.loadDashboard("2026");
102
-        verify(achievementReportService).getAnnualReport("2026");
95
+        verify(achievementMapper).selectByStatYear("2026");
103 96
         verify(projectMapper).selectPublishedForScreen(eq(2026));
104 97
     }
105 98
 
99
+    @Test
100
+    @DisplayName("总览取所选年最大填报月,按月序列为该年 1~最大月")
101
+    void summaryUsesMaxMonthOfSelectedYear()
102
+    {
103
+        BizCommonProsperityAchievement mar = sampleAchievementRowForYear(2025, 3);
104
+        mar.setCollectiveEconomyIncome(new BigDecimal("10"));
105
+        BizCommonProsperityAchievement aug = sampleAchievementRowForYear(2025, 8);
106
+        aug.setCollectiveEconomyIncome(new BigDecimal("88"));
107
+        aug.setEmploymentDrivenCount(50);
108
+        when(achievementMapper.selectByStatYear("2025")).thenReturn(Arrays.asList(mar, aug));
109
+        when(projectMapper.selectPublishedForScreen(2025)).thenReturn(Collections.emptyList());
110
+        when(achievementMapper.selectDistinctStatYears()).thenReturn(Collections.singletonList("2025"));
111
+        when(projectMapper.selectDistinctStatYearsFromPublished()).thenReturn(Collections.emptyList());
112
+
113
+        CommonProsperityScreenDashboardVo vo = service.loadDashboard("2025");
114
+        assertEquals(2025, vo.getStatYear().intValue());
115
+        assertTrue(vo.getAchievement().getHasData());
116
+        assertNotNull(vo.getAchievement().getSummary());
117
+        assertEquals(0, new BigDecimal("88.00").compareTo(vo.getAchievement().getSummary().getCollectiveEconomyIncome()));
118
+        assertEquals(50, vo.getAchievement().getSummary().getEmploymentDrivenCount().intValue());
119
+        assertEquals(8, vo.getAchievement().getMonthly().size());
120
+        assertEquals(2025, vo.getAchievement().getMonthly().get(0).getStatYear().intValue());
121
+        assertEquals(0, new BigDecimal("10").compareTo(
122
+                vo.getAchievement().getMonthly().get(2).getCollectiveEconomyIncome()));
123
+        assertEquals(0, new BigDecimal("88").compareTo(
124
+                vo.getAchievement().getMonthly().get(7).getCollectiveEconomyIncome()));
125
+    }
126
+
106 127
     @Test
107 128
     @DisplayName("列表含运营图查询")
108 129
     void listLoadsOpImages()
@@ -119,22 +140,26 @@ class CommonProsperityScreenServiceImplTest
119 140
         verify(opImageMapper).selectByProjectIds(Arrays.asList(9L));
120 141
     }
121 142
 
122
-    private static com.ruoyi.web.modules.prosperity.domain.BizCommonProsperityAchievement sampleAchievementRow()
143
+    private static BizCommonProsperityAchievement sampleAchievementRow()
144
+    {
145
+        return sampleAchievementRowForYear(2026, 3);
146
+    }
147
+
148
+    private static BizCommonProsperityAchievement sampleAchievementRowForYear(int year, int month)
123 149
     {
124
-        com.ruoyi.web.modules.prosperity.domain.BizCommonProsperityAchievement r =
125
-                new com.ruoyi.web.modules.prosperity.domain.BizCommonProsperityAchievement();
126
-        r.setStatPeriod("2026-03");
127
-        r.setCollectiveEconomyIncome(new java.math.BigDecimal("10"));
150
+        BizCommonProsperityAchievement r = new BizCommonProsperityAchievement();
151
+        r.setStatPeriod(String.format("%d-%02d", year, month));
152
+        r.setCollectiveEconomyIncome(new BigDecimal("10"));
128 153
         r.setEmploymentDrivenCount(1);
129 154
         r.setNewJobPositions(1);
130 155
         r.setEnvProjectCount(1);
131
-        r.setNewGreenArea(new java.math.BigDecimal("1"));
156
+        r.setNewGreenArea(new BigDecimal("1"));
132 157
         r.setCulturalHeritageCount(1);
133 158
         r.setEthnicIntegrationProjectCount(1);
134 159
         r.setEthnicUnityActivityCount(1);
135
-        r.setDigitalPlatformUsageRate(new java.math.BigDecimal("0.1"));
160
+        r.setDigitalPlatformUsageRate(new BigDecimal("0.1"));
136 161
         r.setRegionalPublicBrandCount(1);
137
-        r.setBrandPremiumRate(new java.math.BigDecimal("0.1"));
162
+        r.setBrandPremiumRate(new BigDecimal("0.1"));
138 163
         return r;
139 164
     }
140 165
 }

+ 2 - 2
doc/共同富裕/共同富裕成果年度报告/共同富裕成果年度报告前端技术方案.md

@@ -23,7 +23,7 @@
23 23
 | **方法** | `getAchievementAnnualReport(statYear)` → `GET /commonProsperity/achievementReport` |
24 24
 | **可选** | `getAchievementReportYears()` → `GET /commonProsperity/achievementReport/years`(年份选择器可仅用 `el-date-picker type="year"`,不强制调此接口) |
25 25
 
26
-**响应使用**:`data.hasData`、`data.summary`(12 项年度汇总)、`data.monthly`(长度 12,缺月指标为 0)。
26
+**响应使用**:`data.hasData`、`data.summary`(最大月单月 12 项)、`data.monthly`(长度 = 该年最大月,缺月指标为 0)。
27 27
 
28 28
 ---
29 29
 
@@ -43,7 +43,7 @@
43 43
 | 文化传承 | 柱状 | `culturalHeritageCount` |
44 44
 | 民族团结 | 堆叠柱状 | `ethnicIntegrationProjectCount`、`ethnicUnityActivityCount` |
45 45
 
46
-横轴固定 **1~12 月**;`monthly` 缺月由后端补 0,前端 `seriesByField` 再兜底为 0
46
+横轴为 **1~该年最大统计月**;`monthly` 缺月由后端补 0,前端按返回序列渲染
47 47
 
48 48
 ---
49 49
 

+ 7 - 7
doc/共同富裕/共同富裕成果年度报告/共同富裕成果年度报告功能需求.md

@@ -25,8 +25,9 @@
25 25
 
26 26
 ### 2.2 统计月份(图表横轴)
27 27
 
28
-- 图表按月展示时,横轴**固定**为 **1 月~12 月**,共 12 个刻度。
29
-- **缺月规则(产品定论):** 某月**无**对应成果记录时,该月各指标在图表与参与年度汇总的序列中均按 **0** 取值(**不**断线、**不**留空、**不**跳过该月)。Tooltip 仍展示「0 + 单位」。
28
+- 图表按月展示时,横轴为选定年 **1 月~该年台账中最大统计月 N**,共 **N** 个刻度(与大屏共同富裕一致)。
29
+- **缺月规则:** 1~N 内某月**无**对应成果记录时,该月各指标按 **0** 取值(**不**断线、**不**留空、**不**跳过该月)。Tooltip 仍展示「0 + 单位」。
30
+- 全年无任何台账时走 **§7.2** 空态,`monthly` 为空列表。
30 31
 
31 32
 ### 2.3 共富成果指标(与台账一致)
32 33
 
@@ -48,14 +49,13 @@
48 49
 
49 50
 ### 2.4 汇总口径(年度总览)
50 51
 
51
-**年度总览区**(见 **§5.2**)对选定年份内各月台账数据汇总规则如下(须在界面或帮助说明中标注,避免与「单月快照」混淆)
52
+**年度总览区**(见 **§5.2**)与大屏共同富裕总览口径一致
52 53
 
53 54
 | 指标类型 | 年度展示口径 |
54 55
 | --- | --- |
55
-| 金额、人数、岗位数、项目数、活动数、品牌数、绿化面积 | 对 **1~12 月** 逐月取值,**缺月按 0**,再求**全年加总** |
56
-| 数字平台使用率、品牌产品溢价率 | 对 **1~12 月** 逐月取值,**缺月按 0**,再求 **12 个月算术平均值**(分母固定为 12) |
56
+| 全部 12 项指标(含比率) | 取该年台账中 **`stat_period` 最大月份** 的**单月**指标值(非全年加总、非 12 月均值) |
57 57
 
58
-> 与图表缺月规则一致;**全年无任何台账记录**时仍走 **§7.2** 空态,不将「全年无数据」展示为全 0 汇总。
58
+> **全年无任何台账记录**时仍走 **§7.2** 空态,不将「全年无数据」展示为全 0 汇总。
59 59
 
60 60
 ### 2.5 模块边界
61 61
 
@@ -137,7 +137,7 @@ flowchart TD
137 137
 | 项 | 规则 |
138 138
 | --- | --- |
139 139
 | 标题 | 各图表具备明确标题(与草稿主题一致:集体经济收入、就业带动、环境治理等) |
140
-| 横轴 | **月份**(1~12 月);标签格式与原型一致(如 `1月`…`12月` 或 `2026-01`) |
140
+| 横轴 | **月份**(1~该年最大统计月);标签格式与原型一致(如 `1月`…`N月`) |
141 141
 | 纵轴 | 带单位或图例说明(万元、人、个、亩、% 等) |
142 142
 | 缺月 | 该月无台账记录:柱状图、折线图、堆叠柱状图**均显示为 0**(柱高为 0、折线落点为 0);**12 个月均须渲染**,不得省略横轴刻度 |
143 143
 | 单点数据 | 仅 1 个月有数据时,图表仍须正常渲染,不得报错 |

+ 7 - 6
doc/共同富裕/共同富裕成果年度报告/共同富裕成果年度报告技术方案.md

@@ -44,14 +44,14 @@
44 44
 | **索引** | 沿用 `idx_stat_period` / `uk_stat_period`;单年最多 12 行,无额外索引要求 |
45 45
 | **可选年份列表** | `SELECT DISTINCT LEFT(stat_period, 4) AS stat_year FROM biz_common_prosperity_achievement ORDER BY stat_year DESC` |
46 46
 
47
-### 2.3 汇总计算(服务端,与功能需求对齐
47
+### 2.3 汇总计算(服务端,与大屏共同富裕一致
48 48
 
49
-| 类型 | 字段(库 snake / API 驼峰) | 年度总览算法 | 分月序列 |
50
-| --- | --- | --- | --- |
51
-| 加总 | `collective_economy_income` 等 10 项非比率指标 | 1~12 月逐月取值,**缺月=0**,再 **SUM** | 每月台账值或 **0** |
52
-| 月均 | `digital_platform_usage_rate`、`brand_premium_rate` | 1~12 月逐月取值,**缺月=0**,再 **SUM/12** | 每月台账值或 **0** |
49
+| 类型 | 字段 | 算法 |
50
+| --- | --- | --- |
51
+| 总览 `summary` | 全部 12 项指标 | 取该年台账 **最大统计月** 的单月值 |
52
+| 分月 `monthly` | 同上字段 | 所选年 **1~最大月 N**;区间内缺月补 **0**;无数据返回空列表 |
53 53
 
54
-**`hasData` 判定**:该年查询结果 `COUNT(*) > 0` 为 `true`;为 `false` 时 `summary` 可为 `null`,`monthly` 仍返回 12 个月(全 0)或空数组(与前端约定二选一,**推荐**:`monthly` 仍 12 条全 0,由前端根据 `hasData` 显示空态)
54
+**`hasData` 判定**:该年存在 ≥1 条有效台账为 `true`;为 `false` 时 `summary=null`,`monthly=[]`
55 55
 
56 56
 ---
57 57
 
@@ -188,3 +188,4 @@
188 188
 | 版本 | 说明 |
189 189
 | --- | --- |
190 190
 | 1.0 | 初稿:无新表;单接口年度报告 + 可选年份列表;缺月补 0;`hasData` 控制空态 |
191
+| 1.1 | 与大屏对齐:总览取最大月单月;`monthly` 为 1~最大月 |

+ 1 - 1
doc/共同富裕/共同富裕成果年度报告/共同富裕成果年度报告管理测试用例.md

@@ -3,7 +3,7 @@
3 3
 > 依据:`共同富裕成果年度报告功能需求.md`、`共同富裕成果年度报告技术方案.md`  
4 4
 > **接口 Base Path(示例)**:`/commonProsperity/achievementReport`;若含 `context-path` 或网关前缀须补齐。鉴权与若依一致(Cookie / Token)。
5 5
 
6
-**通用前置(无特殊说明)**:具备本模块**查询**权限的账号已登录。**数据依赖**:成果台账表 `biz_common_prosperity_achievement` 已有测试数据。**缺月规则**:无台账月份指标按 **0**;**全年无记录**时 `hasData=false`,`summary=null`。**年度汇总**:加总类 **SUM(12 月,缺月=0)**;比率类 **(SUM 12 月)/12**
6
+**通用前置(无特殊说明)**:具备本模块**查询**权限的账号已登录。**数据依赖**:成果台账表 `biz_common_prosperity_achievement` 已有测试数据。**缺月规则**:1~最大月内无台账月份指标按 **0**;**全年无记录**时 `hasData=false`,`summary=null`,`monthly=[]`。**总览**:取该年**最大统计月**单月值(与大屏一致)
7 7
 
8 8
 **界面(UI)测试**:借助 **Playwright** 启动 **Chromium**(与 **Google Chrome 同源内核**);可配置 `channel: 'chrome'` 调用本机已安装的谷歌浏览器。
9 9
 

+ 23 - 20
doc/大屏/共同富裕/大屏共同富裕功能需求.md

@@ -21,18 +21,18 @@
21 21
 
22 22
 - 大屏顶部(或全局筛选区)提供**统计年份**选择,格式为自然年 **YYYY**(如 `2026`)。
23 23
 - 进入大屏时默认选中**当前自然年**。
24
-- 变更统计年份后,**总览卡片、项目分类、项目列表**等标注「与 **Y** 相关」的区块须**同步**按新年份重算;**§5.3~5.9 按月图表**为滚动最近 12 月,**不**随 **Y** 切换(见 **§2.2**)
24
+- 变更统计年份后,本页**全部统计区块**(总览卡片、按月图表、数字赋能、项目分类、项目列表)须**同步**按新年份 **Y** 重算与展示,不得出现区块之间年份不一致
25 25
 
26 26
 ### 2.2 统计月份(图表横轴)
27 27
 
28
-**滚动最近 12 个月**(适用于 **§5.3~5.9** 全部按月图表)
28
+**所选年 1~最大统计月**(适用于 **§5.3~5.9** 全部按月图表;与管理端年度报告一致
29 29
 
30
-- 以服务器当前年月为锚点,取**含当月在内向前连续 12 个自然月**;**不**随页面「统计年份 **Y**」切换。
31
-- 接口 `achievement.monthly[]` 每条含 **`statYear`**、**`month`(1~12)**、**`statPeriod`(yyyy-MM)**;按时间升序固定 **12** 条
32
-- **缺月规则:** 窗口槽位内无对应成果台账时,各指标按 **0** 展示;**不**省略刻度、**不**断线留空。
30
+- 横轴为选定统计年 **Y** 的 **1~N** 月,**N** 为该年台账中最大 `stat_period` 月份;切换 **Y** 后同步切换。
31
+- 接口 `achievement.monthly[]` 每条含 **`statYear`**、**`month`**、**`statPeriod`(yyyy-MM)**;按月份升序,长度 **N**(无数据时为空列表)
32
+- **缺月规则:** 1~N 内无对应成果台账时,各指标按 **0** 展示;**不**省略刻度、**不**断线留空。
33 33
 - **前端横轴**仅展示月份(如 `8月`);**鼠标悬停**展示 **YYYY年M月** 及数值。
34 34
 
35
-**年度总览**(**§2.4**)仍按选定 **Y** 全年 1~12 月台账汇总,与滚动图表窗口**独立**
35
+**年度总览**(**§2.4**)取所选年台账中**最大统计月**的单月数据;按月图表为该年 1~N 月序列
36 36
 
37 37
 ### 2.3 共富成果指标(与后台台账一致)
38 38
 
@@ -54,14 +54,14 @@
54 54
 
55 55
 ### 2.4 年度总览区汇总口径
56 56
 
57
-「共富成果」总览卡片对选定年份内各月台账汇总规则如下(须在界面或帮助说明中标注,避免与「单月值」混淆)
57
+「共富成果」总览卡片(及数字赋能区同源字段)对选定年份 **Y** 的规则如下
58 58
 
59 59
 | 指标类型 | 年度总览展示口径 |
60 60
 | --- | --- |
61
-| 集体经济收入、带动就业人数、新增就业岗位、环境治理项目数、新增绿化面积、文化传承人数、民族融合项目数、民族团结活动数、区域公共品牌数 | 对 1~12 月逐月取值(缺月按 0),再求**全年加总** |
62
-| 数字平台使用率、品牌产品溢价率 | 对 1~12 月逐月取值(缺月按 0),再求 **12 个月算术平均值**(分母固定为 12) |
61
+| 集体经济收入、带动就业人数、新增就业岗位、环境治理项目数、新增绿化面积、文化传承人数、民族融合项目数、民族团结活动数、区域公共品牌数、数字平台使用率、品牌产品溢价率 | 取该年台账中 **`stat_period` 最大月份** 的**单月**指标值(非全年加总 / 非 12 月均值) |
63 62
 
64
-> **全年无任何成果台账记录**时,总览区与各图表走 **§7.1** 空态,**不**将「无数据」展示为全 0 汇总。
63
+> **全年无任何成果台账记录**时,总览区与各图表走 **§7.1** 空态,**不**将「无数据」展示为全 0 汇总。  
64
+> 按月图表(**§5.3~5.9**)展示该年 1~最大月序列,缺月填 0;总览取最大月单月值。
65 65
 
66 66
 ### 2.5 共富项目类型(与后台一致)
67 67
 
@@ -147,7 +147,7 @@ flowchart TD
147 147
 
148 148
 | 项 | 规则 |
149 149
 | --- | --- |
150
-| 展示内容 | **§2.3** 所列指标在该年的汇总结果,口径遵循 **§2.4** |
150
+| 展示内容 | **§2.3** 所列指标在该年的取值,口径遵循 **§2.4**(最大填报月单月) |
151 151
 | 展示形式 | 卡片或数字大屏组件;每项含指标名称、数值、单位(比率类带 %) |
152 152
 | 布局 | 原型若仅展示部分核心指标,以产品定稿为准;**不得**改变指标含义与汇总口径 |
153 153
 | 无数据 | 该年无任何成果台账时,本区块 **§7.1** 空态,并提示前往后台维护 |
@@ -202,9 +202,9 @@ flowchart TD
202 202
 
203 203
 | 项 | 规则 |
204 204
 | --- | --- |
205
-| 区域公共品牌数 | 展示 **§2.4** 全年加总结果;可用中心数字 + 环形/仪表盘等组件 |
206
-| 品牌产品溢价率 | 展示 **§2.4** 十二个月算术平均;可按**百分比**展示(如 52%) |
207
-| 数字平台使用率 | 展示 **§2.4** 十二个月算术平均;可按**百分比**展示 |
205
+| 区域公共品牌数 | 展示 **§2.4** 所选年最大填报月单月值;可用中心数字 + 环形/仪表盘等组件 |
206
+| 品牌产品溢价率 | 展示 **§2.4** 所选年最大填报月单月值;可按**百分比**展示(如 52%) |
207
+| 数字平台使用率 | 展示 **§2.4** 所选年最大填报月单月值;可按**百分比**展示 |
208 208
 | 无成果数据 | 该年无台账时,本区块与总览一致走空态,**不**展示误导性 0% 或 0 个品牌 |
209 209
 
210 210
 ---
@@ -284,10 +284,10 @@ flowchart TD
284 284
 | 项 | 规则 |
285 285
 | --- | --- |
286 286
 | 标题 | 各图表有明确中文标题(可与原型一致) |
287
-| 横轴 | **滚动 12 月**;12 个刻度,**仅显示月份**;悬停显 **YYYY年M月** |
287
+| 横轴 | **所选年 1~最大月**;N 个刻度,**仅显示月份**;悬停显 **YYYY年M月** |
288 288
 | 纵轴 | 标注单位或通过图例说明 |
289
-| 单点数据 | 滚动窗口内仅 1 个月有台账时,图表仍须正常渲染 |
290
-| 跨年 | 滚动窗口可跨自然年;按 `statPeriod` 归属槽位 |
289
+| 单点数据 | 该年内仅 1 个月有台账时,图表仍须正常渲染(长度为 1) |
290
+| 切年 | 切换 **Y** 后横轴为新年 1~该年最大月;数据归属新年份,**不**混入其他年 |
291 291
 | 同月唯一 | 依赖后台「同一统计范围不可重复」;若后台允许多条,须约定取**最新登记**一条并修订本文档 |
292 292
 
293 293
 ---
@@ -329,7 +329,7 @@ flowchart TD
329 329
 | --- | --- |
330 330
 | 共同富裕成果 | 按统计年份读取各月指标;驱动总览、按月图表、数字赋能 |
331 331
 | 共同富裕项目 | 读取**已发布**项目;驱动分类饼图与列表(列表含 **§5.11.2** 全部展示字段) |
332
-| 共同富裕成果年度报告(管理端) | 业务口径与年度报告一致;大屏为**大屏专用布局**,指标与汇总规则应对齐,避免同一指标两套口径 |
332
+| 共同富裕成果年度报告(管理端) | 与大屏共用 `AchievementReportAggregator`:总览最大月单月;按月 1~最大月缺月补 0 |
333 333
 
334 334
 ---
335 335
 
@@ -339,9 +339,9 @@ flowchart TD
339 339
 | --- | --- | --- |
340 340
 | DP-01 | 默认年份 | 进入大屏默认当前年,总览与图表年份一致 |
341 341
 | DP-02 | 切换年份 | 切换后全页数据同属一年;无旧年残留 |
342
-| DP-03 | 缺月 | 滚动 12 月窗口内仅 3、8 月有台账时,其余槽位为 0;横轴仍为 12 刻度;悬停显年月 |
342
+| DP-03 | 缺月 | 选定年仅 3、8 月有台账时,其余月为 0;横轴仍为 12 刻度;悬停显年月 |
343 343
 | DP-04 | 全年无成果 | 总览与图表空态,非全 0 假数据 |
344
-| DP-05 | 比率展示 | 溢价率、使用率与后台逻辑一致(12 个月平均后按 % 展示) |
344
+| DP-05 | 比率展示 | 溢价率、使用率取所选年最大填报月单月值,按 % 展示 |
345 345
 | DP-06 | 就业/民族团结双系列 | 图例、Tooltip 可区分两个维度 |
346 346
 | DP-07 | 项目分类 | 仅统计已发布且与年份交集的项目;占比之和为 100%(有项目时) |
347 347
 | DP-08 | 项目列表 | 分页正常;行内可见类型、名称、简介、内容、周期、图视;无单独详情页跳转 |
@@ -356,6 +356,9 @@ flowchart TD
356 356
 | --- | --- | --- |
357 357
 | 1.0 | 2026-05-25 | 由草稿优化:补全流程、口径、十区块需求;对齐成果/项目/年度报告;明确列表行内详情;不涉及库表与接口 |
358 358
 | 1.1 | 2026-07-04 | **§5.3~5.9** 按月图表改为**滚动最近 12 月**;`monthly` 增 `statYear`;总览仍按 **Y** 汇总 |
359
+| 1.2 | 2026-07-24 | **总览与按月图表**统一按所选年 **Y** 的 1~12 月取数;取消滚动 12 月窗口 |
360
+| 1.3 | 2026-07-24 | **总览 / 数字赋能**改为所选年台账**最大统计月**的单月数据;按月图表仍为该年 1~12 月 |
361
+| 1.4 | 2026-07-24 | 按月图表改为所选年 **1~最大月**;与管理端年度报告口径一致 |
359 362
 
360 363
 ---
361 364
 

+ 8 - 4
doc/大屏/共同富裕/大屏共同富裕技术方案.md

@@ -135,8 +135,8 @@
135 135
 | 字段 | 说明 |
136 136
 | --- | --- |
137 137
 | `hasData` | 即 `hasAchievementData`;全年无成果台账为 `false` |
138
-| `summary` | 年度总览;`hasData=false` 时为 **`null`** |
139
-| `monthly` | 固定 **12** 条;**滚动最近 12 自然月**;每项含 `statYear`、`month`、`statPeriod`;缺月指标为 **0** |
138
+| `summary` | 总览 / 数字赋能;取所选年台账**最大统计月**单月值;`hasData=false` 时为 **`null`** |
139
+| `monthly` | **所选年 1~最大统计月 N**,共 N 条;每项含 `statYear`、`month`、`statPeriod`;缺月指标为 **0**;无数据时为空列表 |
140 140
 
141 141
 **图表字段映射**(与功能需求 §5、年度报告一致)
142 142
 
@@ -181,8 +181,9 @@
181 181
 
182 182
 ```
183 183
 1. year = parseStatYear(statYear)
184
-2. achievement = achievementReportService.getAnnualReport(String.valueOf(year));`summary`/`hasData` 仍按 **Y**
185
-3. achievement.monthly = AchievementReportAggregator.buildRollingMonthly(anchor, selectByStatPeriodBetween(...)) — **滚动 12 月**,**不**随 **Y**
184
+2. rows = achievementMapper.selectByStatYear(year)
185
+3. achievement = AchievementReportAggregator.buildReport(year, rows)
186
+   — summary = 最大月单月;monthly = 1~最大月缺月补 0;无数据 monthly=[]、summary=null
186 187
 4. projects = mapper.selectPublishedForScreen(year)
187 188
 5. projectTypeStats = groupByType(projects); hasProjectData = total > 0
188 189
 6. availableYears = union(achievementMapper.selectDistinctStatYears(), projectYears)
@@ -254,3 +255,6 @@ SQL 示例:`sql/big_screen_common_prosperity_perm.sql`(挂载「大屏」父
254 255
 | 版本 | 日期 | 说明 |
255 256
 | --- | --- | --- |
256 257
 | 1.0 | 2026-05-20 | 初稿:无新业务表;复用成果年度报告聚合;双接口(看板 + 项目分页);对齐功能需求 v1.0 |
258
+| 1.1 | 2026-07-24 | 看板 `monthly` 改回所选年 1~12 月(与 `summary` 同源),不再覆盖为滚动 12 月 |
259
+| 1.2 | 2026-07-24 | 看板 `summary` 改为所选年**最大统计月**单月值;`monthly` 仍为该年 1~12 月 |
260
+| 1.3 | 2026-07-24 | `monthly` 改为所选年 1~最大月;与管理端年度报告共用 Aggregator 口径 |

+ 4 - 4
doc/大屏/共同富裕/大屏共同富裕测试用例.md

@@ -3,7 +3,7 @@
3 3
 > 依据:`大屏共同富裕功能需求.md`(v1.1)、`大屏共同富裕技术方案.md`  
4 4
 > **接口 Base Path**:`/bigScreen/commonProsperity`(含 `context-path`、网关前缀须补齐)。鉴权:若依 Cookie / `Authorization` Token;权限 `bigScreen:commonProsperity:query`。
5 5
 
6
-**成果口径(图表)**:`achievement.monthly` 为**滚动最近 12 月**;含 `statYear`+`month`;**不**随 **Y**;缺月 **0**。**总览** `summary` 仍按 **Y** 全年汇总
6
+**成果口径**:`achievement.monthly` 为所选年 **Y** 的 **1~最大统计月**;缺月 **0**;无数据为空列表。**总览** `summary` 为该年台账**最大统计月**的单月值。与管理端年度报告一致
7 7
 
8 8
 **UI 测试**:**Playwright** + **Chromium**,`playwright.config` 中 `use: { channel: 'chrome' }` 调用本机 **Google Chrome**。
9 9
 
@@ -54,7 +54,7 @@
54 54
 | 用例编号 | 测试模块 | 测试项 | 测试类型 | 测试工具 | 测试目的 | 前置条件 | 测试步骤 | 预期结果 |
55 55
 | --- | --- | --- | --- | --- | --- | --- | --- | --- |
56 56
 | DP-GTFY-API-001 | 看板 | 满年成果+项目 | 接口测试 | Postman/curl | 正常流程 DP-01 | 已登录;2026 成果多月+已发布项目 | `GET .../dashboard?statYear=2026` | `code` 成功;`achievement.hasData=true`;`monthly.length=12`;`hasProjectData=true`;`projectTypeStats` 有数据 |
57
-| DP-GTFY-API-002 | 看板 | 滚动 12 月缺月补零 | 接口测试 | Postman | DP-03 | ACH-2026 仅 3、8 月 | `statYear=2026` | `monthly` 含 `statYear`、`month`;缺槽 0;**不**随 **Y** 变窗口 |
57
+| DP-GTFY-API-002 | 看板 | 所选年缺月补零 | 接口测试 | Postman | DP-03 | ACH-2026 仅 3、8 月 | `statYear=2026` | `monthly` 含 `statYear`、`month`;缺槽 0;切年窗口随 **Y** |
58 58
 | DP-GTFY-API-003 | 看板 | 全年无成果 | 接口测试 | Postman | DP-04 | 2026 ACH-EMPTY | `GET dashboard` | `achievement.hasData=false`;`summary=null`;`monthly` 仍 12 条全 0 |
59 59
 | DP-GTFY-API-004 | 看板 | 仅项目无成果 | 接口测试 | Postman | §7.2 | 2026 无成果;有 PRJ-PUB-B | `GET dashboard` | `achievement.hasData=false`;`hasProjectData=true`;饼图 stats 正常 |
60 60
 | DP-GTFY-API-005 | 看板 | 仅成果无项目 | 接口测试 | Postman | §7.2 | 2026 有 ACH;无符合交集项目 | `GET dashboard` | `achievement.hasData=true`;`hasProjectData=false`;`projectTypeStats` 三项 count=0 |
@@ -94,7 +94,7 @@
94 94
 | DP-GTFY-UI-004 | 空态 | 全年无成果 | UI 测试 | Playwright+Chrome | DP-04、§7.1 | 选 ACH-EMPTY 年 | 进入或切换该年 | 总览/图表/数字赋能空态文案;**不**显示全 0 汇总卡片 |
95 95
 | DP-GTFY-UI-005 | 空态 | 无项目有成果 | UI 测试 | Playwright+Chrome | §7.2 | 2026 有成果无项目 | 打开 2026 | 图表正常;饼图空态;列表「本年度暂无共富项目」 |
96 96
 | DP-GTFY-UI-006 | 总览 | 指标与单位 | UI 测试 | Playwright+Chrome | §5.2 | 2026 有成果 | 查看共富成果卡片区 | 名称/数值/单位正确(万元、人、个、亩);比率显示 **%** |
97
-| DP-GTFY-UI-007 | 图表 | 集体经济横向柱滚动 12 月 | UI 测试 | Playwright+Chrome | §5.3、DP-03 | ACH-2026 缺月 | 查看该图+悬停 | 12 刻度**仅月份**;悬停显 **YYYY年M月**+万元 |
97
+| DP-GTFY-UI-007 | 图表 | 集体经济横向柱所选年 12 月 | UI 测试 | Playwright+Chrome | §5.3、DP-03 | ACH-2026 缺月 | 查看该图+悬停 | 12 刻度**仅月份**;悬停显 **YYYY年M月**+万元 |
98 98
 | DP-GTFY-UI-008 | 图表 | 就业双系列 | UI 测试 | Playwright+Chrome | DP-06、§5.4 | 2026 有数据 | 查看就业带动图 | 两图例/两色系;Tooltip 区分人数与岗位 |
99 99
 | DP-GTFY-UI-009 | 图表 | 环境治理/文化传承柱图 | UI 测试 | Playwright+Chrome | §5.5、§5.8 | 2026 有数据 | 查看两图 | 12 月;缺月为 0 |
100 100
 | DP-GTFY-UI-010 | 图表 | 绿化折线 | UI 测试 | Playwright+Chrome | §5.6 | 2026 有数据 | 查看绿化建设 | 12 落点;缺月 0 不断线 |
@@ -157,7 +157,7 @@ await expect(page.locator('[data-chart]')).toHaveCount(6); // 集体经济/就
157 157
 | --- | --- | --- |
158 158
 | DP-01 | 默认当前年 | UI-001;API-001 |
159 159
 | DP-02 | 切年一致 | API-028;UI-002~003 |
160
-| DP-03 | 滚动 12 月缺月=0 | UT-003;API-002;UI-007~011 |
160
+| DP-03 | 所选年缺月=0 | UT-003;API-002;UI-007~011 |
161 161
 | DP-04 | 无成果空态 | UT-004;API-003;UI-004、013 |
162 162
 | DP-05 | 比率 % | UT-005;API-006;UI-006、012 |
163 163
 | DP-06 | 双系列图 | UI-008、011 |