医院预约管理系统 — 设计思路与实现笔记
本文档记录从需求分析 → 架构设计 → 分层实现 → 边界处理的完整思路。
一、需求分析与建模
1.1 业务背景
某社区医院需要一个预约管理系统,管理科室、医生、患者及预约挂号。核心痛点:
- 科室与医生:多对一关系,医生排班信息需直观呈现
- 患者管理:身份证唯一标识,联系方式一对一独立存储
- 预约挂号:需防止超额预约和重复预约
- 数据统计:管理者需快速了解每日运营概况
1.2 实体关系分析
科室 (departments) 1 ── N 医生 (doctors) 医生 (doctors) 1 ── N 预约 (appointments) 患者 (patients) 1 ── N 预约 (appointments) 患者 (patients) 1 ── 1 联系方式 (patient_contacts)1.3 关键设计决策
Q: 联系方式为何独立成表而非放在 patients 表中?
A: 遵循数据库范式,联系方式字段(手机、微信、邮箱、地址、紧急联系人)是可选信息且与患者核心标识(姓名、身份证)职责不同。独立存储便于:
- 按需加载(列表页不需要联系方式,减少数据传输)
- 独立维护(联系方式更新不影响患者主体)
Q: 为何使用 Tortoise ORM 而非 SQLAlchemy?
A: Tortoise ORM 是 Python 异步生态的原生选择,与 FastAPI 的 async/await 无缝配合,无需额外线程池处理数据库操作。
二、架构设计
2.1 分层思想
采用经典的8 层架构,自上而下单向依赖:
┌──────────────────────────────────────────────┐ │ ⑦ api/ 路由层 HTTP 入口/出口 │ ← 薄控制器,不做业务判断 │ ⑥ services/ 业务逻辑层 规则编排 │ ← 核心,所有 if/else 在这里 │ ⑤ repositories/ 数据访问层 ORM 查询封装 │ ← 屏蔽数据库细节 │ ③ models/ ORM模型层 表映射 │ ← 纯声明 ├──────────────────────────────────────────────┤ │ ④ schemas/ Pydantic层 数据校验 │ ← 与路由层配合 │ ② core/ 基础设施层 DB/JWT/异常/依赖 │ ← 横向支撑 │ ① config/ 配置层 .env → Settings │ ← 环境隔离 │ ⑧ utils/ 工具层 响应封装 │ ← 无状态纯函数 └──────────────────────────────────────────────┘2.2 为什么需要 Repository + Service 两层?
| 模式 | 问题 |
|---|---|
| 路由直接写 ORM | 路由层承担太多职责:参数校验 + 业务判断 + 查询构建 + 响应组装,难以测试和复用 |
| 路由 → Service → ORM | Service 既要处理业务规则又要写 SQL,Service 变臃肿 |
| 路由 → Service → Repository → ORM✅ | 每层职责单一:Service 只管"能不能做",Repository 只管"怎么查" |
具体收益:
- 切换数据库时只改 Repository 层
- Service 可以 mock Repository 做单元测试
- 复杂查询(如预约 5 维筛选)封装在 Repository 中,Service 只需传参
2.3 统一响应格式设计
{"code":200,"message":"操作成功","data":null}设计考量:
code用于前端判断成功/失败(不依赖 HTTP 状态码,因为部分代理可能改写)message可直接展示给用户(Toast/提示框)data承载实际业务数据,失败时为null
三、各层实现要点
3.1 配置层 (config/settings.py)
.env 文件 → python-dotenv 加载 → os.getenv() 读取 → Settings 类属性 ↓ DB_URL 属性动态拼接连接串为什么用.env而非硬编码?
- 开发/测试/生产环境分离,
.env.dev/.env.prod各自维护 - 敏感信息(密码)不入 Git
3.2 基础设施层 (core/)
| 模块 | 核心实现 | 设计要点 |
|---|---|---|
database.py | Tortoise.init()+generate_schemas() | 开发自动建表,生产用 Aerich 迁移 |
security.py | python-jose的jwt.encode/decode | exp字段控制过期,HS256 对称加密 |
exceptions.py | 3 个异常处理器注册到 app | 将IntegrityError转为用户友好中文提示 |
dependencies.py | Header(authorization)提取 →verify_token() | 支持Bearer xxx和纯 token 两种格式 |
3.3 ORM 模型层 (models/hospital.py)
classDoctor(models.Model):department=fields.ForeignKeyField("models.Department",related_name="doctors")设计要点:
related_name让反向查询更语义化:dept.doctors而非dept.doctor_setauto_now_add=True创建时自动填时间,auto_now=True更新时自动刷新null=True允许字段为空,与 MySQLDEFAULT NULL对应
3.4 数据访问层 (repositories/)
基类设计:
classBaseRepository:model:Type[Model]# 子类覆写asyncdefpaginate(self,query,page,page_size,order_by):"""通用分页,返回 (items, total)"""所有 Repository 继承此基类,获得get_by_id/create/update/delete/paginate五件套。
批量加载避免 N+1:
# ❌ 坏做法:循环中逐条查(N+1 问题)fordoctorindoctors:dept=awaitDepartment.get(id=doctor.department_id)# 每轮 1 次 SQL# ✅ 好做法:先收集 ID,再批量查dept_ids=list({d.department_idfordindoctors})departments=awaitDepartment.filter(id__in=dept_ids).all()dept_map={d.id:d.namefordindepartments}3.5 业务逻辑层 (services/)
核心模式:三元组返回
asyncdefcreate_appointment(self,data:dict)->tuple[bool,str,dict|None]:""" 返回 (是否成功, 提示消息, 数据或None) 四个校验按序执行,任一失败立即返回: ① 患者存在? → 否则 "患者不存在" ② 医生在岗? → 否则 "该医生当前不在岗" ③ 当天已预约? → 否则 "该患者当天已预约过该医生" ④ 号满? → 否则 "该时段号已满" ⑤ 全部通过 → 创建 """为什么用 fail-fast 而非收集所有错误?
社区医院场景,用户一次只操作一条预约,即时反馈比批量错误提示体验更好。
3.6 路由层 (api/v1/)
路由层只做三件事:
- 接收 HTTP 参数(Query/Path/Body)
- 调用 Service 方法
- 根据返回值组装 JSON 响应
@router.post("/appointments")asyncdefcreate_appointment(data:AppointmentCreate,...):ok,msg,result=awaitappointment_service.create_appointment(data.model_dump())ifnotok:returnerror(msg)# 业务失败 → 400returnsuccess(data=result,message=msg)# 成功 → 200四、边界情况处理清单
| 边界场景 | 处理方式 | 层级 |
|---|---|---|
| 科室名称重复 | 新增/编辑时查重,编辑时排除自身 | Service |
| 删除有医生的科室 | 先查Doctor表,有则返回错误 | Service |
| 删除有待就诊的医生 | 查Appointment(status=待就诊) | Service |
| 身份证重复 | 新增/编辑时查重,编辑排除自身 | Service |
| 同一患者同天重复预约同一医生 | 组合条件查重 | Service |
| 预约数超过日限额 | 先统计同时段数,≥限额则拒绝 | Service |
| 医生不在岗时预约 | 查医生 status != “在岗” | Service |
| 取消/完成非待就诊预约 | 查 status,不符则拒绝 | Service |
| 联系方式不存在时查询 | 返回空结构(id=0),前端据此显示"创建"按钮 | Service |
| 联系方式已存在时再次创建 | 查唯一约束,提示"请使用编辑" | Service |
| 编辑联系方式时不存在 | 容错处理:自动创建 | Service |
| 搜索无结果 | 返回空数组 + total=0 | Repository |
| 分页超出范围 | Tortoise ORM 自动返回空数组 | Repository |
| Token 过期/伪造 | verify_token()返回 None → 401 | Security |
| 数据库唯一键冲突 | 全局异常处理器 → 400 + 友好提示 | Exception |
| 请求参数格式错误 | Pydantic 校验 → 422 + 字段级错误 | Exception |
五、代码风格约定
注释规范
# ── 区块分隔注释(用 ── 区分不同逻辑块) ──# ① 有编号的步骤说明# → 缩进表示结果/后果# ✅ 成功路径 ❌ 失败路径命名约定
- Repository 类:
{Entity}Repo,如DoctorRepo - Service 类:
{Entity}Service,如DoctorService - Schema 类:
{Entity}{Action},如DoctorCreate/DoctorUpdate - 路由文件:实体名复数,如
doctors.py - 路由函数:
{action}_{entity},如list_doctors/create_doctor
文件组织
每层一个模块目录,每个实体一个文件:
- 简单的实体(如科室)把相关逻辑集中在一个文件
- 复杂的实体(如患者 + 联系方式)在 Repository 中合并为
patient_repo.py,类之间用空行分隔
六、可扩展性预留
| 扩展点 | 当前实现 | 扩展方向 |
|---|---|---|
| 多角色认证 | 固定 admin 账号 | core/security.py增加角色字段,dependencies.py增加require_role() |
| 数据库迁移 | generate_schemas() | 改用 Aerich:aerich init→aerich migrate→aerich upgrade |
| 日志系统 | 无 | utils/增加logger.py,core/middleware.py增加请求日志中间件 |
| 缓存 | 无 | repositories/base.py增加 Redis 缓存层 |
| API 版本化 | api/v1/ | 新增api/v2/,路由注册时指定 prefix |
| 单元测试 | 无 | tests/目录,pytest + httpx 异步测试客户端 |
| 定时任务 | 无 | 增加 APScheduler,定时将过期预约状态改为「已过期」 |
七、总结
本项目采用8 层分层架构,核心思想是关注点分离:
- 路由层只管 HTTP 协议(请求解析、响应序列化)
- 业务逻辑层只管业务规则(能不能做、什么条件)
- 数据访问层只管数据查询(怎么查、查什么)
- 模型层只管表结构(字段类型、关系映射)
每层只做一件事,每行代码都有注释,降低了认知负担和维护成本。所有边界情况在 Service 层显式处理,不依赖数据库异常作为业务逻辑控制流。