1. 项目背景与整体设计思路做校园心理咨询平台这个项目最初的想法并不复杂学校里心理中心一直有线下预约、纸质记录的传统模式学生不好意思直接走进咨询室老师整理档案也费劲。疫情之后线上沟通的习惯被彻底培养起来了学校方面就想着能不能做一个线上平台让学生可以匿名或实名预约咨询、做心理测评、查看科普内容咨询师可以在后台管理日程、查看测评结果、记录咨询档案。技术选型上后端用Spring Boot、前端用Vue这一点几乎是毫不犹豫的。Spring Boot在Java技术栈里已经是事实标准无论是自己写着练手、做毕业设计还是以后放到真实环境里跑这套组合都足够稳妥。Vue在国内的前端生态里普及率极高社区资料丰富遇到问题随便一搜就有答案。更重要的是Spring Boot加Vue这套前后端分离的架构正好贴合现在企业里主流的开发模式做完这个项目简历上写一笔面试官问起来你也能把完整流程讲清楚。这个项目从需求层面看核心要解决三个问题第一学生端需要一个低门槛、低压力的入口不能像教务系统那样冷冰冰第二咨询师端需要一套能管理预约、记录咨询过程的工具第三管理员需要能查看整体运行数据、管理用户和内容的权限。再加上心理测评、心理科普文章、留言反馈这些辅助模块整个平台的雏形就出来了。2. 系统功能模块拆解2.1 用户角色与权限设计校园心理咨询平台的用户角色我分成了四种学生、咨询师、管理员、系统超级管理员。权限设计这块是重中之重因为涉及心理咨询这种敏感场景数据访问边界必须清晰。学生角色拥有个人信息维护、心理测评填写与历史记录查看、咨询预约提交与取消、在线留言或匿名提问、查看心理科普文章、查看咨询师列表与排班情况。咨询师角色拥有个人排班设置、预约审核与确认、咨询记录填写、测评结果查看与分析、留言回复、个人案例归档管理。管理员角色拥有用户管理、咨询师资质审核、排班总览与调整、测评量表管理、文章内容管理、数据统计面板。超级管理员额外拥有系统配置、日志查看和角色分配权限。我用Spring Security加JWT做认证授权。JWT的token里只放userId、role这些必要信息过期时间设置为2小时前端拿到token后存在localStorage里每次请求通过axios拦截器自动附加到Authorization头。角色权限用注解控制比如PreAuthorize(hasRole(COUNSELOR))标注在咨询师相关的接口上防止学生直接调用接口越权访问。2.2 核心业务流程梳理咨询预约是平台最核心的流程我把它设计成了三个状态流转的闭环待审核、已确认、已完成外加一个取消状态。具体流程是这样的学生选择咨询师和时间段提交预约申请此时状态为待审核。咨询师收到通知后可以在预约管理里查看申请详情结合自己的安排选择同意或拒绝。同意后学生端会收到状态变更通知咨询时间和地点或者线上会议链接确定下来。咨询完成后咨询师需要填写咨询记录单包括学生的情绪状态、主要问题、建议方案等这份记录只有咨询师本人和管理员可见。心理测评这个模块走的是另一条独立的流程。学生选择测评量表比如SDS抑郁自评量表、SAS焦虑自评量表、SCL-90症状自评量表逐题作答后提交后端根据量表规则自动计算得分和等级生成测评报告。测评报告会给出参考解读同时提示本结果仅供参考不构成临床诊断。这个逻辑很重要既体现了专业性也做了风险规避。3. 后端Spring Boot核心模块实现3.1 项目初始化与依赖配置我用Spring Initializr生成的工程骨架Java版本选的8Spring Boot版本用2.7.x这个版本稳定且社区资料最全。很多人纠结要不要直接上Spring Boot 3我的建议是如果追求稳妥2.7.x是首选因为3.x要求Java 17起步部分老依赖兼容性容易出问题特别是你后面要集成一些生成报表或导出Excel的工具包时版本冲突会把你折磨得够呛。pom.xml里的核心依赖大概是这样dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version2.0.25/version /dependency dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version4.1.2/version /dependency /dependenciesJPA我用的是Spring Data JPA很多人纠结到底用MyBatis还是JPA我的看法是这个项目表结构相对清晰关联查询不算特别复杂JPA的自动建表和CRUD能力能省很多事。如果你更熟悉MyBatis完全可以替换功能上没影响不影响最终效果。3.2 数据库表结构设计数据库命名campus_psy一共设计了12张核心表。这里列几张关键的给你参考用户表sys_user包含id、username、passwordBCrypt加密存储、real_name、role、gender、student_no学号、phone、email、avatar、status、create_time。密码必须加密存储明文密码在真实项目中是特别危险的低级错误。咨询师信息表counselor_info包含id、user_id、title职称、specialty擅长领域、introduction个人简介、qualification资质证书编号、audit_status审核状态、work_years从业年限。预约表appointment包含id、student_id、counselor_id、appointment_date、time_slot时间段如09:00-10:00、type线下/线上、status、location、meeting_url、remark、create_time。状态字段用int存储0待审核、1已确认、2已完成、3已取消、4已拒绝。测评量表相关我拆成了三张表量表定义表scale、测评记录表assessment_record、测评答案表assessment_answer。这样设计的考虑是量表作为模板可以动态维护新增量表不需要改代码测评记录存储每次测评的总体结果答案明细单独存放避免单表数据量过大。3.3 后端接口设计与代码示例后端接口设计遵循RESTful风格统一返回结构。我定义了一个通用返回体Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }以预约接口为例Controller层的代码是这样的RestController RequestMapping(/api/appointment) public class AppointmentController { Autowired private AppointmentService appointmentService; PostMapping(/create) PreAuthorize(hasRole(STUDENT)) public Result? createAppointment(RequestBody AppointmentDTO dto) { // 当前登录用户信息从SecurityContext中获取 User currentUser (User) SecurityContextHolder.getContext() .getAuthentication().getPrincipal(); return appointmentService.createAppointment(dto, currentUser.getId()); } GetMapping(/my) PreAuthorize(hasRole(STUDENT)) public Result? getMyAppointments(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { Long userId getCurrentUserId(); return appointmentService.getMyAppointments(userId, page, size); } PostMapping(/cancel/{id}) PreAuthorize(hasRole(STUDENT)) public Result? cancelAppointment(PathVariable Long id) { return appointmentService.cancelAppointment(id); } }Service层的预约校验逻辑我写得很严谨有以下校验流程一是校验预约时间不能是过去时间这个用LocalDate.now()和传入的appointmentDate对比二是当天同一时间段不能重复预约查询数据库里是否有studentId date timeSlot status in (0,1)的记录三是咨询师在对应时间段是否可预约这就要关联咨询师的排班表counselor_schedule来判断四是每天预约数量做限制避免某个咨询师一天被约满而其他人空闲。这一块我当初踩过一个坑状态判断时漏掉了status为1已确认的情况结果学生可以多次预约同一个已经被确认的时间段后来在Service层加了一个专门的校验方法并且通过给表加联合唯一索引解决了一部分极端并发问题。3.4 心理测评模块的算法实现心理测评模块是整个平台的亮点功能实现逻辑其实不复杂关键在于量表规则的灵活配置。以SDS抑郁自评量表为例它包含20个题目其中10道正向计分题10道反向计分题每道题按1-4级评分。标准分的计算方法是粗分乘以1.25后取整数部分。我在ScaleService里封装了一个通用的评分引擎Service public class ScaleService { public AssessmentResult calculateScore(Scale scale, ListAnswerDTO answers) { int rawScore 0; for (AnswerDTO answer : answers) { Question question scale.getQuestions() .stream() .filter(q - q.getId().equals(answer.getQuestionId())) .findFirst() .orElseThrow(() - new BusinessException(题目不存在 answer.getQuestionId())); int score answer.getScore(); // 反向计分题5 - 得分 if (question.getReverseScore()) { score 5 - score; } rawScore score; } double standardScore rawScore * 1.25; // 根据标准分判定等级 String level judgeLevel(scale.getType(), (int) standardScore); return new AssessmentResult(rawScore, (int) standardScore, level, generateAdvice(scale.getType(), level)); } }为了做到量表可动态配置我把题目、选项、计分规则都存到了数据库里。管理员在后台可以新增量表、维护题目、设置正反向计分。这样做的价值在后续扩展时就会体现出来——学校如果后续想加一个大学生人格问卷UPI后台配置一下就行不用动一行代码。测评报告生成后我会做一次访问权限校验学生只能看自己的报告咨询师要看的话必须通过咨询预约关联才能访问并且操作日志里会记录访问记录。这个权限控制细节在做答辩展示或者面试讲项目时都能成为加分项。4. 前端Vue实现与界面交互4.1 Vue环境搭建与项目结构前端用的Vue 2 Element UI组合没有上Vue 3主要原因是Element UI对Vue 2的支持最稳定资料也最全。如果你不想折腾用Vue 3加Element Plus也不是不行但你要有心理准备部分组件的API有变化网上搜到的老代码未必能直接跑通。环境搭建步骤明确说一下初学者经常在这一步卡住# 安装Vue CLI npm install -g vue/cli # 创建项目 vue create campus-psy-web # 进入项目目录 cd campus-psy-web # 安装Element UI npm install element-ui --save # 安装axios和路由 npm install axios vue-router3 --save # 安装状态管理 npm install vuex3 --save # 启动开发服务器 npm run serve需要特别提醒的是Vue 2项目必须用vue-router3和vuex3如果你不指定版本直接npm install vue-router它会默认给你装4.x版本在Vue 2项目里跑起来全是报错这个问题在Vue社区里属于经典新手坑我当初也被坑过一次后来养成了装依赖时指定大版本号的好习惯。项目目录结构我按模块划分而不是按类型划分这样多人协作时找文件更方便src/ ├── api/ # 接口请求封装 │ ├── request.js # axios实例封装 │ ├── auth.js # 认证相关接口 │ ├── appointment.js # 预约相关接口 │ └── assessment.js # 测评相关接口 ├── assets/ # 静态资源 ├── components/ # 公共组件 │ ├── HeaderNav.vue │ └── FooterInfo.vue ├── router/ # 路由配置 │ └── index.js ├── store/ # Vuex状态管理 │ └── index.js ├── views/ # 页面组件 │ ├── student/ # 学生端页面 │ │ ├── Home.vue │ │ ├── CounselorList.vue │ │ ├── AppointmentCreate.vue │ │ ├── AppointmentList.vue │ │ ├── AssessmentList.vue │ │ └── AssessmentDetail.vue │ ├── counselor/ # 咨询师端页面 │ │ ├── ScheduleManage.vue │ │ ├── AppointmentAudit.vue │ │ └── RecordWrite.vue │ └── admin/ # 管理端页面 │ ├── UserManage.vue │ ├── ScaleManage.vue │ └── ArticleManage.vue ├── App.vue └── main.js4.2 路由守卫与登录状态管理前端路由守卫是控制页面访问权限的第一道关卡。我在router/index.js里注册了全局前置守卫每次路由跳转前检查当前用户有没有登录再根据路由meta里配置的角色信息判断是否有权限访问。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() return } if (!token) { next(/login) return } const userRole store.state.user.role if (to.meta.roles to.meta.roles.indexOf(userRole) -1) { next(/403) return } next() })路由表里每个页面配置meta信息比如咨询师管理页面的meta是这样写的{ path: /counselor/schedule, name: ScheduleManage, component: () import(/views/counselor/ScheduleManage.vue), meta: { roles: [COUNSELOR, ADMIN] } }这里用到的是路由懒加载() import()的形式会在访问到对应路径时才开始加载组件首屏加载速度会明显更快。Vue的按需加载配合Webpack的代码分割开发体验和用户体验都有保障。4.3 前端API封装与拦截器配置axios请求封装我是单独抽了一个request.js文件好处是所有接口请求都统一经过拦截器处理token附加和错误提示。import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器 service.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }, error { return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( response { const res response.data if (res.code 200) { return res } else { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } }, error { if (error.response error.response.status 401) { // token过期跳转登录页 localStorage.removeItem(token) router.push(/login) Message.error(登录状态已过期请重新登录) } else { Message.error(网络请求异常请稍后重试) } return Promise.reject(error) } ) export default service这里有个细节开发环境下需要配置Vue的代理转发把/api开头的请求代理到后端8080端口。在vue.config.js里配置module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }如果你不做代理配置直接在前端请求http://localhost:8080/api/xxx会出现跨域问题需要在后端配CORS两边都麻烦。用代理的方式浏览器看到的是同源请求后端也不用额外配CORS开发体验顺滑很多。4.4 核心页面的交互实现咨询师列表页是一个典型的列表加条件筛选页面。我用了Element UI的卡片布局展示咨询师信息包括头像、姓名、职称、擅长领域、个人简介右侧显示可预约的时间段。预约按钮点击后弹出对话框选择具体日期和时间段提交预约。这里有一个前端交互细节值得说说咨询师的可预约时间段是根据后端返回的排班数据实时计算的。前端拿到排班列表之后还需要过滤掉已经被他人预约的时间段这个逻辑不能只靠前端控制后端预约接口仍然要做兜底校验。前后端双重校验是开发习惯问题不能偷懒只做一端。心理测评页面的体验我下了不少功夫。答题时左侧显示题目列表右侧展示当前题目支持上一题下一题切换进度条实时显示完成百分比。所有题目答完后才能点击提交如果有没有答的题目前端会提示具体是第几题没答。测评报告页面用雷达图展示各维度得分这个用了ECharts图表库配合Vue的响应式数据数据更新时图表自动刷新。5. 部署上线与运维经验5.1 服务器环境准备项目本地开发调试完毕后我打包部署到了阿里云的一台2核4G服务器上操作系统用的CentOS 7。部署架构是Nginx托管前端静态文件加反向代理后端接口后端jar包用systemd守护进程管理。先说一下环境准备步骤每一步都有对应的坑要注意# 安装JDK 8 yum install -y java-1.8.0-openjdk # 安装Nginx yum install -y nginx # 安装MySQL 5.7 wget https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm rpm -ivh mysql57-community-release-el7-11.noarch.rpm yum install -y mysql-community-server5.2 前端打包与Nginx配置前端打包命令很简单npm run build产物在dist目录下。把dist目录上传到服务器/usr/share/nginx/html目录下然后修改Nginx配置server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; # 前端路由history模式需要配置 location / { 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 Router如果用history模式刷新页面时如果直接访问/counselor/appointment这样的路径Nginx会尝试找这个文件找不到就返回404加上这行配置后所有请求都会回退到index.html由前端路由接管。5.3 后端jar包部署与systemd配置后端打jar包用Maven命令mvn clean package -DskipTests生成的jar包上传到/opt/campus-psy目录下。然后创建一个systemd服务文件[Unit] DescriptionCampus Psychology Platform Afternetwork.target [Service] Typesimple Userroot WorkingDirectory/opt/campus-psy ExecStart/usr/bin/java -Xms512m -Xmx1024m -jar campus-psy.jar Restarton-failure RestartSec10 [Install] WantedBymulti-user.target配置好之后执行systemctl daemon-reload systemctl enable campus-psy systemctl start campus-psy通过systemd管理的好处是如果程序崩溃它会自动拉起服务器重启后服务也会自动启动不需要手动干预。部署时的数据库配置要单独说我用的是Spring Boot的多环境配置文件。application-dev.yml配本地开发库application-prod.yml配服务器上的库。启动的时候通过--spring.profiles.activeprod参数指定使用的环境。数据库账号密码不要硬编码在配置里我是通过环境变量的方式注入的spring: datasource: url: jdbc:mysql://${DB_HOST:localhost}:3306/campus_psy?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: ${DB_USERNAME} password: ${DB_PASSWORD}6. 常见问题与排查技巧实录6.1 跨域问题排查前后端分离项目里跨域问题是最常遇到的尤其是初学者。我这里给你一个快速排查思路所有问题的排查时间都会大幅缩短第一看浏览器F12的Network面板找到请求看控制台报错信息。提示CORS policy相关错误说明跨域确实被拦截了第二确认前端是通过代理方式请求还是直接请求后端地址代理方式不会报CORS错误第三检查后端是否配置了CORS过滤器或者CrossOrigin注解。我的建议是开发环境用Vue代理解决生产环境用Nginx反向代理解决后端不需要配置CORS这样架构最干净。如果你后端同时配了CORSNginx那边也做代理可能会因为预检请求OPTIONS请求处理不当出一些奇怪的问题比如带自定义Header的请求被拦截。6.2 JWT token失效与Security上下文问题Spring Security集成JWT时我遇到过一个非常隐蔽的问题用户登录之后部分接口能正常访问部分接口报401。排查了很久才发现是因为JWT过滤器对白名单路径外的所有请求都做了token校验但是某些请求的Authorization头里token带的是token前缀不是Bearer前缀导致解析失败。过滤器里解析token的代码我改成了兼容两种写法String header request.getHeader(Authorization); if (header ! null header.startsWith(Bearer )) { token header.substring(7); } else if (header ! null header.startsWith(token )) { token header.substring(6); }另一个问题是通过SecurityContextHolder.getContext().getAuthentication()获取用户信息时如果过滤器里解析token失败Authentication对象为null后面Controller里直接强转会报空指针异常。解决方法是封装一个工具类获取用户信息时先判断Authentication是否为空为空就抛出统一的业务异常而不是让NPE堆栈直接暴露给前端。6.3 数据库连接池连接耗尽项目上线运行了大约两周后运维反馈说平台偶尔会出现接口响应超时的情况查看日志发现大量HikariPool-1 - Connection is not available, request timed out after 30000ms的报错。分析下来根本原因是有一个定时任务在做报表统计时循环里执行了多次数据库查询每次查询都手动获取连接但没有及时释放。虽然用了try-with-resources但你架不住并发量一高连接池里的连接被占满新的请求拿不到连接就开始排队等待。解决方案有三步第一排查代码里是否有连接泄漏的地方确保所有数据库操作都通过JPA或MyBatis的托管机制不要手动获取连接第二调整HikariCP连接池参数最大连接数从默认的10调整到20连接超时时间从30秒调整到10秒第三给定时任务增加了分布式锁避免多实例部署时重复执行。6.4 前端打包后页面空白前端npm run build之后部署到Nginx打开页面发现白屏控制台报Uncaught SyntaxError: Unexpected token 。这个错误在网上搜索量很大本质原因是静态资源请求路径不对。默认打包后index.html里引用的JS和CSS路径是/js/app.js这种绝对路径如果部署在服务器根目录没问题但如果部署在某个子路径下请求就会打到后端接口上返回的是HTML而不是JS文件浏览器解析JS时就会报这个语法错误。解决办法是在vue.config.js里配置publicPathmodule.exports { publicPath: process.env.NODE_ENV production ? / : /, outputDir: dist, assetsDir: static }如果部署在子目录就把publicPath改成子目录路径比如/campus-psy/。这个配置细节在面试时也可以作为Vue项目的亮点来聊说明你踩过坑并且真正理解了打包机制。7. 实际运行效果与扩展思考平台做完之后我给学校心理中心的老师演示了一遍整体反馈是正向的。学生端最受欢迎的功能是心理测评和匿名提问很多学生不想暴露身份但又确实有一些困惑需要专业意见。咨询师端反馈最好的是排班管理功能以前都是纸质排班表现在线上调整一目了然系统会自动检测时间冲突排班效率提升明显。从技术角度复盘Spring Boot加Vue这套组合做这类管理系统的效率确实很高从零开始到核心功能跑通两周左右就能完成主体开发。这个项目后续做扩展也相对方便比如可以增加在线视频咨询模块用WebRTC技术实现浏览器端的实时视频通话可以对接学校的统一身份认证系统学生用学号直接登录不用单独注册可以考虑用WebSocket做消息推送预约状态变更实时通知到客户端。常见问题速查表整理如下问题现象可能原因解决方案前端请求接口报404路由或接口路径不匹配检查后端Controller的RequestMapper路径检查前端api文件里的URL登录后接口返回403角色权限不足或token异常检查PreAuthorize注解检查token是否正确解析预约时提示时间段已满并发预约冲突数据库加唯一索引Service层做同步校验打包后图片不显示publicPath配置错误修改vue.config.js里的publicPath服务器上接口响应慢数据库连接池耗尽或慢查询优化SQL调整连接池参数加索引测评分数计算错误正反向计分设置错误核对量表规则检查Question表的reverse_score字段Element UI组件样式错乱版本不兼容确认element-ui版本检查main.js引入方式根据我个人经验这类项目最容易出问题的不是某个单一技术点而是多个模块之间的数据流转和状态同步。预约状态从待审核到已完成中间涉及前端状态显示、后端状态变更、运营通知触达任何一个环节断了用户体验都会打折。所以写代码的时候要有一个全局视角不只是把自己的模块写完就结束还要考虑下游怎么用你提供的数据。最后分享一个开发时的小习惯每次后端接口写完我都用Postman或者Apifox把接口测一遍把请求参数、返回结果截图存到项目的docs目录下。这不算额外工作量但后续写接口文档、做结题报告、甚至面试复盘时这些都是最真实的素材。前端页面每做完一个模块我也会在本地跑一遍完整流程从登录到操作到退出确保串联畅通再进入下一个模块的开发。项目开发从不是一蹴而就的但只要把每个细节做扎实最后交付的成果一定会超出预期。
