说实话,“计算机毕业设计SpringBoot前后端分离未成年人心理健康咨询系统”这个选题,在近几年的毕业设计里出场率相当高。技术栈是标准的、也是目前企业里最主流的SpringBoot + Vue前后端分离组合,业务方向又切中了校园儿童心理援助这个真实存在的社会需求。作为带过不少毕业生做完整个流程的人,我可以直接告诉你:这个题做好了的难点根本不在增删改查,而在于权限模型怎么设计、心理测评结果怎么算、咨询预约的并发冲突怎么防,以及预警机制怎么做到既有效又不越界。
这篇文章我会按照实际开发推进的节奏,把这个项目从需求拆解、表结构设计、代码实现,到部署答辩过程中遇到的坑和解决方案,完整盘一遍。内容偏实战,适合正在做类似选题的毕业生,也适合想快速上手一个完整前后端分离项目的人参考。
1. 项目核心定位与需求拆解
1.1 别把系统只做成“咨询管理后台”
这类系统的名字有各种叫法,比如“未成年人心理健康咨询系统”“青少年心理支持平台”“校园儿童心理援助服务系统”,但核心业务都指向同一个场景:给在校未成年人提供一个自我测评、在线倾诉、预约心理老师的线上通道。
我在接手类似需求的时候,第一件事永远是梳理业务场景。校园里的心理援助,跟社会上的心理咨询机构有本质差别。未成年人不能完全独立承担法律和医疗责任,所以系统必须考虑监护人参与、学校心理老师的介入机制。同时,未成年人心理问题又非常敏感,数据隐私保护的要求比普通系统高得多。
所以这个系统在功能上要满足三个角色、六条主线的需求:
- 学生端:心理自评量表、测评结果查看、在线预约咨询师、文字倾诉/树洞、查看心理科普文章。
- 咨询师端:处理预约请求、查看学生测评档案、发起在线咨询会话、提交咨询记录与风险标记。
- 管理员端:维护咨询师与学生的账号、管理量表题库、审核文章内容、查看平台运行的数据统计。
如果只围绕“预约”和“咨询记录”来做,答辩时很容易被问到:你如何体现“未成年人”这个特殊性?你怎么支撑“心理援助”这个严肃场景?所以我对这个题目的建议是:必须把“心理测评”和“风险预警”做成整个系统的业务核心,预约和咨询是围绕它的两条服务闭环。
1.2 前后端分离架构的选择逻辑
毕设里选择前后端分离,有人觉得是因为“热门”,有人觉得是“方便外包”。但从工程角度看,它确实更适合这种多端应用场景。
你想象一下,学生在学校机房打开的是一个浏览器页面,咨询师在办公室用的可能是大屏模板,管理员在手机上也想看个数据概览。传统单体模板渲染,比如JSP或者Thymeleaf,页面逻辑和后端代码耦合在一起,一旦需要多端适配,工作量几乎翻倍。前后端分离之后,后端只提供JSON接口,前端负责页面渲染,后期扩展一个移动端或者小程序,后端接口可以直接复用。
另一个很实际的原因是并行开发。毕业设计周期短,一个人可能又当后端又当前端,但分离架构可以把“接口设计”这个环节提前固定下来:先定义好RESTful接口协议,再分别实现,调试时也能通过Mock数据独立联调。这个习惯对以后进团队工作也非常重要。
我在给学生改设计文档时,最常看到的一句话是“本系统采用前后端分离架构,提高了开发效率、便于维护”。这句话太空了,答辩时老师追问一句“具体体现在哪”——你至少要能说出部署层面的一层分立:前端打包后由Nginx静态托管,后端独立部署在应用服务器,二者通过HTTP/JSON通信。这个会在后面的部署章节详细讲。
1.3 功能模块的最终划分
我习惯把整个系统分成6个业务模块,跟答辩PPT的目录结构保持一致:
| 模块名 | 核心功能 | 涉及角色 |
|---|---|---|
| 用户与权限模块 | 注册登录、角色鉴权、个人信息维护 | 学生、咨询师、管理员 |
| 心理测评模块 | 量表列表、在线答题、结果计算、历史记录 | 学生、咨询师 |
| 预约管理模块 | 咨询师排班、时段预约、预约审核、取消处理 | 学生、咨询师 |
| 咨询互动模块 | 在线文字咨询、留言、咨询记录归档 | 学生、咨询师 |
| 内容管理模块 | 心理文章发布、阅读统计、题目管理 | 管理员、学生 |
| 数据统计模块 | 测评结果趋势、预约量统计、预警学生列表 | 管理员、咨询师 |
这个划分方式,每个模块都能对应到独立的数据库表和核心接口,提交到开题报告里,逻辑清晰,也能撑起至少10张以上的表,工作量上完全没问题。
2. 技术选型与表结构设计要点
2.1 后端为什么是SpringBoot + MyBatis-Plus + Sa-Token
技术栈方面,SpringBoot 2.7.x是当前毕设和中小型公司项目里最稳的选择。SpringBoot 3虽然已经普及,但很多云服务环境和教学环境还在用JDK 8,强行上SpringBoot 3反而给部署添麻烦。
ORM框架我用的是MyBatis-Plus,理由很直接:单表CRUD不用写XML,自带分页插件,代码生成器还能一键生成基础实体和Mapper。像这种管理类系统,80%的接口都是对单表的增删改查,MyBatis-Plus能省掉大量样板代码。复杂的多表查询,比如“查询某个咨询师未来7天的可预约时段并排除已预约部分”,这种还是自己写SQL更可控。
权限和认证这块,我没有用Spring Security,而是选了一个Java生态里非常轻量的权限框架Sa-Token。首先,Sa-Token的API设计对新手极度友好,登录就是一行StpUtil.login(userId),校验登录就是StpUtil.checkLogin(),角色校验是StpUtil.checkRole("student"),没有任何概念负担。如果选Spring Security,新手光理解过滤器链就要花掉一周,而且配置出错往往说不出为什么。Sa-Token默认把会话数据放在Redis里,天然支持分布式场景,对毕设来说绰绰有余。
电脑上的数据存储,MySQL 8.0,字符集直接选utf8mb4。这个是硬性习惯,因为utf8mb4才能完整存储emoji和生僻字,你永远不知道用户会在树洞里发什么。Redis我用在三个地方:token会话、验证码存储、在线咨询的未读消息计数。
2.2 前端为什么是Vue3 + Element Plus + Vite
前端这块,Vue3 + Element Plus是当前最成熟的组合。Vue3的组合式API(Composition API)让逻辑复用变得非常方便,比如“获取当前登录用户信息”这个逻辑,可以封装成一个useUser()函数,在每个页面里直接调用。
构建工具我用了Vite,启动速度和热更新比Webpack快得多,这在开发调试阶段的体验提升是巨大的。状态管理选Pinia,比Vuex更轻,而且对TypeScript的支持更好。
还有一个很容易被忽略的点:校园场景下,学生使用手机访问的比例非常高。所以前端页面我采用了响应式布局,核心页面在移动端也要可用。Element Plus本身不是专门为移动端设计的,但配合简单的媒体查询和弹性布局,在手机上展示测评页面、预约页面完全够用。如果想优化移动端体验,可以引入Vant组件库做移动端专用页面,但会增加工作量,看个人时间取舍。
2.3 数据库表设计:一次想清楚,后面少掉头发
表结构是整个系统的地基,我这几年看过的毕设,凡是后期返工严重的,基本都是因为表结构设计的时候偷了懒。核心表我列出来,并说明设计理由。
用户相关表:
sys_user:用户主表,字段包括id、username、password(BCrypt加密存储)、real_name、gender、age、phone、avatar、role(1学生,2咨询师,3管理员)、student_no(学号)、guardian_phone(监护人电话,学生角色必填)、status、create_time。counselor_info:咨询师扩展表,存咨询师的专业资质、擅长方向、从业年限、个人简介、服务状态。
用户主表和扩展表分开设计,这是典型的垂直拆分思路。学生和咨询师共用的账号密码信息放主表,各自的差异化信息放到扩展表,避免一张表出现大量冗余空字段。
心理测评相关表:
scale:量表信息表,id、scale_name、scale_code、description、total_score、standard_threshold、status。scale_question:题目表,id、scale_id、question_content、score_type(1正向计分,2反向计分)、sort_order。assessment_record:测评记录表,id、student_id、scale_id、raw_score(原始分)、standard_score(标准分)、risk_level(低/中/高)、result_analysis、create_time。
这里我说一个关键设计:把risk_level风险等级作为独立字段存下来。因为测评结果不管作为趋势统计还是预警触发,都要频繁按等级筛选。如果每次都从标准分实时推算,第一是重复计算浪费资源,第二是后续如果调整了判定临界值,历史数据的一致性就没法保证。
咨询预约相关表:
appointment:预约表,id、student_id、counselor_id、scale_id、appointment_date、time_slot(如上午/下午/晚自习前)、status(0待确认,1已确认,2已完成,3已取消)、remark、sign、create_time。
预约表里加了一个sign字段作为乐观锁版本号,用来解决同一个时段被两个学生同时抢到的问题,后面第四章会详细讲。
在线咨询相关表:
chat_message:聊天消息表,id、from_user_id、to_user_id、session_id、content、is_read、create_time。counseling_record:咨询记录表,id、appointment_id、counselor_id、student_id、summary、evaluation、create_time。
聊天消息表是典型的流水表,数据量增长快,所以必须建索引,我建的是(session_id, create_time)联合索引,按会话维度查消息时走索引,不会全表扫描。
内容相关表:
article:文章表,id、title、cover_image、content、category、views、publish_status、create_time。
这套表设计下来,总数在10到12张之间,对于毕设属于非常扎实的体量,也足够支撑开题报告里“数据库设计”章节的篇幅。
2.4 测评模块的专业性从哪里体现
这个系统跟其他CRUD系统拉开差距的关键,就在于测评模块能不能体现“专业”二字。
心理测评不是随便出几道题算个总分就完事的。通常的做法是引入标准化的心理量表。在系统里,我建议至少内置三个经典量表:SAS焦虑自评量表、SDS抑郁自评量表、SCL-90症状自评量表。SAS和SDS各有20道题,4级评分,做题时间短,适合学校快速普查;SCL-90有90道题、5级评分,适合危机干预前的深度评估。
计算逻辑上,SAS和SDS有一个行业统一的换算规则:先算出原始分,再用“标准分 = 原始分 × 1.25后取整数”换算成标准分。临界值方面,SAS标准分50分以下正常,50到59轻度,60到69中度,69以上重度;SDS是53分以下正常,53到62轻度,63到72中度,72以上重度。
关键点在于反向计分题。SAS量表中第5、9、13、17、19题属于反向计分题,学生在这些题上的得分需要先翻转(1变4,2变3,3变2,4变1),再计入原始分。如果漏了这个步骤,测评结果会直接失真。我见过不止一个项目在这里翻车,最后分数算出来怎么都对不上标准。这是在答辩时特别能体现你认真程度的细节。
另外,针对未成年人群体,预警机制不能做成“有风险就告诉所有人”。我的设计是:测评完成后,系统根据标准分自动判定风险等级,所有结果先进入咨询师工作台。当等级达到“高”时,系统给咨询师和管理员发送待办提醒,同时锁住学生的再次测评入口,防止学生因情绪波动反复刷题导致数据失真,后续动作交由咨询师在线下判断是否联系班主任或家长。这个策略在功能合理性和伦理边界上都是稳妥的。
3. 核心模块的代码实现与实现细节
3.1 权限控制的完整落地
后端工程的结构我习惯划分成controller、service、mapper、entity、common、config这几个包。common里放统一的返回结果类Result<T>、全局异常处理器、工具类。
登录接口的逻辑很简单,但有几个细节要注意。用户名密码校验用BCrypt加密核对,登录成功后用Sa-Token签发token:
@PostMapping("/login") public Result<String> login(@RequestBody LoginDTO dto) { LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SysUser::getUsername, dto.getUsername()); SysUser user = userMapper.selectOne(wrapper); if (user == null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) { return Result.error("用户名或密码错误"); } if (user.getStatus() == 0) { return Result.error("账号已被禁用,请联系管理员"); } StpUtil.login(user.getId()); // 用户角色放入会话 StpUtil.getSession().set("role", user.getRole()); StpUtil.getSession().set("userInfo", user); return Result.success(StpUtil.getTokenValue()); }这里有个实际开发中容易踩的坑:登录校验的时候,很多人会先查一次数据库判断用户名是否存在,再查一次密码是否正确。其实一次查询、两条判断就够了,按username查出用户,再统一判断对象是否为空,能省一次无谓的IO。
Sa-Token的拦截配置,我建议用注册拦截器的方式,而不是注解权限控制。因为注解适合单个接口控制,但拦截器可以精准控制“哪些URL前缀需要登录”,并统一处理未登录返回401:
@Configuration public class SaTokenConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle -> { StpUtil.checkLogin(); })) .addPathPatterns("/api/**") .excludePathPatterns("/api/user/login", "/api/user/register", "/api/article/list"); } }前端这边,Axios请求拦截器统一携带token:
axios.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['satoken'] = token } return config })注意这里的Header名称,Sa-Token默认是satoken这个自定义Header,如果你改成Authorization,需要配置SaHolder的tokenName,否则后端接收不到。
响应拦截器统一处理401状态码,跳转登录页,这是前端必须做的兜底,不然随便一个接口token过期,用户看到的就只是加载失败,体验极差:
axios.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )3.2 测评结果的计算与预警逻辑
测评的答题接口,接收的参数是一个包含题目ID和选项分数的列表。后端在做完基础校验后,直接计算原始分、标准分和风险等级。
先说一个性能优化点。测评题目有20到90道不等,如果每道题单独update一次,20道题就是20次数据库交互,体验很差。正确做法是批量保存,用MyBatis-Plus的saveBatch方法,一次提交所有答题记录。数据库交互从20次降到1次,在测评这种高频操作上非常明显。
计算核心代码如下:
@Transactional(rollbackFor = Exception.class) public AssessmentResultVO submitAssessment(AssessmentSubmitDTO dto) { // 1. 查询量表信息 Scale scale = scaleMapper.selectById(dto.getScaleId()); // 2. 查出所有题目及计分方向 List<ScaleQuestion> questions = scaleQuestionMapper.selectList( new LambdaQueryWrapper<ScaleQuestion>() .eq(ScaleQuestion::getScaleId, dto.getScaleId()) .orderByAsc(ScaleQuestion::getSortOrder)); // 3. 校验提交答案数量与题目数量一致,防止同学只做了半套题就提交 if (dto.getAnswers().size() != questions.size()) { throw new BizException("答题未完成,请完成所有题目后再提交"); } // 4. 计算原始分,注意反向计分题 int rawScore = 0; Map<Long, Integer> answerMap = dto.getAnswers().stream() .collect(Collectors.toMap(AnswerItem::getQuestionId, AnswerItem::getScore)); for (ScaleQuestion q : questions) { Integer score = answerMap.get(q.getId()); if (score == null) { throw new BizException("存在未作答的题目,请检查"); } if (q.getScoreType() == 2) { // 反向计分:假如是4级评分,分数翻转 score = scale.getMaxScore() + 1 - score; } rawScore += score; } // 5. 按量表类型换算标准分 int standardScore = scale.getStandardFormula() == 1 ? (int) Math.floor(rawScore * 1.25) : rawScore; // 6. 判定风险等级 String riskLevel = "低"; if (standardScore >= scale.getHighThreshold()) { riskLevel = "高"; } else if (standardScore >= scale.getMediumThreshold()) { riskLevel = "中"; } // 7. 保存记录,返回结果 AssessmentRecord record = new AssessmentRecord(); record.setStudentId(dto.getStudentId()); record.setScaleId(dto.getScaleId()); record.setRawScore(rawScore); record.setStandardScore(standardScore); record.setRiskLevel(riskLevel); assessmentRecordMapper.insert(record); // 8. 如果风险等级为“高”,给咨询师生成预警待办 if ("高".equals(riskLevel)) { createRiskAlert(dto.getStudentId(), record.getId()); } return buildResultVO(record, scale); }这段代码里有几个容易被忽视的点:
- 第2步把题目查出来,不仅仅是为了算分,更重要的是以后台题库为准,而不是前端传过来的题目ID拼接。前端完全可以被篡改,后端必须重新从库里拉取题目。
- 反向计分翻转的分值计算公式
maxScore + 1 - score,是一个通用写法。4级评分翻转就是5减得分,5级评分翻转就是6减得分,不需要针对每个量表单独写映射。 - 提交用
@Transactional保证事务一致性,万一保存记录失败,前面所有操作全部回滚。 - 风险等级判断,临界值的取值逻辑要能自圆其说。我建议把各量表的中度/高度阈值放到scale表里,通过数据库配置,而不是写死在代码里。这样以后想调阈值,改数据就行,不用重新发版。
3.3 预约时段的并发冲突处理
预约功能看起来简单:学生选咨询师、选时段、提交。但一上线就会遇到并发问题——同一个咨询师的同一个时段,被两个学生同时看到空闲,同时提交,两个人都预约成功。这种bug在演示的时候只要老师多刷新几次页面,就可能暴露。
解决方式我推荐乐观锁,实现成本低、讲解也容易。在appointment表加一个sign字段,提交预约的时候,先查当前时段的预约记录,如果不存在则写入,写入时带上版本判断:
@PostMapping("/appointment") public Result<String> createAppointment(@RequestBody AppointmentDTO dto) { // 查询该咨询师该时段是否已被预约 LambdaQueryWrapper<Appointment> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Appointment::getCounselorId, dto.getCounselorId()) .eq(Appointment::getAppointmentDate, dto.getAppointmentDate()) .eq(Appointment::getTimeSlot, dto.getTimeSlot()) .in(Appointment::getStatus, Arrays.asList(0, 1)); Long count = appointmentMapper.selectCount(wrapper); if (count > 0) { return Result.error("该时段已被预约,请选择其他时间"); } Appointment appointment = new Appointment(); // set fields... appointmentMapper.insert(appointment); return Result.success("预约成功"); }这个写法在单机部署下基本够用,但它有一个极限情况:如果两个请求同时通过count校验,同时执行insert,依然存在都插入成功的可能。更稳的方式是在(counselor_id, appointment_date, time_slot)上加一个唯一索引,让数据库从物理层面兜底,双保险。
对于毕设来说,把“乐观锁 + 唯一索引”这套组合拳写进设计文档和答辩稿里,已经能体现出你对并发场景的理解了,比那些张口就说“Redis分布式锁”但没真正落地的人扎实得多。
3.4 在线咨询功能的简化实现
在线咨询这块,完整做音视频通话对毕设来说工作量太大,通常做文字聊天就足够了。实现方式有WebSocket、轮询两种路线。
WebSocket的优势是实时性高,消息延迟低,体验好;缺点是前端和后端都要处理连接状态,如果学生中途断网,还得做重连。我倾向于用WebSocket做在线咨询室,配合心跳机制。
SpringBoot集成WebSocket,核心就是一个处理器TextWebSocketHandler。当收到客户端消息时,解析消息体,保存到chat_message表,再推送给接收方。这里有一个关键细节:WebSocket的Session不能像HTTP请求一样随便从Controller里拿,必须自己维护一个在线连接的映射,用一个ConcurrentHashMap保存userId到Session的关系,从客户端消息里取fromUserId和toUserId来决定推给谁。
消息内容建议JSON格式,比如:
{ "type": "chat", "fromUserId": 1, "toUserId": 2, "content": "最近总是睡不好,感觉压力很大", "timestamp": "2024-05-01 10:30:00" }保存到数据库时,注意把content进行加密存储或至少做脱敏处理。未成年人心理聊天内容属于敏感数据,数据库里明文存放,一旦泄露就是事故。简单的AES对称加密就能挡住大多数内部泄露风险。这个点写进论文里,也是一个加分项。
如果不想引入WebSocket的复杂度,退而求其次用“轮询接口”也能实现基础聊天功能:前端每3秒拉取一次自己的未读消息列表。这个方案的优点是实现极其简单,后端就是普通的查询接口;缺点是实时性差、对服务器压力大。作为备选方案,在时间紧张的时候可以先用轮询顶上,答辩时不主动提实时性,把重点放在业务闭环上。
前端聊天页面,用Vue的watch监听消息列表变化,配合scrollIntoView让最新消息自动滚到底部。输入框发送消息后立即清空并调用发送接口,这个交互虽然简单,但确实是最影响体验的地方。
4. 实测开发中的坑与排查实录
4.1 反向计分题让我白白算错了一下午
这个坑我一定要单独说。第一版测评计算写完,我拿SAS量表的标准数据进行校验,发现怎么算标准分都对不上教科书上的参考范围。排查了很久,最后发现问题出在那5道反向计分题上。
SAS中第5题“我因为头痛、颈痛和背痛而苦恼”,学生在心理上对这个描述的感受越强,反而说明焦虑越重,但它在量表中是按反向计分设计的。也就是说,学生如果选了4分“很经常”,实际计分要按1分算入总分。漏掉这一步,算出来的原始分偏高,风险等级也随之失真。
所以我在代码里专门加了scoreType字段,题目录入时手动标记每道题的计分方向。这个问题也提醒所有做测评类项目的人:一定不要只看题目内容就默认“得分越高越焦虑”,必须先确认量表规则里是否存在反向计分题。
4.2 登录接口在部署后突然报401
开发环境一切正常,部署上线后前端登录接口却一直返回401。查了半天,发现是前端把请求发到了http://服务器IP:8080/api/user/login,后端确实也能响应,但Nginx那边配置了转发规则,把/api/前缀的请求转发到后端服务,后端接口本身没有/api前缀,转发后路径对不上,Sa-Token拦截器直接拦截了。
这个问题本质上是一次前后端路径约定的疏漏。解决办法是统一定义接口规范:后端所有Controller都加@RequestMapping("/api/xxx")前缀,前端请求路径统一以/api开头,Nginx再按前缀转发。三分约定清晰,后面所有接口都不会出类似问题。
4.3 测评数据量一大,页面响应越来越慢
测评记录表的数据量到了一定规模之后,学生查看历史测评记录的接口响应明显变慢。排查发现,最耗时的操作是查询每一条测评记录时,又根据scaleId去查量表名称。N+1查询问题,经典中的经典。
解决方案是用一次连表查询替代循环查询,或者用MyBatis-Plus的selectBatchIds先批量查出所有量表,再在内存里做映射。实际项目里我更倾向于每个列表接口都返回冗余字段,比如测评记录表直接存一个scale_name字段,查列表时少一次关联。适度冗余,是这类业务系统的常见取舍。
4.4 数据库时区问题导致预约时间错乱
学生预约的是上午9点,咨询师看到的却是下午17点。这个问题的根源是MySQL连接URL里没设置serverTimezone参数,或者设置成了UTC,导致Java时间和数据库时间差了8小时。
解决方案是在application.yml中配置:
spring: datasource: url: jdbc:mysql://localhost:3306/psy_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai顺便建议所有时间字段统一用datetime类型,不要用timestamp。timestamp有2038年问题和时区转换的坑,datetime配合后端统一使用LocalDateTime,是最省心的组合。
4.5 跨域配置与拦截器的冲突
前端在开发环境调后端,端口不同必然会遇到跨域问题。常规做法是在后端配置CORS:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*"); } }但如果你同时准备了自己写的拦截器,而且拦截器拦截了/**,那么OPTIONS预检请求也会被拦截器拦截,导致跨域配置失效。这个坑的根源是CORS配置执行顺序和拦截器执行顺序的冲突。
解决办法有两个:一是拦截器不对OPTIONS请求做拦截,判断请求方法并直接放行;二是使用Sa-Token自带的注解@SaIgnore配合CORS,避免预检请求走到登录校验逻辑。
开发阶段可以把跨域配置直接放在前端,用Vite的server.proxy代理转发:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }开发环境用代理,生产环境用Nginx反向代理,这两条路线整理清楚后,跨域问题基本就消失了。
5. 部署上线与答辩准备的实战建议
5.1 本地到云服务器的部署流程
毕设演示阶段,我建议至少把系统部署到一台云服务器上,别只停留在localhost。因为演示时老师可能会从自己的电脑访问,你总不能现场开开发环境。
部署的核心流程如下:
- 前端打包:执行
npm run build,生成dist目录,拷到服务器的Nginx静态目录,比如/usr/share/nginx/html。 - 后端打包:用Maven执行
mvn clean package,生成可执行JAR,上传到服务器。 - 安装环境:JDK 8/11、MySQL 8.0、Redis、Nginx。
- 启动后端:
nohup java -jar psy-system.jar --spring.profiles.active=prod > app.log 2>&1 &。 - 配置Nginx:
server { listen 80; server_name your_domain_or_ip; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里有一个关键点,try_files $uri $uri/ /index.html这一行。Vue是单页应用,前端路由切换时不会真的向服务器请求对应路径的HTML文件,刷新一个二级页面时,如果不加这个配置,Nginx会返回404。加了之后,所有未知路径都会回退到index.html,再由前端路由接管。
5.2 演示数据怎么造才真实
很多人的系统演示时显得“空”,是因为数据太少,或者数据一看就是随便填的。这个细节非常影响答辩效果。
建议至少准备以下演示数据:
- 学生账号:准备4到6个,覆盖不同风险等级的历史测评记录。
- 咨询师账号:2到3个,每个人的排班至少覆盖未来一周。
- 测评数据:针对一个学生准备两条历史记录,一条正常,一条高风险,方便展示预警列表。
- 文章内容:6篇以上心理科普文章,标题严肃专业,比如“考前焦虑的自我调节”“如何识别青少年的情绪信号”。
- 聊天记录:在咨询室留一组完整的对话,演示在线咨询时可以看到历史消息。
造数据的工具一般直接用SQL脚本写入,注意时间字段要有梯度,别全都集中在同一天。演示时老师看到的是一个有历史、有趋势、有内容的系统,而不是一个刚建完的空壳,印象分会好很多。
5.3 答辩前必须准备的技术问题
作为过来人,我整理几个这个选题下高频出现的答辩问题,提前准备就不会被问懵:
- 为什么用Sa-Token而不用Spring Security?直接回答:Sa-Token对中小型系统的开发效率更高,API简洁,支持Redis会话共享,满足本项目需求;Spring Security功能强大但配置复杂,团队开发中如果追求快速交付,Sa-Token更务实。
- 测评结果如何保证准确性?强调三点:题库采用标准量表、后端计算前重新从数据库加载题目、反向计分题做了单独处理。
- 高风险的学生的数据如何保护隐私?回答:测评详情只在咨询师端可见,全程需要登录态;监护人电话仅用于线下紧急联系,系统不主动向学生家长推送测评结果。
- 如果用户量变大了怎么办?可以从数据库加索引、Redis缓存热点数据、静态资源CDN、后端横向扩容这几个方向讲,体现可扩展的思考。
6. 最后想说几句实际的体会
做这个选题,我最大的体会是:一个看起来“平平无奇”的CRUD管理系统,只要在业务逻辑上深挖一个点,比如心理测评的计分规则、预约的并发控制、敏感数据的加密存储,整个项目的含金量就会明显不一样。答辩时老师更看重的是你有没有对问题的独立思考,而不是代码量有多少。
另外一个很朴素的建议:如果你是自己独立开发,一定要给自己留出至少一周的时间做整体联调和Bug修复。我见过太多人把开发周期排得太满,最后连演示环境都稳定不下来。系统做出来是一回事,能稳定地跑完十分钟演示是另一回事。把部署和演示数据放在开发计划里提前安排,别让最后一星期变成连续通宵。
这个系统后期还可以往两个方向扩展:一是加入语音倾诉入口,对接第三方语音识别能力;二是把测评数据做成可视化成长曲线,让学生看到自己一段时间内的心理状态变化。这些方向在公司实际做相关产品时也都是热门课题,做进去之后,无论求职还是继续深造,都是能写进简历的亮点。