ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Flask+SQLite打造睡眠质量数据记录与可视化分析工具

用Flask+SQLite打造睡眠质量数据记录与可视化分析工具 “Shitty Sleep”这个项目名如果直译多少带点自嘲。它并不打算做什么温和的助眠 App而是要解决一个很实际的问题当一个人发现自己入睡越来越晚、早上醒来越来越累时怎么用数据而不是“感觉”把“睡得不怎么样”变成可回溯、可比较、可分析的事实。一句话说清楚这个项目它是一个本地运行的睡眠质量记录与分析工具。用户可以记录每天入睡时间、醒来时间、主观睡眠质量评分以及睡前咖啡因摄入、运动时长、睡前刷手机时长等可能影响睡眠的因素后端把数据写入 SQLite并通过接口返回统计结果前端页面用表格和折线图把趋势呈现出来。项目代号叫 Shitty Sleep是因为它专门面向那些“今晚大概率又睡不好”的真实场景而不是面向已经拥有良好作息的人。这个项目的适用范围很明确适合刚接触 Web 开发的 Python 学习者练手也适合想用轻量工具观察自己作息变化的人。阅读本文需要你对 Python 语法、Flask 基本用法和 SQLite 有初步了解不需要精通前端。最终你会得到一个可运行的睡眠记录页面并理解字段设计、跨天时间计算、数据统计接口这些容易被忽略的关键细节。1. 先弄清楚项目边界Shitty Sleep 到底要帮你解决什么问题1.1 睡眠分析不能只依靠“感觉”很多人复盘睡眠时只会说“昨晚睡得好”或“今天特别困”。这种判断的问题是颗粒度太粗无法回答三个关键问题到底几点睡、几点醒睡眠时长是否在连续下滑睡前喝咖啡、刷手机等行为对第二天的状态影响有多大Shitty Sleep 做的事情是把睡眠拆成可以量化的字段。每一次记录都对应一行数据连续记录一周后趋势就会浮现出来。比如你可能发现自己工作日平均睡眠时长只有 5 小时 40 分而周末恢复到 7 小时也可能发现只要睡前摄入咖啡因第二天质量评分就没有超过 6 分。这种结论不是靠记忆而是靠数据。1.2 项目能力边界记录、统计、可视化这个项目不做实时监测也不连接手环只做三件事能力具体内容数据录入保存日期、入睡时间、醒来时间、质量评分、咖啡因摄入、运动时长、屏幕使用时间、备注数据统计计算平均睡眠时长、平均入睡时间、平均质量评分以及最近 7 天记录可视化使用 ECharts 展示睡眠时长和质量评分的变化趋势需要提前说明这只是一个生活观察工具不是医疗软件。如果你或身边人长期存在严重睡眠问题应该咨询专业医生而不是只靠一个自建项目做判断。项目里记录的主观评分也只能反映个人感受不能替代任何医学指标。1.3 技术选型为什么是 Flask SQLite 原生页面这个项目没有选择重量级方案主要基于“最小可运行”的考虑选型理由Flask两个 API 就能完成核心功能不需要 Django 的 Admin、ORM、中间件等全套能力SQLite单机自用场景不需要安装独立的数据库服务Python 内置 sqlite3 模块即可原生 HTML ECharts只做表格和折线图不引入 React/Vue 构建流程降低落地成本CDN 引入 ECharts网络可用时直接使用适合快速演示生产环境建议改为本地静态文件这套组合的意义在于你不需要为跑通项目安装整套前后端工具链。只要机器上有 Python就可以完成从建库到出图的过程。如果你以后想把工具分享给多人使用再升级到 MySQL 用户系统也不晚。2. 先把睡眠数据模型设计好否则后期改表很痛苦2.1 睡眠日志表要存哪些字段设计表结构时最容易犯的错误是想得过于简单只存日期和“睡得好不好”。这样后期一旦想分析“咖啡因对睡眠的影响”就不得不改表。Shitty Sleep 的睡眠日志表包含以下字段字段名类型说明idINTEGER主键自增record_dateTEXT记录日期格式 YYYY-MM-DDsleep_timeTEXT入睡时间格式 HH:MMwake_timeTEXT醒来时间格式 HH:MMquality_scoreINTEGER主观睡眠质量评分1 到 10caffeine_before_sleepINTEGER睡前是否摄入咖啡因0 表示否1 表示是exercise_minutesINTEGER当天运动时长单位分钟screen_timeINTEGER睡前屏幕使用时间单位分钟moodTEXT入睡前的心情标签例如 anxious、calm、tirednoteTEXT自由文本备注created_atTEXT记录创建时间默认当前本地时间这里有一个设计取舍入睡时间和醒来时间都只存 HH:MM而不是完整的日期时间。原因是录入成本问题。如果让用户每次都选择一个带日期的“入睡时间”和“醒来时间”界面会变得很重。保留 HH:MM 后用户只需要填写“昨晚 23:30 睡今天 07:00 醒”更贴近人的自然表达。代价也很明显跨天计算睡眠时长时需要判断醒来时间是否小于入睡时间。这个逻辑会在后面重点处理。2.2 建表 SQL 和初始化脚本在项目根目录创建schema.sql内容如下CREATE TABLE IF NOT EXISTS sleep_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, record_date TEXT NOT NULL, sleep_time TEXT NOT NULL, wake_time TEXT NOT NULL, quality_score INTEGER NOT NULL DEFAULT 5, caffeine_before_sleep INTEGER NOT NULL DEFAULT 0, exercise_minutes INTEGER NOT NULL DEFAULT 0, screen_time INTEGER NOT NULL DEFAULT 0, mood TEXT, note TEXT, created_at TEXT NOT NULL DEFAULT (datetime(now, localtime)) );注意几个设计点record_date记录的是“这次睡眠所属的日期”一般填写入睡当天的日期。quality_score用 1 到 10 的整数避免用户打分时陷入“3.5 分算几分”的纠结。caffeine_before_sleep用 0 和 1 表示布尔值SQLite 没有独立布尔类型这样做最直观。created_at只用于审计不参与业务统计。初始化数据库时可以写一个一次性脚本。如果使用 Python 交互窗口可以执行import sqlite3 conn sqlite3.connect(sleep_log.db) with open(schema.sql, r, encodingutf-8) as f: conn.executescript(f.read()) conn.commit() conn.close()执行后项目目录下会生成sleep_log.db文件。这个文件就是整个项目的数据库。删除该文件再重新执行脚本就能回到初始状态。2.3 关键指标的计算口径睡眠时长、跨天处理、质量评分睡眠质量分析里最核心的指标是“睡眠时长”。计算方式是把醒来时间减去入睡时间单位是分钟。但这里有一个经典坑从 23:30 睡到 07:00如果直接相减结果是负数。正确做法是先把两个字符串解析成时间对象然后计算差值。如果差值小于等于 0说明睡眠跨过了午夜需要加 24 小时。from datetime import datetime, timedelta def calc_duration_minutes(sleep_time: str, wake_time: str) - int: sleep_dt datetime.strptime(sleep_time, %H:%M) wake_dt datetime.strptime(wake_time, %H:%M) delta wake_dt - sleep_dt if delta.total_seconds() 0: delta timedelta(days1) return int(delta.total_seconds() / 60)示例print(calc_duration_minutes(23:30, 07:00)) # 450 print(calc_duration_minutes(00:15, 07:00)) # 405 print(calc_duration_minutes(22:00, 23:00)) # 60“平均睡眠时长”是对所有记录先计算单次时长再取平均。不能用简单的“平均入睡时间和平均醒来时间相减”因为平均入睡时间会受到极端值影响导致结果失真。“质量评分”是用户输入的主观值。分析时可以计算平均值也可以统计评分小于等于 4 的次数作为状态预警信号。但在最小版本中只做平均和趋势展示。3. 后端 API 实现把记录和统计变成两个核心接口3.1 项目目录结构和依赖安装为了让项目结构清楚建议采用下面的目录shitty-sleep/ app.py # Flask 应用和 API 路由 schema.sql # 建表 SQL sleep_log.db # SQLite 数据库文件 requirements.txt # Python 依赖 static/ index.html # 前端页面requirements.txt内容flask2.2安装命令pip install -r requirements.txt如果你的网络环境无法访问 PyPI也可以直接用当前环境已有的 Flask。若没有 Flask不要跳过这一条否则后面启动时会直接报ModuleNotFoundError: No module named flask。3.2 数据库连接和公共工具函数app.py是后端入口。使用 Flask 内置的sqlite3连接数据库时需要处理两件事数据库连接按请求创建和关闭以及查询结果能按字典读取。import os import sqlite3 from datetime import datetime, timedelta from flask import Flask, g, jsonify, request DATABASE os.path.join(os.path.dirname(__file__), sleep_log.db) app Flask(__name__) def get_db(): if db not in g: g.db sqlite3.connect(DATABASE) g.db.row_factory sqlite3.Row return g.db app.teardown_appcontext def close_db(exception): db g.pop(db, None) if db is not None: db.close() def calc_duration_minutes(sleep_time: str, wake_time: str) - int: sleep_dt datetime.strptime(sleep_time, %H:%M) wake_dt datetime.strptime(wake_time, %H:%M) delta wake_dt - sleep_dt if delta.total_seconds() 0: delta timedelta(days1) return int(delta.total_seconds() / 60)要点g是 Flask 的应用上下文对象适合存放请求级别的数据库连接。row_factory sqlite3.Row让查出来的行支持row[字段名]访问。teardown_appcontext确保每次请求结束后连接被关闭避免连接泄漏。3.3 新增睡眠记录的接口POST /api/sleep接收 JSON写入数据库。核心逻辑是校验字段、计算时长、插入数据。app.route(/api/sleep, methods[POST]) def add_sleep_record(): data request.get_json(silentTrue) if data is None: return jsonify({error: 请求体必须是 JSON}), 400 record_date data.get(record_date) sleep_time data.get(sleep_time) wake_time data.get(wake_time) quality_score data.get(quality_score, 5) if not record_date or not sleep_time or not wake_time: return jsonify({error: record_date、sleep_time、wake_time 为必填字段}), 400 try: datetime.strptime(record_date, %Y-%m-%d) datetime.strptime(sleep_time, %H:%M) datetime.strptime(wake_time, %H:%M) except ValueError: return jsonify({error: 日期时间格式不正确}), 400 try: quality_score int(quality_score) if not 1 quality_score 10: raise ValueError except (TypeError, ValueError): return jsonify({error: quality_score 必须是 1 到 10 的整数}), 400 caffeine_before_sleep int(data.get(caffeine_before_sleep, 0) or 0) exercise_minutes int(data.get(exercise_minutes, 0) or 0) screen_time int(data.get(screen_time, 0) or 0) mood data.get(mood, ) note data.get(note, ) db get_db() db.execute( INSERT INTO sleep_log ( record_date, sleep_time, wake_time, quality_score, caffeine_before_sleep, exercise_minutes, screen_time, mood, note ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) , ( record_date, sleep_time, wake_time, quality_score, caffeine_before_sleep, exercise_minutes, screen_time, mood, note ) ) db.commit() duration calc_duration_minutes(sleep_time, wake_time) return jsonify({ message: 记录成功, id: db.execute(SELECT last_insert_rowid()).fetchone()[0], sleep_duration_minutes: duration }), 201这段代码看起来啰嗦但每一步都有意义使用jsonify统一返回 JSON。日期格式校验能提前拦截错误避免错误数据污染统计结果。质量评分必须在 1 到 10 之间防止数据库里出现不合理数据。布尔字段转成 0 或 1方便后续 SQL 统计。实际项目中你还可以继续压缩这段代码但建议保留校验逻辑。纯插入接口如果省略校验坑会从“接口层”转移到“统计层”那时候更难排查。3.4 查询睡眠统计的接口GET /api/summary返回两类结果基础统计和最近 7 天趋势。基础统计包括总记录数、平均睡眠时长、平均入睡时间、平均质量评分最近 7 天趋势用于前端画折线图。app.route(/api/summary, methods[GET]) def summary(): db get_db() total db.execute(SELECT COUNT(*) AS count FROM sleep_log).fetchone()[count] if total 0: return jsonify({ total: 0, avg_duration_minutes: 0, avg_sleep_time: 暂无数据, avg_quality_score: 0, trend: [] }) rows db.execute( SELECT record_date, sleep_time, wake_time, quality_score FROM sleep_log ORDER BY record_date DESC, id DESC LIMIT 30 ).fetchall() durations [] qualitys [] record_dates [] for row in rows: duration calc_duration_minutes(row[sleep_time], row[wake_time]) durations.append(duration) qualitys.append(row[quality_score]) record_dates.append(row[record_date]) avg_duration round(sum(durations) / len(durations)) avg_quality round(sum(qualitys) / len(qualitys), 1) sleep_times [row[sleep_time] for row in rows] sleep_minutes [] for t in sleep_times: hour, minute t.split(:) sleep_minutes.append(int(hour) * 60 int(minute)) avg_sleep_minute round(sum(sleep_minutes) / len(sleep_minutes)) avg_hour avg_sleep_minute // 60 avg_minute avg_sleep_minute % 60 avg_sleep_time f{avg_hour:02d}:{avg_minute:02d} trend [] for i in range(len(record_dates) - 1, -1, -1): trend.append({ date: record_dates[i], duration_minutes: durations[i], quality_score: qualitys[i] }) return jsonify({ total: total, avg_duration_minutes: avg_duration, avg_sleep_time: avg_sleep_time, avg_quality_score: avg_quality, trend: trend })这里有一个细节LIMIT 30是统计最近 30 条记录避免数据量变大后前端一次加载过重。如果你希望严格统计“最近 7 天”应该改成按日期过滤WHERE record_date date(now, -6 days)两种口径的差异在于有些人可能没有做到每天记录。按记录条数统计更适合数据不规律的情况。3.5 为什么这个阶段不用 ORM很多教程一上来就使用 SQLAlchemy但对只有一张表的项目来说ORM 会带来额外的学习成本和映射配置。直接使用sqlite3的原生 SQL查询过程完全透明出问题时也能直接复制 SQL 到数据库工具中排查。当项目增长到多张表并有复杂关联关系后再迁移到 ORM 也不迟。这个阶段的关键是先把数据流跑通。4. 前端页面表单录入与趋势图要形成一个可用闭环4.1 页面结构static/index.html是一个单页面应用包含以下区域标题和说明区域。录入表单日期、入睡时间、醒来时间、质量评分、咖啡因摄入、运动时长、屏幕时间、心情、备注。最近记录表格。趋势图容器。页面底部显示基础统计。页面会在加载时调用GET /api/summary初始化数据。提交表单时调用POST /api/sleep。4.2 表单和提交逻辑表单部分保持简单不引入第三方 UI 库。form idsleep-form label记录日期/label input typedate idrecord_date required label入睡时间/label input typetime idsleep_time required label醒来时间/label input typetime idwake_time required label质量评分1-10/label input typenumber idquality_score min1 max10 value5 required label睡前是否摄入咖啡因/label select idcaffeine_before_sleep option value0否/option option value1是/option /select label当天运动分钟数/label input typenumber idexercise_minutes min0 value0 label睡前屏幕使用分钟数/label input typenumber idscreen_time min0 value0 label心情/label input typetext idmood placeholder例如 anxious / calm / tired label备注/label textarea idnote rows2/textarea button typesubmit保存记录/button /form提交时使用fetchdocument.getElementById(sleep-form).addEventListener(submit, async function (e) { e.preventDefault(); const payload { record_date: document.getElementById(record_date).value, sleep_time: document.getElementById(sleep_time).value, wake_time: document.getElementById(wake_time).value, quality_score: parseInt(document.getElementById(quality_score).value, 10), caffeine_before_sleep: parseInt(document.getElementById(caffeine_before_sleep).value, 10), exercise_minutes: parseInt(document.getElementById(exercise_minutes).value, 10), screen_time: parseInt(document.getElementById(screen_time).value, 10), mood: document.getElementById(mood).value.trim(), note: document.getElementById(note).value.trim() }; const resp await fetch(/api/sleep, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload) }); const result await resp.json(); if (resp.ok) { alert(记录成功); loadSummary(); this.reset(); } else { alert(保存失败 (result.error || 未知错误)); } });这里最容易出的问题是没写headers里的Content-Type。如果少了这个头Flask 端使用request.get_json()可能拿到None接口返回 400。这个问题在后续排查章节会专门说明。4.3 使用 ECharts 展示趋势图通过 CDN 引入 EChartsscript srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script然后在页面设置一个容器div idchart stylewidth: 100%; height: 400px;/div加载数据并绘图async function loadSummary() { const resp await fetch(/api/summary); const data await resp.json(); document.getElementById(total-records).textContent data.total; document.getElementById(avg-duration).textContent data.avg_duration_minutes; document.getElementById(avg-sleep-time).textContent data.avg_sleep_time; document.getElementById(avg-quality).textContent data.avg_quality_score; renderTrendTable(data.trend); renderChart(data.trend); } function renderTrendTable(trend) { const tbody document.getElementById(trend-tbody); tbody.innerHTML ; trend.slice().reverse().forEach(function (item) { const tr document.createElement(tr); tr.innerHTML td${item.date}/td td${Math.floor(item.duration_minutes / 60)}小时${item.duration_minutes % 60}分/td td${item.quality_score}/td ; tbody.appendChild(tr); }); } function renderChart(trend) { const chart echarts.init(document.getElementById(chart)); chart.setOption({ tooltip: { trigger: axis }, legend: { data: [睡眠时长, 质量评分] }, xAxis: { type: category, data: trend.map(item item.date) }, yAxis: [ { type: value, name: 分钟, min: 0 }, { type: value, name: 评分, min: 0, max: 10 } ], series: [ { name: 睡眠时长, type: line, data: trend.map(item item.duration_minutes), yAxisIndex: 0 }, { name: 质量评分, type: line, data: trend.map(item item.quality_score), yAxisIndex: 1 } ] }); }需要说明的是ECharts 的init必须等页面容器渲染完成后再调用。如果容器高度为 0图表无法正常显示。可以设置固定高度就像示例中的height: 400px。4.4 页面与后端联调使用同源方式访问时不需要处理跨域。启动 Flask 后直接访问http://127.0.0.1:5000/Flask 会自动从static目录寻找index.html。/api/summary和/api/sleep都走同源请求不存在跨域问题。如果前后端分离部署前端用 Nginx 托管后端跑在另一个端口才需要处理跨域。最小方案是在 Flask 里加一个统一响应头app.after_request def add_cors_headers(response): response.headers[Access-Control-Allow-Origin] * response.headers[Access-Control-Allow-Headers] Content-Type response.headers[Access-Control-Allow-Methods] GET, POST, OPTIONS return response这不是生产环境推荐做法因为*暴露了所有接口。生产环境应该将Access-Control-Allow-Origin设置为具体域名。5. 运行与验证从启动服务到看到图表的完整过程5.1 启动服务在项目根目录执行python app.py看到类似输出表示启动成功* Running on http://127.0.0.1:5000浏览器访问http://127.0.0.1:5000/能看到页面但此时数据库还没有数据统计位置会显示“暂无数据”折线图为空这是正常的。5.2 使用 curl 添加测试数据为了方便验证可以先通过命令行插入几条记录curl -X POST http://127.0.0.1:5000/api/sleep \ -H Content-Type: application/json \ -d {record_date:2025-06-01,sleep_time:23:30,wake_time:07:00,quality_score:6,caffeine_before_sleep:1,screen_time:90}再插入一条跨天记录curl -X POST http://127.0.0.1:5000/api/sleep \ -H Content-Type: application/json \ -d {record_date:2025-06-02,sleep_time:00:10,wake_time:06:40,quality_score:4,caffeine_before_sleep:0,screen_time:60}如果接口正确第一次返回的sleep_duration_minutes应该是 450第二次应该是 390。这样可以直接验证跨天计算。5.3 页面验证步骤页面上的验证路径是在录入表单中填写日期、入睡时间、醒来时间点击“保存记录”。页面提示“记录成功”。表格区域出现一条新记录。折线图出现对应数据点。刷新页面后数据仍然存在因为数据已写入 SQLite。5.4 预期结果汇总验证对象预期结果POST /api/sleep 正确提交返回 201包含新记录 id 和睡眠时长POST /api/sleep 缺少字段返回 400提示必填字段POST /api/sleep 时间格式错误返回 400提示格式不正确GET /api/summary返回总记录数、平均时长、平均入睡时间、平均评分、趋势数组页面折线图显示睡眠时长和质量评分两条折线6. 常见问题与排查路径6.1 后端返回“请求体必须是 JSON”现象前端提交表单后接口返回 400提示请求体必须为 JSON。可能原因fetch请求没有设置Content-Type: application/json。使用了new FormData()提交但后端没有解析表单数据。后端接口用的是request.get_json(silentTrue)当数据不是 JSON 时返回None。检查方式curl -X POST http://127.0.0.1:5000/api/sleep \ -H Content-Type: application/json \ -d {record_date:2025-06-03,sleep_time:22:00,wake_time:06:00}如果 curl 正常说明问题在前端请求头。修复方法是确保fetch的headers里有Content-Type。6.2 睡眠时长被算成负数现象明明从 23:30 睡到 07:00计算出来却是负数。原因跨天场景没有处理。当wake_time小于sleep_time时直接相减会得到负值。检查方式在 Python 里单独调用计算函数from app import calc_duration_minutes print(calc_duration_minutes(23:30, 07:00))解决方案使用本文提供的函数当差值小于等于 0 时加上一天。6.3 SQLite 报 database is locked现象页面提交记录时偶尔出现sqlite3.OperationalError: database is locked。原因默认情况下SQLite 同一时刻只允许一个写入连接。Flask 在多线程模式下处理请求多个写入请求同时到达时后到的连接可能拿不到写锁。这个项目是单用户低频写入实际发生概率不高。如果出现可以从三个方向处理将 SQLite 连接设置为check_same_threadFalse但这不是完整解决方案。写入操作加应用级锁。如果并发写入成为常态改用 MySQL 或 PostgreSQL。6.4 折线图空白现象页面能加载但折线图区域空白。可能原因ECharts CDN 加载失败echarts未定义。图表容器高度为 0。接口返回的trend是空数组图表没有数据点。检查方式打开浏览器开发者工具查看 Console 是否有 JS 报错。在 Network 面板查看/api/summary返回的数据。将图表容器的height改成固定值例如 400px。如果确认是 CDN 问题解决方案是下载 ECharts 文件到本地static目录然后使用相对路径引入。6.5 排查顺序清单遇到问题时按以下顺序检查序号检查项工具或位置1参数是否完整浏览器 Network、Flask 控制台2时间格式是否正确数据库里查询 sleep_time、wake_time3数据库是否生成项目目录下是否存在 sleep_log.db4接口是否返回异常curl 直接调用接口5前端 JS 是否报错浏览器 Console6ECharts 是否成功加载Network 面板确认请求7浏览器访问路径是否正确必须是 http://127.0.0.1:5000/ 而不是静态文件路径7. 生产环境部署与后续扩展7.1 单机自用版本与多人使用的区别当前项目适合个人在本地记录不适合直接部署给多人使用原因有几点SQLite 并发写入能力有限。所有用户共享一张表无法区分数据归属。接口没有鉴权任何人知道地址都能新增或查询数据。没有日志和监控出问题后难以回溯。如果要变成多人使用的工具至少需要增加用户表、登录状态、接口鉴权并切换数据库。这些改动不是“加一个字段”那么简单需要在项目初期就预留用户维度。7.2 上线前还要补什么即使只是自己部署到云服务器也建议补上事项建议参数校验对字符串长度做限制例如备注不超过 500 字错误日志使用 Python logging 记录异常和错误请求数据库备份定期把 sleep_log.db 复制到备份目录反向代理使用 Nginx 托管静态文件并代理 API避免直接暴露 Flask 开发服务器环境变量数据库路径、监听端口等配置放到环境变量安全响应头增加 X-Content-Type-Options、X-Frame-Options 等基础防护这些内容会根据不同部署环境有差异落地时以你的实际服务器环境为准。7.3 可以扩展的方向这个项目的代码量不大但扩展空间很充分增加“编辑”和“删除”能力补全 CRUD。增加 CSV 导出方便用 Excel 进一步分析。增加“睡眠效率”指标实际睡眠时间与在床时间的比值。接入手表或手环的睡眠阶段数据用接口导入。增加统计报表页按周、按月对比平均入睡时间和评分。增加简单提醒当连续三天质量评分低于 4 时页面给出观察提示。把前端升级为 Vue 或 React后端改为 RESTful 风格但那是另一个复杂度层级。7.4 给新手的学习建议如果要用这个项目练手可以按顺序做三个改造把 SQLite 换成 MySQL体会数据库连接方式的变化。增加一个“最近 30 天平均评分趋势”接口学习聚合查询。把index.html里的原生图表渲染拆成独立模块理解前后端职责分离。完成这三个改造后你对数据模型、接口设计和前后端联调的理解会明显比跟着教程复制一遍更深入。这个项目最有价值的不是代码本身而是让你在记录睡眠的过程中同时学会用工程方法分析一个生活中模糊的问题。
返回列表