基于SpringBoot的大学生心理健康管理系统:心理测评、预约与预警闭环实现

简介:本资源为基于SpringBoot的大学生心理健康管理系统毕业设计完整源码包,面向计算机相关专业需要完成课程设计或毕业论文的学生,以及希望参考Java全栈项目实战的开发者。系统覆盖个人信息管理、学生管理、心理老师管理、试卷与试题管理、通知管理等模块,支持在线心理测评、自动评分与咨询预约,可帮助读者快速搭建一套可运行的心理健康管理平台。压缩包共846个文件,约23.78MB,以149个Java源码、69个Vue组件、157个JavaScript脚本、53个HTML页面和50个CSS样式为主,另含SQL建库脚本、yml配置、bat启动脚本及图片、字体等静态资源,前后端分离结构清晰,便于按模块阅读与二次开发。目前已有508人学习下载,适合作为毕业设计选题参考、课程设计交付模板或SpringBoot入门练手项目,读者可从中获取完整功能实现思路、数据库设计参考与项目部署结构。

1. 从一份“能跑起来”的毕设说起:大学生心理健康管理系统到底要解决什么

很多同学做毕设,选题定了“大学生心理健康管理系统”,第一反应是去搜现成源码,结果下回来一堆跑不起来的压缩包,或者跑起来只有登录页和空表格。这个标题背后真正要交付的,不是“一个能登录的网站”,而是一套能把心理测评、咨询预约、危机预警、档案管理串起来的业务闭环。它面向的是高校心理中心的真实工作流:学生填量表、系统算分、老师看预警、安排访谈、留档跟踪。技术栈锁定 SpringBoot,是因为它能把权限、事务、定时任务、接口文档这些毕设里绕不开的东西用最少配置搭起来,配合 MyBatis 和 MySQL 就能覆盖绝大多数院校的部署条件。这一章先把边界划清楚:你要做的是一个可演示、可答辩、可扩展的管理系统,不是商业级心理服务平台。适合正在选 SpringBoot 毕设、手里只有一台普通笔记本、需要三个月内出成果的本科生。

2. 需求拆成表:心理测评、预约、预警三条主线怎么落到数据库

2.1 先分清“管理”和“业务”两层,别把量表逻辑写进 Controller

大学生心理健康管理系统的功能看着多,其实可以压成三层。最底下是基础数据层:学生、教师、班级、院系、角色权限。中间是业务层:心理量表、测评记录、预约申请、咨询记录。最上面是决策层:预警规则、统计报表、导出归档。很多同学翻车在把量表计分逻辑直接写在 Controller 里,导致换一个量表就要改接口。常见做法是抽象出Scale、ScaleQuestion、ScaleOption、AssessmentRecord、AssessmentAnswer五张表,计分规则用 JSON 存在Scale的scoring_rule字段里,Service 层只负责解析规则并算分。这样新增一个焦虑自评量表,只需要后台录入题目和规则,不用重新打包。

数据库设计上,学生表和用户表建议分开。sys_user只存账号、密码、角色、状态;student存学号、姓名、性别、院系、班级、联系电话、紧急联系人。这样做的原因是心理档案的敏感字段不应该和登录凭证混在一张表里,后期做数据脱敏和权限隔离会省很多事。预约表appointment要带student_id、counselor_id、appointment_time、status、create_time,状态用枚举值PENDING / CONFIRMED / FINISHED / CANCELED,不要用中文直接存。

2.2 建表 SQL 与索引:把查询最频繁的字段先加上

下面这段 SQL 是核心业务表的最小可用版本,字段类型按 MySQL 8.0 写,字符集统一utf8mb4。注意assessment_record上建了student_id和create_time的联合索引,因为预警扫描和档案列表都按这两个条件查。

-- 学生表:只存业务属性,登录走 sys_user CREATE TABLE `student` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL COMMENT '关联 sys_user.id', `student_no` VARCHAR(20) NOT NULL COMMENT '学号', `name` VARCHAR(50) NOT NULL, `gender` TINYINT DEFAULT 0 COMMENT '0未知 1男 2女', `college` VARCHAR(100) DEFAULT NULL, `class_name` VARCHAR(100) DEFAULT NULL, `phone` VARCHAR(20) DEFAULT NULL, `emergency_contact` VARCHAR(50) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_student_no` (`student_no`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生信息表'; -- 测评记录表:预警扫描的核心表 CREATE TABLE `assessment_record` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `student_id` BIGINT NOT NULL, `scale_id` BIGINT NOT NULL, `total_score` INT DEFAULT 0, `risk_level` TINYINT DEFAULT 0 COMMENT '0正常 1关注 2预警 3危机', `answers_json` JSON DEFAULT NULL COMMENT '原始作答,便于回溯', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_student_time` (`student_id`, `create_time`), KEY `idx_risk_level` (`risk_level`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='心理测评记录表';

逻辑说明:answers_json用 JSON 类型存原始答案,是为了答辩时能展示“系统不是只存一个分数,而是可追溯”。risk_level单独建索引,因为预警列表页几乎只查risk_level >= 2的记录。参数上,total_score用 INT 足够,常见量表总分不会超过 200。如果学校要求保留三年以上数据,create_time建议按月分区,但毕设阶段单表加索引就够用。

2.3 预约状态流转:用状态机思路写 Service,别用 if-else 堆

预约模块最容易写成一坨 if-else。推荐在 Service 层用一个简单的状态流转判断:只有PENDING能变CONFIRMED或CANCELED,只有CONFIRMED能变FINISHED。下面这段 Java 代码放在AppointmentServiceImpl里,配合枚举使用。

public AppointmentVO updateStatus(Long id, AppointmentStatus target) { Appointment appt = appointmentMapper.selectById(id); if (appt == null) { throw new BizException("预约不存在"); } AppointmentStatus current = appt.getStatus(); // 只允许合法流转,避免前端乱传状态 boolean legal = switch (current) { case PENDING -> target == AppointmentStatus.CONFIRMED || target == AppointmentStatus.CANCELED; case CONFIRMED -> target == AppointmentStatus.FINISHED || target == AppointmentStatus.CANCELED; default -> false; }; if (!legal) { throw new BizException("当前状态不允许该操作"); } appt.setStatus(target); appointmentMapper.updateById(appt); return convert(appt); }

逻辑说明:用switch表达式把合法流转集中在一处,比散落的 if 好维护。参数上,AppointmentStatus是枚举,数据库存VARCHAR或TINYINT都行,但建议存字符串,方便直接看表排查。注意updateById前没有加锁,如果同一预约被两个老师同时操作,可能覆盖。毕设阶段可以在方法上加@Transactional并在查询时用select ... for update,但并发量极低,知道这个边界即可。

3. SpringBoot 工程落地:从 Maven 依赖到接口联调的完整路径

3.1 用 Spring Initializr 起项目,依赖别贪多

很多同学一上来就勾选一堆 starter,结果启动报冲突。这个系统真正需要的依赖就几类:spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-j、spring-boot-starter-validation、lombok、spring-boot-starter-test。如果要做登录鉴权,加spring-boot-starter-security或自己写 JWT 拦截器;如果要做接口文档,加knife4j-openapi3-jakarta-spring-boot-starter。注意 SpringBoot 3.x 要求 JDK 17,如果你学校机房还是 JDK 8,就锁 2.7.x 版本,别硬上 3.x 然后被javax和jakarta包名搞崩溃。

pom.xml里建议显式指定spring-boot-maven-plugin,否则打包出来的 jar 可能没有主清单。下面是一个可用的依赖片段,版本号用属性管理,方便统一改。

<properties> <java.version>17</java.version> <spring-boot.version>3.2.5</spring-boot.version> <mybatis-plus.version>3.5.6</mybatis-plus.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

逻辑说明:MyBatis-Plus 省掉大量单表 CRUD 代码,但复杂查询仍建议写 XML。mysql-connector-j是 MySQL 8 之后的驱动坐标,老版本是mysql-connector-java,别混用。参数上,scope设为runtime表示编译时不需要,打包时会进去。

3.2 application.yml 里必须改的四个配置

配置文件是新手翻车重灾区。下面这份application.yml把数据源、MyBatis-Plus、端口、日志都列出来,你按自己环境改。

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mental_health?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl logging: level: com.example.mentalhealth: debug

逻辑说明:serverTimezone必须写,否则 MySQL 8 可能报时区错误。map-underscore-to-camel-case让student_no自动映射到studentNo。log-impl在开发期打开,能看到实际执行的 SQL,答辩演示时如果嫌日志多可以关掉。参数上,mapper-locations的路径要和你的 resources 目录结构一致,常见是src/main/resources/mapper/。

3.3 写一个测评提交接口,把参数校验和事务加上

下面这个 Controller 方法处理学生提交量表答案。注意用@Valid做基础校验,用@Transactional保证记录和答案一起入库。

@RestController @RequestMapping("/api/assessment") @RequiredArgsConstructor public class AssessmentController { private final AssessmentService assessmentService; @PostMapping("/submit") public Result<AssessmentResultVO> submit(@RequestBody @Valid AssessmentSubmitDTO dto) { // dto 里包含 studentId、scaleId、answers 列表 AssessmentResultVO vo = assessmentService.submit(dto); return Result.ok(vo); } }

对应的 DTO 里,answers用List<AnswerItem>,每个AnswerItem有questionId和optionId。Service 层先根据scaleId查出计分规则,遍历答案累加分数,再根据阈值算出riskLevel,最后写assessment_record和assessment_answer。参数说明:riskLevel的阈值不要硬编码在 Java 里,放到scale表的risk_configJSON 字段,格式如{"low":0,"medium":40,"high":60},这样不同量表可以配不同阈值。

4. 预警与定时任务:怎么让系统“主动发现”而不是等人来查

4.1 预警规则表设计:把“什么算危机”交给后台配

预警不是简单查total_score > 60。真实场景里,同一份量表不同维度得分高,风险不同;连续两次测评分数上升,也要关注。建议建一张warning_rule表,字段包括id、scale_id、rule_name、condition_json、risk_level、enabled。condition_json存类似{"field":"total_score","operator":">=","value":60}或{"field":"dimension_anxiety","operator":">=","value":15}的条件。扫描任务读规则,逐条匹配。

这样做的好处是答辩时老师问“如果学校想改预警线怎么办”,你可以直接演示后台改一条规则,不用重新编译。注意condition_json的解析要写单元测试,覆盖>、>=、<、<=、==五种操作符,避免线上因为一个符号写错导致预警失效。

4.2 用 @Scheduled 做每日扫描,注意线程池和幂等

SpringBoot 自带定时任务,加@EnableScheduling就能用。下面这段代码每天凌晨两点扫描前一天新增的测评记录,命中规则的写入warning_record表。

@Component @RequiredArgsConstructor @Slf4j public class WarningScanTask { private final AssessmentRecordMapper recordMapper; private final WarningRuleMapper ruleMapper; private final WarningRecordMapper warningMapper; @Scheduled(cron = "0 0 2 * * ?") public void scan() { List<WarningRule> rules = ruleMapper.selectList( new LambdaQueryWrapper<WarningRule>().eq(WarningRule::getEnabled, 1)); LocalDateTime start = LocalDate.now().minusDays(1).atStartOfDay(); List<AssessmentRecord> records = recordMapper.selectList( new LambdaQueryWrapper<AssessmentRecord>() .ge(AssessmentRecord::getCreateTime, start)); for (AssessmentRecord r : records) { for (WarningRule rule : rules) { if (match(rule, r)) { // 幂等:同一条记录同一条规则只写一次 Long count = warningMapper.selectCount( new LambdaQueryWrapper<WarningRecord>() .eq(WarningRecord::getRecordId, r.getId()) .eq(WarningRecord::getRuleId, rule.getId())); if (count == 0) { warningMapper.insert(buildWarning(r, rule)); } } } } log.info("预警扫描完成,处理记录数:{}", records.size()); } private boolean match(WarningRule rule, AssessmentRecord r) { // 解析 condition_json 并比较,此处省略具体解析 return false; } }

逻辑说明:cron表达式0 0 2 * * ?表示每天 2 点。幂等判断用selectCount先查再插,虽然有小并发窗口,但定时任务单线程执行,实际没问题。参数上,如果数据量大,selectList要改成分页,避免一次性加载过多。另外@Scheduled默认单线程,如果扫描超过一天,下次会排队,毕设数据量小可以忽略,但要知道这个边界。

4.3 预警列表接口:给辅导员看的页面要带学生姓名和规则名

前端展示预警时,不能只给student_id和rule_id。常见做法是写一个联表查询,返回studentName、studentNo、college、ruleName、riskLevel、createTime。MyBatis-Plus 可以用@Select注解写自定义 SQL,也可以放 XML。下面是一个 XML 片段。

<select id="selectWarningPage" resultType="com.example.mentalhealth.vo.WarningVO"> SELECT w.id, s.name AS studentName, s.student_no AS studentNo, s.college, r.rule_name AS ruleName, w.risk_level AS riskLevel, w.create_time AS createTime FROM warning_record w LEFT JOIN student s ON w.student_id = s.id LEFT JOIN warning_rule r ON w.rule_id = r.id WHERE w.risk_level &gt;= #{minLevel} ORDER BY w.create_time DESC </select>

逻辑说明:LEFT JOIN保证即使学生被删除也能看到预警记录。&gt;=是 XML 转义写法。参数minLevel由前端传入,默认 2。注意分页要配合 MyBatis-Plus 的Page对象,否则数据多了页面会卡。

5. 避坑与排查:毕设答辩前最容易翻车的五个地方

5.1 现象:启动报Table 'xxx' doesn't exist,但数据库里明明有表

原因通常是application.yml里的数据库名写错,或者 MyBatis-Plus 的mapper-locations没配导致 XML 没加载。解决:先看启动日志里打印的 JDBC URL,确认库名;再检查target/classes/mapper下有没有 XML 文件,如果没有,在pom.xml的resources里加上<include>**/*.xml</include>。

5.2 现象:前端传中文乱码,存进数据库变成问号

原因一般是数据库连接串没加characterEncoding=utf8,或者表字符集不是utf8mb4。解决:连接串补上useUnicode=true&characterEncoding=utf8,建表时统一DEFAULT CHARSET=utf8mb4。如果已经建错,用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4改。

5.3 现象:定时任务没执行,日志里也没有扫描记录

原因可能是启动类没加@EnableScheduling,或者cron表达式写错。解决:先确认注解,再把cron改成0/30 * * * * ?每 30 秒跑一次,看日志有没有输出。如果还是没有,检查任务类有没有被 Spring 扫描到,包路径要在启动类同级或子级。

5.4 现象:测评提交后分数不对,总是少几分

原因通常是答案选项的分数没取对,比如前端传的是optionId,后端却按questionId去查分数。解决:在 Service 里打印answers列表和查出的option分数,逐条比对。另外注意多选题的计分规则,如果量表有多选,answers_json要存数组,不能只存一个 ID。

5.5 现象:打包成 jar 后运行报no main manifest attribute

原因是没有配置spring-boot-maven-plugin的repackage目标。解决:在pom.xml的build节点里加上插件配置,执行mvn clean package后,用java -jar target/xxx.jar启动。注意不要直接运行原始的xxx.jar.original。

6. 让答辩加分的一个技巧:把测评结果导出成可追溯的 PDF 档案

最后一章说一个我实际做过的进阶用法:把学生的测评记录和预警历史导出成一份 PDF 档案。这个功能在答辩时很讨巧,因为老师能直观看到“系统不只是增删改查”。实现路径不复杂:用iText或OpenPDF在 Service 层生成 PDF,模板里放学生基本信息、每次测评的得分和风险等级、预警记录列表。下面是一个最小示例,用 OpenPDF。

public void exportPdf(Long studentId, HttpServletResponse response) throws IOException { Student student = studentMapper.selectById(studentId); List<AssessmentRecord> records = recordMapper.selectList( new LambdaQueryWrapper<AssessmentRecord>() .eq(AssessmentRecord::getStudentId, studentId) .orderByDesc(AssessmentRecord::getCreateTime)); response.setContentType("application/pdf"); response.setHeader("Content-Disposition", "attachment; filename=student_" + studentId + ".pdf"); Document doc = new Document(PageSize.A4); PdfWriter.getInstance(doc, response.getOutputStream()); doc.open(); doc.add(new Paragraph("学生心理档案", FontFactory.getFont(FontFactory.HELVETICA_BOLD, 18))); doc.add(new Paragraph("姓名:" + student.getName() + " 学号:" + student.getStudentNo())); doc.add(new Paragraph(" ")); for (AssessmentRecord r : records) { doc.add(new Paragraph("测评时间:" + r.getCreateTime() + " 总分:" + r.getTotalScore() + " 风险等级:" + r.getRiskLevel())); } doc.close(); }

逻辑说明:response.getOutputStream()直接写 PDF,前端用window.open或a标签下载。参数上,中文字体需要额外引入iTextAsian或使用系统字体,否则中文会显示成方块。如果不想折腾字体,可以导出成 Excel,用 EasyExcel 更简单。这个技巧的价值在于,它把“数据管理”变成了“档案服务”,答辩时你可以说“辅导员可以一键导出某个学生的完整心理轨迹,用于访谈前准备”。我自己的习惯是,任何管理系统都留一个导出入口,因为这是最容易被忽略但演示效果最稳的功能。希望帮到你。

本文还有配套的精品资源,点击获取