微信小程序睡眠检测实战:从加速度计到状态机的完整拆解 简介压缩包内是一款面向普通用户与轻度失眠人群的睡眠检测微信小程序基于微信小程序原生框架开发通过睡眠时长、深度、翻身次数等数据完成智能分析与可视化展示。资源共110个文件主要包括53个png图标与界面素材、14个js逻辑脚本、13个json配置与数据文件、11个wxss样式、10个wxml页面结构另有doc文档与mp4演示视频等包体约22.59MB结构清晰适合直接学习或二次开发。目前已有88人浏览学习。资源中除完整小程序源码外还附带《睡眠助手》设计与实现文档、开题报告、效果展示gif及echarts图表组件能够帮助开发者快速理解小程序项目从需求分析、界面设计到数据可视化的完整流程。对于关注睡眠健康的用户可直接在微信中运行体验对于小程序初学者则可将其作为实战参考学习页面布局、数据绑定、图表渲染等关键技能。1. 一个能跑的睡眠检测小程序拆开 Zip 后你会看到什么拿到一个落地的“睡眠检测小程序.zip”比看十篇睡眠 App 的界面评测更有价值。它不是一个 PPT 原型也不是几张高保真图为了一等奖打磨出的静态稿而是一份可交付、可运行、可二次改写的微信小程序源码包。你解压后会看到pages目录里的sleep、report、setting三组页面utils里大概率躺着传感器数据处理模块app.json里还配好了加速度计的权限申请。这些文件真正要解决的问题只有一个怎么用手机内置的传感器和微信的 API在不对用户造成打扰的前提下推算出晚上躺下、入睡、深睡与醒来的时间段。这套方案对三类人最有用做微信小程序毕业设计的在校生需要一份能现场演示、不至于答辩时白屏的完整代码初级前端开发想看看小程序里除了“商城 购物车”之外还能切什么赛道以及正在做健康类产品、想低成本验证睡眠检测需求的独立开发者。本文不分析某个我看不到的源码内部注释而是以一个一线工程师顺着标题讲方案的思路把这类小程序从解开压缩包到真机调通的完整路径铺开。2. 解压 Zip 包与小程序基础结构先让项目在开发者工具里跑起来2.1 一份标准微信小程序源码包里必须有哪些文件不管这个压缩包叫“睡眠检测小程序”还是别的名字只要是基于微信小程序原生语法写的解开后必然能看到下图这套固定骨架。微信开发者工具识别一个项目的唯一依据是根目录下的project.config.json而不是app.json。前者描述的是“这个目录是给微信开发者工具用的工程”后者才是小程序真正运行时的全局逻辑入口。sleep-miniapp/ ├── app.js # 小程序逻辑入口注册 App() 实例 ├── app.json # 全局配置页面路由、窗口样式、权限申请 ├── app.wxss # 全局样式表 ├── project.config.json # 开发者工具工程配置含 appid 与 libVersion ├── sitemap.json # 搜索引擎收录配置默认即可 ├── pages/ │ ├── index/ # 首页 / 睡眠总览 │ ├── sleep/ # 监测页启动/停止监测传感器数据显示 │ └── report/ # 报告页查看历史睡眠质量 ├── utils/ │ ├── sensor.js # 加速度计数据处理 │ ├── calc.js # 睡眠质量计算时长、深度、觉醒次数 │ └── format.js # 时间格式化工具 └── images/ # tabBar 图标等静态资源在终端里解压后第一件事是打开project.config.json确认appid字段。如果压缩包作者用的是测试号appid会是一个touristappid你导入自己的开发者工具时得替换成自己小程序的 AppID否则真机预览时会被微信拦在门外。libVersion字段建议升级到2.30.0以上低版本基础库对wx.onAccelerometerChange这类传感器 API 的支持有兼容缺口。{ appid: wx你的真实appid替换这里, compileType: miniprogram, libVersion: 2.30.0, projectname: sleep-miniapp, setting: { es6: true, postcss: true, minified: true } }es6设为true是关键压缩包里如果用了async/await或Promise而这里没开开发者工具会在编译时报一堆“语法错误”提示。postcss负责自动补全样式前缀minified决定上传代码时是否压缩体积本地调试阶段关掉也无妨但正式发布建议打开。2.2 全局配置app.json权限、页面路由和导航栏动态标题睡眠检测绕不开加速度计而微信小程序在申请这类传感器权限时不能像网页那样通过navigator.permissions直接询问而是要依赖基础库的隐私接口。app.json里的permission字段是写在全局配置里的配合sitemapLocation一起声明{ pages: [ pages/index/index, pages/sleep/sleep, pages/report/report ], permission: { scope.userLocation: { desc: 用于校准睡眠时的设备位置 } }, requiredBackgroundModes: [audio], window: { navigationBarTitleText: 睡眠检测, navigationBarBackgroundColor: #1f1f2e, navigationBarTextStyle: white } }requiredBackgroundModes声明音频后台运行是为了万一睡眠检测的增强版本用到录音分析鼾声App 切换到后台时还能继续采音。如果你只用加速度计方案这个字段可以去掉省得微信审核时问你要后台运行的具体说明。navigationBarBackgroundColor用深色是为了配合睡眠检测场景的暗色 UI减少夜间看手机时的刺眼感。导航栏标题在运行时会动态变。比如用户在睡眠监测页启动检测后标题从“睡眠检测”改成“监测中 03:24”这个动作在页面 JS 里用wx.setNavigationBarTitle实现不是改app.json改app.json是全局静态的。压缩包里如果没实现这个交互你拿到后第一件事就应该在onShow生命周期里补上动态标题逻辑增强“实时感”。2.3 从 Zip 到可调试状态的 3 条命令行与导入细节在终端里解压时macOS 和 Windows 有细节差异。macOS 上用unzip直接解到当前目录Windows 上如果没有安装解压工具可以打开 PowerShell 运行下面这条命令不需要额外装第三方软件。# macOS / Linux unzip 睡眠检测小程序.zip -d ./sleep-miniapp# Windows PowerShell Expand-Archive -Path 睡眠检测小程序.zip -DestinationPath .\sleep-miniapp解压后如果发现文件夹里多了一层__MACOSX那是 macOS 的隐藏元数据目录直接删掉即可不影响小程序运行。导入微信开发者工具时选择“导入项目”目录指向解压出来的sleep-miniappAppID 按前面说的替换成自己的。如果开发者工具提示“文件过多”或“项目目录不是空的”检查一下是不是把整个用户主目录选中了而不是选中包含project.config.json的那个子目录。导入成功的标志是模拟器上出现首页“睡眠总览”的界面。如果页面白屏或报app.json解析错误最可能的原因是压缩包在传输过程中被截断导致 JSON 文件缺了一个}。这时用 Node 跑一条校验命令能快速定位问题文件node -e JSON.parse(require(fs).readFileSync(app.json,utf8)); console.log(app.json ok)同理检查project.config.json和每个page下的.json文件。大多数“解压了却跑不起来”的纠纷源头都是这里。源码包作者在打 zip 时可能误用了非 UTF-8 编码导致中文注释变成乱码但 JSON 解析只看括号不看注释如果连解析都报错那必然是文件本身损坏这和“zip 压缩包密码破解工具”没有关系不需要去绕什么加密正常解压后排查才是正道。3. 睡眠检测核心逻辑从加速度计原始数据到睡眠状态机3.1 体动检测模型的原理与误差边界睡眠检测小程序里最容易被忽视、却最决定体验的是数据来源。市面上大多数纯软件睡眠监测 App 采用两种方案加速度计体动检测和麦克风鼾声识别。前者省电、稳定、不涉及录音隐私适合做成微信小程序但精度受“手机放哪儿”影响极大后者能分析 REM 期却需要持续录音微信审核会要求额外声明隐私用途。标题里锁定了“睡眠检测”而非“睡眠监测医疗”所以用加速度计方案是取舍后最可靠的做法。它的物理逻辑很直接人在清醒或浅睡期身体翻转、手臂移动、抓起手机看时间等动作频繁传感器感受到的三轴合加速度变化剧烈进入深睡后体动明显减少三轴加速度曲线接近一条平线。算法实现上不需要做傅里叶变换常见的做法是取一个时间窗口的加速度标准差。小于某个阈值判定为“稳定期”连续稳定超过 20 分钟进入“深睡状态”窗口内有几次剧烈波动则标记为“觉醒事件”。这个四状态模型足够满足毕业设计和初版产品IDLE - FALLING_ASLEEP - ASLEEP - WAKE_UPFALLING_ASLEEP是一个缓冲态用来过滤“躺下玩手机”和“睡着”之间的模糊地带。如果你刚从床上坐起来拿水杯加速度计会立刻探测到剧烈变化但你不能因为一次坐起就把整段睡眠打断。缓冲态就是用来吞掉这种短时扰动的。设置缓冲时长为 5 分钟意思是体动平息后 5 分钟内没有大幅波动才真正从“躺下”转为“睡着”。3.2 用wx.onAccelerometerChange收集数据的完整代码微信小程序原生提供了wx.onAccelerometerChange这个 API它会在每次传感器数据变化时回调一个包含x、y、z三轴加速度的对象单位是m/s²。注意它和我们高中物理里的重力加速度g不是一回事手机静止平放时z轴读数约为9.8。使用前要先通过wx.startAccelerometer设置监听频率这个interval参数的取值直接决定了电池消耗和精度interval 取值回调频率适用场景单小时耗电估算game约 20ms体感游戏夜间检测没必要高ui约 60ms页面滚动加速计特效中normal约 200ms睡眠检测推荐值兼顾精度与续航低选normal就够了。睡眠体动不是一个高频信号200ms 的采样率意味着每秒 5 个数据点一晚上 8 小时就是 14.4 万个点这个体量在数组里做滑动窗口计算完全没有性能压力。下面是收集模块的完整代码放在utils/sensor.js里// 传感器数据管理器缓冲原始三轴数据并对外提供窗口特征 const sensor { dataBuffer: [], // 存放最近 30 秒的加速度模长 windowMaxCount: 150, // 30秒 * 5条/秒 150条 timer: null, start(interval normal) { wx.startAccelerometer({ interval }) wx.onAccelerometerChange(({ x, y, z }) { // 算三轴合加速度消除手机摆放朝向的影响 const magnitude Math.sqrt(x * x y * y z * z) // 重力加速度在9.8附近这里减去9.8取波动残差 this.dataBuffer.push(Math.abs(magnitude - 9.8)) if (this.dataBuffer.length this.windowMaxCount) { this.dataBuffer.shift() } }) }, // 返回最近30秒内的体动指数标准差越大说明动作越剧烈 getActivityIndex() { if (this.dataBuffer.length 50) return 0 const mean this.dataBuffer.reduce((a, b) a b, 0) / this.dataBuffer.length const variance this.dataBuffer.reduce((sum, val) sum Math.pow(val - mean, 2), 0) / this.dataBuffer.length return Math.sqrt(variance) }, stop() { if (this.timer) clearInterval(this.timer) wx.stopAccelerometer() } } module.exports sensor代码里的关键点在合加速度逻辑上手机侧放、竖放、平放三轴的分配完全不同如果单看某一轴同一个翻身动作在不同摆放方式下读到的数值会天差地别。三轴合加速度把方向信息抹平了只剩“整体运动的剧烈程度”这一个标量这对睡眠检测来说是好事因为判断睡眠只需要强度不需要方向。注释里把每个变量的单位标清楚这在自己回看或者答辩演示时非常有帮助。dataBuffer用shift()维持一个长度为 150 的滑动窗口窗口越短对“翻身”这类瞬时动作越敏感但噪声也越大。30 秒窗口是经验值——人在浅睡期的一次翻身大概持续 2 到 3 秒如果窗口只有 3 秒一次翻身就会让标准差飙升导致整个晚上频繁被误判为“清醒”。窗口拉长到 30 秒单次翻身的贡献就被稀释了。3.3 睡眠状态机的状态转移与参数阈值有了getActivityIndex()返回的体动指数下一步是将这批连续的浮点数翻译成可读的睡眠阶段。状态机的好处是判定不是孤立地看当前时刻而是结合前一状态做转移能显著减少抖动。下面是状态机核心代码放在utils/calc.js中const SleepState { IDLE: idle, FALLING_ASLEEP: falling_asleep, ASLEEP: asleep, WAKE: wake } // 状态机配置参数 const config { fallAsleepSeconds: 300, // 从躺下到入睡的缓冲期默认5分钟 deepSleepSeconds: 1200, // 连续稳定20分钟判定为深睡 activityWake: 1.2, // 体动指数超过1.2视为一次觉醒 activityCalm: 0.4 // 体动指数低于0.4视为稳定 } const stateMachine { state: SleepState.IDLE, calmAccumulator: 0, wakeAccumulator: 0, sleepStartTime: null, feed(activityIndex, timestamp) { const isCalm activityIndex config.activityCalm const isActive activityIndex config.activityWake switch (this.state) { case SleepState.IDLE: if (isCalm) { this.calmAccumulator 1 if (this.calmAccumulator * 30 config.fallAsleepSeconds) { this.state SleepState.FALLING_ASLEEP this.sleepStartTime timestamp } } break case SleepState.FALLING_ASLEEP: if (isActive) { // 缓冲期内大幅动作重新计时 this.calmAccumulator 0 this.state SleepState.IDLE } else { this.state SleepState.ASLEEP } break case SleepState.ASLEEP: if (isActive) { this.wakeAccumulator 1 if (this.wakeAccumulator * 30 180) { // 连续3分钟体动剧烈认为醒了 this.state SleepState.WAKE } } else { this.wakeAccumulator 0 } break case SleepState.WAKE: if (isCalm) { this.state SleepState.ASLEEP } break } } } module.exports { stateMachine, SleepState, config }这里的activityWake: 1.2和activityCalm: 0.4两个阈值是最需要根据真机实测调整的参数。压缩包自带的阈值可能在作者的测试机上表现良好换一台手机就完全失灵。原因在于不同型号手机的加速度计噪声基底不一样中低端安卓机在静止状态下标准差能到 0.6iPhone 则能压到 0.2 以下。如果你的测试机静止时getActivityIndex()返回 0.6而阈值是 0.4那机器会一直以为你在翻身整晚状态机都在 IDLE 和 FALLING_ASLEEP 之间跳。参数要能调不能写死在代码里。把这份config对象存到wx.getStorageSync(sleep_config)里页面加载时读取用户可以在设置页修改。体动指数的计算间隔是 30 秒calmAccumulator每次加 1 代表 30 秒稳定累计 10 次就是 5 分钟。asleep状态里连续 6 次指数超过 1.2 才判觉醒这个 180 秒的过滤条件非常关键否则半夜一次的挠头动作就会把整段深睡打断。3.4 睡眠报告数据结构的建模与统计口径状态机每 30 秒产生一个状态标签把一整晚的标签序列汇总成报告。这里要特别提一个容易算错的统计口径睡眠时长到底是“从 FALLING_ASLEEP 到最终 WAKE 的时间”还是“ASLEEP 状态累计的时间”大多数用户能直观理解的答案是后者。你 23:00 躺下玩手机到 23:40 才真正入睡早上 7:00 醒来。那么“总睡眠时长”是 6 小时 20 分钟而不是 8 小时。如果把躺下就开始计时用户会吐槽“我明明玩了半小时手机为什么算我睡了半小时”这个体验损失很大。建设报告模型时把这两者分开记录const reportTemplate { date: 2025-06-01, bedTime: 23:00, // 用户点“开始监测”的时间 sleepStart: 23:40, // 状态机首次进入 ASLEEP 的时间 wakeTime: 07:00, // 状态机进入 WAKE 且后续未回睡的时间 totalSleepMinutes: 380, // 23:40到7:00之间的ASLEEP累计时长 awakeCount: 2, // 夜间体动导致的觉醒次数 deepSleepMinutes: 75, lightSleepMinutes: 305, qualityScore: 82 }压缩包里如果实现的是一整晚结束后一次性生成报告建议改成“每 5 分钟自动保存一次当前时序到本地缓存”。否则用户睡着了小程序被微信后台回收等你醒来打开时数据已经丢了。睡眠小程序的数据丢失是最致命的毕竟没有人愿意为了再次监测重新睡一觉。4. 数据持久化与睡眠报告页面用本地存储撑起整个历史记录4.1 用wx.setStorageSync按天存储睡眠数据微信小程序的本地缓存容量上限是 10MB对于纯文本的睡眠记录来说绰绰有余。一天的睡眠报告加上状态时序撑死了也就 20KB存一年 365 天不过 7.3MB。所以本地存储方案完全可行不需要一上来就上云开发。按天分 key 存储key 直接用日期字符串例如sleep_report_20250601查找逻辑简单也方便做日历视图。// pages/sleep/sleep.js 中保存报告的核心逻辑 function saveDailyReport(report) { const key sleep_report_${report.date.replace(/-/g, )} try { wx.setStorageSync(key, report) // 同时维护一个“最近30天报告索引”供日历视图读取 const indexKey sleep_report_index const index wx.getStorageSync(indexKey) || [] if (!index.includes(key)) { index.push(key) // 只保留最近60天的索引防止缓存无限膨胀 wx.setStorageSync(indexKey, index.slice(-60)) } console.log(睡眠报告已保存, key) } catch (e) { // 常见于存储已满或隐私模式下写入被拒 console.error(保存失败, e) } }存储策略要把“写入频率”和“写入粒度”都说清楚。睡眠过程中的中间态报告每 5 分钟覆盖写一次同一个 key避免碎片化写操作。当天睡眠结束后用最终状态覆盖完整报告。这样即使小程序被杀至少用户丢的数据不超过 5 分钟。index.slice(-60)这行的边界处理值得借鉴数组长度超过 60 时从头截断保证缓存目录永恒可控。4.2 报告页面的加载与动态标题切换报告页pages/report/report.js的onLoad接收从首页传过来的date参数读出对应存储并渲染。微信小程序的页面传参只能通过 URL 字符串所以日期格式要么用20250601这种纯数字要么对2025-06-01做encodeURIComponent否则中间那个减号在 URL 解析时会出现意外截断。// pages/report/report.js Page({ data: { report: null, scoreText: --, scoreColor: #ccc }, onLoad(query) { const dateKey query.date || 20250601 const report wx.getStorageSync(sleep_report_${dateKey}) if (report) { // 动态设置页面标题显示具体日期 wx.setNavigationBarTitle({ title: ${report.date} 睡眠报告 }) this.setData({ report }) } else { wx.setNavigationBarTitle({ title: 无报告 }) } } })wx.setNavigationBarTitle必须在页面onLoad里调用不能在onLaunch全局设置。刷新的标题会覆盖app.json中配置的静态标题这就是“微信小程序动态设置标题”的正确姿势。报告页里的scoreColor按分数变化好分数用暖黄色差分数用灰色不要用红绿两色色弱用户可能会看不清。5. 真机调试、阈值校准与 3 个可以长期用的小技巧5.1 为什么开发者工具里的加速度计数据是假的微信开发者工具模拟器没有物理加速度计wx.onAccelerometerChange在模拟器里会返回一组预先写死的模拟数据通常是完美平放的{ x: 0, y: 0, z: 9.8 }。这意味着模拟器上体动指数永远是 0状态机永远判“稳定”看起来一切正常到了真机上就原形毕露。排查睡眠检测小程序的问题第一步就是拿真实手机扫码预览。真机调试时打开vConsole在sensor.js的getActivityIndex里加一行console.log(activity:, this.getActivityIndex())。然后把手机放桌上静止 30 秒记录下静止基线值再拿在手里走路 30 秒记录活动峰值。这两个数字之差决定了你的activityCalm和activityWake应该怎么设。如果静止基线是 0.5活动峰值是 0.8说明这台手机传感器噪声高你可以考虑提高采样频率到ui级别让数据更密集后再做滑动平均。5.2 参数实测校准流程拍平手机与侧放手机的基线对照校准流程建议做成一个隐藏页面别让用户自己找。常见做法是在设置页长按“监测灵敏度”标题 3 秒弹出校准面板运行下面这段流程手机平放桌面保持 60 秒计算平均体动指数记为baseline_flat手机侧放立起来靠在枕头边保持 60 秒计算平均体动指数记为baseline_side手拿手机从平放到立起快速翻转 5 次记录最大体动指数记为peak_active最终activityCalm Math.max(baseline_flat, baseline_side) * 1.5最终activityWake peak_active * 0.4。这套流程录完后把结果写回存储里的sleep_config状态机每次启动前读取。有了真机基线不同手机间的体验就能保持大体一致。这一步做完睡眠检测的准确率从“图一乐”提升到“能看出昨晚睡得好不好”的日常可用程度体感差异巨大。5.3 把校准参数做成可分享二维码二次分发你的配置睡眠检测小程序的调试往往需要多人参与不同手机混测时“我这台阈值不对啊”就成了高频反馈。与其把参数发来发去不如把sleep_config的 JSON 按encodeURIComponent编码后塞进一个小程序码。用户扫码后自动解析参数并写入本地这就是给“睡眠检测小程序.zip”这份代码附加的分发技巧。不过要注意微信小程序的二维码生成接口需要后端调用独立开发者没有服务器的话可以用wx.scanCode让已经配好的手机直接扫码通过临时会话转发方式同步配置。// 分享配置把阈值编码进分享参数 function shareConfig() { const config wx.getStorageSync(sleep_config) const encoded encodeURIComponent(JSON.stringify(config)) wx.shareAppMessage({ title: 我的睡眠监测校准参数, path: /pages/sleep/sleep?config${encoded} }) }接收方在onLoad里检测到query.config时执行JSON.parse(decodeURIComponent(query.config))后写回存储。这样一位测试者校准完参数其余人扫码即可复用省去了反复讲解activityCalm和activityWake含义的时间。本文还有配套的精品资源点击获取