# 实施文档 - 监管看板 > 模块名称:监管看板 > 关联需求:需求-01-监管看板.md > 创建日期:2026-01-14 --- ## 一、实施概览 ### 1.1 功能清单 | 序号 | 功能模块 | 后端 | 前端 | 优先级 | |------|---------|------|------|--------| | 1 | 核心指标卡片 | ✅ | ✅ | P0 | | 2 | 年龄分布饼图 | ✅ | ✅ | P0 | | 3 | 刑期分布饼图 | ✅ | ✅ | P0 | | 4 | 文化程度饼图 | ✅ | ✅ | P0 | | 5 | 省份分布地图 | ✅ | ✅ | P0 | | 6 | 数据脱敏组件 | ✅ | ✅ | P0 | ### 1.2 预估工时 | 阶段 | 后端 | 前端 | 合计 | |------|------|------|------| | 数据库 | 0.5h | - | 0.5h | | 接口开发 | 4h | - | 4h | | 前端页面 | - | 6h | 6h | | 联调测试 | 2h | 2h | 4h | | **总计** | **6.5h** | **8h** | **14.5h** | --- ## 二、数据库实施 > **说明**:使用现有表进行统计,无需新增表 > - "本月已移交" 使用现有的 `prison_prisoner_area_log` 表(调监记录) > - "本月已释放" 使用现有的 `prison_prisoner_release` 表 ### 2.1 新增索引 ```sql -- 罪犯表:年龄计算需要用到 birthday 字段 ALTER TABLE prison_prisoner ADD INDEX idx_birthday (birthday); -- 释放记录表:按月统计需要 ALTER TABLE prison_prisoner_release ADD INDEX idx_actual_release_date (actual_release_date); -- 监区变动记录表:按月统计需要 ALTER TABLE prison_prisoner_area_log ADD INDEX idx_operate_time_change_type (operate_time, change_type); ``` ### 2.3 菜单权限 SQL ```sql -- 监管看板菜单 INSERT INTO system_menu (name, permission, type, sort, path, icon, component, parent_id) VALUES ('监管看板', '', 2, 1, 'dashboard', 'ep:data-board', 'prison/dashboard/index', (SELECT id FROM system_menu WHERE name = '监狱管理' LIMIT 1)); -- 获取刚插入的菜单ID SET @dashboard_menu_id = LAST_INSERT_ID(); -- 监管看板查询权限 INSERT INTO system_menu (name, permission, type, sort, parent_id) VALUES ('监管看板查询', 'prison:dashboard:query', 3, 1, @dashboard_menu_id); ``` --- ## 三、后端实施 ### 3.1 文件结构 ``` backend/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/ ├── controller/admin/dashboard/ │ ├── PrisonDashboardController.java # 控制器 │ └── vo/ │ ├── DashboardStatisticsVO.java # 看板统计响应 │ ├── DashboardCardVO.java # 卡片数据响应 │ ├── ChartDataVO.java # 图表数据项 │ └── ProvinceChartVO.java # 省份数据项 ├── service/dashboard/ │ ├── PrisonDashboardService.java # 服务接口 │ └── impl/ │ └── PrisonDashboardServiceImpl.java # 服务实现 ├── dal/mysql/dashboard/ │ └── PrisonDashboardMapper.java # Mapper接口 ├── dal/mysql/transfer/ │ └── PrisonTransferMapper.java # 移交记录Mapper(新增) └── util/ └── DataMaskUtils.java # 数据脱敏工具类(新增) ``` ### 3.2 VO 定义 #### 3.2.1 DashboardStatisticsVO.java ```java package cn.iocoder.yudao.module.prison.controller.admin.dashboard.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; import java.util.List; @Data @Schema(description = "管理后台 - 监管看板统计 Response VO") public class DashboardStatisticsVO { // 核心指标卡片 @Schema(description = "在册罪犯总数") private Integer totalPrisoners; @Schema(description = "本月已释放人数") private Integer monthlyReleased; @Schema(description = "本月已移交人数") private Integer monthlyTransferred; @Schema(description = "当前就医人数") private Integer hospitalCount; @Schema(description = "当前禁闭人数") private Integer solitaryCount; // 图表数据 @Schema(description = "年龄分布") private List ageDistribution; @Schema(description = "刑期分布") private List sentenceDistribution; @Schema(description = "文化程度分布") private List educationDistribution; @Schema(description = "省份分布") private List provinceDistribution; @Schema(description = "统计时间") private LocalDateTime statisticsTime; } ``` #### 3.2.2 ChartDataVO.java ```java package cn.iocoder.yudao.module.prison.controller.admin.dashboard.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @Builder @NoArgsConstructor @AllArgsConstructor @Schema(description = "图表数据项") public class ChartDataVO { @Schema(description = "分组名称", example = "18-30岁") private String name; @Schema(description = "数量", example = "100") private Integer value; @Schema(description = "占比", example = "28.5") private Double percentage; } ``` #### 3.2.3 ProvinceChartVO.java ```java package cn.iocoder.yudao.module.prison.controller.admin.dashboard.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @Builder @NoArgsConstructor @AllArgsConstructor @Schema(description = "省份图表数据") public class ProvinceChartVO { @Schema(description = "省份名称", example = "河南省") private String province; @Schema(description = "省份编码", example = "41") private Integer provinceCode; @Schema(description = "人数", example = "156") private Integer count; } ``` ### 3.3 Mapper 接口 #### 3.3.1 PrisonDashboardMapper.java ```java package cn.iocoder.yudao.module.prison.dal.mysql.dashboard; import cn.iocoder.yudao.module.prison.controller.admin.dashboard.vo.ChartDataVO; import cn.iocoder.yudao.module.prison.controller.admin.dashboard.vo.DashboardCardVO; import cn.iocoder.yudao.module.prison.controller.admin.dashboard.vo.ProvinceChartVO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; @Mapper public interface PrisonDashboardMapper { /** * 查询核心指标卡片数据 */ @Select(""" SELECT COALESCE(SUM(CASE WHEN p.status = 1 THEN 1 ELSE 0 END), 0) AS total_prisoners, COALESCE(SUM(CASE WHEN pa.type = 5 THEN 1 ELSE 0 END), 0) AS hospital_count, COALESCE(SUM(CASE WHEN pa.type = 6 THEN 1 ELSE 0 END), 0) AS solitary_count FROM prison_prisoner p LEFT JOIN prison_area pa ON p.prison_area_id = pa.id WHERE p.deleted = 0 """) DashboardCardVO selectDashboardCards(); /** * 查询本月释放人数 */ @Select(""" SELECT COUNT(*) FROM prison_prisoner_release WHERE deleted = 0 AND DATE_FORMAT(actual_release_date, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') AND status = 1 """) Integer selectMonthlyReleased(); /** * 查询本月调监次数(移交) * change_type: 1-调入 2-调出 * 使用现有的 prison_prisoner_area_log 表 */ @Select(""" SELECT COUNT(*) FROM prison_prisoner_area_log WHERE deleted = 0 AND change_type IN (1, 2) AND DATE_FORMAT(operate_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') """) Integer selectMonthlyTransferred(); /** * 查询年龄分布 */ @Select(""" SELECT CASE WHEN TIMESTAMPDIFF(YEAR, p.birthday, CURDATE()) < 18 THEN '未成年(≤17)' WHEN TIMESTAMPDIFF(YEAR, p.birthday, CURDATE()) BETWEEN 18 AND 30 THEN '青年(18-30)' WHEN TIMESTAMPDIFF(YEAR, p.birthday, CURDATE()) BETWEEN 31 AND 50 THEN '中年(31-50)' WHEN TIMESTAMPDIFF(YEAR, p.birthday, CURDATE()) BETWEEN 51 AND 60 THEN '中老年(51-60)' ELSE '老龄(60+)' END AS name, COUNT(*) AS value FROM prison_prisoner p WHERE p.status = 1 AND p.deleted = 0 GROUP BY name """) List selectAgeDistribution(); /** * 查询刑期分布 */ @Select(""" SELECT CASE WHEN p.death_sentence_reprieve = 1 THEN '死缓' WHEN p.life_imprisonment = 1 THEN '无期' WHEN (COALESCE(p.sentence_years, 0) * 12 + COALESCE(p.sentence_months, 0)) <= 36 THEN '短刑(≤3年)' WHEN (COALESCE(p.sentence_years, 0) * 12 + COALESCE(p.sentence_months, 0)) BETWEEN 37 AND 120 THEN '中刑(3-10年)' ELSE '长刑(10年以上)' END AS name, COUNT(*) AS value FROM prison_prisoner p WHERE p.status = 1 AND p.deleted = 0 GROUP BY name """) List selectSentenceDistribution(); /** * 查询文化程度分布 */ @Select(""" SELECT CASE p.education WHEN 1 THEN '文盲' WHEN 2 THEN '小学' WHEN 3 THEN '初中' WHEN 4 THEN '高中' WHEN 5 THEN '中专' WHEN 6 THEN '大专' WHEN 7 THEN '本科' WHEN 8 THEN '研究生及以上' ELSE '未知' END AS name, COUNT(*) AS value FROM prison_prisoner p WHERE p.status = 1 AND p.deleted = 0 GROUP BY p.education """) List selectEducationDistribution(); /** * 查询省份分布 */ @Select(""" SELECT LEFT(p.native_place, 2) AS province_code, COUNT(*) AS count FROM prison_prisoner p WHERE p.status = 1 AND p.deleted = 0 AND p.native_place IS NOT NULL GROUP BY LEFT(p.native_place, 2) """) List selectProvinceDistributionRaw(); } ``` ### 3.4 Service 实现 ```java package cn.iocoder.yudao.module.prison.service.dashboard.impl; import cn.iocoder.yudao.module.prison.controller.admin.dashboard.vo.*; import cn.iocoder.yudao.module.prison.dal.mysql.dashboard.PrisonDashboardMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.List; @Slf4j @Service @RequiredArgsConstructor public class PrisonDashboardServiceImpl implements PrisonDashboardService { private final PrisonDashboardMapper dashboardMapper; private static final String DASHBOARD_CACHE_KEY = "prison:dashboard:stats:"; @Override @Cacheable(value = "dashboard", key = "#root.target.getCacheKey()", unless = "#result == null") public DashboardStatisticsVO getDashboardStatistics() { DashboardStatisticsVO vo = new DashboardStatisticsVO(); vo.setStatisticsTime(LocalDateTime.now()); // 核心指标卡片 DashboardCardVO cards = dashboardMapper.selectDashboardCards(); vo.setTotalPrisoners(cards.getTotalPrisoners()); vo.setHospitalCount(cards.getHospitalCount()); vo.setSolitaryCount(cards.getSolitaryCount()); vo.setMonthlyReleased(dashboardMapper.selectMonthlyReleased()); vo.setMonthlyTransferred(dashboardMapper.selectMonthlyTransferred()); // 图表数据 vo.setAgeDistribution(dashboardMapper.selectAgeDistribution()); vo.setSentenceDistribution(dashboardMapper.selectSentenceDistribution()); vo.setEducationDistribution(dashboardMapper.selectEducationDistribution()); vo.setProvinceDistribution(convertProvinceData(dashboardMapper.selectProvinceDistributionRaw())); return vo; } private List convertProvinceData(List rawData) { // 省份编码转省份名称的映射逻辑 return rawData.stream() .map(item -> ProvinceChartVO.builder() .province(ProvinceCodeMapper.getName(item.getProvinceCode())) .provinceCode(item.getProvinceCode()) .count(item.getCount()) .build()) .toList(); } public String getCacheKey() { return DASHBOARD_CACHE_KEY + "all"; } } ``` ### 3.5 Controller ```java package cn.iocoder.yudao.module.prison.controller.admin.dashboard; import cn.iocoder.yudao.module.prison.controller.admin.dashboard.vo.DashboardStatisticsVO; import cn.iocoder.yudao.module.prison.service.dashboard.PrisonDashboardService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/prison/dashboard") @Tag(name = "管理后台 - 监管看板") @RequiredArgsConstructor public class PrisonDashboardController { private final PrisonDashboardService dashboardService; @GetMapping("/statistics") @Operation(summary = "获取看板统计数据") @PreAuthorize("@ss.hasPermission('prison:dashboard:query')") public DashboardStatisticsVO getStatistics() { return dashboardService.getDashboardStatistics(); } } ``` ### 3.6 数据脱敏工具 ```java package cn.iocoder.yudao.module.prison.util; import cn.hutool.core.util.StrUtil; public class DataMaskUtils { /** * 身份证号脱敏:110101199001011234 -> 110***********1234 */ public static String maskIdCard(String idCard) { if (StrUtil.isBlank(idCard) || idCard.length() < 7) { return idCard; } return idCard.substring(0, 3) + "*".repeat(idCard.length() - 7) + idCard.substring(idCard.length() - 4); } /** * 手机号脱敏:13812345678 -> 138****5678 */ public static String maskPhone(String phone) { if (StrUtil.isBlank(phone) || phone.length() < 7) { return phone; } return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4); } /** * 姓名脱敏:张三丰 -> 张** */ public static String maskName(String name) { if (StrUtil.isBlank(name) || name.length() == 1) { return name; } return name.charAt(0) + "*".repeat(name.length() - 1); } /** * 地址脱敏:北京市海淀区*** -> 北京市海淀区*** */ public static String maskAddress(String address) { if (StrUtil.isBlank(address)) { return address; } int lastSeparator = Math.max(address.lastIndexOf('区'), address.lastIndexOf('县')); if (lastSeparator > 0 && lastSeparator < address.length() - 1) { return address.substring(0, lastSeparator + 1) + "***"; } return address; } } ``` --- ## 四、前端实施 ### 4.1 文件结构 ``` frontend/src/ ├── views/prison/dashboard/ │ ├── index.vue # 主页面组件 │ └── components/ │ ├── StatCard.vue # 指标卡片组件 │ ├── AgePieChart.vue # 年龄分布饼图 │ ├── SentencePieChart.vue # 刑期分布饼图 │ ├── EducationPieChart.vue # 文化程度饼图 │ └── ChinaMap.vue # 中国地图热力图 ├── api/prison/dashboard/ │ └── index.ts # API 接口定义 ├── components/DataMaskingCell/ │ ├── index.vue # 脱敏单元格组件 │ └── index.ts # 组件类型定义 └── plugins/echarts.ts # ECharts 地图注册 ``` ### 4.2 API 定义 ```typescript // frontend/src/api/prison/dashboard/index.ts /** 看板统计响应 */ export interface DashboardStatisticsVO { totalPrisoners: number; monthlyReleased: number; monthlyTransferred: number; hospitalCount: number; solitaryCount: number; ageDistribution: ChartDataVO[]; sentenceDistribution: ChartDataVO[]; educationDistribution: ChartDataVO[]; provinceDistribution: ProvinceChartVO[]; statisticsTime: string; } /** 图表数据项 */ export interface ChartDataVO { name: string; value: number; percentage?: number; } /** 省份数据 */ export interface ProvinceChartVO { province: string; provinceCode: number; count: number; } export const DashboardApi = { getStatistics: async (): Promise => { return await request.get({ url: '/prison/dashboard/statistics' }) } } ``` ### 4.3 指标卡片组件 ```vue ``` ### 4.4 主页面 ```vue ``` ### 4.5 地图资源配置 ```typescript // frontend/src/plugins/echarts.ts import * as echarts from 'echarts' import chinaJson from 'echarts/map/json/china.json' // 注册中国地图 echarts.registerMap('china', chinaJson) export { echarts } ``` > **注意**:需要下载中国地图 JSON 文件到 `frontend/public/china.json` > 下载地址:https://echarts.apache.org/examples/zh/data/asset/geo/china.json ### 4.6 路由配置 ```typescript // frontend/src/router/routes/modules/prison.ts { path: 'dashboard', name: 'PrisonDashboard', component: () => import('@/views/prison/dashboard/index.vue'), meta: { title: '监管看板', icon: 'ep:data-board', permission: 'prison:dashboard:query' } } ``` --- ## 五、任务分解 ### 5.1 后端任务 > **说明**:使用现有表,无需新建移交记录表 | 序号 | 任务 | 负责人 | 预计工时 | 状态 | |------|------|--------|---------|------| | 1 | 新增数据库索引 SQL(3个索引) | | 0.5h | 待开发 | | 2 | 菜单权限 SQL | | 0.5h | 待开发 | | 3 | VO 类开发(4个文件) | | 1h | 待开发 | | 4 | PrisonDashboardMapper 接口 | | 1h | 待开发 | | 5 | PrisonDashboardService 接口和实现 | | 1h | 待开发 | | 6 | PrisonDashboardController | | 0.5h | 待开发 | | 7 | DataMaskUtils 工具类 | | 0.5h | 待开发 | | 8 | 接口测试 | | 1h | 待开发 | ### 5.2 前端任务 | 序号 | 任务 | 负责人 | 预计工时 | 状态 | |------|------|--------|---------|------| | 1 | 下载并配置中国地图 JSON | | 0.5h | 待开发 | | 2 | API 接口定义 | | 0.5h | 待开发 | | 3 | StatCard 组件 | | 1h | 待开发 | | 4 | ChinaMap 组件 | | 2h | 待开发 | | 5 | 主页面开发 | | 2h | 待开发 | | 6 | 路由配置 | | 0.5h | 待开发 | | 7 | 联调测试 | | 2h | 待开发 | --- ## 六、验收标准 ### 6.1 功能验收 - [ ] 核心指标卡片数据与数据库一致 - [ ] 年龄分布饼图按要求分段显示 - [ ] 刑期分布饼图按要求分段显示 - [ ] 文化程度饼图按要求分类显示 - [ ] 中国地图正确显示各省热力分布 - [ ] 点击地图省份显示详细信息 - [ ] 响应式布局在各种屏幕尺寸下正常显示 - [ ] 数据加载时显示骨架屏 ### 6.2 性能验收 - [ ] 页面加载时间 < 3秒 - [ ] 图表渲染时间 < 1秒 - [ ] 支持多用户并发访问 ### 6.3 安全验收 - [ ] 敏感信息已配置脱敏规则 - [ ] 数据权限控制生效(按监区) --- ## 七、联调步骤 ### 7.1 后端启动 ```bash cd backend/yudao-module-prison mvn clean package -DskipTests cd ../yudao-server mvn spring-boot:run ``` ### 7.2 前端启动 ```bash cd frontend pnpm dev ``` ### 7.3 测试数据 ```sql -- 确保有以下测试数据 -- 1. 罪犯表有在押状态数据 -- 2. 释放记录表有本月数据 -- 3. 监区表有医院(5)和禁闭室(6)类型数据 ``` --- ## 八、风险与应对 | 风险 | 等级 | 应对措施 | |------|------|----------| | 移交记录表缺失 | 高 | 优先完成数据库设计 | | 地图 JSON 资源 | 中 | 提前下载或使用 CDN | | 数据权限复杂性 | 中 | 先实现基础版本,再迭代 | | 性能问题 | 低 | 添加 Redis 缓存 | --- ## 九、依赖项 ### 9.1 前置依赖 - [ ] 罪犯信息管理模块(数据来源) - [ ] 释放记录模块(数据来源) - [ ] 监区管理模块(数据来源) ### 9.2 外部依赖 - [ ] ECharts 地图 JSON 文件 --- **文档版本**:v1.0 **创建日期**:2026-01-14 **评审人**:前端架构师、后端架构师