Przeglądaj źródła

牦牛出栏数据

wwh 1 miesiąc temu
rodzic
commit
5b830b5093

+ 9 - 0
baqing-admin/src/main/java/com/ruoyi/web/modules/screen/controller/HomeScreenController.java

@@ -53,4 +53,13 @@ public class HomeScreenController extends BaseController
53
         List<HomeScreenStandardListItemVo> list = homeScreenService.listStandards(statYear, category);
53
         List<HomeScreenStandardListItemVo> list = homeScreenService.listStandards(statYear, category);
54
         return getDataTable(list);
54
         return getDataTable(list);
55
     }
55
     }
56
+
57
+    /**
58
+     * 地图标注:各乡镇牦牛存栏(上一年填报)与出栏(当年填报)。
59
+     */
60
+    @GetMapping("/townMapMarkers")
61
+    public AjaxResult townMapMarkers()
62
+    {
63
+        return success(homeScreenService.loadTownMapMarkers());
64
+    }
56
 }
65
 }

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

@@ -7,6 +7,7 @@ import com.ruoyi.web.modules.screen.domain.dto.HomeScreenAppointmentStatDto;
7
 import com.ruoyi.web.modules.screen.domain.dto.HomeScreenMonthCountDto;
7
 import com.ruoyi.web.modules.screen.domain.dto.HomeScreenMonthCountDto;
8
 import com.ruoyi.web.modules.screen.domain.dto.HomeScreenSubsidyTypeSumDto;
8
 import com.ruoyi.web.modules.screen.domain.dto.HomeScreenSubsidyTypeSumDto;
9
 import com.ruoyi.web.modules.screen.domain.dto.HomeScreenTrainingMonthDto;
9
 import com.ruoyi.web.modules.screen.domain.dto.HomeScreenTrainingMonthDto;
10
+import com.ruoyi.web.modules.screen.domain.vo.HomeScreenTownMapMarkerVo;
10
 
11
 
11
 /**
12
 /**
12
  * 大屏首页聚合查询。
13
  * 大屏首页聚合查询。
@@ -62,4 +63,11 @@ public interface HomeScreenStatsMapper
62
     List<Integer> selectDistinctTechPublishYears();
63
     List<Integer> selectDistinctTechPublishYears();
63
 
64
 
64
     List<Integer> selectDistinctTrainingYears();
65
     List<Integer> selectDistinctTrainingYears();
66
+
67
+    /**
68
+     * 地图标注:全县填报乡镇 + 存栏(上一年)/出栏(当年)头数。
69
+     */
70
+    List<HomeScreenTownMapMarkerVo> selectTownMapMarkers(
71
+            @Param("herdStatYear") int herdStatYear,
72
+            @Param("outboundStatYear") int outboundStatYear);
65
 }
73
 }

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

@@ -3,6 +3,7 @@ package com.ruoyi.web.modules.screen.service;
3
 import java.util.List;
3
 import java.util.List;
4
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenDashboardVo;
4
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenDashboardVo;
5
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenStandardListItemVo;
5
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenStandardListItemVo;
6
+import com.ruoyi.web.modules.screen.domain.vo.HomeScreenTownMapVo;
6
 
7
 
7
 /**
8
 /**
8
  * 大屏首页统计服务。
9
  * 大屏首页统计服务。
@@ -24,4 +25,9 @@ public interface IHomeScreenService
24
      * @param category {@link com.ruoyi.web.modules.screen.support.HomeScreenStandardCategory} 枚举名
25
      * @param category {@link com.ruoyi.web.modules.screen.support.HomeScreenStandardCategory} 枚举名
25
      */
26
      */
26
     List<HomeScreenStandardListItemVo> listStandards(String statYear, String category);
27
     List<HomeScreenStandardListItemVo> listStandards(String statYear, String category);
28
+
29
+    /**
30
+     * 地图标注:各乡镇存栏(上一年填报)与出栏(当年填报)。
31
+     */
32
+    HomeScreenTownMapVo loadTownMapMarkers();
27
 }
33
 }

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

@@ -3,6 +3,7 @@ package com.ruoyi.web.modules.screen.service.impl;
3
 import java.math.BigDecimal;
3
 import java.math.BigDecimal;
4
 import java.math.RoundingMode;
4
 import java.math.RoundingMode;
5
 import java.time.LocalDate;
5
 import java.time.LocalDate;
6
+import java.time.Year;
6
 import java.time.ZoneId;
7
 import java.time.ZoneId;
7
 import java.util.Collections;
8
 import java.util.Collections;
8
 import java.util.Date;
9
 import java.util.Date;
@@ -18,6 +19,8 @@ import com.ruoyi.web.modules.screen.domain.vo.HomeScreenDashboardVo;
18
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenIndustryOverviewVo;
19
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenIndustryOverviewVo;
19
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenInventoryChangeVo;
20
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenInventoryChangeVo;
20
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenStandardListItemVo;
21
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenStandardListItemVo;
22
+import com.ruoyi.web.modules.screen.domain.vo.HomeScreenTownMapMarkerVo;
23
+import com.ruoyi.web.modules.screen.domain.vo.HomeScreenTownMapVo;
21
 import com.ruoyi.web.modules.screen.mapper.HomeScreenStandardMapper;
24
 import com.ruoyi.web.modules.screen.mapper.HomeScreenStandardMapper;
22
 import com.ruoyi.web.modules.screen.mapper.HomeScreenStatsMapper;
25
 import com.ruoyi.web.modules.screen.mapper.HomeScreenStatsMapper;
23
 import com.ruoyi.web.modules.screen.service.IHomeScreenService;
26
 import com.ruoyi.web.modules.screen.service.IHomeScreenService;
@@ -171,6 +174,20 @@ public class HomeScreenServiceImpl implements IHomeScreenService
171
         return rows;
174
         return rows;
172
     }
175
     }
173
 
176
 
177
+    @Override
178
+    public HomeScreenTownMapVo loadTownMapMarkers()
179
+    {
180
+        int outboundStatYear = Year.now().getValue();
181
+        int herdStatYear = outboundStatYear - 1;
182
+        List<HomeScreenTownMapMarkerVo> markers = homeScreenStatsMapper.selectTownMapMarkers(herdStatYear,
183
+                outboundStatYear);
184
+        HomeScreenTownMapVo vo = new HomeScreenTownMapVo();
185
+        vo.setHerdStatYear(herdStatYear);
186
+        vo.setOutboundStatYear(outboundStatYear);
187
+        vo.setMarkers(markers != null ? markers : Collections.emptyList());
188
+        return vo;
189
+    }
190
+
174
     private List<Long> safeActivePastureIds()
191
     private List<Long> safeActivePastureIds()
175
     {
192
     {
176
         List<Long> ids = breedingIndustryStatsMapper.selectActivePastureIds();
193
         List<Long> ids = breedingIndustryStatsMapper.selectActivePastureIds();

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

@@ -145,4 +145,22 @@
145
         where del_flag = '0' and publish_status = 1
145
         where del_flag = '0' and publish_status = 1
146
         order by 1 desc
146
         order by 1 desc
147
     </select>
147
     </select>
148
+
149
+    <select id="selectTownMapMarkers"
150
+            resultType="com.ruoyi.web.modules.screen.domain.vo.HomeScreenTownMapMarkerVo">
151
+        select d.dept_id as townDeptId,
152
+               d.dept_name as townName,
153
+               COALESCE(h.yak_total, 0) as yakInventoryCount,
154
+               COALESCE(o.yak_outbound_count, 0) as yakOutboundCount
155
+        from sys_dept d
156
+        left join biz_yak_herd_inventory h
157
+            on h.town_dept_id = d.dept_id and h.stat_year = #{herdStatYear}
158
+        left join biz_yak_outbound_report o
159
+            on o.town_dept_id = d.dept_id and o.stat_year = #{outboundStatYear}
160
+        where d.parent_id = 100
161
+          and d.order_num &gt; 12
162
+          and d.del_flag = '0'
163
+          and d.status = '0'
164
+        order by d.order_num asc, d.dept_id asc
165
+    </select>
148
 </mapper>
166
 </mapper>

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

@@ -112,6 +112,38 @@ class HomeScreenControllerApiTest
112
                 .andExpect(jsonPath("$.code").value(200));
112
                 .andExpect(jsonPath("$.code").value(200));
113
     }
113
     }
114
 
114
 
115
+    @Test
116
+    @DisplayName("DP-SY-API-038 GET townMapMarkers 地图标注")
117
+    void api024_townMapMarkers() throws Exception
118
+    {
119
+        when(homeScreenService.loadTownMapMarkers()).thenReturn(sampleTownMap());
120
+        mockMvc.perform(get("/bigScreen/home/townMapMarkers"))
121
+                .andExpect(status().isOk())
122
+                .andExpect(jsonPath("$.code").value(200))
123
+                .andExpect(jsonPath("$.data.herdStatYear").value(2025))
124
+                .andExpect(jsonPath("$.data.outboundStatYear").value(2026))
125
+                .andExpect(jsonPath("$.data.markers[0].townDeptId").value(101))
126
+                .andExpect(jsonPath("$.data.markers[0].townName").value("巴青乡"))
127
+                .andExpect(jsonPath("$.data.markers[0].yakInventoryCount").value(21587))
128
+                .andExpect(jsonPath("$.data.markers[0].yakOutboundCount").value(1523));
129
+    }
130
+
131
+    private static com.ruoyi.web.modules.screen.domain.vo.HomeScreenTownMapVo sampleTownMap()
132
+    {
133
+        com.ruoyi.web.modules.screen.domain.vo.HomeScreenTownMapMarkerVo marker =
134
+                new com.ruoyi.web.modules.screen.domain.vo.HomeScreenTownMapMarkerVo();
135
+        marker.setTownDeptId(101L);
136
+        marker.setTownName("巴青乡");
137
+        marker.setYakInventoryCount(21587);
138
+        marker.setYakOutboundCount(1523);
139
+        com.ruoyi.web.modules.screen.domain.vo.HomeScreenTownMapVo vo =
140
+                new com.ruoyi.web.modules.screen.domain.vo.HomeScreenTownMapVo();
141
+        vo.setHerdStatYear(2025);
142
+        vo.setOutboundStatYear(2026);
143
+        vo.setMarkers(Collections.singletonList(marker));
144
+        return vo;
145
+    }
146
+
115
     private static HomeScreenDashboardVo sampleDashboard()
147
     private static HomeScreenDashboardVo sampleDashboard()
116
     {
148
     {
117
         HomeScreenDashboardVo vo = new HomeScreenDashboardVo();
149
         HomeScreenDashboardVo vo = new HomeScreenDashboardVo();

+ 2 - 0
baqing-admin/src/test/java/com/ruoyi/web/modules/screen/mapper/HomeScreenStatsMapperXmlTest.java

@@ -24,6 +24,8 @@ class HomeScreenStatsMapperXmlTest
24
         assertTrue(xml.contains("biz_yak_asset"));
24
         assertTrue(xml.contains("biz_yak_asset"));
25
         assertTrue(xml.contains("biz_service_appointment"));
25
         assertTrue(xml.contains("biz_service_appointment"));
26
         assertTrue(xml.contains("biz_subsidy_grant_record"));
26
         assertTrue(xml.contains("biz_subsidy_grant_record"));
27
+        assertTrue(xml.contains("biz_yak_herd_inventory"));
28
+        assertTrue(xml.contains("biz_yak_outbound_report"));
27
         assertTrue(xml.contains("status not in (2, 3)"));
29
         assertTrue(xml.contains("status not in (2, 3)"));
28
     }
30
     }
29
 }
31
 }

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

@@ -2,11 +2,13 @@ package com.ruoyi.web.modules.screen.service.impl;
2
 
2
 
3
 import static org.junit.jupiter.api.Assertions.assertEquals;
3
 import static org.junit.jupiter.api.Assertions.assertEquals;
4
 import static org.junit.jupiter.api.Assertions.assertNotNull;
4
 import static org.junit.jupiter.api.Assertions.assertNotNull;
5
+import static org.mockito.ArgumentMatchers.eq;
5
 import static org.mockito.ArgumentMatchers.any;
6
 import static org.mockito.ArgumentMatchers.any;
6
 import static org.mockito.ArgumentMatchers.anyList;
7
 import static org.mockito.ArgumentMatchers.anyList;
7
 import static org.mockito.Mockito.when;
8
 import static org.mockito.Mockito.when;
8
 
9
 
9
 import java.math.BigDecimal;
10
 import java.math.BigDecimal;
11
+import java.time.Year;
10
 import java.util.Arrays;
12
 import java.util.Arrays;
11
 import java.util.Collections;
13
 import java.util.Collections;
12
 import java.util.List;
14
 import java.util.List;
@@ -20,6 +22,8 @@ import com.ruoyi.web.modules.industryservice.domain.dto.BreedingIndustryYakSnaps
20
 import com.ruoyi.web.modules.industryservice.mapper.BreedingIndustryStatsMapper;
22
 import com.ruoyi.web.modules.industryservice.mapper.BreedingIndustryStatsMapper;
21
 import com.ruoyi.web.modules.industryservice.support.BreedingIndustryRules;
23
 import com.ruoyi.web.modules.industryservice.support.BreedingIndustryRules;
22
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenDashboardVo;
24
 import com.ruoyi.web.modules.screen.domain.vo.HomeScreenDashboardVo;
25
+import com.ruoyi.web.modules.screen.domain.vo.HomeScreenTownMapMarkerVo;
26
+import com.ruoyi.web.modules.screen.domain.vo.HomeScreenTownMapVo;
23
 import com.ruoyi.web.modules.screen.mapper.HomeScreenStandardMapper;
27
 import com.ruoyi.web.modules.screen.mapper.HomeScreenStandardMapper;
24
 import com.ruoyi.web.modules.screen.mapper.HomeScreenStatsMapper;
28
 import com.ruoyi.web.modules.screen.mapper.HomeScreenStatsMapper;
25
 
29
 
@@ -84,6 +88,27 @@ class HomeScreenServiceImplTest
84
         assertNotNull(vo.getTrainingTrend());
88
         assertNotNull(vo.getTrainingTrend());
85
     }
89
     }
86
 
90
 
91
+    @Test
92
+    @DisplayName("DP-SY-UT-026 地图标注年份与字段")
93
+    void ut011_townMapMarkers()
94
+    {
95
+        int currentYear = Year.now().getValue();
96
+        HomeScreenTownMapMarkerVo marker = new HomeScreenTownMapMarkerVo();
97
+        marker.setTownDeptId(101L);
98
+        marker.setTownName("巴青乡");
99
+        marker.setYakInventoryCount(100);
100
+        marker.setYakOutboundCount(50);
101
+        when(homeScreenStatsMapper.selectTownMapMarkers(eq(currentYear - 1), eq(currentYear)))
102
+                .thenReturn(Collections.singletonList(marker));
103
+
104
+        HomeScreenTownMapVo vo = service.loadTownMapMarkers();
105
+        assertEquals(currentYear - 1, vo.getHerdStatYear().intValue());
106
+        assertEquals(currentYear, vo.getOutboundStatYear().intValue());
107
+        assertEquals(1, vo.getMarkers().size());
108
+        assertEquals(100, vo.getMarkers().get(0).getYakInventoryCount().intValue());
109
+        assertEquals(50, vo.getMarkers().get(0).getYakOutboundCount().intValue());
110
+    }
111
+
87
     private void stubBreedingEmpty()
112
     private void stubBreedingEmpty()
88
     {
113
     {
89
         when(breedingIndustryStatsMapper.selectActivePastureIds()).thenReturn(Collections.singletonList(1L));
114
         when(breedingIndustryStatsMapper.selectActivePastureIds()).thenReturn(Collections.singletonList(1L));

+ 17 - 1
doc/大屏/首页/大屏首页功能需求.md

@@ -182,12 +182,13 @@
182
 - **养殖标准**(分类 Tab + 分页列表,行内查阅);
182
 - **养殖标准**(分类 Tab + 分页列表,行内查阅);
183
 - **农技课堂**(课程视频、实战培训、培训内容合计);
183
 - **农技课堂**(课程视频、实战培训、培训内容合计);
184
 - **实战培训报名**(按月活动数与报名人数双曲线);
184
 - **实战培训报名**(按月活动数与报名人数双曲线);
185
+- **地图标注**(全县乡镇牦牛存栏/出栏头数,供首页地图展示);
185
 - 无数据、加载中、加载失败等状态展示。
186
 - 无数据、加载中、加载失败等状态展示。
186
 
187
 
187
 ### 3.2 范围外
188
 ### 3.2 范围外
188
 
189
 
189
 - 各业务模块台账的增删改、发布下架、预约受理、补贴登记、标准维护;
190
 - 各业务模块台账的增删改、发布下架、预约受理、补贴登记、标准维护;
190
-- 导出、打印;地图标点、下钻后台菜单(若后续增加外链须单独评审);
191
+- 导出、打印;地图下钻后台菜单(若后续增加外链须单独评审);
191
 - 按牧场、区域、自定义日期段筛选(见 **§8**);
192
 - 按牧场、区域、自定义日期段筛选(见 **§8**);
192
 - 气象、疫病风险专页、共同富裕专页等**其他大屏子页**(各自独立需求);
193
 - 气象、疫病风险专页、共同富裕专页等**其他大屏子页**(各自独立需求);
193
 - 大屏端权限与网关策略(由部署约定,本文档不展开)。
194
 - 大屏端权限与网关策略(由部署约定,本文档不展开)。
@@ -377,6 +378,20 @@ flowchart TB
377
 
378
 
378
 ---
379
 ---
379
 
380
 
381
+### 5.12 区块十一:地图标注
382
+
383
+| 项 | 规则 |
384
+| --- | --- |
385
+| 展示位置 | 首页地图各乡镇标注(以原型为准) |
386
+| 乡镇范围 | `sys_dept` 中 `parent_id=100` 且 `order_num>12` 的有效乡镇 |
387
+| 标注字段 | **所属乡镇** `dept_id`、**乡镇名称**、**牦牛存栏数**(头)、**牦牛出栏数**(头) |
388
+| 存栏口径 | `biz_yak_herd_inventory.yak_total`;统计年 = **当前自然年 − 1** |
389
+| 出栏口径 | `biz_yak_outbound_report.yak_outbound_count`;统计年 = **当前自然年** |
390
+| 无填报 | 对应乡镇该年未填报时展示 **0** |
391
+| 与看板年份 | **不**随页面「统计年份 **Y**」切换;始终按服务器当前自然年取上一年存栏、当年出栏 |
392
+
393
+---
394
+
380
 ## 6. 图表与列表通用规则
395
 ## 6. 图表与列表通用规则
381
 
396
 
382
 | 项 | 规则 |
397
 | 项 | 规则 |
@@ -401,6 +416,7 @@ flowchart TB
401
 | 某标准分类无已发布标准 | 该 Tab 列表空态 |
416
 | 某标准分类无已发布标准 | 该 Tab 列表空态 |
402
 | **Y** 年无视频课程且无实战培训 | 农技课堂空态 |
417
 | **Y** 年无视频课程且无实战培训 | 农技课堂空态 |
403
 | **Y** 年无已发布实战培训 | 实战培训报名趋势空态 |
418
 | **Y** 年无已发布实战培训 | 实战培训报名趋势空态 |
419
+| 某乡镇无存栏/出栏填报 | 地图标注该乡镇对应头数为 **0** |
404
 
420
 
405
 ### 7.2 加载与失败
421
 ### 7.2 加载与失败
406
 
422
 

+ 48 - 2
doc/大屏/首页/大屏首页技术方案.md

@@ -36,6 +36,9 @@
36
 | `biz_breeding_standard` 等 5 张标准表 | 养殖标准分页列表 |
36
 | `biz_breeding_standard` 等 5 张标准表 | 养殖标准分页列表 |
37
 | `biz_tech_resource` | 视频课程(`004008`) |
37
 | `biz_tech_resource` | 视频课程(`004008`) |
38
 | `biz_practical_training` | 实战培训场次与报名趋势 |
38
 | `biz_practical_training` | 实战培训场次与报名趋势 |
39
+| `biz_yak_herd_inventory` | 地图标注 — 乡镇牦牛存栏(**上一年** `yak_total`) |
40
+| `biz_yak_outbound_report` | 地图标注 — 乡镇牦牛出栏(**当年** `yak_outbound_count`) |
41
+| `sys_dept` | 地图标注 — 乡镇列表(`parent_id=100` 且 `order_num>12`) |
39
 
42
 
40
 ---
43
 ---
41
 
44
 
@@ -183,8 +186,9 @@
183
 | --- | --- | --- | --- | --- |
186
 | --- | --- | --- | --- | --- |
184
 | 3.1 | 看板主数据(除标准分页外全部区块) | GET | `/bigScreen/home/dashboard` | `bigScreen:home:query` |
187
 | 3.1 | 看板主数据(除标准分页外全部区块) | GET | `/bigScreen/home/dashboard` | `bigScreen:home:query` |
185
 | 3.2 | 养殖标准分页列表 | GET | `/bigScreen/home/standards` | 同上 |
188
 | 3.2 | 养殖标准分页列表 | GET | `/bigScreen/home/standards` | 同上 |
189
+| 3.3 | 地图标注(乡镇存栏/出栏) | GET | `/bigScreen/home/townMapMarkers` | 同上 |
186
 
190
 
187
-前端:切换 `statYear` → **3.1** + **3.2**(`pageNum=1`);仅翻页 / 切 Tab → **3.2**。
191
+前端:切换 `statYear` → **3.1** + **3.2**(`pageNum=1`);仅翻页 / 切 Tab → **3.2**;进入首页 / 地图刷新 → **3.3**(**不**随 `statYear` 参数变化,按服务器当前自然年计算)
188
 
192
 
189
 ---
193
 ---
190
 
194
 
@@ -380,6 +384,48 @@
380
 
384
 
381
 ---
385
 ---
382
 
386
 
387
+### 3.3 地图标注
388
+
389
+#### Query
390
+
391
+无参数。统计年份由服务器当前自然年 `Y` 推导:
392
+
393
+| 字段 | 年份 |
394
+| --- | --- |
395
+| `herdStatYear` | `Y - 1`(存栏取 `biz_yak_herd_inventory.stat_year`) |
396
+| `outboundStatYear` | `Y`(出栏取 `biz_yak_outbound_report.stat_year`) |
397
+
398
+#### 响应 `data`
399
+
400
+| 字段 | 类型 | 说明 |
401
+| --- | --- | --- |
402
+| `herdStatYear` | int | 存栏数据年份(上一年) |
403
+| `outboundStatYear` | int | 出栏数据年份(当年) |
404
+| `markers` | array | 乡镇标注列表,见下表 |
405
+
406
+`markers[]`:
407
+
408
+| 字段 | 类型 | 说明 |
409
+| --- | --- | --- |
410
+| `townDeptId` | long | 所属乡镇 `dept_id`(`sys_dept`) |
411
+| `townName` | string | 乡镇名称 |
412
+| `yakInventoryCount` | int | 牦牛存栏数(头),`biz_yak_herd_inventory.yak_total`;无填报为 **0** |
413
+| `yakOutboundCount` | int | 牦牛出栏数(头),`biz_yak_outbound_report.yak_outbound_count`;无填报为 **0** |
414
+
415
+#### 乡镇范围
416
+
417
+与牦牛存栏/出栏填报模块一致:`sys_dept` 中 `parent_id = 100`、`order_num > 12`、`del_flag = '0'`、`status = '0'`,按 `order_num`、`dept_id` 升序。
418
+
419
+#### 服务端逻辑(摘要)
420
+
421
+```
422
+1. outboundStatYear = Year.now(); herdStatYear = outboundStatYear - 1
423
+2. markers = HomeScreenStatsMapper.selectTownMapMarkers(herdStatYear, outboundStatYear)
424
+3. return AjaxResult.success(vo)
425
+```
426
+
427
+---
428
+
383
 ## 4. 菜单与权限(示例)
429
 ## 4. 菜单与权限(示例)
384
 
430
 
385
 | 类型 | 名称 | 权限标识 |
431
 | 类型 | 名称 | 权限标识 |
@@ -396,7 +442,7 @@ SQL 示例:`sql/big_screen_home_perm.sql`(挂载「大屏」父菜单下)
396
 - [ ] 复用 `BreedingIndustryStatsMapper` / `BreedingIndustryCalculator`(按 **Y** 与全县牧场)
442
 - [ ] 复用 `BreedingIndustryStatsMapper` / `BreedingIndustryCalculator`(按 **Y** 与全县牧场)
397
 - [ ] 预约、补贴、标准、科技资源、实战培训 Mapper 扩展(**§2.6**)
443
 - [ ] 预约、补贴、标准、科技资源、实战培训 Mapper 扩展(**§2.6**)
398
 - [ ] 单元测试:历史年 `statDate`、12 月补 0、主数据不随 **Y** 变、补贴万元、无培训空态、标准 Tab 路由
444
 - [ ] 单元测试:历史年 `statDate`、12 月补 0、主数据不随 **Y** 变、补贴万元、无培训空态、标准 Tab 路由
399
-- [ ] 接口测试:`/bigScreen/home/dashboard`、`/standards`
445
+- [ ] 接口测试:`/bigScreen/home/dashboard`、`/standards`、`/townMapMarkers`
400
 - [ ] 菜单权限 SQL;依赖各业务表 DDL 已执行
446
 - [ ] 菜单权限 SQL;依赖各业务表 DDL 已执行
401
 
447
 
402
 ---
448
 ---

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

@@ -73,9 +73,10 @@
73
 | DP-SY-UT-020 | 养殖标准 | 纳入条件 | 单元测试 | JUnit5 | SY-10 | `STD-PUB`+`STD-DRAFT`+`STD-OUT` | `filterStandards(2026)` | 仅 `STD-PUB` 通过 |
73
 | DP-SY-UT-020 | 养殖标准 | 纳入条件 | 单元测试 | JUnit5 | SY-10 | `STD-PUB`+`STD-DRAFT`+`STD-OUT` | `filterStandards(2026)` | 仅 `STD-PUB` 通过 |
74
 | DP-SY-UT-021 | 可选年份 | 并集含当前年 | 单元测试 | JUnit5 | §2.1 | Mock 出栏年 `[2025]`,其余空;当前 2026 | `buildAvailableYears` | 含 `2026`;降序 |
74
 | DP-SY-UT-021 | 可选年份 | 并集含当前年 | 单元测试 | JUnit5 | §2.1 | Mock 出栏年 `[2025]`,其余空;当前 2026 | `buildAvailableYears` | 含 `2026`;降序 |
75
 | DP-SY-UT-022 | 约束 | 复用 Calculator | 单元测试 | JUnit5 | 技术§2.6 | 检查 HomeScreenService | 月末存栏调用 `BreedingIndustryCalculator` | 无重复实现月末判定 |
75
 | DP-SY-UT-022 | 约束 | 复用 Calculator | 单元测试 | JUnit5 | 技术§2.6 | 检查 HomeScreenService | 月末存栏调用 `BreedingIndustryCalculator` | 无重复实现月末判定 |
76
-| DP-SY-UT-023 | 约束 | 只读无写 | 单元测试 | JUnit5 | §3.2、SY-14 | 检查 Controller/Service | 无 insert/update/delete | 仅 `dashboard`、`standards` 查询 |
76
+| DP-SY-UT-023 | 约束 | 只读无写 | 单元测试 | JUnit5 | §3.2、SY-14 | 检查 Controller/Service | 无 insert/update/delete | 仅 `dashboard`、`standards`、`townMapMarkers` 查询 |
77
 | DP-SY-UT-024 | 约束 | 无标准详情接口 | 单元测试 | JUnit5 | §5.9 | 检查 Controller 映射 | 无 `GET .../standards/{id}` | 仅列表分页 |
77
 | DP-SY-UT-024 | 约束 | 无标准详情接口 | 单元测试 | JUnit5 | §5.9 | 检查 Controller 映射 | 无 `GET .../standards/{id}` | 仅列表分页 |
78
 | DP-SY-UT-025 | 缓存 | forceRefresh | 单元测试 | JUnit5 | 技术§2.3 | 启用缓存未过期 | `loadDashboard(2026,true)` | 跳过缓存重新聚合 |
78
 | DP-SY-UT-025 | 缓存 | forceRefresh | 单元测试 | JUnit5 | 技术§2.3 | 启用缓存未过期 | `loadDashboard(2026,true)` | 跳过缓存重新聚合 |
79
+| DP-SY-UT-026 | 地图标注 | 年份与字段 | 单元测试 | JUnit5 | §5.12 | Mock Mapper 返回 1 乡镇 | `loadTownMapMarkers()` | `herdStatYear=当前年-1`;`outboundStatYear=当前年`;含 `townDeptId`/`townName`/存栏/出栏 |
79
 
80
 
80
 ---
81
 ---
81
 
82
 
@@ -120,6 +121,7 @@
120
 | DP-SY-API-035 | 权限 | 无大屏权限 | 接口测试 | Postman | 越权 | 无 `bigScreen:home:query` | `GET dashboard`、`GET standards` | 403 或无权限 |
121
 | DP-SY-API-035 | 权限 | 无大屏权限 | 接口测试 | Postman | 越权 | 无 `bigScreen:home:query` | `GET dashboard`、`GET standards` | 403 或无权限 |
121
 | DP-SY-API-036 | 权限 | 有大屏无台账写 | 接口测试 | Postman | 只读 SY-14 | 仅有 query | `GET` 成功;`POST` 牦牛/标准维护接口 | 大屏可读;台账写权限独立 |
122
 | DP-SY-API-036 | 权限 | 有大屏无台账写 | 接口测试 | Postman | 只读 SY-14 | 仅有 query | `GET` 成功;`POST` 牦牛/标准维护接口 | 大屏可读;台账写权限独立 |
122
 | DP-SY-API-037 | 一致性 | 切年双请求 | 接口测试 | Postman | 技术§3 | 已登录 | 模拟前端切年同时调 `dashboard`+`standards` | 两接口 `statYear` 一致;列表为新年 `pageNum=1` |
123
 | DP-SY-API-037 | 一致性 | 切年双请求 | 接口测试 | Postman | 技术§3 | 已登录 | 模拟前端切年同时调 `dashboard`+`standards` | 两接口 `statYear` 一致;列表为新年 `pageNum=1` |
124
+| DP-SY-API-038 | 地图标注 | 乡镇存栏出栏 | 接口测试 | Postman/curl | §5.12 | 乡镇填报表有样本 | `GET .../townMapMarkers` | `code` 成功;含 `herdStatYear`、`outboundStatYear`、`markers[]`;字段与 SQL 一致;无填报为 0 |
123
 
125
 
124
 ---
126
 ---
125
 
127