1. 项目缘起与技术选型
社区老人健康管理这件事,看着简单,真做起来却是一堆细活。去年帮一个社区服务站做信息化调研,发现他们还在用纸质表格登记老人的血压、血糖数据,体检报告翻箱倒柜地找,慢性病随访全靠打电话问。我当时就想,与其纠结买一套动辄几万的第三方系统,不如用Python和Flask自己搭一套轻量级的,既能满足日常记录需求,又能快速定制功能。
这个项目定位于“社区服务站的日常健康管理工具”,核心解决三件事:老人健康档案的电子化、健康指标的持续跟踪、异常数据的及时提醒。技术栈选择了Python + Flask + SQLite这套组合,没有引入前端框架,页面服务端渲染,数据交互用Ajax。选择Flask而非Django,主要原因是Flask足够轻,路由和视图函数自由度高,一个文件就能起步,对后期功能扩展和二次开发都友好。SQLite作为数据库,零配置、单文件存储,社区服务站那点数据量完全够用,省去了部署MySQL的环境成本。
这套设计思路,核心不是“炫技”,而是“够用且能维护”。社区管理员可能就是普通工作人员,系统如果搞得像企业级应用一样复杂,反而没人会用。我给自己定的标准是:所有操作三步之内完成,页面响应不超过两秒,部署时一条命令启动。
2. 系统架构与核心功能模块设计
2.1 总体架构设计
系统采用经典的MVC分层思路,但不刻意做成三层架构,而是按照Flask的Blueprint模块化方式组织。项目结构如下:
community_health/ ├── app.py # 应用入口 ├── config.py # 配置文件 ├── models.py # 数据模型定义 ├── forms.py # 表单验证逻辑 ├── views/ │ ├── __init__.py │ ├── auth.py # 登录认证 │ ├── elders.py # 老人档案管理 │ ├── health.py # 健康记录管理 │ └── overview.py # 数据概览与统计 ├── templates/ # Jinja2模板 ├── static/ │ ├── css/ │ ├── js/ │ └── uploads/ # 头像上传目录 └── requirements.txt为什么用Blueprint?当一个系统涉及档案管理、健康数据、预警提醒、后台统计等功能时,全部堆在app.py里面会非常痛苦。按功能模块拆分后,每个模块负责自己的路由和视图,代码可读性和维护性大大提高。实际开发中,我习惯每个Blueprint对应一个数据模型,这样职责边界清晰,出问题时能快速定位。
2.2 数据库表结构设计
数据库的设计直接影响后续功能的复杂度和查询效率,我花了比较多时间在这块。总共设计了四张核心表:老人基本信息表(elder)、健康记录表(health_record)、系统用户表(user)、异常预警表(alert)。
老人基本信息表需要注意的点是:身份证号要唯一索引,因为同一个老人不能重复建档;联系电话要加正则校验;紧急联系人字段一定要有,这是老人健康管理区别于其他业务系统的关键点。
CREATE TABLE elder ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(20) NOT NULL, id_card VARCHAR(18) UNIQUE NOT NULL, gender VARCHAR(2), age INTEGER, phone VARCHAR(11), address VARCHAR(100), emergency_contact VARCHAR(20), emergency_phone VARCHAR(11), chronic_disease VARCHAR(200), allergy_history VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP );健康记录表是核心中的核心,每次量血压、测血糖、记录心率,都生成一条记录。字段包含测量项目、测量值、单位、测量时间、备注。这里有个设计细节:测量值用VARCHAR而不是FLOAT,目的是兼容不同项目的数值范围,比如血压可以存"120/80"这样的字符串,血糖存"5.6",数值计算时再单独处理。
2.3 健康记录与预警规则设计
健康管理系统的灵魂不是“记录”,而是“发现异常”。我设计了一套简单的预警规则引擎,在录入健康数据时自动触发判断。比如血压的收缩压大于160、舒张压大于100时,系统自动生成一条预警信息;血糖空腹大于7.0时,提示“需复查空腹血糖”。
预警规则在代码中是用策略模式实现的,每个指标对应一个判断函数,方便后期增加新的健康指标:
def check_blood_pressure(systolic, diastolic): alerts = [] if systolic >= 180 or diastolic >= 110: alerts.append(("danger", "血压异常偏高,请尽快就医")) elif systolic >= 160 or diastolic >= 100: alerts.append(("warning", "血压偏高,建议增加测量频率")) elif systolic < 90 or diastolic < 60: alerts.append(("warning", "血压偏低,注意观察是否有头晕乏力")) return alerts预警数据除了在系统中展示外,我预留了短信通知接口。社区管理员在页面上一键开启“短信关注模式”,指定重点关注的老人,一旦新录入的数据触发预警,系统会在首页置顶提醒。这个功能在实际使用中反馈很好,护理人员不用每天翻记录就能掌握重点老人的健康状况。
3. 核心技术点详解
3.1 Flask与SQLAlchemy的集成配置
数据库操作使用Flask-SQLAlchemy扩展,配置非常简单,但有一些细节值得注意。SQLite数据库路径需要动态拼接,保证部署在不同环境时都能正确找到数据库文件。我在config中这样处理:
import os BASE_DIR = os.path.abspath(os.path.dirname(__file__)) class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key' SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, 'health.db') SQLALCHEMY_TRACK_MODIFICATIONS = False MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 上传文件大小限制SQLALCHEMY_TRACK_MODIFICATIONS必须设为False,不然会有内存消耗警告。SECRET_KEY如果不设置,Flask的Session机制会直接报错。其实在生产环境我建议用环境变量注入密钥,而不是写在代码里,防止源码泄露带来的安全隐患。
模型类定义时,我特别注意了时间字段的处理。SQLite存储DATETIME时是字符串,如果直接用SQLAlchemy的DATETIME类型,读取出来的是字符串,和Python的datetime对象比较时容易出bug。解决方法是自定义一个类型:
from sqlalchemy import TypeDecorator import datetime class DateTimeType(TypeDecorator): impl = sqlalchemy.DateTime def process_bind_param(self, value, dialect): return value.strftime('%Y-%m-%d %H:%M:%S') if value else None def process_result_value(self, value, dialect): return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S') if value else None这个小工具类让时间字段在数据库和Python对象之间的转换不再闹心。
3.2 用户登录与权限控制的实现
系统不能只有健康数据功能,登录认证和权限控制也是系统的重要部分。Flask的Session机制默认存在客户端Cookie中,虽然对小型管理系统够用,但涉及健康这类敏感数据时还是要谨慎。我做了两层防护:第一层,SECRET_KEY使用足够复杂的随机字符串;第二层,Session中只存用户ID和用户名,不存任何额外敏感信息。
权限控制采用“装饰器”方式实现:
from functools import wraps from flask import session, redirect, url_for def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): if 'user_id' not in session: return redirect(url_for('auth.login', next=request.url)) return f(*args, **kwargs) return decorated_function使用方式就是在需要登录的路由函数上加一个@login_required装饰器。这个方案虽然简单,但拦截效果不错,未登录用户会被重定向到登录页。唯一要注意的是,装饰器需要放在路由装饰器下面,否则route注册时视图函数还是原函数,login_required就不会生效。
3.3 老人健康数据的可视化展示
光有数据表格不算完,管理层需要更直观的健康趋势图。ECharts虽然是前端图表库,但配合Ajax调用后端接口,效果非常好。我在健康记录模块中开发了一个“健康趋势”页面,通过折线图展示老人最近30天的血压、血糖变化。
后端接口代码:
@health_bp.route('/api/trend/<int:elder_id>') @login_required def health_trend(elder_id): records = HealthRecord.query.filter_by( elder_id=elder_id, item_type='blood_pressure' ).order_by(HealthRecord.measure_time.desc()).limit(30).all() data = { 'dates': [r.measure_time.strftime('%m-%d') for r in reversed(records)], 'systolic': [float(r.value.split('/')[0]) for r in reversed(records)], 'diastolic': [float(r.value.split('/')[1]) for r in reversed(records)] } return jsonify(code=0, data=data)前端用Ajax请求这个接口,拿到数据后丢给ECharts渲染。这样前后端分离的交互方式,比传统的form提交刷新页面体验好得多,页面无刷新就能看到最新趋势图。实际使用中,我观察到护理人员特别喜欢这个功能,它能一眼看出老人近期的血压控制情况,方便调整随访计划。
4. 实操过程:从环境搭建到系统部署
4.1 开发环境准备
如果是从零开始复现这套系统,首先需要准备Python环境。我建议使用Python 3.8以上的版本,用venv创建独立虚拟环境,避免依赖冲突:
python -m venv venv source venv/bin/activate # Windows下使用 venv\Scripts\activate pip install flask flask-sqlalchemy flask-wtf开发过程中一定要用虚拟环境,不然不同项目之间依赖打架是常有的事。我第一次做Flask项目时没这习惯,装了某个包的更新版本,直接把另一个项目的运行环境搞崩了。踩过坑之后才明白,每个项目单独建虚拟环境,是省心的大事。
依赖版本方面,我固定用了这些组合:
| 包名 | 版本 | 说明 |
|---|---|---|
| Flask | 2.2.5 | 2.x版本兼容性最好 |
| Flask-SQLAlchemy | 3.0.5 | 配合Flask 2.x使用 |
| Flask-WTF | 1.1.1 | 表单CSRF保护 |
| Werkzeug | 2.2.3 | Flask底层依赖,注意版本匹配 |
版本锁定非常关键。Flask 3.x和2.x的API有细微差异,有些扩展包还没适配,贸然升级容易碰壁。我的做法是,项目稳定后直接把requirements.txt的内容固定下来,下次部署时一键还原环境。
4.2 核心功能拆解与页面实现
页面UI这部分,没有用Bootstrap之类的前端框架,直接手写了CSS。模板继承起了大作用,base.html定义整体布局,其他页面通过{% extends "base.html" %}继承公共部分,开发效率提升很多。
老人档案管理页面的核心操作包括:新增档案、编辑信息、查看档案详情。新增和编辑共用同一个表单模板,通过路由参数区分操作类型。表单类使用了Flask-WTF:
class ElderForm(FlaskForm): name = StringField('姓名', validators=[DataRequired(), Length(max=20)]) id_card = StringField('身份证号', validators=[DataRequired(), Length(18), Regexp(r'^\d{17}[\dX]$')]) phone = StringField('联系电话', validators=[DataRequired(), Regexp(r'^1\d{10}$')]) submit = SubmitField('保存')身份证号的正则校验严格区分了18位数字和最后一位大写X,电话号码使用1开头加10位数字的规则。表单验证失败时,前端会显示对应的错误提示,用户能直观看到哪里填错了。这个小细节比后端静默失败友好得多。
4.3 系统部署与启动
部署策略我分两种场景。本地演示用直接运行开发服务器:
python app.py访问http://127.0.0.1:5000即可。部署到生产环境时,建议使用gunicorn(Linux系统)或waitress(Windows系统),配合命令行启动:
waitress-serve --host=0.0.0.0 --port=8080 app:appWindows上waitress是比gunicorn更靠谱的选择,gunicorn在Windows下支持不完善。如果服务器有公网IP,把0.0.0.0作为host就能让局域网内其他电脑访问。
首次启动时,系统会自动创建数据库文件,并初始化一个管理员账号。我专门写了一个初始化脚本,生成默认管理员admin/admin123,同时创建一些测试数据方便功能演示。生产部署后第一件事就是修改默认密码,这一点我在使用说明中反复强调过。
5. 常见问题与排查技巧实录
5.1 表单提交后CSRF验证失败
Flask-WTF的CSRF保护默认开启,页面上如果没有渲染{{ form.csrf_token }},提交时就会报400错误。排查这个问题的思路是:检查模板里有没有加上csrf_token字段,有没有在表单定义时指定SECRET_KEY。
排查要点:
- 模板表单中添加
{{ form.hidden_tag() }},会自动生成CSRF字段 - SECRET_KEY必须一致,如果应用重启时重新生成了SECRET_KEY,之前页面上的CSRF token就失效了
我在开发中遇到过最诡异的一个场景是,登录页跳转后CSRF失效,原因是跳转时浏览器缓存了旧页面,重新刷新就正常了。给关键页面加Cache-Control: no-cache响应头可以避免这类问题。
5.2 数据库文件被锁定
SQLite在并发写入时偶尔会报database is locked。这类问题的原因多半是多个线程同时写数据库。解决方法是:在Flask应用配置中关闭自动提交修改跟踪,同时确保SQLAlchemy的连接池设置合理:
SQLALCHEMY_ENGINE_OPTIONS = { 'connect_args': {'timeout': 30} }connect_args字典中的timeout参数告诉SQLite等待30秒后再放弃,能明显减少“数据库被锁定”的报错。社区服务站的访问量一般不大,这个配置完全够用。但如果系统并发量很高,建议迁移到MySQL或者PostgreSQL。
5.3 中文乱码问题
开发初期在页面上显示中文时出现了乱码,排查后发现是编码设置问题。解决方案是:HTML模板头部声明meta charset,Flask返回JSON时加上app.config['JSON_AS_ASCII'] = False,同时确保Python源文件用UTF-8编码保存。三个环节缺一不可。
5.4 上传图片后页面无法显示
老人头像上传功能,如果图片上传到static/uploads目录下,但页面加载时用的是绝对路径,服务器对静态文件的映射没配置对,就会导致404。我这里踩过一个坑:使用了/static/uploads/xxx.jpg为相对路径,和Flask的默认静态目录映射冲突了。解决的思路是:要么用完整的URL构造函数url_for('static', filename='uploads/' + filename),要么把上传目录配置成独立的Blueprint,两者选其一即可。
为保证安全,上传文件的白名单校验必须有,只允许jpg/jpeg/png/gif后缀,并对文件大小做限制。我用MAX_CONTENT_LENGTH做了16MB的上限,同时用文件扩展名加MIME类型的双重校验,防止直接上传可执行脚本文件。
6. 系统测试与数据安全加固
6.1 功能测试要点
系统开发完成后需要对核心功能进行系统测试,尤其是健康数据的读写操作。我整理了重点测试用例:
| 模块 | 测试场景 | 预期结果 |
|---|---|---|
| 登录 | 输入错误密码 | 提示认证失败,不暴露数据库错误信息 |
| 档案管理 | 身份证号格式错误 | 表单校验拦截,给出明确提示 |
| 健康记录 | 血压值录入异常 | 保存后自动触发预警提醒 |
| 数据统计 | 按时间段查询 | 返回双轴折线图,数据完整 |
| 权限控制 | 未登录直接访问详情页 | 自动跳转登录页 |
每条用例过一遍,能发现很多逻辑层面的问题。我在测试阶段发现,健康记录查询接口对于空的老人档案没有做异常处理,导致前端图表渲染时数据格式错误。补上了空数据处理逻辑后,页面即使在无数据时也能正常展示“暂无记录”的占位图。
6.2 数据备份与隐私保护
SQLite数据库本身就是单文件,备份只需要复制health.db文件,用压缩包保存到指定位置就行。我写了一个简单的定时备份脚本,每天凌晨自动把数据库文件备份到指定目录,保留最近30天的副本。
cp /path/to/health.db /backup/health_$(date +%Y%m%d).db find /backup -name "health_*.db" -mtime +30 -delete两行命令解决备份和清理问题,建议放到crontab中执行。老人健康数据属于敏感数据,隐私保护这根弦得绷紧。除了登录认证,我还在系统里加了操作日志表,记录谁在什么时间修改了什么数据。虽然只是简单的insert语句,但真正出事时能追溯到责任人,这也是在给系统做“合规背书”。
6.3 SQL注入防护
使用SQLAlchemy的ORM查询方式本身就能一定程度上避免SQL注入问题,但有一种场景要特别注意:动态排序字段。如果排序字段名直接拼接字符串,用户可能构造出恶意的SQL。
正确的写法是加入白名单机制:
ALLOWED_ORDER_FIELDS = {'create_time', 'age', 'name'} def safe_order_field(field): if field not in ALLOWED_ORDER_FIELDS: return 'create_time' return field所有与用户输入相关的动态字段,都必须经过白名单校验再进入查询条件。这一点是防注入的重中之重。
7. 经验总结与后续扩展建议
坦白说,这套系统不是那种炫技型项目,它的价值在于“实用”和“可落地”。整个开发过程从需求调研到测试部署,历时三周左右,中间经历了需求变更、表单校验逻辑调整、前端页面优化等反复迭代,最后拿出来的产品虽然朴素,但真正解决了社区服务站的日常痛点。
最后分享一个开发小技巧:Flask的debug模式开发时确实方便,能自动重载代码和展示详细报错页面,但部署上线一定要关闭debug,同时设置app.run(host='0.0.0.0', port=5000)确保监听所有对外IP。日志记录也尽量留好,我习惯在请求入口处加一行中间件,把关键操作都打到日志文件里,这样排查线上问题时会有很大帮助。
如果继续往深处扩展,可以考虑对接智能穿戴设备,通过API自动采集老人的步数、心率数据;也可以引入简单的机器学习模型,基于历史健康数据做风险预测,识别潜在的高危人群。但归根结底,系统的核心还是服务于人,好用的工具应该让人“愿意用”,而不是“不得不学用”。这套轻量化方案的边界就在这儿,做深做广都留给有真实需求的那一天。