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

25 KiB
Raw Permalink Blame History

实施文档 - 监管看板

模块名称:监管看板 关联需求:需求-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 新增数据库索引 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 后端启动

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 评审人:前端架构师、后端架构师