FastAPI + Vue3 实训项目复盘:医疗咨询与预约系统的架构设计

说明:本文是课程实训项目的技术复盘,只讨论系统设计和代码实现思路,不涉及真实医疗服务,也不提供任何医疗建议。

一、项目背景

这次实训项目我做的是 医云问(MedCloud Ask)。它的主题是医疗咨询与预约管理,目标是把 AI 问答、知识库、预约、号源管理和后台数据查看串成一个完整的练习项目。

一开始我没有把它设计成单纯的聊天页面,因为只做一个“输入问题、返回回答”的页面,业务链路会比较短,也很难体现后端的数据建模和工程能力。所以我把项目拆成了两个入口:

  • 机构后台:用于维护知识库、配置科室和号源、查看预约、管理文章、查看咨询记录。
  • 患者端页面:用于 AI 咨询、查看科普内容、选择科室和时间段并提交预约。

这样设计之后,项目就从一个简单的问答 Demo,变成了一个包含多角色、多模块和多条业务链路的全栈练习项目。

二、技术选型

项目采用前后端分离架构。

层次 技术选型 作用
前端 Vue 3、TypeScript、Element Plus、Pinia、Vue Router、Vite 管理后台和患者端页面
后端 FastAPI、SQLAlchemy Async、Pydantic API 服务和业务逻辑
数据库 MySQL 8.0 存储机构、用户、预约、文章、会话等数据
缓存与并发控制 Redis 缓存、去重、分布式锁、布隆过滤器
消息队列 RabbitMQ 对话消息异步处理
搜索组件 Elasticsearch 咨询记录检索的扩展基础
AI 问答 Dify RAG 知识库检索增强问答
部署 Docker Compose 管理 MySQL、Redis、RabbitMQ、Elasticsearch 等服务

整体思路是:MySQL 负责业务主数据,Redis 负责高频访问和并发控制,RabbitMQ 用于解耦后处理任务,Dify 负责知识库问答,Vue3 负责前端交互。

三、系统模块设计

1. 多租户机构模型

项目中最核心的模型是机构。每个机构都有自己的后台账号、知识库、患者端入口、科室、号源、预约记录、文章和咨询记录。

后台接口会从 JWT 中解析当前登录用户,并拿到用户所属的 organization_id。后续查询预约、文章、咨询记录、号源等数据时,都必须带上这个机构 ID 作为过滤条件。

这样可以避免不同机构之间的数据互相访问。例如预约列表接口不会相信前端传来的机构 ID,而是以后端解析出的当前用户机构 ID 为准。

这个点看起来很基础,但它是多租户系统里非常重要的安全边界。

2. AI 咨询与知识库问答

AI 咨询模块主要包括三部分:

  • 机构上传知识库文档;
  • 患者端发起问题;
  • 后端调用 Dify 的流式问答接口。

知识库上传时,后端会先校验文件类型,目前支持 PDF、TXT、DOCX。文件会上传到 OSS,同时转发给 Dify,用于后续知识库检索。

患者提问时,后端主要流程如下:

患者提交问题
    ↓
根据 org_slug 查询机构
    ↓
创建或复用 Conversation 会话
    ↓
保存用户问题到 MySQL
    ↓
调用 Dify 流式接口
    ↓
通过 SSE 返回给前端
    ↓
流结束后保存 AI 回复
    ↓
投递消息到 RabbitMQ

这里使用 SSE 的原因是 AI 回复可能需要一定时间,如果等完整回答生成后再一次性返回,用户体验会比较差。流式输出可以让前端边接收边展示,交互会更自然。

3. 预约与号源管理

预约模块的核心是号源。后台可以维护科室和可预约时间段,每个时间段都有容量和剩余数量。

患者端预约时,需要选择科室、时间段,并填写基本信息。后端创建预约前会检查:

  • 时间段是否存在;
  • 时间段是否启用;
  • 时间段是否已经过期;
  • 是否还有剩余号源;
  • 同一手机号是否已经预约过同一时间段。

其中比较关键的是并发控制。如果同一个时间段只剩一个号源,多个请求同时提交,就可能出现超卖问题。因此我在预约接口里使用了 Redis 的 SET NX

预约流程大致是:

Redis 写入去重 key,防止重复预约
    ↓
Redis 写入 lock key,锁定 time_slot_id
    ↓
查询并校验号源
    ↓
扣减 remaining
    ↓
创建预约记录
    ↓
释放锁

相关 key 的设计如下:

appt:dedup:{patient_phone}:{time_slot_id}
appt:lock:{time_slot_id}

第一个 key 用于防止同一手机号重复预约同一时间段,第二个 key 用于保证同一时间段的扣减过程不会被多个请求同时执行。

这个模块让我更直观地理解了:并发问题不是“前端按钮禁用一下”就能解决,真正的保护必须放在后端。

4. 后台工作台

后台工作台主要用于展示系统当前的基础数据,包括:

  • 今日咨询数;
  • 今日预约数;
  • 待确认预约数;
  • 知识库文档数;
  • 开放号源数;
  • 待处理线索数;
  • 最近咨询记录;
  • 最近预约记录。

这个模块本身的业务逻辑不复杂,但能让后台从“菜单集合”变得更像一个完整系统。管理员进入后台后,可以先看到系统运行情况,再进入具体模块处理数据。

5. 咨询记录与高频问题

AI 咨询结束后,如果不保存记录,后续就无法分析用户都问了什么。因此项目中设计了 ConversationMessage 两张表。

后台咨询记录页面支持:

  • 查询会话列表;
  • 查看最新问题和最新回答;
  • 按关键词搜索;
  • 按日期筛选;
  • 查看完整上下文;
  • 统计高频问题。

当前实现主要基于 MySQL 查询,同时项目里也接入了 RabbitMQ 和 Elasticsearch 的基础链路。后续如果继续优化,可以把咨询记录同步到 Elasticsearch,再做更完整的全文检索。

6. 人工跟进线索

AI 问答并不能处理所有问题,所以项目中加入了人工跟进线索模块。

当患者输入内容中出现“转人工”“联系医生”“电话联系”等关键词,或者文本中识别到手机号时,系统会生成一条待处理线索。患者也可以主动在页面中留下手机号和问题摘要。

后台可以查看线索,并进行状态流转:

待处理 -> 已联系 -> 已关闭

这个设计的重点不是复杂算法,而是给 AI 问答加一个边界:能自动回答的走 AI,不能自动处理的转人工记录。

7. 文章与预约来源统计

项目中还包含健康科普文章模块。后台可以发布、下架、编辑文章,患者端可以查看文章。

为了让文章模块不只是静态内容展示,我在预约表中加入了来源字段。当患者从文章详情进入预约页面并成功提交预约时,系统会记录对应的文章 ID。

这样后台可以看到文章的:

  • 阅读次数;
  • 预约点击次数;
  • 成功预约次数;
  • 简单转化率。

这个模块让我练习了一个常见思路:业务数据不只要能新增、查询、删除,还可以通过来源字段串联出一条用户行为链路。

四、关键技术实现

1. Redis 布隆过滤器防缓存穿透

患者端访问机构信息时,会通过机构 slug 查询。如果大量请求使用不存在的 slug,可能会绕过缓存直接访问数据库。

因此项目在启动时会从 MySQL 读取所有机构 slug,并重建 Redis 中的布隆过滤器。

查询机构信息时,流程如下:

收到 slug
    ↓
布隆过滤器判断是否可能存在
    ↓
不存在:直接返回 404
    ↓
可能存在:查询 Redis 缓存
    ↓
缓存未命中:查询 MySQL
    ↓
回填 Redis

这样可以减少无效请求对数据库的压力。

2. Redis 分布式锁控制预约并发

预约并发问题的本质,是多个请求可能同时读取到相同的剩余号源。

比如某个时间段只剩 1 个号,两个请求同时读到 remaining = 1,如果都继续执行扣减,就可能出现超卖。

项目中通过 Redis SET NXtime_slot_id 加锁,保证同一时间段在同一时刻只有一个请求能执行扣减逻辑。

这部分代码虽然不长,但比普通 CRUD 更接近真实业务问题。

3. RabbitMQ 异步解耦

AI 回复保存后,后端会将消息投递到 RabbitMQ。这样后续同步到 Elasticsearch 或做统计分析时,不需要阻塞当前问答接口。

如果直接在接口中同步写入搜索组件,那么一旦搜索组件响应慢或者异常,用户当前问答也会受影响。

使用消息队列后,主流程只需要完成“保存消息 + 投递事件”,后续处理交给消费者完成。这就是异步解耦的价值。

4. JWT 与后端权限边界

后台接口中,很多数据都不能直接相信前端传参。例如预约列表、号源列表、咨询记录列表,都应该根据当前登录用户所属机构查询。

因此项目中通过 JWT 获取当前用户,再使用用户的 organization_id 作为数据过滤条件。

这个设计可以防止水平越权问题,也是多租户系统必须注意的地方。

五、项目中遇到的思考

1. AI 应用不只是调用接口

刚开始我以为 AI 模块主要就是调用一个接口。但实际做下来发现,一个完整的 AI 应用还要考虑:

  • 知识库从哪里来;
  • 文档上传失败怎么记录;
  • 回答内容怎么保存;
  • 多轮会话怎么续上;
  • 用户问题怎么沉淀;
  • 不能自动处理的问题怎么转人工。

所以 AI 问答只是入口,后面的数据建模和业务闭环同样重要。

2. 并发控制要放在后端

预约模块让我体会比较深。前端可以做按钮禁用、加载状态、防重复点击,但这些都不能替代后端校验。

真正的幂等、防重和防超卖逻辑必须在后端完成,因为接口可能被重复请求,也可能被绕过页面直接调用。

3. 业务字段会影响后续分析能力

文章来源统计这个功能让我意识到,很多分析能力不是后期凭空出现的,而是在建表时就要预留字段。

比如 source_article_idsource_page 这类字段,单看创建预约时不是必须的,但它们能帮助后续分析预约来源。

六、后续优化方向

目前项目已经完成基础闭环,但还有不少可以继续优化的地方:

  1. 咨询记录检索可以进一步切换到 Elasticsearch,支持分词、高亮和更复杂的搜索条件。
  2. 号源管理可以支持批量生成一周或一个月的排班。
  3. 人工跟进线索可以加入负责人、备注和跟进时间线。
  4. 知识库文档可以对接更完整的处理进度,例如向量化状态轮询。
  5. 文章推荐可以根据当前咨询内容做简单匹配,而不是只靠后台手动设置。

七、总结

医云问这个项目让我练习了从需求拆解到前后端实现的一整套流程。它不是单点功能,而是把咨询、知识库、预约、号源、文章、线索和后台统计串起来。

通过这个项目,我对下面几个问题有了更清晰的理解:

  • 多租户系统要把机构边界放到所有数据查询里;
  • 预约场景需要考虑幂等、防重和并发安全;
  • AI 问答系统除了模型回复,还需要知识库、会话、记录和人工兜底;
  • Redis、RabbitMQ、Elasticsearch 这些组件要结合具体业务场景使用,才真正有意义。

如果简单总结,这个项目对我来说不是“写了一个 AI 聊天页面”,而是完成了一次相对完整的全栈系统设计练习。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐