25 KiB
25 KiB
实施文档 - 监管看板
模块名称:监管看板 关联需求:需求-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 新增索引
-- 罪犯表:年龄计算需要用到 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
-- 监管看板菜单
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
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
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
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
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 实现
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
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 数据脱敏工具
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 定义
// 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 指标卡片组件
<!-- 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 主页面
<!-- 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 地图资源配置
// 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 路由配置
// 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 后端启动
cd backend/yudao-module-prison
mvn clean package -DskipTests
cd ../yudao-server
mvn spring-boot:run
7.2 前端启动
cd frontend
pnpm dev
7.3 测试数据
-- 确保有以下测试数据
-- 1. 罪犯表有在押状态数据
-- 2. 释放记录表有本月数据
-- 3. 监区表有医院(5)和禁闭室(6)类型数据
八、风险与应对
| 风险 | 等级 | 应对措施 |
|---|---|---|
| 移交记录表缺失 | 高 | 优先完成数据库设计 |
| 地图 JSON 资源 | 中 | 提前下载或使用 CDN |
| 数据权限复杂性 | 中 | 先实现基础版本,再迭代 |
| 性能问题 | 低 | 添加 Redis 缓存 |
九、依赖项
9.1 前置依赖
- 罪犯信息管理模块(数据来源)
- 释放记录模块(数据来源)
- 监区管理模块(数据来源)
9.2 外部依赖
- ECharts 地图 JSON 文件
文档版本:v1.0 创建日期:2026-01-14 评审人:前端架构师、后端架构师