☰
心理健康测评小程序开发:从SCL-90量表到Spring Boot系统架构
2026/10/1 17:55:37 网站建设 项目流程

简介:面向计算机相关专业毕业设计场景的微信小程序心理健康测评管理系统,基于Java与微信小程序开发,包含完整前后端代码与毕业论文文档,适合需要快速搭建校园心理健康服务平台的学生参考学习。系统覆盖咨询师管理、用户管理、心理健康分析、咨询师预约、心理测评、题目库维护、通知公告、论坛、字典值及轮播图配置等模块,可支撑从测评发布到结果分析的完整业务链路,帮助理解校园心理健康平台的开发思路。压缩包共1508个文件,大小约41.99MB,以Vue前端组件、Java后端逻辑、WXML/WXSS小程序页面、JS交互脚本及SQL数据库脚本等类型为主,目录结构清晰,便于按模块查阅。资源已有79人学习,除源代码外还附有配套论文与说明文档,能帮助读者快速梳理系统设计、数据库表结构及核心功能实现,同时包含数据库脚本与后端服务配置,可直接导入运行或按需二次开发,适合作为课程设计或毕业设计的参考模板。

1. 心理健康测评小程序从哪来:一个毕业设计题目背后的真实需求

高校心理中心每年新生入学都要做一轮心理健康筛查,传统做法是发纸质量表,SCL-90 一共 90 道题,5000 份问卷从发放、回收、录入到统计,心理中心的老师要忙两三周,而且录入环节还容易出错。把量表搬进微信小程序之后,学生手机答题 10 分钟搞定,后台自动判分、自动生成预警名单,辅导员按班级查看测评报告——这就是这个管理系统要解决的核心问题。

这个题目表面是个毕业设计,实际覆盖了一条完整的软件链路:微信小程序做答题端、Java 后端做判分引擎和数据处理、MySQL 存储量表与测评记录,最后还要配一篇能过查重的论文。适合正在选毕业设计题目的计算机专业学生,也适合想在校内搭建心理测评平台但预算有限的技术人员。我接下来的方案全部围绕「低成本、能演示、可扩展」来展开,先把架构立住,再逐层实现。

2. 技术选型与系统分层:Spring Boot 后端 + 原生微信小程序的取舍

做毕业设计最怕的不是功能多,而是选型上眼高手低:用了记不住的技术栈,答辩被问住;或者用了太老的技术,面试官不认可。心理健康测评系统这个题目,技术选型要同时照顾三个目标——开发效率、演示效果、论文可写性。

2.1 后端框架对比:Spring Boot、SSM 与纯 Servlet 谁适合毕业设计

后端选型上,我见过三类方案:纯 Servlet + JSP、SSM(Spring + Spring MVC + MyBatis)、Spring Boot + MyBatis Plus。纯 Servlet 适合课程设计,写两个 Servlet 就能交差,但要实现测评判分、用户体系、历史记录这些完整业务,代码量会失控,答辩时也显得单薄。SSM 是前几年毕业设计的主流,配置繁琐,光是 XML 配置就能写几百行,现在再选 SSM 属于给自己挖坑。

Spring Boot 是目前最稳的选择:内嵌 Tomcat,一个 jar 包跑起来,不用单独装容器;自动配置省掉了 Spring 的 XML 配置;而且 Spring Boot 的知识点在 java 基础面试里经常被问到,做完这个项目能顺带把面试八股文里的 IoC、AOP、自动配置这些概念串起来。MyBatis Plus 比原生 MyBatis 更省事,单表 CRUD 不用写 SQL,内置分页插件,论文里的「数据访问层」章节也好写。

依赖版本我一般建议用 Spring Boot 2.7.x,不是最新的 3.x 一定更好,而是 2.7.x 的资料多、遇到报错容易搜到解决方案,对毕业设计来说稳定比新特性重要。JDK 用 8 或 11 都行,Maven 项目结构是标准的三层:controller 接收小程序请求、service 处理业务逻辑、mapper 操作数据库。

2.2 小程序端选型:原生框架还是 uni-app

小程序端也有两条路:微信原生小程序框架和 uni-app 跨端框架。原生框架的好处是调试直接,微信开发者工具打开就能跑,组件和 API 的报错信息在全网都能搜到,课程设计阶段的代码量完全在可控范围内。用原生框架还有一个现实好处:答辩演示时微信开发者工具就是现成的演示环境,评委扫码就能真机预览。

uni-app 的卖点是「一套代码多端运行」,但代价是多一层编译链路,Vue 语法和微信原生语法之间的差异会在踩坑时消耗大量时间。如果你确实想顺手出一套安卓 App 或者鸿蒙应用,uni-app 值得考虑;如果题目只要求微信小程序,我的建议是原生,把省下来的时间用来打磨测评逻辑和论文。另外一个很现实的考量是,原生小程序可以直接用微信官方的<radio-group>组件实现单选题,90 道量表题的数据结构天然适合用一个数组变量管理。

2.3 系统分层与请求数据流

系统的整体结构分成三层:小程序客户端、Java 后端服务、MySQL 数据库。小程序负责答题交互和结果展示,后端负责量表下发、判分、结果存储、预警处理,数据库负责持久化。各模块职责如下表。

模块核心职责关键技术点
小程序端登录授权、答题、报告展示、历史查询wx.request 封装、radio-group 单选框、路由跳转
后端 Controller接收请求、参数校验、统一返回格式@RestController、全局异常处理
后端 Service量表题目下发、判分算法、预警规则事务管理、工厂模式拆分量表类型
后端 Mapper用户、题目、记录、结果表的增删改查MyBatis Plus BaseMapper
数据库持久化业务数据6 张核心表,外键逻辑关联

请求链路是:小程序 wx.request 发送请求带 token → Spring Boot 拦截器校验用户身份 → Controller 接收参数 → Service 处理业务 → Mapper 查库返回 → Controller 包装成统一 JSON → 小程序渲染。这条链路在论文里画成时序图,答辩时能清晰讲出每一步的数据变化,比堆砌功能列表更有说服力。

3. 数据库设计与量表落库:SCL-90 的 6 张核心表怎么建

数据库是整个测评系统的地基,量表题目怎么存、测评结果怎么算、预警记录怎么联动,全看表结构设计。很多同学上来就把 90 道题写死在代码里,这是最糟糕的做法——换一份量表就得改 Java 代码,这也是我在第 3 章重点讲量表配置化的原因。

3.1 功能边界决定表结构:用户、测评、结果与预警

先梳理系统涉及的角色:学生(被测者)、辅导员(查看班级报告)、心理中心管理员(系统管理)。围绕三个角色,业务可以拆成四个模块:用户管理、测评管理、报告管理、预警管理。对应的核心表有 6 张:学生表 student、辅导员表 counselor、量表表 questionnaire、题目表 question、测评记录表 assessment_record、测评结果表 assessment_result。如果要做预警名单,再加一张 warning_record 预警记录表。

量表和题目的关系是一对多:一张量表有 90 道题(SCL-90),每道题属于一个因子维度,比如「躯体化」「强迫症状」「人际关系敏感」等。SCL-90 的每题有 5 个选项,从「没有」到「严重」,分别记 1 到 5 分。这里的关键是把因子名称作为题目的一个字段 question 表里的 factor 字段存起来,判分时按因子分组聚合,就能算出 10 个因子分。

测评记录表存每次答题的会话信息,包括什么时候开始、什么时候结束、状态是否完成。测评结果表存判分后的总分、阳性项目数和 10 个因子分。把记录和结果拆成两张表,是为了支持「中途退出后可以继续答题」这个场景——记录表先写入一条未完成记录,全部答完后再写结果表,这个设计后面避坑章节还会详细讲。

3.2 六张核心表的建表 SQL

下面给出这批表的建表 SQL,直接复制到 MySQL 里执行即可。我统一使用 utf8mb4 字符集,避免小程序端传中文时出现乱码。

-- 学生表 CREATE TABLE `student` ( `id` BIGINT PRIMARY KEY COMMENT '学号', `name` VARCHAR(20) NOT NULL COMMENT '姓名', `gender` TINYINT DEFAULT NULL COMMENT '性别:1男 2女', `class_name` VARCHAR(50) DEFAULT NULL COMMENT '班级', `phone` VARCHAR(11) DEFAULT NULL COMMENT '手机号', `openid` VARCHAR(64) DEFAULT NULL COMMENT '微信openid,同一用户唯一', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生表';
-- 量表配置表 CREATE TABLE `questionnaire` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `code` VARCHAR(20) NOT NULL COMMENT '量表编码,如SCL-90', `name` VARCHAR(50) NOT NULL COMMENT '量表名称', `description` VARCHAR(255) DEFAULT NULL COMMENT '量表说明', `question_total` INT DEFAULT 0 COMMENT '题量', `status` TINYINT DEFAULT 1 COMMENT '1启用 0停用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='量表配置表'; -- 题目表 CREATE TABLE `question` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `questionnaire_id` INT NOT NULL COMMENT '所属量表ID', `sort_no` INT NOT NULL COMMENT '题号', `content` VARCHAR(255) NOT NULL COMMENT '题目内容', `factor` VARCHAR(30) NOT NULL COMMENT '所属因子维度', `option_count` TINYINT DEFAULT 5 COMMENT '选项数,SCL-90默认5' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='量表题目表';
-- 测评记录表 CREATE TABLE `assessment_record` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `student_id` BIGINT NOT NULL COMMENT '学号', `questionnaire_id` INT NOT NULL COMMENT '量表ID', `status` TINYINT DEFAULT 0 COMMENT '0答题中 1已完成', `start_time` DATETIME DEFAULT NULL, `end_time` DATETIME DEFAULT NULL, KEY `idx_student` (`student_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评记录表'; -- 测评结果表 CREATE TABLE `assessment_result` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `record_id` BIGINT NOT NULL COMMENT '关联测评记录ID', `student_id` BIGINT NOT NULL COMMENT '学号', `total_score` INT NOT NULL COMMENT '量表总分', `positive_count` INT NOT NULL COMMENT '阳性项目数', `factor_json` JSON DEFAULT NULL COMMENT '十个因子分,JSON格式', `alert_level` TINYINT DEFAULT 0 COMMENT '预警等级 0正常 1关注 2预警', `result_text` VARCHAR(500) DEFAULT NULL COMMENT '结果描述', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评结果表';

学生表的主键直接用学号,避免自增 ID 和业务脱节;openid 加唯一索引是因为微信登录需要靠 openid 识别用户。题目表把 factor 字段直接冗余在题上,而不是单独建一张因子表,是为了判分时减少一次关联查询——90 道题的量表,一次查题直接带上因子信息,后端在内存里分组即可。factor_json 字段用 MySQL 5.7 以上的 JSON 类型存储十个因子分,比拆 10 个字段更灵活,也更好应对后续扩展 SDS 等其他量表。

3.3 量表题目做成数据配置,新增问卷不用改 Java

把题目从代码里抽到数据库表,设计意图是:换量表只改数据不改代码。SCL-90 的 90 道题,按题号顺序插入 question 表,每道题的 factor 字段填好对应因子,比如第 1 题「头痛」属于躯体化因子,第 38 题「受伤」属于偏执因子。判分引擎则根据因子名称动态分组。

-- 插入SQL示例:SCL-90的前5题,实际写入需要90条 INSERT INTO `question` (`questionnaire_id`, `sort_no`, `content`, `factor`) VALUES (1, 1, '头痛', '躯体化'), (1, 2, '神经过敏,心中不踏实', '强迫症状'), (1, 3, '头脑中有不必要的想法或字句盘旋', '强迫症状'), (1, 4, '头晕或昏倒', '躯体化'), (1, 5, '对异性的兴趣减退', '抑郁');

批量插入时可以用 Navicat 的导入功能,从 Excel 粘贴 90 行数据,注意 sort_no 从 1 到 90 连续编号。question_total 字段在插入后可以用一条UPDATE questionnaire SET question_total = 90 WHERE id = 1回填。这样做的工程意义是:以后量表中心想加一个 SDS 抑郁自评量表(20 题),只需要新增一条 questionnaire 记录和 20 条 question 记录,测评模块自动适配题型和判分维度,论文里可以把这一点作为「系统可扩展性设计」的亮点来写。

4. 后端测评接口实现:题目下发与 SCL-90 因子分计算

后端是测评系统的核心,承担两类关键工作:把题目按量表配置下发到小程序,以及接收学生答案完成判分。判分不是简单的求和,SCL-90 的结果解析涉及总分、阳性项目数和因子均分三个维度,这套逻辑写清楚,论文的「核心算法设计」章节就有内容可写了。

4.1 测评接口的契约设计:先定 JSON 再写代码

前后端联调最容易出问题的地方是接口格式不统一,所以在写代码之前先把契约定死。我用两个接口完成一次完整测评:开始测评接口下发题目,提交答案接口返回结果。

开始测评接口的请求参数是量表 ID,响应返回量表名称、题量、题目列表,JSON 结构设计如下:

{ "code": 200, "message": "success", "data": { "recordId": 10001, "questionnaireName": "SCL-90 症状自评量表", "total": 90, "questions": [ { "id": 1, "sortNo": 1, "content": "头痛", "factor": "躯体化", "options": [ {"label": "没有", "score": 1}, {"label": "很轻", "score": 2}, {"label": "中等", "score": 3}, {"label": "偏重", "score": 4}, {"label": "严重", "score": 5} ]} ] } }

options 不存数据库,而是后端在返回题目时统一拼装 5 个选项,好处是选项的显示文案和分值逻辑只维护在一处。recordId 是学生点击「开始测评」时后端创建的一条 assessment_record 记录,之后每次提交答案都带着这个 ID,最后汇总判分时用。提交答案接口的请求结构是:recordId、答题总时长、答案数组,其中答案数组里每个元素包含 questionId 和 score,score 就是学生勾选的「没有/很轻/中等/偏重/严重」对应的 1 到 5 分。

4.2 题目下发接口:Controller-Service-Mapper 三层实现

后端用 Spring Boot 组织代码,Controller 层只做参数接收和结果封装,Service 层写业务逻辑。以「开始测评」接口为例,完整代码分三层展示。

@RestController @RequestMapping("/api/assessment") public class AssessmentController { @Autowired private AssessmentService assessmentService; /** * 开始测评:创建测评记录并下发题目 * @param questionnaireId 量表ID * @param studentId 学号 */ @GetMapping("/start") public Result<StartQuizVO> start(@RequestParam Long questionnaireId, @RequestParam Long studentId) { if (questionnaireId == null || studentId == null) { return Result.error("参数不能为空"); } return Result.success(assessmentService.startQuiz(questionnaireId, studentId)); } }

Controller 里做了基础的参数非空校验,返回结构用统一的 Result 包装类,里面包含 code、message、data 三个字段。把 Result 做成泛型类,不同接口复用同一套返回格式,小程序端拦截器只需要解析一次结构。

@Service public class AssessmentServiceImpl implements AssessmentService { @Autowired private QuestionMapper questionMapper; @Autowired private RecordMapper recordMapper; @Override public StartQuizVO startQuiz(Long questionnaireId, Long studentId) { // 1. 创建一条测评记录,状态为0(答题中) AssessmentRecord record = new AssessmentRecord(); record.setStudentId(studentId); record.setQuestionnaireId(questionnaireId); record.setStatus(0); record.setStartTime(new Date()); recordMapper.insert(record); // 2. 根据量表ID查出所有题目,按题号排序 List<Question> questions = questionMapper.selectList( new LambdaQueryWrapper<Question>() .eq(Question::getQuestionnaireId, questionnaireId) .orderByAsc(Question::getSortNo)); // 3. 拼装题目VO,每道题补上5个选项的JSON结构 StartQuizVO vo = new StartQuizVO(); vo.setRecordId(record.getId()); vo.setTotal(questions.size()); List<QuestionVO> questionVOList = questions.stream().map(q -> { QuestionVO qv = new QuestionVO(); qv.setId(q.getId()); qv.setSortNo(q.getSortNo()); qv.setContent(q.getContent()); qv.setFactor(q.getFactor()); qv.setOptions(buildDefaultOptions()); // 固定5个选项 return qv; }).collect(Collectors.toList()); vo.setQuestions(questionVOList); return vo; } }

Service 层的关键逻辑是第 1 步先插入测评记录并拿到自增 ID,再查题目组装返回。先插记录的意义在于:学生中途退出时,数据库里已经存在一条状态为 0 的记录,下次进来可以继续作答或者重新开始。这里用了 MyBatis Plus 的 LambdaQueryWrapper 构造查询条件,eq表示等值匹配,orderByAsc按题号升序,题目顺序由 sort_no 控制而不是数据库的自增 ID。

4.3 答案提交与判分:SCL-90 的总分、因子分与阳性筛查

判分接口是整个系统的算法核心。学生提交全部答案后,后端做四件事:计算总分、计算阳性项目数、按因子维度聚合因子均分、判定预警等级。

SCL-90 的判分规则是:总分 = 90 道题得分之和;阳性项目数 = 得分 ≥ 2 的题目数量;因子均分 = 该因子下所有题目得分的平均值。有一条经验:因子均分比总分更能反映学生在某一心理维度的具体情况,所以结果表里两个指标都要存。

@Override @Transactional public SubmitResultVO submitAnswer(SubmitRequest req) { // 1. 根据recordId查出测评记录,校验状态 AssessmentRecord record = recordMapper.selectById(req.getRecordId()); if (record == null || record.getStatus() == 1) { throw new BizException("记录不存在或已提交"); } // 2. 查出该量表的所有题目,建立题目ID到因子名称的映射 List<Question> questions = questionMapper.selectList( new LambdaQueryWrapper<Question>() .eq(Question::getQuestionnaireId, record.getQuestionnaireId())); Map<Long, String> idToFactor = questions.stream() .collect(Collectors.toMap(Question::getId, Question::getFactor)); // 3. 遍历学生提交的答案数组,累加总分并统计每个因子得分 int totalScore = 0; int positiveCount = 0; Map<String, List<Integer>> factorScores = new HashMap<>(); for (AnswerItem item : req.getAnswers()) { totalScore += item.getScore(); if (item.getScore() >= 2) { positiveCount++; } String factor = idToFactor.get(item.getQuestionId()); factorScores.computeIfAbsent(factor, k -> new ArrayList<>()).add(item.getScore()); } // 4. 计算每个因子均分 Map<String, Double> factorAvg = new HashMap<>(); for (Map.Entry<String, List<Integer>> entry : factorScores.entrySet()) { double avg = entry.getValue().stream().mapToInt(Integer::intValue).average().orElse(0.0); factorAvg.put(entry.getKey(), Math.round(avg * 100.0) / 100.0); } // 5. 判定预警等级:总分>160或任一因子均分>=2为筛查阳性 int alertLevel = 0; boolean positive = totalScore > 160 || positiveCount > 43 || factorAvg.values().stream().anyMatch(avg -> avg >= 2); if (positive) { alertLevel = factorAvg.values().stream().anyMatch(avg -> avg >= 3) ? 2 : 1; } // 6. 更新记录状态,写入结果表 record.setStatus(1); record.setEndTime(new Date()); recordMapper.updateById(record); AssessmentResult result = new AssessmentResult(); result.setRecordId(record.getId()); result.setStudentId(record.getStudentId()); result.setTotalScore(totalScore); result.setPositiveCount(positiveCount); result.setFactorJson(JSON.toJSONString(factorAvg)); result.setAlertLevel(alertLevel); resultMapper.insert(result); return buildResultVO(totalScore, positiveCount, factorAvg, alertLevel); }

代码里加@Transactional的原因在第 4 步和第 6 步之间:更新记录状态和插入结果必须同时成功或同时失败,否则会出现「记录已完成但结果表没有数据」的数据不一致问题,这个点在面试里通常会被追问到事务隔离级别。预警等级的规则是我建议的一种简化版本,论文里写清楚规则来源即可。两个阈值可以做成系统参数而不是硬编码,但毕业设计阶段写常量完全够用,也能减少答辩时被问「参数配置界面在哪」的概率。

5. 小程序端实现与避坑:90 道题的答题流程怎么做才不卡

小程序端承担答题交互的全部工作。90 道题的量表如果一次性渲染,低端手机上会出现明显的卡顿和延迟,这是做测评类小程序最容易翻车的地方。第 5 章把页面结构、答题逻辑、结果展示依次讲完,最后集中说明我踩过的坑。

5.1 小程序页面结构与 request 封装

小程序端的页面结构按业务拆分,共 5 个页面:登录页、测评列表页、答题页、结果页、历史记录页。登录页用wx.login获取 code 传给后端换取 openid,后端返回一个自定义 token,后续所有请求在 header 里带 token 识别身份。

统一的请求封装是必须做的一步,避免每个页面重复写 wx.request。我在 utils 目录建一个 request.js,把 baseURL、token 注入、错误提示全部收口:

// utils/request.js const BASE_URL = 'https://your-server.com/api'; // 上线时换成正式域名 function request(url, method, data) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'token': wx.getStorageSync('token') || '' }, success(res) { if (res.statusCode === 200 && res.data.code === 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.message || '请求失败', icon: 'none' }); reject(res.data); } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } module.exports = { request };

封装逻辑中值得说明的是 token 的获取时机:wx.getStorageSync('token')在每次请求时读取,保证登录过期后重新登录能自动带上新 token。如果后端返回 code 401 表示 token 失效,可以在 success 里判断并跳转登录页,代码中为了简洁没有在这一版加入,但实际开发一定要处理。BASE_URL 在开发阶段可以填本地局域网 IP,但注意微信开发者工具要勾选「不校验合法域名」才能请求 HTTP 接口。

5.2 答题页和结果页的核心代码

答题页是交互最复杂的页面,核心设计是「一次只渲染一道题」。用一个 currentIndex 变量记录当前题号,每答完一题自动进入下一题,而不是把 90 道题一次性渲染到页面上。这种设计的意义在于:页面数据量控制在最小,滑动切换时没有卡顿;答完一题立即把答案存入本地的 answers 数组,即使用户退出,已答的答案也不丢。

<!-- 答题页 quiz.wxml 核心结构 --> <view class="quiz-container"> <view class="progress">第 {{currentIndex + 1}} 题 / 共 {{total}} 题</view> <view class="question-content">{{questions[currentIndex].content}}</view> <radio-group class="options" bindchange="onOptionChange"> <label class="option-item" wx:for="{{questions[currentIndex].options}}" wx:key="label"> <radio value="{{item.score}}" checked="{{item.score === answers[currentIndex]}}"/> <text>{{item.label}}</text> </label> </radio-group> <view class="btn-group"> <button wx:if="{{currentIndex > 0}}" bindtap="prevQuestion">上一题</button> <button bindtap="nextQuestion" disabled="{{!answers[currentIndex]}}"> {{currentIndex === total - 1 ? '提交' : '下一题'}} </button> </view> </view>

radio 的 value 直接绑定选项分值 1 到 5,选中时把分值存到 answers 数组的对应位置。answers[currentIndex]用来回显已选答案,这样用户点「上一题」回来时能看到自己之前的选择。下面的 JS 逻辑配合这套渲染结构:

// 答题页 quiz.js 核心逻辑 Page({ data: { recordId: null, questions: [], currentIndex: 0, total: 0, answers: [], startTime: null }, onLoad(options) { const { questionnaireId } = options; request('/assessment/start', 'GET', { questionnaireId, studentId: wx.getStorageSync('studentId') }) .then(data => { this.setData({ recordId: data.recordId, questions: data.questions, total: data.total }); this.setData({ answers: new Array(data.total).fill(null) }); this.data.startTime = Date.now(); }); }, onOptionChange(e) { const score = Number(e.detail.value); const idx = this.data.currentIndex; this.setData({ [`answers[${idx}]`]: score }); }, nextQuestion() { if (!this.data.answers[this.data.currentIndex]) { wx.showToast({ title: '请先选择', icon: 'none' }); return; } if (this.data.currentIndex < this.data.total - 1) { this.setData({ currentIndex: this.data.currentIndex + 1 }); } else { this.submitAnswer(); } }, submitAnswer() { const answers = this.data.answers.map((score, i) => ({ questionId: this.data.questions[i].id, score: score })); const costSeconds = Math.floor((Date.now() - this.data.startTime) / 1000); request('/assessment/submit', 'POST', { recordId: this.data.recordId, costSeconds: costSeconds, answers: answers }).then(result => { wx.redirectTo({ url: `/pages/result/result?resultId=${result.id}` }); }); } });

setData里用answers[${idx}]这种动态 key 语法,可以只更新数组中的某一项而不是整个数组,这是小程序性能优化的一个基础技巧。提交时把所有答案一次性发送给后端,本地 answers 数组是从答题开始就一直维护的,所以最后组装时不需要再访问页面 DOM。90 道题全部答完后的提交数据量约为 90 个 JSON 对象,体积在几 KB 级别,网络耗时可以接受。

结果页展示相对简单,从后端拿到的数据渲染总分、因子分和预警建议。这里可以用 ec-canvas 组件画一个因子分雷达图,但要注意 canvas 的版本兼容问题,小程序新版的 canvas 2d 接口和旧版画法不兼容,稳妥做法是直接用 view 加 CSS 宽度百分比做条形图,答辩演示效果也不差。

5.3 踩坑记录:五个真实遇到过的问题与解决

这一段不是理论推导,是我在这个项目里实打实踩过的坑,每条按「现象 → 原因 → 解决」说明。

问题一:真机预览请求一直失败,开发工具却正常。现象:微信开发者工具里接口请求正常,但手机扫码预览后所有请求全部报错,Network 面板显示 request 失败。 原因:微信小程序规定正式环境请求必须是 HTTPS 域名且在小程序后台配置了白名单,开发工具勾选了「不校验合法域名」所以正常,真机不认这个设置。 解决:开发阶段在开发工具详情里勾选「不校验合法域名、web-view 域名、TLS 版本以及 HTTPS 证书」;上线前必须申请 HTTPS 证书并配置合法域名。注意,云开发环境可以免域名配置,但这是另一套方案,这里不做展开。

问题二:wx.login 的 code 传给后端后,后端调用微信接口报 appid 错误。现象:后端用小程序传过来的 code 换 openid 时返回40029 invalid code或40163 code been used。 原因:code 是单次有效的,小程序端如果多次调用wx.login,新 code 会覆盖旧 code;更常见的是后端用了错误的小程序 AppSecret。 解决:前端只在登录入口调用一次wx.login,拿到 code 立即传给后端,后端再调用jscode2session接口。如果返回 invalid code,优先检查 appid 和 secret 是否和自己小程序后台的一致。

问题三:把 90 道题一次性 setData,安卓低端机卡到没法操作。现象:答题页滑动时明显的掉帧感,快速点下一题会出现白屏。 原因:setData 的 90 道题加上 5 个选项,页面数据总量过大,小程序每次 setData 都会进行序列化传输和视图层重绘。 解决:改为一次只渲染当前题,即第 5.2 节的方案。实际测试中,一次性 setData 的数据量超过 50KB 就会有感知延迟,逐题渲染控制在 2KB 以内,流畅度明显提升。

问题四:学生答到一半退出,再进来所有答案全没了。现象:学生中断答题后,过几天重新进入系统,测评记录显示从头开始。 原因:答案只存在于小程序的临时变量里,退出页面后变量销毁;后端记录表状态为 0 表示未完成,但答案明细没有持久化。 解决:方案一是每答一题立即把答案提交到后端,缺点是请求频繁;方案二是退出时自动把未完成的答案存到本地wx.setStorageSync,再次进入检测到有未完成记录时恢复答案。我实际用的是方案二,成本低且天然支持离线状态下的答题。

问题五:自定义导航栏标题在不同手机上位置不一致。现象:用自定义导航栏做标题时,iPhone 上标题偏上,部分安卓机上标题和胶囊按钮重叠。 原因:小程序自定义导航栏需要自己计算状态栏高度,不同机型的系统状态栏高度不同。 解决:用wx.getMenuButtonBoundingClientRect()获取胶囊按钮的位置信息,动态计算导航栏高度,而不是写死在样式里。标准代码可以在app.js里全局计算一次,存到 globalData 供所有页面使用。

5.4 线上联调时留意的东西

小程序端和后端联调阶段,最容易忽略的是编码统一问题。后端接口返回的 JSON 如果包含中文字段,必须保证 Spring Boot 的响应编码是 UTF-8,否则小程序端看到的就是乱码。调试时建议打开微信开发者工具的 Network 面板,核对每个请求的响应结构是否和约定的契约一致。另外就是 token 过期时的统一处理,如果后端返回 code 401,小程序端要跳回登录页重新走登录流程,这个逻辑我在 request.js 里预留了位置但没有完整实现,实属经验之谈——先让核心链路通,再补异常分支。

另一个实务建议是给小程序端加一个「测评记录」页面,展示历史测评记录和每次的结果摘要。这个页面业务逻辑简单,但能明显提升系统的完整度,论文用例图里也能多一个功能点。记录页的列表用<scroll-view>实现下拉刷新,接口设计为分页查询,mybatis plus 自带的 Page 插件可以直接支持,小程序端在onReachBottom触发时请求下一页。

6. 进阶技巧:测评报告导出、分班统计与上线部署

测评结果如果只在手机屏幕上看,数据的价值就只发挥了一半。心理中心老师需要按班级汇总筛查结果,也需要导出每个学生的详细测评报告存档。这两个需求在答辩演示时特别能加分,因为评委看到的是「系统能产出实际使用的文档和数据」,而不是停留在页面上点来点去。

报告导出我在项目中用 Apache POI 实现,生成 Word 格式的测评报告。每个学生一份文档,包含基本信息、总分、阳性项目数、因子均分表和心理中心老师的建议语。有一个关键认知:POI 的原生 API 不支持直接插入图表,XWPFChart 接口在旧版本里并不好用,我的做法是用表格替代图形展示——在 Word 里生成一个 10 行 2 列的因子分表格,把因子名称和均分填进去,再用简单的文字描述每个因子的高低含义。这样既绕开了图表 API 的坑,论文里的「报告导出模块」一章也能正常写清楚实现思路。

分班统计是另一个低成本高价值的功能。后端写一个班级统计接口,按 class_name 分组,统计该班已完成测评的人数、预警人数、平均总分和阳性项目数。小程序端做两个 Tab:学生端看个人报告,辅导员端看班级概览。这个功能在论文中对应「系统应用效果分析」章节,用测试数据跑出几张统计表,答辩时的说服力远高于口头描述。

上线部署我建议用最低成本路径:服务器装 MySQL 和 JDK,打包 Spring Boot 项目为 jar 后直接用nohup java -jar assessment-server.jar启动,前面套一个 Nginx 做反向代理,微信小程序后台配置 HTTPS 域名白名单。这套部署流程属于通用 Web 服务方案,网上资料非常多,照着做一遍就能跑通。

最后说一个我自己的演示习惯:每次给人演示这个系统之前,一定会清空数据库里的测试数据、重新注册一个测试账号从头跑一遍流程,确保从登录到报告的每个环节都是干净可用的状态。这个习惯帮我避免过至少两次演示时被旧数据干扰的尴尬。做测评类系统,数据真实性和流程完整性比功能数量更能体现工程的成熟度,希望这个项目方案能帮你在答辩或实际落地时少走弯路。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询