1. 项目先导:这套"罪犯危险性评估系统"到底在解决什么问题
第一次看到"基于SpringBoot+Vue的狱内罪犯危险性评估系统管理系统"这个标题,很多人第一反应是:这不就是一套CRUD后台管理系统?换了个业务场景而已。但真正接触这个需求之后你会发现,它的核心难点完全不在增删改查上,而在于"危险性评估"这四个字背后的业务逻辑怎么落地。
监狱管理部门对罪犯的管理,过去大量依赖一线狱警的经验判断,但经验判断有三个痛点:不好量化、不好留痕、不好跨部门共享。同一个罪犯,A民警觉得危险程度高,B民警可能觉得还行,没有统一的标准来衡量。所以就需要一套信息化系统,把罪犯的基本信息、在押状态、奖惩记录、心理评估结果等分散的数据汇总起来,通过一套统一的评分模型,计算每个罪犯当前的危险性等级,给监区调整管理措施、开展个别化矫正提供数据参考。
用SpringBoot+Vue这套技术栈来做,恰好契合这类业务系统的典型开发模式:后端用SpringBoot做底座,MyBatis操作MySQL,前端用Vue做单页应用。整条链路成熟、网上资料多、用人市场认可度高,非常适合作为毕业设计、项目实训或者中小型管理系统的开发蓝本。
这篇文章我会从需求拆解、数据库设计、后端打分逻辑、前端交互实现、常见部署坑点五个维度来拆解这套系统。文章里给的代码都是可以实际跑起来的核心片段,结构上你也可以直接参考,希望能帮你少走那些我已经替你们踩过的弯路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 需求拆解与整体架构设计:别急着写代码,先把角色和流程理清楚
2.1 业务角色与核心流程
动手建项目之前,首先要搞清楚有哪几类人会使用这套系统。我大致归纳为三类角色:
- 系统管理员:负责用户管理、权限分配、基础数据维护,比如维护评估维度配置、管理账号。
- 狱政管理民警:录入罪犯档案信息、发起危险性评估、填写各维度评分、查看评估报告。
- 监区管理人员:查看评估结果,结合风险等级调整监区管理措施和管控力度。
这三类角色的功能边界不同,但共用同一套底层的罪犯档案和评估数据。所以在整体设计上,我按"人员管理、档案管理、评估管理、统计分析"四条主线来划分功能模块。这样一拆分,后续数据库表怎么建、接口怎么分、前端页面怎么组织,都会清晰很多。
核心业务链路是这样的:民警登录系统 -> 维护罪犯档案 -> 选择某个罪犯发起评估 -> 按维度填写评分 -> 系统按权重计算总分并判定等级 -> 生成评估报告并回写罪犯当前风险等级 -> 监区管理人员查看报告并调整管理措施。这条链路看起来简单,但你编码前一定要在纸上走一遍,任何一个环节遗漏了,后面联调的时候都会付出代价。
2.2 技术选型:为什么是SpringBoot + Vue + MyBatis,而不是别的
很多初学者会问,现在都2025年了,新项目为什么不直接上Spring Cloud微服务?前端为什么不选React?我的回答始终是:项目的规模和维护成本决定技术选型,而不是技术热度。
SpringBoot的价值在于"约定大于配置",它对Spring生态做了大量自动装配,你只需要引入对应的starter依赖,项目就能跑起来。对监狱内部管理系统来说,用户数通常不多、并发量不大,核心诉求是稳定、可维护、能完整覆盖业务流程,SpringBoot单机部署加MySQL完全够用。真用微服务架构,光服务拆分、注册中心、配置中心就够你折腾好几天,收益却很有限。
前端选Vue而不是React,有两个原因。第一,Vue的上手曲线更平缓,对熟悉HTML、CSS、JavaScript的后端开发者更友好;第二,Vue生态里的Element UI组件库做后台管理界面效率极高,表格、表单、弹窗、日期选择器这些组件开箱即用,几乎不用自己写样式。对包含大量数据录入和表格展示的评估系统来说,Vue + 组件库能节省非常多开发时间。
MyBatis作为数据访问层,核心优势是SQL可控、表结构关系透明、排查问题直观。尤其像评估记录这种涉及多表关联、条件统计、动态查询的场景,手写SQL比拼JPA的Criteria或JPQL要舒服得多。而且很多Java岗位面试都在问MyBatis的缓存机制、动态SQL、Mapper代理原理,用这个项目练手,面试时也能聊得很实在。
提示:这个组合不是"最先进"的,但它是当前Java后端生态里最稳妥、资料最全、招人市场认可度最高的组合之一,作为学习项目完全没有问题。
2.3 后端分层与前端工程结构设计
后端我采用经典的四层结构:
- Controller层:接收HTTP请求,做参数校验,返回统一响应结果。
- Service层:承载业务逻辑,核心的评估打分计算就在这里。
- Mapper层:定义数据访问接口,配合XML文件写SQL。
- Entity/DTO层:实体类对应数据库表,DTO负责接口传输。
前端用Vue单页应用,配合Vue Router做页面路由。系统规模不大的时候,不必强行引入Vuex或Pinia做全局状态管理,用一个简单的全局对象或者localStorage存用户信息就够了。只有当你发现多个非父子组件要共享同一份频繁变化的数据时,再引入状态管理不迟。
3. 数据库设计:把抽象的"危险性"变成可计算的分数
3.1 评估维度怎么设定,依据是什么
"罪犯危险性"不是数据库里的一个直接字段,必须通过多个维度去量化。我在查阅实务资料和咨询相关业务人员后,把评估维度提炼为以下六项:
| 维度名称 | 维度编码 | 权重 | 评分范围 | 说明 |
|---|---|---|---|---|
| 犯罪严重程度 | CRIME_SEVERITY | 0.25 | 0-10 | 罪名严重性、是否暴力犯罪、是否有累犯情节 |
| 刑期剩余时长 | REMAIN_SENTENCE | 0.15 | 0-10 | 剩余刑期越长,潜在风险越高 |
| 在押违规记录 | VIOLATION_RECORD | 0.20 | 0-10 | 是否有违规违纪记录、违规次数和性质 |
| 教育改造态度 | REFORM_ATTITUDE | 0.15 | 0-10 | 参加教育学习、技能培训的表现 |
| 心理测评结果 | PSYCHOLOGY_SCORE | 0.15 | 0-10 | 心理测评得分,关注攻击性和情绪稳定性 |
| 家庭社会支持 | SOCIAL_SUPPORT | 0.10 | 0-10 | 家庭关系稳定度、出狱后社会支持情况 |
为什么权重这样分配?犯罪严重程度是评估危险性的基础,权重最高是合理的;在押违规记录反映的是服刑期间的实际表现,如果一个人入狱后仍然频繁违规,说明行为矫正效果差,当前危险性高,所以给了0.2的权重。家庭社会支持虽然重要,但属于间接影响因素,所以权重最低。
需要强调的是,这套维度体系不是凭空设计的,参考了司法行政系统对罪犯改造质量评估和危险性评估的常用思路。如果你在实际项目中拿到了当地的评估表或实施细则,应该以实际文件为准,把维度、权重、评分标准替换掉。系统设计上我把维度配置做成了独立表,就是为了方便后续调整。
3.2 核心表结构设计与SQL实现
数据库我命名为risk_management,核心表一共五张:
- sys_user:用户表
- criminal_info:罪犯档案表
- assessment_record:评估主表
- assessment_detail:评估明细表
- dimension_config:评估维度配置表
先看罪犯档案表:
sql复制CREATE TABLE criminal_info (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
name VARCHAR(50) NOT NULL COMMENT '姓名',
gender TINYINT NOT NULL COMMENT '性别 0-男 1-女',
age INT COMMENT '年龄',
id_card VARCHAR(18) UNIQUE COMMENT '身份证号',
crime_type VARCHAR(100) COMMENT '主要罪名',
sentence_years INT COMMENT '刑期(年)',
sentence_start DATE COMMENT '刑期开始日期',
sentence_end DATE COMMENT '刑期结束日期',
confined_regions VARCHAR(255) COMMENT '所在监区',
risk_level VARCHAR(10) DEFAULT 'LOW' COMMENT '当前风险等级 HIGH/MEDIUM/LOW',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='罪犯档案表';
评估主表:
sql复制CREATE TABLE assessment_record (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
criminal_id BIGINT NOT NULL COMMENT '罪犯ID',
assessor VARCHAR(50) COMMENT '评估人',
total_score DECIMAL(5,2) COMMENT '总得分',
risk_level VARCHAR(10) COMMENT '风险等级 HIGH/MEDIUM/LOW',
conclusion TEXT COMMENT '评估结论',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY idx_criminal_id (criminal_id),
KEY idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评估记录表';
评估明细表:
sql复制CREATE TABLE assessment_detail (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
record_id BIGINT NOT NULL COMMENT '评估记录ID',
dimension_code VARCHAR(30) COMMENT '维度编码',
dimension_name VARCHAR(50) COMMENT '维度名称',
raw_score DECIMAL(5,2) COMMENT '原始评分 0-10',
weight DECIMAL(4,2) COMMENT '权重',
weighted_score DECIMAL(6,2) COMMENT '加权得分',
remark VARCHAR(255) COMMENT '评估说明'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评估明细表';
评估维度配置表:
sql复制CREATE TABLE dimension_config (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
dimension_code VARCHAR(30) NOT NULL UNIQUE,
dimension_name VARCHAR(50) NOT NULL,
weight DECIMAL(4,2) NOT NULL COMMENT '权重系数',
remark VARCHAR(255) COMMENT '配置说明',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评估维度配置表';
这里最关键的设计是评估主表和评估明细表分离。criminal_info保存罪犯静态信息,一个罪犯对应多条评估记录;每条评估记录关联多条明细记录,明细里存的是该罪犯在本次评估中每个维度得到的原始分、权重和加权分。这样设计的好处是:历史评估全部留痕,随时可以追溯"这个人某次为什么被评定为高风险",而不是只存一个总分数了事。评估结论到底怎么来的,打开明细就能看到,这是这套系统作为管理工具最重要的信任根基。
3.3 风险等级判定规则:不是拍脑袋,是数理逻辑
风险等级判定我采用的是加权求和再分档的模型。原始分每个维度0到10分,六个维度加权后总分为0到60分,判定规则如下:
- 总分 >= 35,判定为高风险(HIGH)
- 总分在 20 ~ 35 之间(含20),判定为中风险(MEDIUM)
- 总分 < 20,判定为低风险(LOW)
为什么不是百分制?因为每个维度满分10分,6个维度加权之后总分天然分布在0-60之间,这个区间语义上已经足够区分风险层级了。如果业务方要求百分制,只需要把维度满分改成100分再乘以权重即可,核心逻辑不用变。
这套规则的关键在于:阈值不是必须固定的。在实际使用中,要根据评估数据的分布情况回看阈值是否合理,比如如果大部分罪犯都集中在高风险区间,说明阈值偏低或者评分标准过严,需要调整。所以阈值我建议收敛在Service层的一个独立方法里,方便后续调整。
4. 后端开发实战:从环境配置到核心打分逻辑
4.1 项目初始化与版本选型
我使用的是SpringBoot 2.7.x + JDK 1.8。为什么不用JDK 17或SpringBoot 3.x?因为在实际环境中,很多单位的服务器还运行在JDK 8上,老项目的技术栈迁移成本高,而且SpringBoot 2.7是2.x系列的最后一个稳定版本,资料非常丰富,遇到问题基本都能搜到答案。作为学习项目,用稳妥的版本组合比追新更重要。面试时被问到"SpringBoot版本太高导致的问题",你也可以说出一二三来。
通过Spring Initializr初始化项目,选择Web、MySQL Driver依赖,然后手动在pom.xml补充MyBatis依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
注意mybatis-spring-boot-starter的版本号需要单独指定,因为SpringBoot父工程不管理它的版本。这个坑我印象太深了,不写版本号的话,有时候会拉到一个老版本,和当前SpringBoot的自动配置机制冲突,启动直接报错。
核心配置文件application.yml:
yaml复制server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/risk_management?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: yourpassword
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.riskmanagement.entity
configuration:
map-underscore-to-camel-case: true
这里最关键的是map-underscore-to-camel-case: true,开启后数据库字段sentence_start可以自动映射到实体属性sentenceStart,省掉大量手写resultMap的重复劳动。allowPublicKeyRetrieval=true是MySQL 8.0版本的连接要求,不加会被安全策略拦截,报"Public Key Retrieval is not allowed"。
4.2 评估计算引擎的实现:核心业务逻辑
整个项目最核心的类,我命名为RiskAssessmentService,它负责完成一次完整的评估计算和落库操作。逻辑本身不复杂,但有几个设计上的讲究:
java复制@Service
public class RiskAssessmentService {
@Autowired
private AssessmentRecordMapper recordMapper;
@Autowired
private AssessmentDetailMapper detailMapper;
@Autowired
private CriminalInfoMapper criminalInfoMapper;
@Autowired
private DimensionConfigMapper dimensionConfigMapper;
@Transactional(rollbackFor = Exception.class)
public AssessmentResult evaluate(Long criminalId, Map<String, Double> dimensionScores, String assessor) {
// 1. 获取启用状态的维度配置
List<DimensionConfig> configs = dimensionConfigMapper.selectEnabled();
// 2. 校验维度数据完整性
for (DimensionConfig config : configs) {
if (!dimensionScores.containsKey(config.getDimensionCode())) {
throw new BusinessException("缺少维度评分: " + config.getDimensionName());
}
}
// 3. 计算加权总分
double totalScore = 0;
List<AssessmentDetail> details = new ArrayList<>();
for (DimensionConfig config : configs) {
double rawScore = dimensionScores.get(config.getDimensionCode());
if (rawScore < 0 || rawScore > 10) {
throw new BusinessException("维度评分必须介于0-10之间: " + config.getDimensionName());
}
double weighted = rawScore * config.getWeight();
totalScore += weighted;
AssessmentDetail detail = new AssessmentDetail();
detail.setDimensionCode(config.getDimensionCode());
detail.setDimensionName(config.getDimensionName());
detail.setRawScore(rawScore);
detail.setWeight(config.getWeight());
detail.setWeightedScore(roundToTwoDecimal(weighted));
details.add(detail);
}
totalScore = roundToTwoDecimal(totalScore);
// 4. 判定风险等级
String riskLevel = judgeLevel(totalScore);
// 5. 写入评估主表
AssessmentRecord record = new AssessmentRecord();
record.setCriminalId(criminalId);
record.setAssessor(assessor);
record.setTotalScore(totalScore);
record.setRiskLevel(riskLevel);
recordMapper.insert(record);
// 6. 批量写入评估明细
for (AssessmentDetail detail : details) {
detail.setRecordId(record.getId());
}
detailMapper.batchInsert(details);
// 7. 更新罪犯当前风险等级
criminalInfoMapper.updateRiskLevel(criminalId, riskLevel);
return new AssessmentResult(record.getId(), totalScore, riskLevel);
}
private String judgeLevel(double totalScore) {
if (totalScore >= 35) {
return "HIGH";
} else if (totalScore >= 20) {
return "MEDIUM";
}
return "LOW";
}
private double roundToTwoDecimal(double value) {
BigDecimal bd = new BigDecimal(Double.toString(value));
return bd.setScale(2, RoundingMode.[HAL](https://taotoken.net/?utm_source=general)F_UP).doubleValue();
}
}
这段代码有几个细节值得展开:
首先,整个方法加了@Transactional事务注解。因为步骤5、6、7要同时成功或同时失败,尤其是插入明细记录和更新罪犯风险等级,一旦步骤6写完明细但步骤7更新失败,就会出现罪犯当前风险等级和最新评估结果不一致的情况。这种数据不一致在管理系统中是非常严重的问题。
其次,评分在后端做了二次校验。前端虽然用滑块限制了0-10的范围,但接口是公开的,不能信任任何一个客户端传来的值。之前遇到过前端被绕过直接传100分的情况,所以后端一定要校验边界。
第三,评分计算中用了BigDecimal做四舍五入。double直接计算0.1 + 0.2会出现浮点精度问题,加权分虽然误差极小,但在总分需要精确比较阈值(比如20分和19.9999分)时可能会出bug,所以我在计算完后统一保留两位小数。
4.3 Mapper接口与XML动态SQL:写SQL还是比拼框架好用
MyBatis的Mapper接口按功能划分,核心是AssessmentRecordMapper和CriminalInfoMapper。这里说两个典型的SQL场景。
场景一:评估记录分页查询,需要关联罪犯姓名、按关键词搜索、按风险等级筛选。
xml复制<select id="selectRecordPage" resultType="com.riskmanagement.dto.RecordPageDTO">
SELECT r.id, r.total_score, r.risk_level, r.create_time, r.assessor,
c.name AS criminal_name, c.id_card, c.crime_type
FROM assessment_record r
LEFT JOIN criminal_info c ON r.criminal_id = c.id
<where>
<if test="keyword != null and keyword != ''">
AND (c.name LIKE CONCAT('%', #{keyword}, '%')
OR c.id_card LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test="riskLevel != null and riskLevel != ''">
AND r.risk_level = #{riskLevel}
</if>
</where>
ORDER BY r.create_time DESC
LIMIT #{offset}, #{pageSize}
</select>
这里用了<where>和<if>动态SQL标签,条件为空时自动忽略对应片段,不用担心多出多余的AND关键字,这是MyBatis比传统JDBC拼接SQL舒服太多的地方。
场景二:统计分析,按监区统计各风险等级人数分布,用于首页可视化展示。
xml复制<select id="countByRegionAndLevel" resultType="map">
SELECT confined_regions AS region, risk_level AS riskLevel, COUNT(*) AS cnt
FROM criminal_info
GROUP BY confined_regions, risk_level
ORDER BY confined_regions
</select>
查询结果直接返回List<Map>,前端拿来就能转成图表数据,非常方便。
批量插入明细用的<foreach>标签在评估明细只有几条数据时性能优势不明显,但如果是批量录入历史评估数据,一条SQL插入几百条明细,性能差距就大了。
5. 前端实现:Vue页面从登录到评估结果可视化
5.1 前端工程搭建与环境配置
前端我使用Vue 2.7 + Vue Router 3 + Element UI这套组合。虽然Vue 3 + Element Plus已经是新方向,但很多公司维护的老系统、以及教学环境里的存量代码仍然以Vue 2为主。用Vue 2做这套系统,在面试时聊到实际项目经验,反而更能体现你接手过真实生产项目的能力。
初始化工程用vue-cli:
bash复制vue create risk-assessment-web
创建完成后安装依赖:
bash复制npm install element-ui
npm install axios
npm install vue-router@3
这里有个关键问题:Vue 2项目中vue和vue-template-compiler两个包的版本必须完全一致。比如vue是2.7.14,vue-template-compiler也必须精确到2.7.14,否则编译阶段会报错。我见过太多人卡在这个问题上,通常都是因为用npm install vue@latest安装到了Vue 3,但代码却按Vue 2的写法来写,导致启动白屏或者编译失败。
路由文件router/index.js:
javascript复制const routes = [
{ path: '/login', component: Login },
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{ path: 'dashboard', component: Dashboard, meta: { title: '首页概览' } },
{ path: 'criminal', component: CriminalList, meta: { title: '罪犯档案' } },
{ path: 'assessment/add', component: AssessmentAdd, meta: { title: '发起评估' } },
{ path: 'assessment/list', component: AssessmentList, meta: { title: '评估记录' } },
{ path: 'system/user', component: UserManage, meta: { title: '用户管理' } }
]
}
]
router.beforeEach((to, from, next) => {
const [token](https://taotoken.net?utm_source=general) = localStorage.getItem('token')
if (to.path !== '/login' && !token) {
next('/login')
} else {
next()
}
})
路由守卫用来做登录鉴权,虽然用localStorage存token有XSS风险,真实生产环境一般建议HttpOnly Cookie,但教学项目里localStorage简单直观,能看到完整的登录态流转过程,学习成本低。
5.2 评估表单组件实现:滑块评分与数据双向绑定
评估页面是整套系统交互最核心的部分。我把它拆成了三个子组件:罪犯选择组件、维度评分组件、结果展示组件。这里重点说维度评分组件:
vue复制<template>
<div class="dimension-score-item">
<div class="dimension-header">
<span class="label">{{ dimension.dimensionName }}</span>
<span class="weight">权重 {{ dimension.weight }}</span>
<span class="score">{{ form.scores[dimension.dimensionCode] }} 分</span>
</div>
<el-slider
v-model="form.scores[dimension.dimensionCode]"
:min="0"
:max="10"
:step="0.5"
show-input
/>
<div class="dimension-tip">{{ dimension.remark }}</div>
</div>
</template>
滑块组件使用0到10分、步长0.5,操作直观,也防止了用户手动输入超范围值。每行维度下面显示权重的样子,提交前民警心里对每个维度的重要性有个基本概念。
提交评估的核心逻辑:
javascript复制submitAssessment() {
if (!this.form.criminalId) {
this.$message.warning('请先选择罪犯')
return
}
const payload = {
criminalId: this.form.criminalId,
assessor: this.$store.state.userName,
dimensionScores: this.form.scores
}
axios.post('/api/assessment/evaluate', payload).then(res => {
const data = res.data
this.$alert(`评估完成,总分:${data.totalScore},等级:${data.riskLevel}`, '评估结果', {
confirmButtonText: '确定',
type: data.riskLevel === 'HIGH' ? 'warning' : 'success'
})
this.$router.push('/assessment/list')
}).catch(err => {
this.$message.error(err.response?.data?.message || '评估提交失败')
})
}
这里我要专门说一个Vue 2的大坑:响应式数据检测不到动态新增的属性。如果form.scores是一个空对象,然后通过form.scores[dimension.dimensionCode] = value这种方式去赋初始值,Vue 2的响应式系统检测不到新增属性,界面上滑块、分数标签不会更新。解决办法是在初始化form的时候把维度配置里所有的dimensionCode都预先填进scores对象里,比如都设为0,或者用this.$set()方法。这个坑对新手极其隐蔽,我当时排查了很久。
5.3 统计面板可视化:用图表直观呈现评估结果分布
首页概览页我用ECharts做了两个图表:风险等级分布饼图和近30天评估趋势折线图。饼图能直观看到高、中、低风险人群占比,趋势图能看出评估工作的节奏和变化趋势。
javascript复制import * as echarts from 'echarts'
export default {
data() {
return {
riskLevelData: [],
chart: null
}
},
mounted() {
this.initChart()
},
beforeDestroy() {
if (this.chart) {
this.chart.dispose()
}
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.riskChart)
this.chart.setOption({
tooltip: { trigger: 'item' },
legend: { bottom: '0%' },
series: [{
type: 'pie',
radius: '60%',
data: this.riskLevelData,
label: { formatter: '{b}: {d}%' }
}]
})
}
}
}
两个容易踩的坑:
第一个,ECharts图表的实例销毁问题。Vue组件切换时如果不调用chart.dispose()把实例销毁,会造成内存泄漏。长时间操作下来,页面会越来越卡,甚至浏览器崩溃。我习惯在beforeDestroy钩子里统一处理。
第二个,数据更新后图表不刷新。如果是从接口异步加载数据,要在setOption之前准备好数据,或者使用chart.setOption(option, true)强制覆盖模式。我踩过数据加载完成但图表还是空白的坑,那次是因为在mounted里先初始化了图表,接口数据返回后才setOption,但数据引用的this指向已经变了。
6. 系统联调、部署与常见问题排查实录
6.1 环境搭建与启动顺序
这套系统的启动顺序有讲究,我建议按以下步骤来:
- 先安装JDK 1.8、MySQL 8.0、Node.js 16.x。
- 创建数据库risk_management,导入建表SQL。
- 在application.yml里修改数据库账号密码,启动后端SpringBoot服务,后端启动成功后再启动前端。
- 前端安装依赖
npm install,启动开发服务器npm run serve。 - 浏览器访问
http://localhost:8080(如果前端配了代理,则走代理转发到后端8080端口)。
后端接口测试建议先用Postman或Apifox把各接口单独调一遍,确认CRUD正常后再联调前端。切忌前端后端同时出问题然后一起联调,那种场景下问题定位会非常痛苦。
前端开发环境跨域问题,最简单的方案是通过Vue CLI的devServer配置代理:
javascript复制// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
}
这样前端请求/api/assessment/list实际上会代理到后端的http://localhost:8080/api/assessment/list,不会触发浏览器跨域拦截。比在后端写CORS配置更省事,而且生产环境可以自动去掉代理直接通过Nginx转发。
6.2 常见问题与排查思路速查表
我整理了在这套系统开发过程中遇到的高频问题,按领域分类,方便你直接对照排查。
环境与工程类:
| 现象 | 根因 | 解决办法 |
|---|---|---|
| 启动报Error creating bean with name 'sqlSessionFactory' | mybatis-spring-boot-starter版本冲突或未指定版本 | 在pom中显式指定2.3.x版本 |
| IDEA找不到mybatis XML文件或命名空间标红 | MyBatis插件未安装或XML文件没有放在mapper-locations指定目录 | 安装MyBatis插件;确认XML文件在src/main/resources/mapper下 |
| Vue项目npm run serve报错 | node版本过高或依赖版本冲突 | 使用Node 16.x版本;删除node_modules和package-lock.json后重新install |
| 前端页面白屏、控制台报SyntaxError | vue和vue-template-compiler版本不一致 | 将两个包版本精确对齐到同一个版本号 |
| npm install速度极慢 | 默认使用的是官方源 | 切换到国内镜像:npm config set registry https://registry.npmmirror.com |
数据库与MyBatis类:
| 现象 | 根因 | 解决办法 |
|---|---|---|
| 查询结果中实体属性为null | 数据库下划线字段没有映射为驼峰 | 开启map-underscore-to-camel-case,或编写resultMap |
| 插入记录后获取不到自增主键 | 没有配置useGeneratedKeys和keyProperty | insert标签加useGeneratedKeys="true" keyProperty="id" |
| 前端传过来的中文乱码 | 数据库字符集不是utf8mb4,或连接URL没有指定编码 | 建库时指定utf8mb4;URL加characterEncoding=utf8 |
| 时间字段和本地时间相差8小时 | 时区配置不正确 | URL添加serverTimezone=Asia/Shanghai |
| MySQL 8连接报Public Key Retrieval is not allowed | 连接参数缺少allowPublicKeyRetrieval | 在URL中追加allowPublicKeyRetrieval=true |
业务逻辑类:
| 现象 | 根因 | 解决办法 |
|---|---|---|
| 评估明细没有生成,但评估主表有了记录 | 明细插入和主表插入不在同一个事务中 | 在Service方法上加@Transactional |
| 罪犯当前风险等级未更新 | 更新SQL条件写错或事务未提交 | 检查updateRiskLevel的where条件,确保按criminal_id更新 |
| 同一个罪犯重复点击评估按钮生成多条记录 | 前后端都缺少防重复提交控制 | 前端按钮loading禁用,后端可加唯一索引或业务判重 |
| 评估总分和明细加权分对不上 | float精度丢失 | 改用BigDecimal或double统一四舍五入到两位小数 |
6.3 事务与并发:一个容易被忽略的细节
评估系统虽然并发量不高,但有一种情况必须考虑:如果多个民警几乎同时对同一个罪犯发起评估,各自的评估事务都会去更新criminal_info的risk_level字段,后提交的会覆盖先提交的结果。正常业务流程中这种概率不大,但如果作为正式系统上线,建议采用乐观锁方案:在criminal_info表加一个version字段,更新时检查version是否匹配:
sql复制UPDATE criminal_info
SET risk_level = #{riskLevel}, version = version + 1
WHERE id = #{criminalId} AND version = #{oldVersion}
如果更新影响行数为0,说明版本冲突,让当前评估重试或者提示"该罪犯档案已被其他民警更新,请刷新后重试"。这个方案实现成本低,值得作为加分项加进系统里。
6.4 性能与安全方面的注意点
这套系统的数据量不会太大,性能压力主要在评估记录的历史积累和统计查询上。我给出几个可落地的建议:
- 索引设计:assessment_record表的criminal_id和create_time字段都要建索引,列表查询和统计都依赖这两个字段。如果数据量大了,再考虑联合索引。
- SQL避免SELECT之外的风险:动态SQL拼接的参数统一用#{}预编译占位符,禁止直接拼接字符串,防止SQL注入。MyBatis的#{}底层是PreparedStatement,天然防注入,而${}是字符串直接拼进去,这个区别一定不要搞混。
- 密码加密:用户表的密码不能存明文,至少要用BCrypt加盐哈希。Spring Security自带的BCryptPasswordEncoder可以直接用,或者用JBCrypt库。
- 接口鉴权:评估接口、用户管理接口不能让未登录的用户访问
