简介:一份睡眠检测小程序毕业设计/课程设计完整源码包,面向计算机相关专业学生或小程序开发者,解决毕设选题、前后端实现与文档配套问题。项目采用Java/PHP服务端与MySQL数据库,小程序端基于uniapp/微信原生开发,功能涵盖助眠音乐、睡眠数据记录、知识问答、个人信息管理等模块。包体共110个文件,含53张png界面图、14个js逻辑文件、13个json配置、11个wxss样式、10个wxml页面,以及doc/docx说明文档、mp4演示视频等,压缩包总大小14.63MB,目录结构清晰。目前已有170人学习下载,适合使用HBuilder X与微信开发者工具二次开发。资源提供从开题报告、设计文档、演示录制到echarts图表组件等完整素材,可快速理解睡眠数据线性图、音乐定时关闭、心情与睡眠质量选择等模块实现思路,便于直接部署或作为课设参考。
1. 睡眠检测小程序毕设源码:先搞清楚这包东西装的是什么
拿到一套「睡眠检测小程序源码(完整前后端+MySQL+说明文档+LW)」压缩包时,多数人第一反应是赶紧把代码跑起来,但往往卡在第一步:这包东西到底该怎么拆、怎么启动、哪些文件是核心。这套毕设方案的本质,是用小程序端采集手机加速度计与声音数据,上报给后端做睡眠状态判定,最终把入睡时间、醒来时间、深睡时长和睡眠评分呈现在报告页面上。它解决的是「睡眠监测 Demo 的数据链路完整性问题」——从端上采集到落库再到图表展示,评审老师真正关心的是这条链路是否闭环,而不是算法有多高级。适合两类人:一类是拿它做毕业设计、需要快速跑通并讲清原理的同学;另一类是想在智能健康方向搭一个可演示原型的开发者。先说结论:睡眠检测小程序最难的从来不是检测算法,而是数据在哪采、怎么传、怎么存、怎么展示这一整条链路。
2. 睡眠判定逻辑与 MySQL 表设计:先把后端数据底座立住
2.1 睡眠状态判定原理:为什么「体动检测」是毕设首选方案
市面上睡眠检测大致分三条路线:脑电 EEG、心率与血氧、体动与声音。EEG 精度最高但要靠头戴设备,不适合小程序场景;心率方案依赖手环等硬件,毕设如果只做小程序端,拿不到心率数据;体动检测只要手机加速度计就能跑,是最贴合「小程序 + 手机放床上」这个场景的做法。
体动检测的核心假设:人在深睡期身体几乎不动,浅睡期会有翻身等小幅度动作,清醒期活动频繁。小程序端用wx.onAccelerometerChange拿到三轴加速度,计算合成加速度的变化量,超过某个阈值就记为一次体动。后端按分钟聚合,统计这一分钟内的体动次数,再映射到睡眠状态。
| 体动次数(次/分钟) | 判定状态 | 说明 |
|---|---|---|
| 0~3 | 深睡 | 身体基本静止 |
| 4~12 | 浅睡 | 存在翻身等小幅动作 |
| ≥13 | 清醒 | 活动频繁或已起床 |
这套阈值模型简单、可控、答辩讲得清。注意阈值不是写死的,我把三档阈值放到配置表里,方便演示时调参。声音通道(鼾声检测)可选,常见做法是在后端另开一个分贝统计接口,和体动结果做加权,后面第 6 章会讲。
2.2 三张核心表:用户表、睡眠记录表、睡眠明细表
后端我建议用 Spring Boot,前端小程序原生,数据库 MySQL。表设计是这套源码的骨架,评审老师一定会盯表结构。至少要有三张表:用户表存账号信息;睡眠记录表存一次睡眠的总览;睡眠明细表存每一分钟的判定结果,支撑报告页的睡眠结构图。
-- 用户表 CREATE TABLE `t_user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '账号', `password` varchar(128) NOT NULL COMMENT 'MD5加密后的密码', `phone` varchar(20) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 睡眠记录表(一次完整睡眠的汇总) CREATE TABLE `t_sleep_record` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL COMMENT '关联t_user.id', `sleep_date` date NOT NULL COMMENT '睡眠日期', `start_time` datetime DEFAULT NULL COMMENT '入睡时间', `end_time` datetime DEFAULT NULL COMMENT '醒来时间', `deep_minutes` int DEFAULT '0' COMMENT '深睡时长(分钟)', `light_minutes` int DEFAULT '0' COMMENT '浅睡时长(分钟)', `awake_minutes` int DEFAULT '0' COMMENT '清醒时长(分钟)', `total_minutes` int DEFAULT '0' COMMENT '总睡眠时长(分钟)', `score` int DEFAULT '0' COMMENT '睡眠评分0-100', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_date` (`user_id`, `sleep_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 睡眠明细表(每分钟一条判定结果) CREATE TABLE `t_sleep_detail` ( `id` int NOT NULL AUTO_INCREMENT, `record_id` int NOT NULL COMMENT '关联t_sleep_record.id', `minute_mark` datetime NOT NULL COMMENT '该分钟的时间点', `status` tinyint NOT NULL COMMENT '0清醒 1浅睡 2深睡', `motion_count` int DEFAULT '0' COMMENT '该分钟体动次数', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_record` (`record_id`, `minute_mark`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这套表设计的核心思路是「总表 + 明细表」双层结构:明细表负责把每分钟的状态存下来,报告页的睡眠结构图直接查明细表就能画;总表存汇总数据,列表页和历史记录查询直接走总表,避免每次都要聚合明细。sleep_date单独建索引要重视,因为查询历史睡眠记录最常用的条件就是「某用户 + 某日期区间」。status用tinyint而不是字符串,省空间而且查询快。motion_count字段是调试和答辩的抓手,评审问「你怎么判定深睡」时,直接指这个字段讲阈值逻辑。
2.3 判定与评分接口:后端如何把体动数据变成睡眠报告
小程序端不能原始上报,否则每分钟十几条数据会刷爆接口。常见做法是端上每 10 秒记一次体动计数,攒够 60 秒就在本地做一次聚合,再调用上报接口推送这一分钟的体动次数。后端的判定接口就是一个「状态机 + 阈值映射」的处理逻辑。
@RestController @RequestMapping("/api/sleep") public class SleepController { @PostMapping("/report") public Result report(@RequestBody SleepReportDTO dto) { // dto.userId 用户ID // dto.minuteMark 该分钟的时间点 // dto.motionCount 这一分钟内的体动次数 int status = SleepRule.judgeStatus(dto.getMotionCount()); sleepDetailService.save(dto.getUserId(), dto.getMinuteMark(), status, dto.getMotionCount()); return Result.ok(); } @PostMapping("/finish") public Result finish(@RequestBody SleepFinishDTO dto) { // 停止监测时调用:汇总明细,计算总时长和评分 Integer recordId = sleepDetailService.summary(dto.getUserId(), dto.getStartTime(), dto.getEndTime()); return Result.ok(recordId); } }SleepRule.judgeStatus就是前文那张判定表的代码化:motionCount <= 3返回深睡,<= 12返回浅睡,否则清醒。注意阈值建议放到配置文件里,因为不同型号手机的加速度计灵敏度差异很大,真机调试时经常需要把阈值从 3 调到 5,写死在代码里每次都要重新编译。
finish接口的逻辑关键:查询该用户从入睡到醒来的所有明细,按status分组求和,得到深睡/浅睡/清醒分钟数,再套一个评分公式。常见评分公式是加权分:
public static int calcScore(int deep, int light, int awake, int total) { if (total == 0) return 0; // 深睡占比权重最高,清醒占比扣分 double score = (deep / (double) total) * 60 + (light / (double) total) * 30 - (awake / (double) total) * 10; return (int) Math.max(0, Math.min(100, score + 40)); }评分公式是答辩高频提问点。这个公式里我给深睡 60 分权重、浅睡 30 分、清醒倒扣 10 分,最后再加 40 分基础分。意图很直观:深睡比例越高分越高、清醒越多分越低,而且分母用total而不是deep + light + awake,避免除零。你可以按自己的理解改权重,但基础分建议保留,否则评分会普遍偏低、不好看。
3. 小程序端睡眠检测流程:从加速度计授权到报告渲染
3.1 小程序端四大页面与检测状态机
小程序端建议采用原生框架,页面拆成四个:首页(功能入口)、监测页(正在检测的实时状态)、报告页(睡眠结果图表)、我的(个人信息与历史记录)。核心是监测页,它管理着一个检测状态机:待开始 → 检测中 → 已结束。状态切换的细节决定用户体验,检测中要持续采集数据并本地聚合,切后台时要靠wx.setKeepScreenOn保持屏幕常亮,否则部分机型息屏后加速度计会停止回调。
Page({ data: { status: 'idle', // idle | running | finished elapsedSeconds: 0, motionCount: 0 }, onStart() { this.setData({ status: 'running' }); this._resetCounter(); wx.setKeepScreenOn({ keepScreenOn: true }); this._startAccelerometer(); this._timer = setInterval(() => this._tick(), 1000); }, _startAccelerometer() { wx.onAccelerometerChange((res) => { const acc = Math.sqrt(res.x * res.x + res.y * res.y + res.z * res.z); // 上一次的合成加速度与本次比较,变化超过阈值记为一次体动 if (this._lastAcc && Math.abs(acc - this._lastAcc) > 0.15) { this._bumpMotion(); } this._lastAcc = acc; }); wx.startAccelerometer({ interval: 'game' }); // game档约20次/秒 }, _tick() { const seconds = this.data.elapsedSeconds + 1; this.setData({ elapsedSeconds: seconds }); if (seconds % 60 === 0) { this._uploadMinute(); // 每60秒上报一次聚合结果 } } });interval: 'game'是小程序加速度计的档位参数,约 20 次/秒,是三者里最高的,能捕捉翻身这种短促动作。Math.sqrt(x² + y² + z²)是把三轴合成一个标量,比只看单轴更抗手机放置角度的影响。阈值0.15是经验值,单位约等于重力加速度 g,手机平放时静止状态的合成加速度约等于 1g,翻身瞬间会跳到 1.3g 以上。手机放在床上和拿在手里,数值基线不同,实机调试时如果发现误判,优先调这个阈值。
3.2 分钟级本地聚合与上报接口对接
端上不能一有体动就请求接口,否则一晚上几千条请求会把服务器打挂。正确做法是本地做分钟级聚合:_bumpMotion只做内存计数,_tick每秒更新倒计时,满 60 秒才把这一分钟的体动次数上报一次。这样一晚 8 小时睡眠,最多产生 480 条明细数据,接口压力完全可控。
_uploadMinute() { const data = { userId: wx.getStorageSync('userId'), minuteMark: this._formatTime(new Date()), motionCount: this.data.motionCount }; wx.request({ url: 'http://localhost:8080/api/sleep/report', method: 'POST', data: data, success: (res) => { if (res.data.code === 200) { this._resetCounter(); } else { // 上报失败:先缓存到本地,避免丢数据 this._saveToLocalCache(data); } }, fail: () => { this._saveToLocalCache(data); // 网络异常时写缓存,停止监测时做补偿上报 } }); }上报失败的处理是源码里最容易忽略但最值得讲的一处。前端上报失败如果直接丢弃,睡眠明细就会出现整段缺失,报告页的睡眠结构图会变成断崖。常见做法是失败时把数据写入wx.setStorageSync的本地缓存数组,等用户点结束监测时,finish接口先补偿上报缓存数据,再汇总,能兜住大部分弱网场景。这个设计答辩时是加分项。
3.3 报告页数据渲染:睡眠结构图与评分展示
监测结束后,报告页从后端拉取t_sleep_record和t_sleep_detail,用小程序原生的ec-canvas或简单canvas绘制横向条形图,展示每一分钟的睡眠状态。也可以用纯 CSS 的色块平铺方案——一天 480 分钟,用 480 个小色块按时间轴排列,深睡用深蓝色、浅睡用浅蓝、清醒用灰色,视觉上就是一条「睡眠彩虹条」,实现简单且信息密度高。
async onLoad(options) { const recordId = options.recordId; const res = await wx.request({ url: `http://localhost:8080/api/sleep/detail?recordId=${recordId}`, }); if (res.data.code === 200) { const detail = res.data.data.detailList; // 把每分钟的状态映射为色块:0清醒(灰) 1浅睡(浅蓝) 2深睡(深蓝) const colors = detail.map(item => { return item.status === 2 ? '#1e3a5f' : (item.status === 1 ? '#7fb3d5' : '#cccccc'); }); this.setData({ colors: colors.join(','), score: res.data.data.score }); } }这里用options.recordId而不是把整份报告数据塞进页面,是为了支持从历史列表二次进入报告页。colors.join(',')只是一个把数据塞给视图层的快速方案,实际项目里我一般会把它组装成数组对象、带上时间点,下面用wx:for渲染色块和 tooltip,这样手指滑到某个色块能显示对应时刻的睡眠状态,演示效果更好。
4. 把前后端源码跑起来:环境搭建、数据库初始化与联调顺序
4.1 交付物里有什么,运行环境需要哪些东西
源码包解压后,典型结构是:backend(后端代码)、miniprogram(小程序前端)、sql(数据库脚本)、docs(说明文档与 LW 文档)。先别急着双击源码,按「数据库 → 后端 → 前端 → 联调」的顺序来。环境要求不复杂:JDK 1.8 以上、Maven 3.x、MySQL 5.7 或 8.x、微信开发者工具稳定版。
需要注意一点:后端我用的是 Spring Boot,端口默认 8080,数据库账号密码是root/123456。这些配置集中在application.yml,拿到源码第一件事是先改这个文件,否则启动会因连不上数据库而报错。
4.2 第一步:初始化 MySQL 表结构与测试数据
mysql -u root -p123456 < sql/init.sql如果对命令行不熟,也可以用 Navicat 等图形工具。执行完init.sql后,库里会生成t_user、t_sleep_record、t_sleep_detail三张表,并在用户表里写入一个测试账号test/123456。这里有个血泪经验:init.sql里如果带了DROP TABLE IF EXISTS,执行前一定确认没有正在使用的历史数据;反之如果没有DROP语句,重复执行会重复插入测试用户导致主键冲突。
# application.yml 关键配置段 server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/sleep_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver sleep: threshold: deep: 3 # 体动次数<=3判定深睡 light: 12 # 体动次数<=12判定浅睡 motion-delta: 0.15 # 加速度合成量变化阈值,单位gserverTimezone=Asia/Shanghai必须加上,否则 MySQL 8 在 JDBC 连接时默认取 UTC 时区,导致sleep_date日期错位 8 小时。sleep.threshold.*是自定义配置项,对应 2.3 节里SleepRule读取的阈值。调试期改阈值不用重新编译,改完重启服务即可热生效。
4.3 第二步:启动后端并用 curl 验证接口
后端启动命令常规是mvn spring-boot:run,也可以mvn clean package后运行 jar 包。启动完成后,先用最轻量的接口验证环境是否通,不要一上来就调睡眠上报接口,那样一旦失败不好定位是网络问题还是代码问题。
# 验证后端进程与端口 curl http://localhost:8080/api/user/login -X POST -H "Content-Type: application/json" -d '{"username":"test","password":"123456"}' # 预期返回类似 {"code":200,"data":{"id":1,"username":"test"}}如果 curl 返回 401 或连接失败,优先看两个地方:一是后端控制台有没有报「数据库连接拒绝」;二是 8080 端口是否被占用。定位手段是netstat -ano | grep 8080(Windows 是netstat -ano | findstr 8080)。端口被占用的处理办法不是强杀进程,而是改application.yml的server.port为 8081,前后端一并改掉。
4.4 第三步:微信开发者工具导入小程序并打开「不校验域名」
小程序端导入miniprogram目录,然后在开发者工具的「详情 → 本地设置」里勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」。这一步是所有小程序本地联调必做的,不勾选的话,向http://localhost:8080发起的请求会直接报url not in domain list。
联调时把项目里的requestUrl配置指向http://localhost:8080。真机预览时有两个选择:一是手机和电脑连同一 Wi-Fi,把 localhost 改成电脑的局域网 IP,比如http://192.168.1.101:8080;二是用工具自带的隧道方案。前者的坑在于局域网 IP 可能因 DHCP 漂移变化,所以每次真机调试前先ipconfig确认一下当前 IP 再填。
// config.js 小程序端接口地址配置 module.exports = { baseUrl: 'http://192.168.1.101:8080/api', // 真机调试时替换为电脑局域网IP uploadInterval: 60, // 上报周期,单位秒,默认60 motionThreshold: 0.15 // 与后端 motionDelta 保持一致 };真机上测试时,手机要放在床垫上而非拿在手里。放在床头柜上和放在床垫上,加速度计感受到的震动幅度差异非常大——床垫能传递翻身震动,床头柜基本不行。这个细节直接影响「能否测出深睡和浅睡的区分度」。我第一次真机测试放在桌面上跑了一小时,结果全是深睡,还以为是算法写错了。
4.5 第四步:完整联调一条链路
联调的目标不是「接口通了」,而是「能造出一条真实的睡眠记录并看到报告」。
1. 小程序登录 test/123456 2. 点击开始监测,手机平放到床垫上 3. 模拟睡眠:保持静止5分钟(产生深睡数据),再翻身几次(产生浅睡数据) 4. 点击结束监测 5. 在报告页查看结果:应能看到深睡/浅睡分段、睡眠评分 6. 再到 MySQL 查询 t_sleep_record 和 t_sleep_detail 确认数据落库第 6 步很多人不做,但恰恰是最重要的。mysql> SELECT * FROM t_sleep_record WHERE user_id = 1;能确认端上数据是否真的进了库里,如果记录存在但报告页报错,问题大概率在后端查询接口或前端渲染。建议把「数据库查到记录」作为联调通过的验收标准。
5. 毕设项目避坑实录:部署、数据处理与演示现场的 5 个典型问题
5.1 现象:后端启动报「Unknown database」或「Access denied」
原因:application.yml里的库名和实际建的库名不一致,或者 MySQL 8 的认证插件与驱动不兼容。解决:先确认init.sql里有没有CREATE DATABASE sleep_demo;如果是 MySQL 8,确认pom.xml里的驱动版本是mysql-connector-java8.x,并且连接 URL 带useSSL=false。这个问题在首次部署时出现率最高,属于「启动即劝退」级别的问题。
5.2 现象:小程序端请求报「url not in domain list」
原因:微信开发者工具默认校验业务域名,而本地开发用的是http://localhost或局域网 IP,不在白名单。解决:在详情 → 本地设置勾选「不校验合法域名」。注意这个设置是工具级、非项目级的,换一台电脑重开工具需要重新勾选。另一个理解上的坑:真机预览时这个勾选依旧生效,但必须在「预览」之前确认手机端的调试模式打开,否则真机照样拦截。
5.3 现象:报告页睡眠结构图出现整段空白
原因:上报接口在弱网或小程序切后台时被系统冻结,wx.request失败后数据没有补偿,导致这分钟的数据直接丢失。解决:检查 3.2 节的_saveToLocalCache补偿逻辑是否被删过,或者在finish接口调用前先把本地缓存的失败数据重新推一遍。这类问题隐蔽在「网络正常时根本不会触发」,我通常建议在_uploadMinute里加一条日志或计数显示,演示时故意开飞行模式 10 秒再关掉,验证补偿链路。
5.4 现象:体动次数异常高,整晚都是「清醒」
原因:手机放置位置不当,或者motion-delta阈值过低。合成加速度在静止时本来就有 ±0.05g 的抖动噪声,如果阈值设到 0.05,传感器自身的噪声都会触发体动计数。解决:把motion-delta调到 0.15~0.25,并检查放置位置——放在床垫弹簧附近会因为翻身整个床垫震动而剧烈触发。你可以先做一个 30 秒的静止对照测试:手机放稳不动,观察体动计数应稳定为 0;如果大于 0,说明阈值太低。
5.5 现象:答辩现场睡了一小时还是「检测中」
原因:演示者真的躺下等数据,但睡眠监测没有「自动结束」机制,需要手动点结束按钮,而演示者睡着了。解决:这是演示体验问题。我一般在测试账号里提前用第 6 章的模拟数据脚本生成一份 3 小时的完整睡眠数据,答辩时直接进历史记录看报告图。睡眠检测这类功能本来就是「慢数据」——真实采集一晚才有结果,演示现场永远等不起,必得准备模拟数据通道。
6. 进阶一步:用模拟数据脚本和双通道判定补齐演示与精度短板
模拟数据生成器是这套源码最值得做的一个附加工具。真实睡眠检测的演示痛点在于时间成本太高,你不可能在评审现场睡 3 小时。常见做法是写一段 Java 或 Python 脚本,按时间序列生成一张sleep_detail表的模拟数据:前 20 分钟深睡、中间穿插浅睡、后面清醒,直接插入数据库。这样既不需要真实采集,又能让报告页有完整的图表可展示。
#!/usr/bin/env python3 # 生成3小时模拟睡眠数据,直接写入MySQL import random from datetime import datetime, timedelta import pymysql conn = pymysql.connect(host='localhost', user='root', password='123456', database='sleep_demo', charset='utf8mb4') cursor = conn.cursor() record_id = 1001 start = datetime.now().replace(minute=0, second=0, microsecond=0) for i in range(180): # 180分钟 minute = start + timedelta(minutes=i) # 前20分钟深睡,21-90浅睡与深睡交替,之后清醒居多 if i < 20: status, motions = 2, random.randint(0, 2) elif i < 90: status = 2 if i % 3 == 0 else 1 motions = random.randint(0, 8) else: status = 1 if i % 4 == 0 else 0 motions = random.randint(5, 20) cursor.execute( "INSERT INTO t_sleep_detail(record_id, minute_mark, status, motion_count) " "VALUES(%s, %s, %s, %s)", (record_id, minute, status, motions) ) conn.commit()脚本里i < 20强制前 20 分钟为深睡,是有意为之——模拟数据要符合「睡眠由深到浅、清晨清醒增多」的常识曲线,才经得起评审推敲。演示时直接查t_sleep_record或从历史列表进报告页即可,不用真睡。
算法侧还有一个低成本优化:体动检测叠加声音通道。在后端SleepRule里增加一个参数,接收小程序端上报的一分钟平均分贝。夜间安静时分贝低,翻身和鼾声会推高分贝。综合判定规则常见做法是:体动说深睡但分贝超过阈值时降级为浅睡,体动说清醒但分贝极低时仍判浅睡——双通道交叉校验能明显减少「翻个身就被记成清醒」的误判。实现上不复杂,多一张t_sleep_audio明细表或在t_sleep_detail加一列均可。
这段实践给我的教训很具体:毕设级项目的核心不是把算法做得多准,而是把「数据从哪来、怎么判、存哪里、怎么展示」这条链路保证 100% 可演示。我见过太多同学在算法上投入大量时间,结果评审现场小程序连不上后端。先把模拟数据生成器和接口补偿链路做好,再谈精度优化,这个顺序不要颠倒。希望这些踩坑记录能帮你把这套源码顺利跑起来,答辩时把注意力放在讲清设计决策上,而不是被部署问题拖住。
本文还有配套的精品资源,点击获取