前阵子帮某高校心理健康中心搭了一套基于 Spring Boot + Vue 高校心理健康系统,从需求梳理、模块设计到部署试点,整个过程比一般的信息管理系统要特殊不少。这个系统表面上就是“测评+预约+咨询记录”,真正做进去之后才会发现,难点全在用户角色边界、危机预警闭环和数据隐私上。这篇文章就把我的拆分思路、接口设计、并发处理和数据权限踩坑过程整理出来,给准备做类似项目的同行一个参考。
1. 高校心理健康管理的特殊场景:为什么不能用通用预约小程序拼一拼
1.1 核心用户与使用场景
高校心理健康系统的使用对象不是单一的“患者-医生”,而是“学生、辅导员、咨询师、心理中心管理员”这四个角色交叉协作。学生需要自助预约咨询、填写心理测评问卷、查看自己的预约记录;辅导员需要看到本学院学生的预警状态,但又不应该看到具体的咨询内容;咨询师需要管理自己的排班、填写咨询记录、做随访提醒;管理员则要负责量表配置、账号导入、数据统计和预警审核。
如果只做一个简单的预约小程序,学生约时间、咨询师确认,那确实两三天就能出来。但高校心理中心真正的业务链条是:学期初心理普查 → 系统批量生成测评任务 → 学生完成量表 → 系统按维度自动计分 → 达到阈值的学生生成预警 → 辅导员核实情况 → 心理中心评估干预 → 咨询师安排面谈 → 后续随访。这个闭环里的每个环节都可能因为一个字段、一个状态没设计好,导致线下表格满天飞。
另外还要考虑使用频率的波峰波谷。开学普查阶段可能几千人同时登录作答,平时又只有零星预约。系统在架构上没必要一上来就做高并发,但数据库设计、接口响应速度、批量导入方案必须提前规划,否则普查数据导入的时候就会把人逼疯。
1.2 校园环境特有的约束
校园场景有三个约束是普通商业项目里不太会遇到的。
第一个是身份数据来自统一身份认证。学生已经存在于学校数据中心,系统通常要先同步学号、姓名、学院、专业、年级这些基础信息,而不是让学生自己注册。自己注册就会带来审核成本和滋生小号的问题,而且心理中心后续做统计时数据质量会很难看。
第二个是敏感数据的可见范围特别讲究。同样一个测评结果,咨询师看得到原始分和维度分,辅导员只能看到“是否需要关注”的结论,学生本人看到的是友好版解释,不能把一堆因子分扔给非专业用户。这个不是技术做不到,而是业务规则必须在一开始就定清楚,否则后期每个页面都会因为权限问题反复改。
第三个是危机干预的责任边界。系统只要能找出“可能有风险的学生”,就必须提供下一步处理路径:由谁在什么时间内初步联系、是否需要转介、超时未处理系统怎么提醒。哪怕只是做给学校内部用,这个流程也要留下痕迹,否则出问题的时候系统反而成为责任认定的难点。
基于这些约束,我的选型思路从一开始就明确:后端用 Spring Boot 做稳定的业务服务和权限控制,前端用 Vue 做信息收集、表单填写和管理界面,中间通过 JWT 维护登录态,数据落在 MySQL。这套组合在高校信息中心里最常见,后续让校内老师接手维护也相对容易。
2. 技术底座与工程骨架:Spring Boot + Vue 的分工
2.1 后端为什么用 Spring Boot
Spring Boot 在这个场景下不是性能最优解,也不是代码最少的方案,但它有两点很契合。
一是生态足够成熟。高校系统绕不开 Excel 批量导入学生名册、邮件通知、定时任务、消息推送这几种能力。Spring Boot 里直接用 Easy Excel 或 POI 做导入导出,用 @Scheduled 做随访提醒和预警超时任务,用 Spring Data JPA 或 MyBatis-Plus 做数据访问,参考资料多,遇到问题随便一搜就有解决方案。对于一个需要长期维护、人员流动比较大的校内系统来说,这比引入 Node.js 或 Django 更稳妥。
二是权限体系好落地。心理系统最核心的安全问题是数据隔离,Spring Security 配合 JWT 可以很自然地实现基于角色的接口访问控制。虽然自己写一套拦截器也行,但 Spring Security 提供的过滤器链、注解权限校验、BCrypt 密码加密这些能力不需要重复造轮子,而且后续接学校统一身份认证时(多半是 CAS 或 OAuth2),Spring Security 也都有现成的适配方向。
我当时搭建后端工程时按这样的分包结构组织:
com.example.psy-server ├── common # 统一返回、异常处理、常量 ├── config # Security、MyBatis、Redis、定时任务配置 ├── controller # 接口层 ├── service # 业务逻辑 ├── mapper # 数据访问 ├── entity # 数据库实体 ├── dto # 前端交互对象 └── utils # 日期、脱敏、导出等工具这里有一点容易被忽略:entity 和 dto 必须分开。直接把数据库实体返回给前端,在心理系统里会非常危险,因为实体里可能有咨询记录备注、内部评估标记这些不该暴露的字段。哪怕只有一个字段不该返回,也建议显式定义 DTO,宁可多写几个转换方法。
2.2 前端为什么用 Vue 3 + Element Plus
前端我选了 Vue 3 加 Element Plus,理由很朴素:这类管理界面大多是表格、表单、抽屉、弹窗,Element Plus 组件成熟,能省下大量写样式和交互的时间。Vue 3 的 Composition API 在组织复杂表单状态时也比 Options API 舒服,比如测评页面的动态题目加载、计时、暂存答案,用 ref 和 computed 管理起来思路更清晰。
前端工程我按这样划分模块:
src ├── api # 所有后端请求 ├── components # 通用组件:上传、人员选择、量表卡片 ├── layouts # 布局:顶部导航+侧边栏 ├── router # 路由与权限守卫 ├── stores # 用户状态、菜单权限 └── views ├── student # 学生端:测评、预约、我的记录 ├── counselor # 咨询师端:排班、咨询记录、随访 ├── advisor # 辅导员端:预警列表、学生关注列表 └── admin # 管理端:量表配置、账号管理、数据统计前端有一个关键设计:路由和菜单根据登录用户的角色动态生成,而不是把所有页面都写在静态路由表里。比如辅导员前端只加载跟学生关注、预警确认相关的页面,咨询师前端加载排班、咨询记录页面。这样做的好处是减少前端暴露的接口范围,也降低低权限用户通过前端代码猜到后台管理路径的概率。配合路由守卫里的角色判断,实现大概是这样:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const role = localStorage.getItem('role') if (!token && to.path !== '/login') { next('/login') } else if (token && to.meta.roles && !to.meta.roles.includes(role)) { next('/403') } else { next() } })代码本身不复杂,但必须在联调前就定好“后端接口返回 403 后,前端跳转到什么页面”的约定,否则权限测试阶段会发现一堆接口确实校验了,但用户看到的是白屏。
2.3 数据库建模的几个关键点
心理系统的表结构看起来跟普通业务系统没啥两样,但在细节上要多想几层。核心表大致有这些:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| sys_user | 用户账号,教师和学生都在此表 | role_type、user_no、college、shift |
| assessment_scale | 量表定义 | scale_code、title、dimension_config |
| assessment_task | 测评任务,如“2025级新生入学普查” | task_name、publish_time、deadline |
| assessment_paper | 学生答题详情 | task_id、student_id、status、score_json |
| scale_answer | 具体每一题的答案 | paper_id、question_id、option_id |
| consultation_schedule | 咨询师排班时段 | counselor_id、start_time、end_time、is_booked |
| consultation_appointment | 学生预约记录 | student_id、schedule_id、status |
| consultation_record | 咨询记录 | appointment_id、summary、assessment |
| follow_up_task | 随访任务 | record_id、plan_date、done_status |
| warning_alert | 预警记录 | student_id、task_id、level、handle_status |
重点说两个细节。
一是测评结果不要只存一个总分。SCL-90、SDS、SAS 这类量表通常由多个维度构成,每个维度下有若干题,不同题的分值还要按选项对应权重计算。如果只在 assessment_paper 表里存一个总分,后续想做维度分析和导出报告就得重新解析原始答案,非常麻烦。一个比较实用的做法是:用 score_json 字段把每个维度的原始分、标准分、预警等级都序列化存起来,同时单独建一张 scale_answer 表保存每题答案,两份数据互为备份。JSON 字段虽然不适合做复杂查询,但读取报告场景足够用了。
二是学生档案和咨询记录不要强耦合。很多系统习惯把咨询记录挂在 student 表上,做成一条一对多关系。但心理中心实际上有时要接待的个案并不仅仅是测评预警的学生,也可能是学生自己主动预约的。所以预约记录应该挂 schedule 和 student 两个外键,咨询记录再挂预约记录,链路是“学生 → 预约时段 → 咨询记录 → 随访任务”,而不是“学生 → 咨询记录”。这样还能支持一个学生多次预约、多个咨询师分别记录的场景。
3. 测评、预约、预警:三个核心业务模块如何落地
3.1 心理测评:不只是问卷,重点是维度计分与解释规则
测评模块是心理系统的地基,也是我最初低估的部分。本来我以为无非是维护一套题目、学生作答、统计分数,结果发现量表计分规则远不只是“选A得1分”这么简单。
常见的量表计分方式包括:正向题和反向题不同分、多个维度独立计分、原始分换算标准分、按性别或年级常模判断阈值。比如一个量表有“抑郁倾向”“焦虑倾向”“敌对情绪”三个维度,每个维度 10 道题,有的题是正向计分,题上标“反向计分”,计算时要先反转分值再加总。不同维度的阈值也不一样,不能用一个总分一刀切。
因此我建议把量表的计分规则做成配置化的,而不是写在代码里。数据库里用两张配置表:
scale_question:题目、选项、所属维度、计分方向 scale_dimension:维度名称、包含题目数、计分公式、阈值后端在做计分时,先按维度把题目分组,根据每个题目的计分方向计算出维度原始分,再按维度阈值映射出风险等级。代码大致是这样:
public AssessmentScore calculate(ScalePaper paper) { Map<String, Integer> dimensionRaw = new HashMap<>(); for (ScaleAnswer answer : paper.getAnswers()) { ScaleQuestion q = answer.getQuestion(); int score = answer.getOptionScore(); if (q.getDirection() == 1) { // 反向题 score = q.getMaxScore() - score; } dimensionRaw.merge(q.getDimensionCode(), score, Integer::sum); } // 再根据阈值判断等级 }测评模块还有一个被反复问到的点:学生能不能在测评中途退出、之后接着做?从业务上最好允许,尤其是一些问卷要做 20 分钟以上。我的方案是:进入测评时生成 paper 草稿,每答一题即时保存到 scale_answer,最后点击提交时才计算分数。中间任何一次退出都保留草稿。这个即时保存的设计在后端实现上多几行代码,但对学生的体验和测评完成率帮助很大。
测评结果展示页同样要小心。学生端不能直接展示“你的 SCL-90 躯体化因子为 2.1,抑郁因子为 2.5”这样的专业术语,更不能用“重度风险”这种刺激性的结论。我当时的做法是:根据预警等级分层展示,绿色等级显示“目前心态较平稳”,黄色显示“近期情绪有波动,可预约咨询聊聊”,红色只显示“建议尽快预约中心咨询,也可以拨打中心电话”,把具体分数全部隐藏。这个设计是我们和心理中心老师反复确认过的:系统要引导学生求助,而不是让学生自我诊断。
3.2 咨询预约:时段的并发控制与爽约处理
预约模块是需求最明确、实现最直接的部分,但也是最容易在并发场景下出 bug 的地方。核心模型就是一张“排班时段表”,咨询师在后台每周生成一批可预约时段,每个时段只对应一个学生,学生点击预约后写预约记录并占用时段。
最常见的并发问题是这样的:热门时段比如周一下午 14:00-14:50,两个学生同时点预约,后端先查时段是否空闲,再写预约记录,结果两个学生都预约成功了。这个问题的根源是“先查再写”中间存在时间窗口,解决方案不是单纯加一条 if 判断,而是通过数据库层面保证原子性。
一种做法是利用排班表上的字段做乐观锁。给 consultation_schedule 表加一个 student_id 字段,默认 null,预约时执行:
update consultation_schedule set student_id = #{studentId}, status = 'BOOKED' where id = #{scheduleId} and student_id is null通过受影响行数判断是否预约成功,行数为 0 说明该时段已经被占用。这个方案在单体应用里足够可靠,也容易理解。如果预约时还要同时插入 appointment 记录,那就把“更新时段”和“插入预约”放在同一个 @Transactional 事务里,并对 schedule 表加行锁或唯一索引兜底。
用 Redis 做分布式锁也可以,但对大多数高校项目的体量来说有点过度设计。我更倾向于先用数据库条件更新解决,等并发量上升到足够影响用户体验时,再在时段维度加 Redis 锁也不迟。好的设计不是一上来就用最重的方案,而是预测业务增长曲线后选择最合适的那层。
爽约问题也需要系统配合。学生预约成功后不来,又不取消,会浪费咨询师时间。我在预约表里设计了状态流转:PENDING(待咨询)→ DONE(已完成)/ CANCELLED(已取消)/ NO_SHOW(爽约)。每天凌晨定时任务扫描“已过时段但仍处于 PENDING”的预约,自动标记为 NO_SHOW,并给学生发送站内信提醒。连续爽约三次的系统自动限制未来两周不能预约,这样能明显降低放鸽子比例。这个功能听起来不复杂,但不上线的系统里人工催查预约记录累死人。
3.3 预警闭环:从测评结果到辅导员介入的流程状态机
预警可能是整个系统里最不能出错的模块。它不只是一个“分数超过阈值就弹个红点”的简单规则,而是一个带状态流转的干预流程。
我的实现方式是为预警记录建立一套状态机,状态大致为:
PENDING → CONFIRMED → INTERVENED → CLOSED │ └→ MISFIRED(误报排除)测评任务批量收卷后,定时任务对每一份已提交答卷做等级判断,达到红色或部分黄色等级的就生成 warning_alert,状态为 PENDING。心理中心管理员登录后首先看到预警待办列表,逐条查看学生基本信息和测评维度摘要,判断是否确实需要关注。如果觉得该学生只是考试焦虑导致的本维度偏高,可以在系统里标记为 MISFIRED,备注原因;如果确认需要关注,状态变为 CONFIRMED,同时系统会给对应的辅导员发送一条待办提醒。
辅导员收到提醒后,需要在线填写“初步接触情况”,写下是否已面谈、学生情绪状态、是否需要转介心理中心。这一步完成时状态变为 INTERVENED。之后咨询师或管理员根据情况关闭预警,或者生成一轮随访任务。整个过程的关键是“谁在什么时间做了什么”都要有记录,因此每个状态变更都会写入一张 audit_log 表,包含操作人、操作时间、变更前后状态和备注。
别小看流程状态机带来的工作量。一开始我以为只需要一个 level 字段存红黄绿,后来发现在真实运行中,“看过但还没处理”“处理到一半”“转介了但咨询师还没接单”的状态是并存的,用单一等级字段根本表达不清楚。把状态拆出来之后,前端的待办列表、后端的统计报表都清晰了很多。
还有一点要特别注意:预警只是系统给专业人员的提示,不是给学生贴“问题学生”的标签。所以预警列表在辅导员端只显示“需学业生活关注”这个栏位,不显示具体测评分数和维度名称。心理中心管理员端的预警详情里虽然有完整数据,但页面也加了明显的提示文字“本结果仅供参考,不能作为临床诊断依据”。这个说明既是业务免责,也是一种负责任的产品设计。
4. 联调阶段最容易踩的坑:数据权限、会话过期、时区
4.1 数据权限的三种实现方式
高校心理系统的数据权限比普通后台管理系统复杂得多。普通后台通常只有“管理员-普通用户”两级,而这里要区分:管理员看全部、咨询师看名下个案、辅导员看本学院学生、学生只看自己。如果只在接口里判断“有没有权限访问这个接口”,那很容易出现“接口能调通但数据范围错了”的问题。
我实际采用的方式是“三层过滤”。第一层是 Spring Security 的注解权限,控制角色能否访问某个 URL。第二层是 Service 层里根据当前用户信息拼接数据范围条件。第三层是 MyBatis 拦截器动态拼接 SQL,保证整表查询时也不会越权。
举一个具体场景:辅导员要查自己学院学生的预警列表,Service 里需要这样拼条件:
// 当前用户是辅导员 if ("ADVISOR".equals(currentRole)) { queryWrapper.eq("s.college", currentUser.getCollege()); } if ("STUDENT".equals(currentRole)) { queryWrapper.eq("s.id", currentUser.getId()); }这种方式写起来比较啰嗦,但胜在直观,后续换人维护也看得懂。另一种做法是自定义一个数据权限注解,在 Mapper 层用拦截器统一注入权限 SQL。拦截器更优雅,但调试起来一旦有问题很难排查。我的建议是:如果项目时间紧、团队年轻,先把肉眼可见的 Service 层权限写好,再考虑拦截器。毕竟权限系统最忌讳藏得太深,一旦出问题没人敢改。
4.2 并发预约与 Redis 锁的具体处理
上文提到可以用数据库条件更新解决并发预约,这里补充我在联调时实际遇到的问题。当时我们在测试环境用 JMeter 模拟 20 个学生同时抢一个时段,发现数据库条件更新能拦住多余的预约,但记录到 appointment 表里仍然出现了重复。
原因在于“更新 schedule 表”和“插入 appointment 表”是两个独立操作,如果不加事务,可能出现两个人先后拿着一个“已经成功”的结果去执行后续操作。解决办法很简单:把更新排班表和插入预约表放到同一个事务方法里,并对预约表加唯一约束(schedule_id + student_id 联合唯一)。这样哪怕 update 判断异常,数据库的唯一索引也会兜底,插入第二条时抛异常,系统统一回滚。
Redis 锁的方案我也写了,不过只用在“取消预约并重新释放时段”的场景。因为取消操作往往伴随着“时段状态更新+预约记录状态更新+通知学生”三步,用 Redis 锁把整个操作串行化,可以避免学生手动刷新多次导致重复取消。核心代码类似:
String lockKey = "appointment:cancel:" + appointmentId; boolean locked = false; try { locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS); if (!locked) { throw new ServiceException("系统繁忙,请稍后重试"); } // 取消预约并释放时段 } finally { if (locked) redisTemplate.delete(lockKey); }10 秒的过期时间在这个场景下很宽裕,因为取消预约的操作正常不会超过 1 秒。如果过期时间设太短,操作刚好卡在临界点会导致锁失效;设太长,万一服务卡死,其他请求会一直碰锁。这个度要在联调时多压一下。
4.3 前后端联调中我花时间最多的问题
联调阶段最让人崩溃的问题往往不是业务逻辑,而是“后端查出来是对的,前端显示是错的”,或者反过来。
第一个高发问题就是时区。后端存的是 UTC 时间或数据库服务器的本地时间,前端浏览器所在时区是东八区,如果前后端没有约定好时间格式,前端拿到的字符串会差 8 个小时。预约功能尤其不能错,时段错乱会导致学生约到错误的咨询师排班。我的处理方式比较粗暴:所有后端接口的时间字段统一返回“yyyy-MM-dd HH:mm:ss”字符串,并且统一使用东八区时区。Jackson 全局配置加上:
spring.jackson.time-zone=GMT+8 spring.jackson.date-format=yyyy-MM-dd HH:mm:ss前端不再做二次格式转换,只在展示时按这个格式解析。这个方案虽然不够“国际化”,但对高校内部系统足够稳定,省掉很多时区扯皮。
第二个高发问题是长列表性能。测评完成后管理员要查看全院答题明细,往往会一次搜出几千条记录,直接渲染成表格,页面卡成 PPT。解决思路是后端做分页,前端配合表格的远程排序和远程筛选,只加载当前页的数据。同时姓名和学号字段要做脱敏展示,比如中间四位打星号,这样既保证管理员能核对到人,又避免无关人员在页面上直接复制到完整敏感信息。
第三个问题是会话过期后前端表现不一致。有的页面点击跳登录页,有的接口返回 401 后页面直接空白。我在前端 axios 拦截器里统一做了全局处理:任何接口返回 401,清空本地登录态并跳转到登录页;返回 403,弹提示“没有访问权限”。后端也在全局异常处理器里把所有权限异常统一归类,不能一会儿返回 401,一会儿返回 403,前后端约定不一致会浪费大量调试时间。
5. 上线前必须处理的数据隐私与危机干预安全清单
5.1 最小授权与脱敏显示
高校心理系统的数据属于敏感个人信息,上线前一定要把“最小授权原则”落实到每一个界面和接口上。所谓最小授权,就是每个角色只能看到完成本职工作必要的信息,多一个字段都不给。
实操中可以从四个层面检查:
- 接口字段层面:每个 DTO 只包含当前接口需要的字段。比如辅导员端的学生列表接口,只返回学生姓名(脱敏)、学院、专业、预警等级、最近跟进时间,不返回具体测评分数和咨询记录。
- 页面按钮层面:按角色隐藏“导出”“删除”“查看详情”这类按钮。比如辅导员列表不要提供导出完整名单的功能,管理员才有导出权限。
- 文件下载层面:导出 Excel 时如果包含学号、手机号,必须设置导出密码,或对文件本身加密。曾经有系统通过邮件把全班学生隐私数据发给辅导员,结果附件被转发到班级群,这种事故一定要避免。
- 日志层面:操作日志中禁止打印测评明细和咨询记录内容。我在日志框架里加了过滤规则,凡是标记为 SENSITIVE 的字段,输出时统一替换为 ***。
脱敏显示也需要做成工具方法。比如前端列表里的姓名显示“张**”,学号显示“2023****01”。后端负责真正的脱敏,而不是前端拿到完整数据后再打码,因为前端打码只是视觉效果,网络抓包还是能看到原始数据。
5.2 数据保留、归档与日志审计
学生数据保留多久、毕业后怎么处理,往往是需求文档里不会写、但真出事就特别显眼的问题。我的做法是给关键业务表统一加上 deleted 逻辑删除字段和审计字段:created_by、created_time、updated_by、updated_time。这样学生毕业时并不需要物理删除数据,只要调用一次“归档接口”,把相关学生的测评、预约、咨询记录全部标记为 ARCHIVED,然后在列表查询中默认过滤掉。
归档并不等于永久保存。学校内部一般会有数据管理条例,心理中心要根据实际情况设定保存期限。我做系统时留了一个定时清理任务参数,由管理员配置“档案保留年限”,到期自动匿名化:把姓名替换为随机编号、学号替换为随机串、学院和辅导员字段清空。匿名化之后的数据还可以用于统计分析和科研,但任何一次导出都无法还原到具体个人。
操作审计方面,所有敏感接口都要写操作日志:谁在什么时间查看或导出了哪些学生的测评结果、咨询记录。审计日志单独放一张表,不随业务主表一起归档,且日志只能追加、不能修改,后台管理界面也只提供查询,不提供编辑删除。当时我为了省事一开始没做审计日志,测试老师一句“我要追踪是谁导出了学生名单”就把我拉回来补上了,所以这里建议一开始就留好。
5.3 危机预警的兜底机制
预警模块上线后,最担心的不是误报,而是漏报和“报了没人管”。漏报的根源往往是测评作答质量差,比如学生随便选答案,系统把它识别为正常。我当时加了一个简单规则:答题总时长小于规定分钟数一半的答卷,标记为“疑似无效答卷”,不计入预警,但会提醒管理员审核。这样至少能拦住一部分刷题行为,避免统计数据被严重污染。
“报了没人管”的问题要靠超时升级机制解决。预警记录 PENDING 状态超过 48 小时未审核,系统自动给心理中心管理员发送站内信;CONFIRMED 状态超过 24 小时辅导员未填写初步接触信息,系统再次发送提醒,并抄送给分管学生工作的副院长账号。这个自动升级的逻辑用定时任务加时间戳判断就能实现:
@Scheduled(cron = "0 0 9 * * ?") public void checkWarningTimeout() { List<WarningAlert> pending = warningMapper.selectTimeoutPending(2); pending.forEach(alert -> notifyService.sendReminder(alert)); }同时还要考虑假期场景。测评任务如果在寒暑假期间开放或自动生成,预警出现后没有值班人员处理,系统就要把超时阈值自动放宽,或者直接发送短信给心理中心值班手机,而不能让“超时未处理”的告警石沉大海。这个细节是我和测试老师一起复盘时想到的,上线前一定得确认学校心理中心有没有假期值班安排,再设定自动升级的生效时间范围。
6. 复盘:先小步快跑,还是先追求齐全
如果让我重新做一遍,我会把项目推进节奏改成“两步走”:第一步只上线“信息采集+测评+预约”的最小闭环,第二步再上预警和随访。第一版先把学生身份同步、量表配置、测评作答、咨询预约这些主干功能跑顺,让心理中心的老师先拿真实数据体验一轮,收集他们对状态流转和字段命名习惯的反馈,再进入预警闭环的开发。
原因很简单,心理中心老师知道自己想要什么“结果”,但很难一次说清中间的“过程”。比如预警反馈表里到底要填什么字段,必须在老师们真正操作过一次之后才讨论得出来。如果我一口气把预警、随访、超时升级全做完,中间任何一个字段定义错了,返工成本极高,还会让老师对整个系统失去信任。
第二个建议是小规模试点之后再全校推开。我当时是先找了一个学院做试点,导入 300 名学生跑普查流程,重点观察测评提交成功率、预约爽约率和预警处理时长。试点阶段暴露出的问题非常多:部分学生用手机访问时下拉框组件在部分浏览器里显示异常、统一身份认证在高峰时段响应很慢、提前配置好的批次任务在日期边界上因为时区导致发布失败。这些问题在 300 人的规模下定位和修复,难度远低于全校几万人同时使用时手忙脚乱。
最后再分享一个体会:高校心理健康系统的核心不是技术多炫,而是让每个角色都觉得“用这个系统并没有增加我的工作负担”。学生不应该为了约一次咨询填三次个人信息,辅导员不应该每天登录三个后台去看待办,咨询师不应该在电子表格和系统里同步录入排班。只要有一个角色觉得系统是负担,整个项目就会被默默弃用。想要避免这种情况,就要在每个功能的边界上多问一句:这条数据到底该谁录入、谁修改、谁可见、留多久。把这些边界想明白了,Spring Boot 和 Vue 写起来反而顺畅,系统上线后的维护才算真正开始。