news 2026/9/26 14:10:25

微信小程序+MySQL青少年心理健康系统:毕业设计源码与教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微信小程序+MySQL青少年心理健康系统:毕业设计源码与教程

简介:这份资源是面向高校学生与初学者的微信小程序毕业设计完整项目包,以青少年心理健康科普为主题,适合用作毕业设计、课程设计或自学练手。项目前端基于微信小程序,后端采用SpringBoot/SSM框架,数据库为MySQL,配套源码、数据库脚本与部署说明,代码含注释,新手也能看懂。压缩包共1453个文件,约38.84MB,涵盖png、svg、jpg等图片素材,js、vue、java等前后端代码,json、wxml、wxss等小程序配置与样式文件,以及sql脚本、bat启动脚本和部署文档,结构完整、层次清晰。目前已有181人学习下载。项目功能完善、界面美观、操作简单,经过严格调试可稳定运行,读者可据此掌握小程序页面搭建、接口联调与数据库设计思路,并借助部署教程快速跑通服务端与开发者工具导入流程,具备较高的参考与复用价值。

1. 青少年心理健康科普小程序:从毕业设计选题到能跑起来的完整交付

青少年心理健康这件事,落到技术实现上,其实是一个典型的「内容 + 测评 + 记录」三件套小程序。我见过太多毕业设计卡在同一个地方:选题方向没问题,数据库表也画了,但真到写代码时发现——量表题目怎么存、测评结果怎么算、科普文章怎么分类、用户匿名记录怎么保护,这些细节没人讲清楚。这个标题里的「微信小程序 + 数据库 + 源码 + 教程」组合,本质上要解决的就是:一个学生或者刚接触小程序开发的工程师,能不能在两周内从零把这个系统跑起来,并且数据库设计经得起答辩老师的追问。适合的人群很明确:正在做计算机毕业设计的学生、想练手微信小程序全栈开发的新手、以及需要一套心理健康类小程序基础框架的开发者。接下来我会按「数据库怎么设计 → 小程序端怎么搭 → 测评逻辑怎么写 → 踩过哪些坑」的顺序,把每个环节的可复现步骤和参数选择讲透。

2. 数据库设计:从量表题目到匿名记录的六张核心表

2.1 为什么选 MySQL 而不是 SQLite 或云数据库

毕业设计场景下,数据库选型的第一原则是「答辩时能演示,换电脑能跑起来」。SQLite 虽然零配置,但多端并发写入时容易锁库,而且答辩老师看到.db文件往往觉得「不够正式」。云数据库比如微信云开发自带的 JSON 数据库,上手快但导出和迁移麻烦,查复杂关联时也不如 SQL 直观。MySQL 8.0 是稳妥选择:学校机房大概率装好了,网上安装教程一抓一大把,mysqldump导出 SQL 文件后换台电脑导入就能继续用。

我一般会建议用 Docker 跑 MySQL,避免「在我电脑上好好的」这种玄学问题。下面这条命令启动一个带密码的 MySQL 8.0 容器,端口映射到本机 3306,数据卷挂到当前目录下的mysql_data文件夹,删容器不丢数据:

docker run -d \ --name mental-health-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=Root123456 \ -e MYSQL_DATABASE=teen_mental_health \ -v $(pwd)/mysql_data:/var/lib/mysql \ mysql:8.0 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_unicode_ci

参数说明:MYSQL_DATABASE会自动建库,省去手动CREATE DATABASE;utf8mb4字符集必须指定,否则 emoji 和部分生僻字会报错;-v挂载保证容器重启后数据还在。如果你用本机安装的 MySQL,把my.cnf里的character-set-server改成utf8mb4效果一样。

2.2 六张核心表的字段设计与建表语句

这个系统的数据关系不复杂,但有几个容易设计错的地方。我拆成六张表:用户表、科普文章表、文章分类表、量表题目表、测评记录表、匿名留言表。先看建表语句,再逐条解释为什么这么设计。

-- 用户表:只存微信 openid 和基础信息,不存敏感身份 CREATE TABLE `user` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `openid` VARCHAR(64) NOT NULL COMMENT '微信唯一标识', `nickname` VARCHAR(64) DEFAULT '匿名用户', `avatar_url` VARCHAR(512) DEFAULT '', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 文章分类表:支持一级分类,方便小程序端 tab 切换 CREATE TABLE `article_category` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(32) NOT NULL COMMENT '如:情绪管理、人际交往', `sort_order` INT DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 科普文章表:content 用 TEXT,摘要单独存 CREATE TABLE `article` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `category_id` INT UNSIGNED NOT NULL, `title` VARCHAR(128) NOT NULL, `summary` VARCHAR(256) DEFAULT '', `content` TEXT NOT NULL, `cover_url` VARCHAR(512) DEFAULT '', `view_count` INT UNSIGNED DEFAULT 0, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 量表题目表:options 存 JSON,避免多表关联 CREATE TABLE `scale_question` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `scale_type` VARCHAR(32) NOT NULL COMMENT '如:PHQ-9、GAD-7', `question_text` VARCHAR(512) NOT NULL, `options` JSON NOT NULL COMMENT '[{"label":"完全不会","score":0},...]', `sort_order` INT DEFAULT 0, PRIMARY KEY (`id`), KEY `idx_scale_type` (`scale_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 测评记录表:answers 存 JSON,result_score 冗余存储方便统计 CREATE TABLE `assessment_record` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` INT UNSIGNED NOT NULL, `scale_type` VARCHAR(32) NOT NULL, `answers` JSON NOT NULL COMMENT '{"1":0,"2":1,...}', `result_score` INT NOT NULL, `result_level` VARCHAR(16) NOT NULL COMMENT '正常/轻度/中度/重度', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_scale` (`user_id`, `scale_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 匿名留言表:不关联 user_id,只存设备指纹哈希 CREATE TABLE `anonymous_message` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `device_hash` VARCHAR(64) NOT NULL COMMENT '设备指纹哈希,用于防刷', `content` VARCHAR(1024) NOT NULL, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_device` (`device_hash`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明:scale_question的options用 JSON 类型而不是单独建选项表,是因为量表题目数量固定、选项结构统一,JSON 查询在 MySQL 8.0 里已经够用,省去一次 JOIN。assessment_record的answers存 JSON 是为了保留原始作答,result_score和result_level冗余存储是为了列表页不用每次重新计算。anonymous_message不存user_id是刻意设计——匿名就要彻底,只留设备哈希做防刷,答辩时这一点可以展开讲隐私保护思路。

2.3 初始化数据:量表题目和分类的插入脚本

建完表要灌入基础数据,否则小程序端打开是空的。PHQ-9 和 GAD-7 是青少年心理健康筛查里最常用的两个量表,各 9 题和 7 题,选项都是四级评分。下面这段 SQL 插入 PHQ-9 的前三题作为示例,完整版按同样格式补全即可:

INSERT INTO `article_category` (`name`, `sort_order`) VALUES ('情绪管理', 1), ('人际交往', 2), ('学业压力', 3), ('自我认知', 4); INSERT INTO `scale_question` (`scale_type`, `question_text`, `options`, `sort_order`) VALUES ('PHQ-9', '做事时提不起劲或没有兴趣', '[{"label":"完全不会","score":0},{"label":"好几天","score":1},{"label":"一半以上时间","score":2},{"label":"几乎每天","score":3}]', 1), ('PHQ-9', '感到心情低落、沮丧或绝望', '[{"label":"完全不会","score":0},{"label":"好几天","score":1},{"label":"一半以上时间","score":2},{"label":"几乎每天","score":3}]', 2), ('PHQ-9', '入睡困难、睡不安稳或睡眠过多', '[{"label":"完全不会","score":0},{"label":"好几天","score":1},{"label":"一半以上时间","score":2},{"label":"几乎每天","score":3}]', 3);

参数说明:sort_order控制题目顺序,不要依赖自增 ID 排序,因为后期可能插入新题。options里的score是计算总分的关键,PHQ-9 总分 0-27,分级标准是 0-4 正常、5-9 轻度、10-14 中度、15-27 重度。GAD-7 总分 0-21,分级是 0-4 正常、5-9 轻度、10-14 中度、15-21 重度。这些阈值要写死在代码里还是存数据库?我建议存代码常量,因为分级标准相对固定,存数据库反而增加查询开销。

3. 微信小程序端搭建:从项目初始化到文章列表渲染

3.1 项目初始化和目录结构

微信开发者工具新建项目时,AppID 可以先用测试号,后端接口地址填http://localhost:3000但要在开发者工具里勾选「不校验合法域名」。目录结构我习惯这样分:

miniprogram/ ├── pages/ │ ├── index/ # 首页:文章分类 + 推荐阅读 │ ├── article/ # 文章详情 │ ├── scale/ # 量表列表 │ ├── assessment/ # 答题页 │ ├── result/ # 测评结果 │ └── message/ # 匿名留言 ├── utils/ │ ├── request.js # 封装 wx.request │ └── auth.js # 登录态管理 ├── app.js ├── app.json └── app.wxss

app.json里要配置tabBar,至少放「首页」「测评」「我的」三个 tab。request.js封装统一加 token 和错误处理,避免每个页面重复写wx.request。

3.2 封装 wx.request 和登录态管理

小程序端所有请求都要带登录态,但wx.login换取的code只能用一次,所以流程是:启动时调wx.login拿 code → 发给后端换自定义 token → 存wx.setStorageSync→ 后续请求带 token。下面是对应的封装代码:

// utils/request.js const BASE_URL = 'http://localhost:3000'; function request(options) { const token = wx.getStorageSync('token'); return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '' }, success(res) { if (res.statusCode === 401) { // token 过期,重新登录 wx.removeStorageSync('token'); wx.showToast({ title: '请重新进入小程序', icon: 'none' }); reject(new Error('unauthorized')); return; } if (res.data.code === 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(new Error(res.data.msg)); } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } module.exports = { request };

逻辑说明:Authorization头用Bearer格式是常见做法,后端解析时取Bearer后面的部分。res.data.code === 0是约定好的成功码,具体值可以改但前后端要一致。401 处理里没有自动重登,是因为重登后原请求需要重发,逻辑复杂,毕业设计场景下提示用户重新进入更简单可靠。

3.3 文章列表页的数据绑定和分页加载

首页文章列表用scroll-view或页面自带滚动都行,关键是分页参数。我一般用page和pageSize两个参数,pageSize设 10,触底时page + 1再请求。下面是对应页面的 JS 和 WXML 核心片段:

// pages/index/index.js const { request } = require('../../utils/request'); Page({ data: { articles: [], page: 1, hasMore: true, loading: false }, onLoad() { this.loadArticles(); }, async loadArticles() { if (this.data.loading || !this.data.hasMore) return; this.setData({ loading: true }); try { const list = await request({ url: `/api/articles?page=${this.data.page}&pageSize=10` }); this.setData({ articles: this.data.articles.concat(list), page: this.data.page + 1, hasMore: list.length === 10, loading: false }); } catch (e) { this.setData({ loading: false }); } }, onReachBottom() { this.loadArticles(); } });

参数说明:hasMore判断依据是返回条数是否等于pageSize,等于说明可能还有下一页,小于说明到底了。loading锁防止触底时重复请求。concat而不是直接赋值,是为了保留已加载的数据。后端接口返回的list直接是数组,如果后端返回{ list: [], total: 100 },这里要改成list.list。

4. 测评逻辑实现:量表计分、结果分级和前后端职责划分

4.1 计分逻辑放在前端还是后端

这是个容易纠结的点。放前端,答题完立刻出结果,体验好,但用户改答案或伪造请求就绕过了;放后端,安全但多一次网络请求。我的做法是:前端算一遍用于即时展示,后端算一遍用于存库,两边用同一套分级阈值。这样既保证体验,又保证数据可信。后端算的时候不信任前端传的分数,只接收answers原始作答,重新计算。

4.2 前端答题页的状态管理和提交

答题页要记录每道题的选择,用对象存{ questionId: score }。下面是一个简化的答题页逻辑:

// pages/assessment/assessment.js const { request } = require('../../utils/request'); Page({ data: { questions: [], answers: {}, currentIndex: 0, scaleType: 'PHQ-9' }, onLoad(options) { this.setData({ scaleType: options.scaleType || 'PHQ-9' }); this.loadQuestions(); }, async loadQuestions() { const questions = await request({ url: `/api/scale/questions?scaleType=${this.data.scaleType}` }); this.setData({ questions }); }, selectOption(e) { const { questionId, score } = e.currentTarget.dataset; const answers = { ...this.data.answers, [questionId]: score }; this.setData({ answers }); // 自动跳下一题 if (this.data.currentIndex < this.data.questions.length - 1) { this.setData({ currentIndex: this.data.currentIndex + 1 }); } }, async submit() { const { answers, scaleType } = this.data; if (Object.keys(answers).length < this.data.questions.length) { wx.showToast({ title: '还有题目未作答', icon: 'none' }); return; } const result = await request({ url: '/api/assessment/submit', method: 'POST', data: { scaleType, answers } }); wx.redirectTo({ url: `/pages/result/result?score=${result.score}&level=${result.level}` }); } });

逻辑说明:answers用对象而不是数组,是因为题目 ID 可能不连续,对象更灵活。selectOption里自动跳下一题是常见交互,但最后一题不跳,留给用户点提交。提交前校验答题数量,防止漏答。redirectTo而不是navigateTo,是因为结果页不需要返回答题页。

4.3 后端计分接口和分级阈值

后端收到answers后,从数据库查出该量表的题目和选项,按questionId匹配score累加,再根据总分查分级表。下面是对应的 Node.js 伪代码:

// 后端计分逻辑 async function calculateScore(scaleType, answers) { const questions = await db.query( 'SELECT id, options FROM scale_question WHERE scale_type = ?', [scaleType] ); let total = 0; for (const q of questions) { const selectedScore = answers[q.id]; if (selectedScore === undefined) { throw new Error(`题目 ${q.id} 未作答`); } total += selectedScore; } const level = getLevel(scaleType, total); return { score: total, level }; } function getLevel(scaleType, score) { if (scaleType === 'PHQ-9') { if (score <= 4) return '正常'; if (score <= 9) return '轻度'; if (score <= 14) return '中度'; return '重度'; } if (scaleType === 'GAD-7') { if (score <= 4) return '正常'; if (score <= 9) return '轻度'; if (score <= 14) return '中度'; return '重度'; } return '未知'; }

参数说明:answers的 key 是题目 ID,value 是选项分值。getLevel的阈值写死在代码里,如果后期要调整分级标准,改这里就行。注意 PHQ-9 和 GAD-7 的分级阈值恰好一样,但不要因此合并逻辑,因为未来可能加其他量表。

5. 避坑与排查:毕业设计交付前最容易翻车的五个地方

5.1 现象:小程序真机预览时请求全部失败,开发者工具却正常

原因:开发者工具默认不校验合法域名,但真机预览会校验。你的后端如果是http://localhost:3000,手机根本访问不到,而且微信要求必须 HTTPS。

解决:毕业设计演示阶段,可以在微信公众平台配置「开发环境不校验请求域名」,但真机调试时手机和电脑要在同一局域网,后端地址改成电脑的局域网 IP,比如http://192.168.1.100:3000。如果答辩要求正式环境,需要备案域名和 HTTPS 证书,这个周期较长,要提前准备。

5.2 现象:量表题目选项的 JSON 在数据库里存进去后,取出来变成字符串

原因:MySQL 的 JSON 类型在 Node.js 驱动里默认返回字符串,需要手动JSON.parse。或者建表时用了TEXT而不是JSON。

解决:确认建表语句里options字段类型是JSON。查询后在代码里判断typeof options === 'string'就JSON.parse。更稳妥的做法是在数据库连接配置里加typeCast处理,但毕业设计场景下手动 parse 更直观。

5.3 现象:测评提交后结果页显示「undefined」

原因:后端返回的数据结构和前端取值不一致。比如后端返回{ code: 0, data: { score: 10, level: '轻度' } },前端request封装里已经取了res.data.data,结果页又取result.data.score,多了一层。

解决:统一约定request返回的就是业务数据本身,结果页直接用result.score。调试时在request的resolve前加console.log(res.data),看清楚结构再取值。

5.4 现象:匿名留言功能被刷屏,同一设备反复提交

原因:device_hash生成逻辑太简单,或者根本没做频率限制。

解决:device_hash用wx.getSystemInfo的多个字段拼接后哈希,不要只用openid(匿名场景不该拿 openid)。后端加频率限制,同一device_hash一分钟内只能提交一条,用 Redis 或内存缓存都行。毕业设计用内存 Map 就够,注意重启后清空。

5.5 现象:换电脑部署时数据库导入报错「Unknown character set: utf8mb4」

原因:目标电脑的 MySQL 版本低于 5.5.3,不支持 utf8mb4。或者导出 SQL 时没有指定字符集。

解决:确认 MySQL 版本,8.0 肯定支持。导出时用mysqldump --default-character-set=utf8mb4。导入前先SET NAMES utf8mb4;。如果目标环境实在不支持,降级用utf8,但 emoji 会丢,量表题目里一般没有 emoji,影响不大。

6. 让答辩加分的两个进阶技巧:数据可视化和隐私保护说明

6.1 用 ECharts 在小程序里画测评趋势图

毕业设计如果只展示单次测评结果,显得单薄。加一个「历史测评趋势」折线图,答辩时很加分。微信小程序用 ECharts 需要引入ec-canvas组件,核心代码如下:

// pages/result/result.js 中初始化图表 import * as echarts from '../../components/ec-canvas/echarts'; function initChart(canvas, width, height, dpr) { const chart = echarts.init(canvas, null, { width, height, devicePixelRatio: dpr }); canvas.setChart(chart); chart.setOption({ xAxis: { type: 'category', data: ['第1次', '第2次', '第3次'] }, yAxis: { type: 'value', min: 0, max: 27 }, series: [{ data: [8, 12, 6], type: 'line', smooth: true, markLine: { data: [{ yAxis: 10, name: '中度阈值' }], lineStyle: { color: '#f5222d', type: 'dashed' } } }] }); return chart; }

参数说明:markLine标出中度阈值线,让趋势图有临床参考意义。数据从后端/api/assessment/history?scaleType=PHQ-9获取,按时间排序。注意ec-canvas组件需要单独下载放到components目录,app.json里不用注册,页面 JSON 里注册即可。

6.2 隐私保护说明怎么写进答辩 PPT

青少年心理健康数据敏感,答辩老师大概率会问「你怎么保护隐私」。我的经验是准备三句话:第一,用户表只存 openid 不存手机号和真实姓名;第二,测评记录和匿名留言不关联真实身份,匿名留言连 user_id 都不存;第三,所有接口鉴权用 token,token 过期时间设 2 小时,敏感操作二次校验。这三条写进 PPT 的一页,比堆技术名词管用。

最后说个血泪教训:我当年做类似系统时,把量表分级阈值写在了前端,后端没校验,结果答辩演示时老师问「我改一下请求分数会怎样」,当场翻车。后来改成前后端各算一遍,前端算的只用于展示,后端算的才入库。这个习惯保持到现在——凡是涉及结果判定的逻辑,永远不要只信客户端。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 14:09:34

主动配电网故障恢复统一模型:重构与孤岛划分的Matlab实现

1. 为什么故障恢复非得把“重构”和“孤岛划分”塞进同一个模型1.1 传统两阶段做法的问题在哪做主动配电网故障恢复的人&#xff0c;几乎都经历过这种纠结&#xff1a;配电网某条馈线跳闸后&#xff0c;一部分负荷失电&#xff0c;手上有分布式电源&#xff08;DG&#xff09;&…

作者头像 李华
网站建设 2026/9/26 14:09:34

Guohua Diffusion实战:从扩散模型参数调优到国画风格批量生成落地

Guohua Diffusion这个词&#xff0c;最近在几个产区的设计圈里被反复提起。起初我以为又是个追着AI风口跑的概念包装&#xff0c;直到自己下场把整套流程跑通&#xff0c;从一线产区的高端定制打样&#xff0c;一路推到二线产区的批量生产规范&#xff0c;才意识到这其实是国画…

作者头像 李华
网站建设 2026/9/26 14:07:43

六自由度机械臂视觉伺服抓取:OpenCV与深度学习全链路实战

简介&#xff1a;这份资源面向工业自动化与智能物流分拣方向的开发者与学习者&#xff0c;提供一套基于视觉伺服控制的六自由度机械臂自主抓取系统方案&#xff0c;覆盖实时图像处理、目标识别、深度学习模型训练与位姿估计、ROS集成及运动规划等关键环节&#xff0c;适合具备P…

作者头像 李华
网站建设 2026/9/26 14:07:39

AI辅助开发实战:从零散代码到可运行项目的五个阶段

1. 从零散需求到可运行原型&#xff1a;AI辅助代码开发的整体思路拆解1.1 业余开发者的真实处境与核心痛点先说清楚这篇内容面向谁。如果你是一个有正职工作、利用晚上和周末写点小工具或者做副业项目的开发者&#xff0c;或者你压根不是科班出身、靠着AI对话工具硬啃代码的爱好…

作者头像 李华
网站建设 2026/9/26 14:06:32

PyTorch实战:DeepLabV3在Cityscapes上的语义分割训练与避坑指南

简介&#xff1a;这份资源面向计算机视觉方向的研究者、算法工程师与深度学习学习者&#xff0c;提供在Cityscapes数据集上训练DeepLabV3语义分割模型的完整PyTorch实现&#xff0c;帮助读者理解ASPP空洞空间金字塔池化与全局上下文模块的设计思路&#xff0c;并掌握从数据预处…

作者头像 李华
网站建设 2026/9/26 14:06:04

Python爬虫实战:从租房数据采集到可视化看板

接手这个项目的时候&#xff0c;我脑子里第一个念头很简单&#xff1a;能不能用Python把某租房平台的数据扒下来&#xff0c;然后做成一套直观的看板。这个念头落地之后&#xff0c;实际的收获比我预想的大得多——爬虫只是前半场&#xff0c;后半场的数据清洗、存储设计、可视…

作者头像 李华