xlcp/requirements/实施文档-01-监管看板.md
2026-01-14 22:18:15 +08:00

886 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 实施文档 - 监管看板
> 模块名称:监管看板
> 关联需求:需求-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<ChartDataVO> ageDistribution;
@Schema(description = "刑期分布")
private List<ChartDataVO> sentenceDistribution;
@Schema(description = "文化程度分布")
private List<ChartDataVO> educationDistribution;
@Schema(description = "省份分布")
private List<ProvinceChartVO> 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<ChartDataVO> 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<ChartDataVO> 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<ChartDataVO> 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<ProvinceChartVO> 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<ProvinceChartVO> convertProvinceData(List<ProvinceChartVO> 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<DashboardStatisticsVO> => {
return await request.get({ url: '/prison/dashboard/statistics' })
}
}
```
### 4.3 指标卡片组件
```vue
<!-- frontend/src/views/prison/dashboard/components/StatCard.vue -->
<template>
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-info">
<div class="stat-value" v-loading="loading">
<el-skeleton v-if="loading" animated :rows="0" />
<template v-else>{{ formatNumber(value) }}</template>
</div>
<div class="stat-title">{{ title }}</div>
</div>
<div class="stat-icon" :class="iconClass">
<Icon :icon="icon" :size="36" />
</div>
</div>
</el-card>
</template>
<script setup lang="ts">
interface Props {
title: string
value: number
icon: string
iconClass?: string
loading?: boolean
}
const props = defineProps<Props>()
const formatNumber = (num: number) => {
return new Intl.NumberFormat('zh-CN').format(num)
}
</script>
<style lang="scss" scoped>
.stat-card {
height: 100%;
.stat-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.stat-value {
font-size: 28px;
font-weight: bold;
color: #303133;
}
.stat-title {
font-size: 14px;
color: #909399;
margin-top: 8px;
}
.stat-icon {
width: 56px;
height: 56px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
&.primary { background: #ecf5ff; color: #409eff; }
&.success { background: #f0f9eb; color: #67c23a; }
&.warning { background: #fdf6ec; color: #e6a23c; }
&.danger { background: #fef0f0; color: #f56c6c; }
}
}
</style>
```
### 4.4 主页面
```vue
<!-- frontend/src/views/prison/dashboard/index.vue -->
<template>
<div class="dashboard-container">
<el-row :gutter="16" class="mb-16px">
<el-col :xs="24" :sm="12" :md="8" :lg="24/5" v-for="item in statCards" :key="item.title">
<StatCard v-bind="item" :loading="loading" :value="item.value" />
</el-col>
</el-row>
<el-row :gutter="16" class="mb-16px">
<el-col :xs="24" :sm="24" :md="8">
<el-card>
<template #header>年龄分布</template>
<BasePieChart :options="ageChartOptions" :loading="loading" />
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="8">
<el-card>
<template #header>刑期分布</template>
<BasePieChart :options="sentenceChartOptions" :loading="loading" />
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="8">
<el-card>
<template #header>文化程度</template>
<BasePieChart :options="educationChartOptions" :loading="loading" />
</el-card>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24">
<el-card>
<template #header>籍贯分布</template>
<ChinaMap :data="provinceData" :loading="loading" />
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { StatCard } from './components'
import { DashboardApi, type DashboardStatisticsVO } from '@/api/prison/dashboard'
import BasePieChart from '@/components/Echart/src/BasePieChart.vue'
import ChinaMap from './components/ChinaMap.vue'
const loading = ref(true)
const data = ref<DashboardStatisticsVO | null>(null)
const statCards = computed(() => [
{ title: '在册罪犯', value: data.value?.totalPrisoners || 0, icon: 'ep:user', iconClass: 'primary' },
{ title: '本月释放', value: data.value?.monthlyReleased || 0, icon: 'ep:select', iconClass: 'success' },
{ title: '本月移交', value: data.value?.monthlyTransferred || 0, icon: 'ep:rank-list', iconClass: 'warning' },
{ title: '当前就医', value: data.value?.hospitalCount || 0, icon: 'ep:first-aid-kit', iconClass: 'danger' },
{ title: '当前禁闭', value: data.value?.solitaryCount || 0, icon: 'ep:lock', iconClass: 'danger' }
])
const ageChartOptions = computed(() => ({
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { orient: 'vertical', right: 10, top: 'center' },
series: [{
type: 'pie',
radius: ['40%', '70%'],
data: data.value?.ageDistribution || []
}]
}))
const sentenceChartOptions = computed(() => ({
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { orient: 'vertical', right: 10, top: 'center' },
series: [{
type: 'pie',
radius: ['40%', '70%'],
data: data.value?.sentenceDistribution || []
}]
}))
const educationChartOptions = computed(() => ({
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { orient: 'vertical', right: 10, top: 'center' },
series: [{
type: 'pie',
radius: ['40%', '70%'],
data: data.value?.educationDistribution || []
}]
}))
const provinceData = computed(() => data.value?.provinceDistribution || [])
const fetchData = async () => {
loading.value = true
try {
data.value = await DashboardApi.getStatistics()
} finally {
loading.value = false
}
}
onMounted(fetchData)
</script>
<style lang="scss" scoped>
.dashboard-container {
padding: 16px;
}
.mb-16px { margin-bottom: 16px; }
</style>
```
### 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 | 新增数据库索引 SQL3个索引 | | 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
**评审人**前端架构师后端架构师