在做体检信息管理系统的时候,很多人的第一反应是“那不就是增删改查吗”,但真把需求拆到血常规、乙肝等检验数据的录入、历史归档、异常分析和可视化报表时,会发现并没有想象中那么简单。这里我希望记录一下我用 Flask 实现一个电子健康体检信息记录分析系统的完整过程,重点围绕体检档案、检验数据(尤其血常规、乙肝标志物)的结构化建模、大数据分析思路的落地、以及一套能直接跑通的前后端实现方案。无论是作为毕业设计、职业技能训练还是中小型健康管理平台的前置原型,这篇内容都值得参考。
先说这个系统解决的问题:传统纸质体检单数据零散、难检索、难对比。同一个受检者今年和去年的血常规指标如果都躺在 PDF 报告里,想快速找出异常趋势基本靠人肉翻档案。基于 Flask 的大数据电子体检系统,正好可以把体检记录结构化存储,再用 pandas 或 SQL 方式做趋势分析、异常判定、年龄性别分组统计,最后用 ECharts 展示出来。项目把“采集 — 存储 — 分析 — 呈现”串成一条完整链路。
1. 项目定位与需求拆解:这个系统到底解决什么问题
1.1 典型体检业务场景
先理清楚体检系统的用户和业务边界。平时使用这类系统的可能是一个医院体检中心,也可能是一个企业健康管理平台,甚至是一个中学、高校的校医院。我按最常见的场景梳理了一下主要流程:受检者先登记基本信息(姓名、性别、年龄、联系方式、既往病史),然后按体检套餐去各个科室采集数据,比如身高体重、内外科、心电图,再抽血做检验。
检验部分最有价值的,也最需要数据管理的,就是血液检验那一大类。血常规里有白细胞、红细胞、血红蛋白、血小板等二十多个字段;乙肝相关检测又涉及乙肝表面抗原、表面抗体、e抗原、e抗体、核心抗体等标志物。这些数据有一个共同点:它们是数值或定性符号,天然适合数据库存储,也适合后续做趋势分析、异常统计。
体检数据的量级看起来不像互联网日志那种每天上亿条的规模,但在一个大型单位批量体检时,一次活动就能产生上千人的完整报告,累计几年就是几万到几十万条检验记录。每条记录里又有几十个指标,如果报表设计不合理,等到做跨年同比分析时,SQL 写起来会非常痛苦。所以从需求阶段就必须把“总量不大、但字段多且关系复杂”的医疗检验特性考虑进去。
1.2 功能模块怎么切分
我在做功能设计时没有把它做成一个大而全的医院 HIS 系统,而是聚焦在体检数据的“记录 + 分析”上。一般把它拆成四个模块:
| 模块 | 功能点 | 核心角色 |
|---|---|---|
| 系统管理 | 登录、角色权限、用户管理 | 管理员 |
| 档案管理 | 受检者建档、体检批次、套餐选择、检前信息 | 医生 / 录入员 |
| 检验数据管理 | 血常规录入、乙肝标志物录入、批量导入、历史记录 | 医生 / 检验人员 |
| 数据分析与报表 | 单项指标异常判定、历年趋势、体检报告生成、统计分析可视化 | 医生 / 受检者 |
1.3 对“大数据”的正确理解方式
很多同学看到“大数据”三个字就以为项目里必须得部署 Hadoop、Spark,其实这是选题时一个常见的误解。作为 Flask 框架的毕业设计或个人项目,“大数据”的价值更多体现在:面对海量历史体检记录时,如何做快速检索、批量数据清洗、指标趋势挖掘与可视化。MySQL 存储原始数据,pandas 做中间分析和计算,ECharts 做前端呈现,这套方案已经能处理几十万条记录。如果将来体检站点的数据真的大到单机 MySQL 扛不住,再引入 ClickHouse 或 Spark SQL 作为分析引擎即可,Flask 这一层并不需要推倒重来。
所以项目的技术核心其实是“如何使用大数据思维优化医疗数据的存储和索引,并利用分析算法从历史体检数据中发现健康变化趋势”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与架构设计:为什么是 Flask 而不是别的
2.1 Flask 的优势与取舍
做这类管理系统绕不开一个经典选择题:用 Flask 还是用 Django?我自己最后选了 Flask,理由很直接:这个系统的复杂度不需要 Django 内置的 Admin、Auth、ORM 全家桶,而 Flask 足够轻,路由和视图模型都由我显式控制,更适合把精力集中到业务实现上。同时 Flask 生态里有大量方便分析的库可以混用,例如 SQLAlchemy 做 ORM 操作 MySQL,pandas 读取数据库结果做统计,再通过 Flask-RESTful 或普通 Jsonify 接口把结果抛给前端。
如果不太熟悉 Flask,我解释下它的工作模式:Flask 是一个用 Python 写的轻量级 Web 微框架。所谓“微框架”,核心意思是它默认只帮你把 HTTP 请求分发到 Python 函数、把函数返回值变成响应,其余组件自由组合。用生活化的类比来说,Django 像一个精装修交付的套餐房,Flask 则是一套清水房加基础管线,你可以根据自己的习惯把家具逐个搬进去。
2.2 前后端技术组合:传统模板渲染还是前后端分离
关于前后端组织,我的建议是:如果你的重点在数据处理逻辑和系统实现,不要一上来就搞 Vue + Flask 的前后端分离。分离架构本身没有错,但会在联调、跨域、Token 鉴权、打包部署上分散掉大量精力。这个系统我采用了 Flask 自带 Jinja2 模板负责页面骨架,页面内嵌 ECharts 做图表渲染,后端只负责提供页面数据和 JSON 接口。这种方式 demo 展示方便、代码量少,也能达到完整系统的效果。
前端页面主要包含这几块:登录页、受检者列表、体检记录录入表单、检验数据登记页、历史报告详情页、数据看板页。真正页面级的渲染交给 Jinja2,而看板页中的图表数据则通过 Ajax 异步从后端接口获取,后端返回 JSON,由 ECharts 去动态渲染。
2.3 数据存储和大数据相关组件的定位
整体架构里的数据存储以 MySQL 为主。原因很简单:体检记录之间有明确关系模型,比如受检者和体检记录之间是一对多关系,体检记录和血常规检验结果之间又是一对一或一对多关系,用关系型数据库管理最合理。Flask 通过 SQLAlchemy 连接 MySQL,ORM 模型可以直接映射为数据表。
大数据组件在这个系统中并不占核心地位,但可以把它们放在扩展层面来理解:体检历史记录累计到一定量后,可以同步到 ClickHouse 中做高性能聚合;算法分析需要计算群体百分位数、标准差等指标时,用 pandas 或直接使用 MySQL 聚合函数,二者速度都够。建议不要在毕业设计或个人项目里为了“大数据”三个字而强行引入 Hadoop,这会让部署复杂度直线上升,而且无法突出你的系统设计能力。
3. 数据建模与体检指标存储方案
3.1 核心表结构与 ORM 模型
我把数据库表分为“基础资料表”和“检验记录表”两大类。基础资料表主要有用户表、受检者信息表、体检批次表;检验记录表按业务类型拆出血常规表、乙肝标志物表。下面以 SQLAlchemy 模型做一个简化示例:
python复制from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
db = SQLAlchemy()
# 受检者档案
class Person(db.Model):
__tablename__ = 'person'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
gender = db.Column(db.String(10), nullable=False) # male / female
birthday = db.Column(db.Date, nullable=False)
phone = db.Column(db.String(20), index=True)
create_time = db.Column(db.DateTime, default=datetime.now)
# 一次体检记录
class ExamRecord(db.Model):
__tablename__ = 'exam_record'
id = db.Column(db.Integer, primary_key=True)
person_id = db.Column(db.Integer, db.ForeignKey('person.id'), nullable=False)
exam_date = db.Column(db.Date, nullable=False, index=True)
exam_batch = db.Column(db.String(50), comment='体检批次号')
height = db.Column(db.Numeric(5, 1))
weight = db.Column(db.Numeric(5, 1))
note = db.Column(db.Text)
# 血常规检验结果
class BloodRoutine(db.Model):
__tablename__ = 'blood_routine'
id = db.Column(db.Integer, primary_key=True)
record_id = db.Column(db.Integer, db.ForeignKey('exam_record.id'), nullable=False)
wbc = db.Column(db.Numeric(6, 2), comment='白细胞计数')
rbc = db.Column(db.Numeric(6, 2), comment='红细胞计数')
hgb = db.Column(db.Numeric(6, 2), comment='血红蛋白')
plt = db.Column(db.Integer, comment='血小板计数')
neut = db.Column(db.Numeric(6, 2), comment='中性粒细胞百分比')
lymph = db.Column(db.Numeric(6, 2), comment='淋巴细胞百分比')
这里有个经验想多说一句:检验字段尽量不要存成“一个 record 带一个 text 型 JSON”。血常规字段虽然多,但它们的分布极其稳定,如果为了偷懒把所有指标塞进一个 JSON 字段,后续做按字段筛选、按字段计算时就必须频繁解析 JSON,索引也派不上用场,性能很容易劣化。
3.2 血常规常见指标与参考值设计
血常规的核心价值在于快速筛查感染、贫血、凝血和血液系统问题。系统里存得最多的字段主要有这几项:
| 指标 | 英文缩写 | 常见参考范围(示例值,需按医院标准更新) | 主要临床意义 |
|---|---|---|---|
| 白细胞计数 | WBC | 3.5-9.5 ×10^9/L | 升高可能提示感染或炎症 |
| 红细胞计数 | RBC | 男性4.3-5.8,女性3.8-5.1 ×10^12/L | 辅助判断贫血 |
| 血红蛋白 | HGB | 男性130-175,女性115-150 g/L | 贫血诊断核心指标 |
| 血小板计数 | PLT | 125-350 ×10^9/L | 出血或凝血风险评估 |
| 中性粒细胞百分比 | NEUT% | 40%-75% | 细菌感染时会升高 |
| 淋巴细胞百分比 | LYMPH% | 20%-50% | 病毒感染时可能升高 |
在系统实现时,参考范围不是写在页面硬编码里的,而应该单独建一张 reference_range 表,按性别、年龄、指标代码维护下限和上限。这样如果不同医院标准不同,只需要调整数据表配置,不需要重新发版。
3.3 乙肝五项的数据怎么存
乙肝检测的模型常见有两种:一种以定性结果为主,显示阴性或阳性;另一种是定量检测,会给出具体数值和 cut-off。我建议用两张思维合并的方式设计:每个乙肝项目用一个字段存储定量原始值,再用一个字段存储标准化的结果显示,比如 negative / positive / weak_positive。
五个关键项目如下:
- HBsAg 乙肝表面抗原:提示是否感染乙肝病毒。
- HBsAb 乙肝表面抗体:阳性通常说明有保护性抗体。
- HBeAg 乙肝e抗原:阳性反映病毒复制活跃。
- HBeAb 乙肝e抗体:阳性提示病毒复制减弱。
- HBcAb 乙肝核心抗体:曾经感染或正在感染的标志。
像“HBsAg 阳性、HBeAg 阳性、HBcAb 阳性”的组合就是大家常听到的大三阳模式;“HBsAg 阳性、HBeAb 阳性、HBcAb 阳性”的组合是小三阳模式。但必须强调,这类判断只是辅助数据解释,不能替代医生诊断。在设计系统时不能硬编码输出“大三阳”等带有定性结论的文本,而应该输出组合结果,再由医务端出具正式判断。为什么这么设计?因为类似结论涉及诊疗责任,把组合结果做出来让医生决策才是合规方向。
3.4 索引、唯一约束和一些字段上的坑
检验记录表的数据量会持续增长,写入字段又多,需要为高频查询预留索引。比较实用的做法:exam_record 表的 person_id + exam_date 建联合索引,blood_routine 的 record_id 建唯一索引。业务上为了防止同一个受检者同一批次被重复录入,最好给 person_id、exam_time、batch 加唯一约束;若发现重复,录入页面需要给出提示而不是静默失败。
单位上的细节也很容易踩坑。生化检验项目的单位在不同 LIS 系统里可能完全不同,比如白细胞有的报告写 10^9/L,有的写 /μL。不同单位之间差 1000 倍,如果不做统一,数据库里同一指标的数值含义就会崩掉。我的建议是:存储时统一使用最通用的绝对单位,页面展示时再根据单位喜好转换,分析计算时全部走规范化的数值字段。
4. 体检核心功能模块的代码级实现
4.1 登录、会话与角色权限设计
Flask 里做登录鉴权最轻量的方案是自己写一个装饰器,配合 session 记录当前登录用户。虽然也可以用 Flask-Login,但自己写能更直观地理解权限流,也方便按角色做页面级别控制。大致实现逻辑如下:
python复制from functools import wraps
from flask import session, redirect, url_for, abort
def login_required(view):
@wraps(view)
def wrapped(*args, **kwargs):
if not session.get('user_id'):
return redirect(url_for('auth.login'))
return view(*args, **kwargs)
return wrapped
def role_required(role_name):
def decorator(view):
@wraps(view)
def wrapped(*args, **kwargs):
if session.get('role') != role_name:
abort(403)
return view(*args, **kwargs)
return wrapped
return decorator
用起来就很简单了:受检者只能查看自己的报告,医生可以录入和查看所有人数据,管理员可以维护账号和设置参考区间范围。最后导出报告时,对受检者手机号、身份证等敏感字段自动做部分隐藏,比如手机号只保留前三位和后四位,这样演示的时候也不至于泄露真实信息。
4.2 Excel 批量导入:处理几百人的检验数据不用靠手工
血常规项目和乙肝检测的数据录入如果只靠人工一个个表单填,效率很低。我建议做一个 Excel 导入功能。体检中心的 LIS 系统通常能导出标准格式的 Excel 报告,我们批量导入后可以自动匹配字段并写入数据库。
因为我做的是 Flask 应用,Excel 解析直接用 pandas 的 read_excel 就能完成,非常方便。字段映射关系写成一个字典,导入前先做格式校验:例如性别列只能取“男/女”或“male/female”,血红蛋白值必须是数值,否则会直接提示模板格式错误。下面是简化代码逻辑:
python复制import pandas as pd
from io import BytesIO
def parse_blood_excel(file_storage):
df = pd.read_excel(BytesIO(file_storage.read()))
required_cols = ['姓名', '性别', '体检日期', '白细胞', '血红蛋白']
if not required_cols == required_cols:
# 只需检查含有所需列,不必顺序完全一致
for col in required_cols:
if col not in df.columns:
raise ValueError(f'缺少必要列: {col}')
records = []
for _, row in df.iterrows():
records.append({
'name': row['姓名'],
'wbc': float(row['白细胞'])
})
return records
批量导入的实际体验里,我发现最花时间的是数据清洗而不是导入本身。比如同一个 Excel 表里,有些行数值是字符串“3.5↑”,有些是文本“未见异常”,有些有缺失单元格,这些都需要先通过 Python 做异常值标记,再决定是跳过还是按缺失处理。
4.3 用 Flask Blueprint 组织业务模块
大型系统不能把路由全堆在 app.py 里。Flask 提供了 Blueprint 机制,可以把不同模块的路由拆到不同文件中。这个项目我按业务分为:auth 蓝图负责登录,person 蓝图负责受检者列表与档案,exam 蓝图负责体检记录录入与报告详情,analysis 蓝图负责统计数据接口。
Blueprint 注册后,目录结构类似这样:
code复制project_root/
app.py
models/
person.py
exam.py
reference.py
views/
auth.py
person.py
exam.py
analysis.py
templates/
static/
views 里每个文件都返回一个 Blueprint 对象,最终在 app.py 中统一注册。很多人容易在这里犯循环导入的错误,根因是 models 和 views 如果相互 import,注册顺序不对就会出现 module 还没完全加载就导入另一个模块。解决办法是所有模型文件只依赖 db 对象,view 文件最后统一在 create_app 内部 import,这样能避免循环依赖。
4.4 报告生成和历史报告对比
单次体检报告要展示的不只是本次数值,更重要的是和历史检查结果的对比。展示页上我做了两段式:上半部分是本次所有体检项目的汇总表,每个数值旁边显示参考范围并自动标识是否异常;下半部分是历年关键指标的折线趋势,比如血红蛋白 HGB 和白细胞 WBC 近三年的变化。
后端实现这个功能主要靠 SQL 分组和 pandas 透视。大致流程是:拿到某个人全部体检记录,连表查出血常规数据,再用 date 排序。前端用一个趋势接口拿到数组,例如:
json复制{
"dates": ["2022-03-12", "2023-03-18", "2024-05-01"],
"hgb_values": [152, 148, 156],
"wbc_values": [6.1, 7.5, 5.9]
}
数据量很小,可以用列表直接返回。如果以后数据量大,可以加个 limit 只返回最近10次。
5. 基于“大数据”思路的体检分析可视化
5.1 数据清洗和异常数据剔除
做分析之前必须处理“脏数据”。体检数据里最典型的问题是:录入单位不一致、字符编码混乱、漏检、以及仪器自动生成的上下箭头符号混进数值列。这些脏数据如果直接进 pandas 统计,会拉高整体均值,导致分析结论失真。
我的一般处理流程是这样的:
- 数值列强制转换,不能转的就置为 NaN。
- 按生理可能范围过滤,比如血小板计数不可能为 0 或超过 1000(如果单位为 ??,可能),做基本逻辑校验。
- 异常高/低值不直接删除,而是在数值旁加 abnormal 标记。
- 缺失字段按指标单独处理,不做全表删除,否则样本会少很多。
生活化类比的话,这一步就像做饭前挑菜叶:不是把整棵菜扔掉,而是只摘掉坏掉的部分。医疗数据样本量本来就宝贵,不能轻易丢弃整条记录。
5.2 参考区间判断和异常程度分级
每个检验指标是否异常,核心是拿检测值和参考范围比较。参考范围通常维护在 reference_range 表里,同时要区分性别和年龄。例如血红蛋白的正常范围男女差异很大,所以查询参考区间时,SQL 条件是:
sql复制WHERE indicator_code = 'HGB'
AND gender = 'male'
AND age_min <= 30
AND age_max >= 30
这样设计比在代码里写死 if 判断要优雅得多,也方便以后接入新的检测项目。
系统的分析模块还可以对异常程度做分级:轻度偏高(超过上限10%以内)、明显偏高(超过上限10%-30%)、严重偏高(超过30%)。项目里我用一个 Python 函数统一计算偏离率:
python复制def calc_abnormal_level(value, low, high):
if low <= value <= high:
return 'normal'
if value < low:
delta = (low - value) / low
return 'low_severe' if delta > 0.3 else 'low_mild'
delta = (value - high) / high
return 'high_severe' if delta > 0.3 else 'high_mild'
5.3 群体分析和趋势分析怎么做才有看点
单个人趋势是一个基础功能,但真正的“大数据”体现是在群体分布上。我觉得在本系统里最有展示价值的是几个分析页面:
- 历年参检人群年龄和性别结构分布,用柱状图或饼图展示;
- 血常规关键指标异常率随年份的变化,例如某个单位员工中血红蛋白偏低的比例有没有逐年上升;
- 不同年龄段的白细胞分布箱线图;
- 乙肝五项筛查中表面抗体阴性的人群占比,辅助健康管理师判断是否需要推进疫苗接种。
这些统计在数据库里可以通过分组聚合完成一部分,但更灵活的方式是查出来后在 pandas 里做。
python复制import pandas as pd
from sqlalchemy import text
def get_hgb_distribution():
sql = text("""
SELECT p.gender, p.birthday, b.hgb
FROM person p
JOIN exam_record e ON p.id = e.person_id
JOIN blood_routine b ON b.record_id = e.id
WHERE b.hgb IS NOT NULL
""")
df = pd.read_sql(sql, db.session.bind)
df['age'] = (pd.Timestamp.now() - pd.to_datetime(df['birthday'])).dt.days // 365
age_group = pd.cut(df['age'], bins=[0, 18, 30, 45, 60, 100],
labels=['0-18', '19-30', '31-45', '46-60', '60+'])
result = df.groupby(['gender', age_group], observed=False)['hgb'].agg(
['count', 'mean', 'median', lambda x: x.quantile(0.25), lambda x: x.quantile(0.75)]
)
result.columns = ['count', 'mean', 'median', 'p25', 'p75']
return result.reset_index()
这一段代码基本就是体检数据分析系统里最核心的骨架了。用 pandas 的好处是可以直接得出分位数和分组统计,如果全靠手写 SQL,语句会非常长且难维护,而出图数据也正好用这些聚合结果。
5.4 用 ECharts 整合可视化看板
后端返回聚合结果后,前端用 ECharts 画图。由于 Flask 模板渲染的是完整 HTML,所以只需要在页面中引入 ECharts 的 CDN 文件。看板页由一个总体指标卡片区、两个主要折线图和一个箱线图组成,信息量够且实现压力小。
下面是一个通过 Ajax 获取数据并渲染柱状图的示例:
javascript复制fetch('/analysis/hgb_distribution')
.then(res => res.json())
.then(data => {
const chart = echarts.init(document.getElementById('hgbChart'));
chart.setOption({
tooltip: {},
legend: { data: ['男', '女'] },
xAxis: { type: 'category', data: data.ageGroups },
yAxis: { type: 'value' },
series: [{
name: '男',
type: 'bar',
data: data.male
}, {
name: '女',
type: 'bar',
data: data.female
}]
});
});
个人建议:这一块属于展示型功能,不要过度追求花样。分析的价值更多在于能看出哪些人群异常率更高,所以图表的坐标轴、单位、图例要清晰,鼠标悬停要能看到人数和均值。
6. 实际运行中容易踩的坑与排查速查表
6.1 为什么血常规查询越跑越慢
项目初期数据量小,怎么写 SQL 都很流畅,但当单次导入几千条血液检验记录后,会发现页面开始变慢。排查时我建议先看 MySQL 慢查询日志,很多慢查询出在按 person_id 查最新记录时没有走索引。
解决办法有两条路:一是把高频查询字段建联合索引,二是避免在 ORM 中把整个表加载到 Python 再做过滤。比如查询某人的最新体检记录,可以用:
python复制ExamRecord.query.filter_by(person_id=pid).order_by(ExamRecord.exam_date.desc()).first()
这里一定要让 exam_date 和 person_id 都有索引。
6.2 用 pandas 读数据库返回的对象不兼容 SQLAlchemy Session
pandas 的 read_sql 如果和 SQLAlchemy session 混用,经常会报“This session is in 'prepared' state”的错。原因是读的时候 session 有未提交事务或连接被占用。最稳妥的做法是使用 db.session.bind 作为连接,并在读完后调用 db.session.remove() 或者不用 ORM session,而是用自定义的 engine 连接:
python复制from sqlalchemy import create_engine
from flask import current_app
def get_engine():
uri = current_app.config['SQLALCHEMY_DATABASE_URI']
return create_engine(uri)
这样独立出来的只读连接和分析逻辑不共用 ORM 的事务状态,排查时会省很多时间。
6.3 日期范围边界带来的统计不准
体检统计经常需要查“最近三个月内来过的人”或“今年某月的数据”。时间边界问题是数据分析里最容易被忽略的一个坑。如果体检日期以字符串存成 datetime,那么查早于某一天时,必须包含起始日期的0点,不能只按日期字符串筛选。为避免踩坑,我统一把日期字段存成 date 类型,所有查询使用 >= 和 < 语义,右边界主动加一天。
6.4 Excel 里的隐形字符和公式
LIS 导出的 Excel 可能包含单元格格式是文本的数字,例如带了单位“g/L”或“×10^9/L”,pandas 读出来直接是字符串。录入数据库前必须做一次逆转换,比如去掉单位符号后用正则提取数字。这是批量导入验证最容易遗漏的部分。
6.5 安全合规方面:脱敏和权限
健康数据属于高度敏感的个人信息,即使只是演示和课程设计,也必须有基本的合规意识。数据库中的手机号需要加密或脱敏,页面根据角色隐藏身份证信息,批量导出 Excel 需要对记录加权限水印,服务器日志不能记录查询返回的具体体检数值。这些在真正交付系统时都是硬指标。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 批量导入提示必填列缺失 | 列名包含隐藏空格或全半角括号差异 | 用 list(df.columns) 检查列名,统一做 strip |
| 某一指标异常数据显示错位 | Excel 列顺序和数据库字段不对应 | 导入逻辑按列名匹配,不要按下标取值 |
| Flask 页面显示 500 错误但无日志 | 数据库字段长度超限 | 开启 app.debug=True,查看异常栈 |
| 多人同时导出血常规报告 | 服务器内存不足 | 用分页查询或 stream 流式响应 |
| 当前登录人能看到他人报告 | URL 上直接拼接 record_id | 查询时必须校验 person_id 归属 |
6.6 关于循环导入和 Flask 迁移的教训
刚开始写 Flask 项目时常会图省事把所有模块 import 在文件头部,结果 Blueprint、models、db 三者相互引用,应用一启动就报 ImportError。我后面形成了一套固定习惯:db 单独一个模块,models 只依赖 db,view 层函数内部不要直接 import 其它蓝图中的路由,所有 R 路由注册统一在 create_app 完成。这样一来每个文件职责清晰,不会再绕晕。
模型字段变更后,建议立刻使用 Flask-Migrate(基于 Alembic)生成迁移脚本。没有迁移脚本的项目,后期改动字段只能手动 alter table,容易造成开发库和生产库表结构不一致。数据模型里这种隐性不一致很难排查,往往到跑分析脚本时才发现结果对不上。
7. 部署上线、性能优化与后续扩展
7.1 从开发服务器切到 Gunicorn + Nginx
Flask 自带的 app.run 只适合开发调试,一旦并发稍微上来就会出现阻塞和超时。部署方案我用的是 Gunicorn 做 WSGI 服务,多个 worker 处理请求,再前置 Nginx 做静态文件服务和反向代理。实际最小配置可以是:
bash复制gunicorn -w 3 -b 127.0.0.1:8000 app:app
Nginx 里再把 80 端口的流量代理到 8000,同时把 /static/ 路径直接指向本地静态目录,能显著减轻 Flask 压力。第一次上线时容易漏配置超时时间,如果有些统计接口处理大量数据需要 10 秒以上,nginx 默认的 60s 其实还够,但如果用云厂商的 SLB,一定要检查代理超时。
7.2 数据库连接池与慢查询优化
SQLAlchemy 默认维护连接池,但如果每个请求里都新建 engine,连接数会爆。部署时建议统一设置:
python复制SQLALCHEMY_ENGINE_OPTIONS = {
'pool_size': 10,
'pool_recycle': 3600,
'pool_pre_ping': True
}
pool_recycle 这个参数很多人忽略,MySQL 默认 wait_timeout 是 8 小时,如果数据库把连接断掉但连接池不知道,程序就会报“MySQL server has gone away”。pool_pre_ping 能在请求前先做一次连通性检测,成本不大但对稳定性提升明显。这属于上线后才遇到的经典问题,讲出来给大家避坑。
分析侧的性能优化主要靠数据库聚合代替 Python 循环。比如统计受检者年龄段分布时,应该用 DATEFORM 或按身份证段前缀分组完成,别把几十万行全读进 pandas 再 for 循环,那样内存占用会明显。
7.3 后续还能扩展哪些方向
如果这个项目要继续延伸,我比较推荐三个方向:
- 对接医院的 LIS 接口或 HL7 协议,让检验结果自动进入系统,减少人工导入。
- 引入更细粒度的年龄/性别特异性参考范围,增加慢病风险评估模型,比如根据 BMI、血压和血常规指标建立代谢综合征风险分数。
- 把前端报告页做成可打印 PDF,支持体检结论盖章导出,做企业团体报告汇总。
比如对“体检异常推荐就医”这种功能,后端可以定义异常规则,达到规则后生成复查建议,但所有结论必须显示“请以执业医师正式报告为准”。这是系统合规和安全设计的一部分,而不是一句可有可无的免责声明。
7.4 一点个人体会
在我自己把这套系统完整跑通之后,最大的感受是:这类健康管理系统难的不是 Flask 路由,也不是简单的 CRUD,而是如何把一个领域知识密集、关系复杂、多单位多标准的业务域抽成一套清晰的关系模型,并且让分析结果能真正支撑出可视化结论。血常规、乙肝这些项目背后有无穷多的临床细节,但我们做系统的人首先要做的,是先把数据基础打牢。数据一旦是脏的、乱的,后面所有花哨的大屏和报告模版都立不住。
做完这个系统,后续如果再遇到类似健康档案项目,我会优先确认三个问题:一是检验项目是否长期固定,二是参考区间是否随性别年龄变化,三是历史数据迁移的字段映射要不要保留原值。这三个问题直接决定数据库怎么设计、分析代码怎么组织。如果一开始就把它们想明白,整个项目会顺利很多。
