ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue前后端分离未成年人心理健康咨询系统设计实战

SpringBoot+Vue前后端分离未成年人心理健康咨询系统设计实战

说实话,“计算机毕业设计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。因为演示时老师可能会从自己的电脑访问,你总不能现场开开发环境。

部署的核心流程如下:

  1. 前端打包:执行npm run build,生成dist目录,拷到服务器的Nginx静态目录,比如/usr/share/nginx/html。
  2. 后端打包:用Maven执行mvn clean package,生成可执行JAR,上传到服务器。
  3. 安装环境:JDK 8/11、MySQL 8.0、Redis、Nginx。
  4. 启动后端:nohup java -jar psy-system.jar --spring.profiles.active=prod > app.log 2>&1 &。
  5. 配置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修复。我见过太多人把开发周期排得太满,最后连演示环境都稳定不下来。系统做出来是一回事,能稳定地跑完十分钟演示是另一回事。把部署和演示数据放在开发计划里提前安排,别让最后一星期变成连续通宵。

这个系统后期还可以往两个方向扩展:一是加入语音倾诉入口,对接第三方语音识别能力;二是把测评数据做成可视化成长曲线,让学生看到自己一段时间内的心理状态变化。这些方向在公司实际做相关产品时也都是热门课题,做进去之后,无论求职还是继续深造,都是能写进简历的亮点。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进