“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 的睡眠日志表包含以下字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INTEGER | 主键,自增 |
| record_date | TEXT | 记录日期,格式 YYYY-MM-DD |
| sleep_time | TEXT | 入睡时间,格式 HH:MM |
| wake_time | TEXT | 醒来时间,格式 HH:MM |
| quality_score | INTEGER | 主观睡眠质量评分,1 到 10 |
| caffeine_before_sleep | INTEGER | 睡前是否摄入咖啡因,0 表示否,1 表示是 |
| exercise_minutes | INTEGER | 当天运动时长,单位分钟 |
| screen_time | INTEGER | 睡前屏幕使用时间,单位分钟 |
| mood | TEXT | 入睡前的心情标签,例如 anxious、calm、tired |
| note | TEXT | 自由文本备注 |
| created_at | TEXT | 记录创建时间,默认当前本地时间 |
这里有一个设计取舍:入睡时间和醒来时间都只存 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', encoding='utf-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(days=1) 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内容:
flask>=2.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(days=1) 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(silent=True) 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 id="sleep-form"> <label>记录日期</label> <input type="date" id="record_date" required> <label>入睡时间</label> <input type="time" id="sleep_time" required> <label>醒来时间</label> <input type="time" id="wake_time" required> <label>质量评分(1-10)</label> <input type="number" id="quality_score" min="1" max="10" value="5" required> <label>睡前是否摄入咖啡因</label> <select id="caffeine_before_sleep"> <option value="0">否</option> <option value="1">是</option> </select> <label>当天运动分钟数</label> <input type="number" id="exercise_minutes" min="0" value="0"> <label>睡前屏幕使用分钟数</label> <input type="number" id="screen_time" min="0" value="0"> <label>心情</label> <input type="text" id="mood" placeholder="例如 anxious / calm / tired"> <label>备注</label> <textarea id="note" rows="2"></textarea> <button type="submit">保存记录</button> </form>提交时使用fetch:
document.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 引入 ECharts:
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>然后在页面设置一个容器:
<div id="chart" style="width: 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(silent=True),当数据不是 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_thread=False,但这不是完整解决方案。 - 写入操作加应用级锁。
- 如果并发写入成为常态,改用 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_time |
| 3 | 数据库是否生成 | 项目目录下是否存在 sleep_log.db |
| 4 | 接口是否返回异常 | curl 直接调用接口 |
| 5 | 前端 JS 是否报错 | 浏览器 Console |
| 6 | ECharts 是否成功加载 | 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里的原生图表渲染拆成独立模块,理解前后端职责分离。
完成这三个改造后,你对数据模型、接口设计和前后端联调的理解会明显比跟着教程复制一遍更深入。这个项目最有价值的不是代码本身,而是让你在记录睡眠的过程中,同时学会用工程方法分析一个生活中模糊的问题。