news 2026/8/27 5:11:39

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用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-DD
sleep_timeTEXT入睡时间,格式 HH:MM
wake_timeTEXT醒来时间,格式 HH:MM
quality_scoreINTEGER主观睡眠质量评分,1 到 10
caffeine_before_sleepINTEGER睡前是否摄入咖啡因,0 表示否,1 表示是
exercise_minutesINTEGER当天运动时长,单位分钟
screen_timeINTEGER睡前屏幕使用时间,单位分钟
moodTEXT入睡前的心情标签,例如 anxious、calm、tired
noteTEXT自由文本备注
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', 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 页面验证步骤

页面上的验证路径是:

  1. 在录入表单中填写日期、入睡时间、醒来时间,点击“保存记录”。
  2. 页面提示“记录成功”。
  3. 表格区域出现一条新记录。
  4. 折线图出现对应数据点。
  5. 刷新页面后数据仍然存在,因为数据已写入 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 正常,说明问题在前端请求头。修复方法是确保fetchheaders里有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是空数组,图表没有数据点。

检查方式:

  1. 打开浏览器开发者工具,查看 Console 是否有 JS 报错。
  2. 在 Network 面板查看/api/summary返回的数据。
  3. 将图表容器的height改成固定值,例如 400px。

如果确认是 CDN 问题,解决方案是下载 ECharts 文件到本地static目录,然后使用相对路径引入。

6.5 排查顺序清单

遇到问题时,按以下顺序检查:

序号检查项工具或位置
1参数是否完整浏览器 Network、Flask 控制台
2时间格式是否正确数据库里查询 sleep_time、wake_time
3数据库是否生成项目目录下是否存在 sleep_log.db
4接口是否返回异常curl 直接调用接口
5前端 JS 是否报错浏览器 Console
6ECharts 是否成功加载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 给新手的学习建议

如果要用这个项目练手,可以按顺序做三个改造:

  1. 把 SQLite 换成 MySQL,体会数据库连接方式的变化。
  2. 增加一个“最近 30 天平均评分趋势”接口,学习聚合查询。
  3. index.html里的原生图表渲染拆成独立模块,理解前后端职责分离。

完成这三个改造后,你对数据模型、接口设计和前后端联调的理解会明显比跟着教程复制一遍更深入。这个项目最有价值的不是代码本身,而是让你在记录睡眠的过程中,同时学会用工程方法分析一个生活中模糊的问题。

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

Claude Code插件稳定接入与API错误排查实战指南

1. 为什么Claude Code的“稳定接入”是个技术活&#xff1f;如果你最近在VS Code里折腾过Claude Code插件&#xff0c;大概率经历过这么几个阶段&#xff1a;先是兴奋地装上&#xff0c;然后发现登录界面卡住、API报错&#xff0c;或者用着用着突然提示“服务不可用”。折腾一圈…

作者头像 李华
网站建设 2026/8/27 5:09:36

超集成MCU:嵌入式系统架构重构的核心引擎

1. 为什么“超集成MCU”正在悄悄改写嵌入式开发的底层逻辑最近三个月&#xff0c;我帮三家做工业传感器、智能楼宇控制器和医疗可穿戴设备的团队做技术选型&#xff0c;发现一个共同现象&#xff1a;他们不再盯着STM32F4或NXP Kinetis系列反复比参数&#xff0c;而是不约而同地…

作者头像 李华
网站建设 2026/8/27 5:09:02

微软免费的 Visual Studio 卸载工具:3 步彻底清空 VS 残留

微软免费的 Visual Studio 卸载工具&#xff1a;3 步彻底清空 VS 残留 【免费下载链接】VisualStudioUninstaller Visual Studio Uninstallation sometimes can be unreliable and often leave out a lot of unwanted artifacts. Visual Studio Uninstaller is designed to tho…

作者头像 李华
网站建设 2026/8/27 5:08:12

MCP协议深度解析:连接AI与外部世界的标准化桥梁

1. 项目概述&#xff1a;从“协议”到“模型上下文协议”的认知升级当我们在技术社区里看到“MCP协议”这个词时&#xff0c;第一反应可能会有点懵。是Modbus通信协议&#xff1f;还是某个硬件接口的专有名词&#xff1f;实际上&#xff0c;在当前的AI应用开发浪潮中&#xff0…

作者头像 李华
网站建设 2026/8/27 5:05:40

PCIe数字化仪实时处理链路:FPGA与DMA的关键实践

做PCIe Digitizer&#xff08;PCIe数字化仪/高速采集卡&#xff09;这套东西&#xff0c;最磨人的从来不是模拟前端&#xff0c;而是“实时处理”四个字。信号进ADC只是开始&#xff0c;数据要在FPGA里做触发、抽取、FFT&#xff0c;再穿过PCIe总线进主机内存&#xff0c;最后还…

作者头像 李华
网站建设 2026/8/27 5:04:58

UNet图像分割实战:从原理到代码,掌握医学图像分割核心技术

简介&#xff1a;图像分割是计算机视觉的核心任务之一&#xff0c;其目标是对图像中每个像素进行语义分类。传统深度学习模型往往通过连续下采样提取高层特征&#xff0c;却容易丢失边缘细节。U-Net通过对称的编码器-解码器结构和跳跃连接&#xff0c;将浅层空间信息与深层语义…

作者头像 李华