Python FastAPI+uniapp+微信小程序预约挂号系统实战解析

发布时间:2026/10/6 8:42:31
Python FastAPI+uniapp+微信小程序预约挂号系统实战解析
去年接了个挺有意思的活儿——给本地一家医院做核酸检测预约挂号系统。需求听起来很简单患者在小程序里选日期、选时段、预约、到院采样、查报告。但等我真正把需求拆开才发现这套由Python、uniapp、微信小程序三个关键词组成的系统要处理的坑比想象中多得多。这篇文章不讲虚的直接把整套系统的设计思路、核心实现、以及我在前后端联调和小程序发布阶段踩过的具体问题都摆出来。如果你正准备用 Python 写后端、用 uniapp 套一层微信小程序做医疗预约类应用或者只是想把一个真实项目从零推到线上这篇应该能帮你少走不少弯路。1. 项目整体设计与技术选型思路1.1 后端为什么选 Python而且选了 FastAPI先说后端。医院那边原有的信息科技术栈比较杂但团队里 Python 底子最稳所以后端语言没怎么纠结。真正纠结的是框架选哪个Django、Flask、FastAPI 三个都试过最终落地用了 FastAPI。Django 的好处是全家桶齐全自带 Admin 后台和 ORM做一个预约管理系统绰绰有余。但问题是 Django 的重在这种中轻量接口场景里反而成负担尤其是我只需要提供给小程序调用的 JSON API不太需要服务端渲染页面。Flask 轻是轻但很多能力靠第三方拼装项目一大光理清楚插件之间的兼容性就够喝一壶。FastAPI 打动我的点有三个第一基于 Pydantic 做请求参数校验前端传参不对直接返回带具体字段的报错接口联调时省下大把沟通成本第二自动生成 Swagger 文档医院那边的信息化负责人不写代码但能打开/docs看接口这对项目验收帮助很大第三async 语法在预约高峰时段比如早上 8 点放号扛并发时表现比 Flask 稳得多。数据库用的是 MySQL 8.0ORM 选了 SQLAlchemy 2.x。Redis 用来做缓存和分布式锁这个后面讲到防重复预约时会再展开。1.2 前端为什么用 uniapp而不是原生微信小程序前端这块同期有两个选择微信小程序原生 WXML/WXSS或者 uniapp。原生的性能确实好而且微信开发者工具里调试起来最直接但问题在于医院并不是只要小程序端——他们明确提出以后可能要做 App 或者支付宝小程序。如果每个端都单独写一套后续维护成本会翻倍。uniapp 的核心价值就是一套 Vue 代码编译到多端。我用 HBuilderX 直接创建项目支持 Vue 3 和 TypeScript。编译到微信小程序时产物是原生小程序代码底层交互走的是微信的 API只是在开发层面用 uni.xxx 统一封装了一层。实际使用下来如果你不做太复杂的原生功能90% 的代码都能跨端复用。组件库选了 uview-plus这是 uview 的 Vue 3 兼容版在 HBuilderX 插件市场直接导入。它提供了一套接近业务后台风格的组件比如日期选择、表单、弹窗、步骤条做预约类页面基本够用。不过我后来吃了它一个亏——包体积太大这个到第 5 章详细讲。1.3 系统模块划分与整体调用链路整套系统拆成六个模块边界画得越清楚联调期越省心用户模块微信登录、手机号绑定、个人信息维护预约模块检测点查询、日期时段排班、名额预扣、订单生成支付模块对接微信支付支持预约费线上支付和到院支付两种模式报告模块检测结果回填、报告生成、历史报告查询消息模块预约成功通知、报告出具通知走微信订阅消息管理后台排班管理、名额配置、预约记录查看、数据统计。整体调用链路是微信小程序端发起请求 → HTTPS 到 Nginx → FastAPI 应用服务 → MySQL 和 Redis。小程序端所有请求都走uni.request后端统一返回{ code, message, data }结构。这个结构约定得越早越好我见过太多项目因为前端要 data、后端返回 obj联调期来回扯皮。提示中间层没有再加单独的网关服务初期没必要。等后续如果要做多端App、H5再引入统一网关也不迟先把核心业务跑通更重要。2. 数据库设计与预约核心流程解析2.1 核心表结构与字段设计原则预约系统的数据库设计核心在于把排班资源和用户订单清晰分离。我最终落地的核心表有六张下面这张表是精简后的结构表名关键字段说明userid, openid, phone, name, id_cardopenid 唯一索引手机号用于到院核验detection_pointid, name, address, business_hours检测点比如院区广场、门诊楼前scheduleid, point_id, date, period, capacity, booked_count, status某个检测点在某天某个时段的总名额和已约数bookingid, user_id, schedule_id, status, pay_status, sample_type, qr_code预约订单主表reportid, booking_id, result, pdf_url, generated_at检测报告一对一关联预约单operation_logid, admin_id, action, target, created_at后台操作审计医院很看重这个设计时有一条很重要的原则不要直接在前端做时段表的大范围扫描。比如查本周剩余号源最稳妥的方式是 Redis 里存一份当日缓存的排班汇总而不是每次实时COUNT订单表。预约系统的高频操作是查余量和写预约这两类操作的数据流要分开设计。2.2 预约订单状态机设计预约订单不是只有已预约和已取消两个状态。实际业务里从用户点下按钮到最终拿到报告中间至少经历 7 个状态状态含义触发方式PENDING_PAY待支付若启用线上支付创建订单后进入BOOKED已预约待到院支付成功或选择到院支付后CHECKED_IN已到院核验现场扫二维码或身份证SAMPLING已采样护士录入采样信息TESTING检测中样本进入实验室后FINISHED已出报告结果回填并生成报告CANCELLED已取消用户取消或超时取消EXPIRED已过期未到院系统定时任务标记状态流转最怕乱跳。我建议把所有状态流转统一到一个BookingStatusMachine类里管理而不是在各个业务代码里随手改状态。每个状态变更同时写一条status_log后面用户投诉我明明预约了为什么查不到时翻日志几秒钟就能定位是哪个环节断的。2.3 防重复预约与并发控制最容易被忽视的硬骨头这是整个系统里我花时间最多的地方。核酸预约的特点是同一时间放 500 个号用户集中在放号后 10 秒内涌入手机端可能连点好几次提交。第一个坑是重复预约。同一个用户对同一个schedule_id只能有一条有效订单。这个靠数据库层面唯一索引兜底UNIQUE KEY (user_id, schedule_id, status)其中 status 只约束有效状态。注意别在代码层先 SELECT 再 INSERT并发下一定会漏。第二个坑是库存超卖。schedule表里有capacity和booked_count如果直接用UPDATE schedule SET booked_count booked_count 1 WHERE id ?是没问题的MySQL 的行锁会保证原子性。但问题在于如果某个用户创建订单后一直不支付名额就被白白占住。所以我的方案是双保险——数据库行锁保证不超卖Redis 预扣名额 定时任务超时释放保证真实号源利用率。具体流程是创建订单时先SETNX抢占 Redis 名额比如当日 500 个号按照秒级过期抢成功再写数据库订单用户 15 分钟未支付定时任务把 Redis 名额回补同时把订单标记为EXPIRED。这里有个细节要注意Redis 回补和数据库状态更新必须做幂等避免同一个名额被释放两次。2.4 从预约到报告的业务闭环用户的视角很简单预约、采样、等结果。但从系统角度完整链路是用户在预约页选择检测点和日期后端返回该日期下的时段余量提交预约后创建订单生成一个带参数的二维码预约单号 时间戳签名到院后护士用扫码枪或手机扫二维码调用核验接口状态从BOOKED变成CHECKED_IN采样完成后护士在管理端标记SAMPLING实验室出结果后检验科上传结果文件后端把状态置为FINISHED同时调用模板生成报告 PDF用户在小程序报告页看到结果点击可查看详情或下载。这个闭环里最容易断的是最后一步——报告回填。医院检验科不一定愿意用你的系统录入结果我最后的方案是留一个 Excel 导入接口检验科每天导出的结果表按规定格式上传后端批量匹配booking_id并更新状态。别试图让检验科老师在你们的 Web 后台手动敲结果用户体验差距太远了。3. 后端 Python 接口实现与实操细节3.1 微信登录态与 JWT 鉴权设计小程序端用uni.login拿code传到后端/api/v1/auth/login后端拿 code 调微信的jscode2session接口换openid。这个环节有两个高频报错invalid code一般是 code 已被使用过一次前端必须保证每次登录重新调uni.login不能缓存网络超时微信接口偶发抽风必须做重试。我当时的策略是tenacity库配合指数退避最多重试 3 次。拿到 openid 后我生成 JWT token 返回给前端。JWT 的有效期设了 7 天但小程序端每次冷启动都会静默调一次登录接口刷新。这里我踩了一个坑直接把 openid 放进了 JWT payload。虽然 JWT 本身有签名不容易被篡改但 payload 是 Base64 编码可读的一旦 token 泄露用户身份就暴露了。后来改成只有user_id和session_key的哈希openid 只存在服务端 Redis 里前端永不可见。3.2 预约接口的参数校验与业务规则预约创建接口是最核心的接口我把校验逻辑完整列一下你可以直接抄作业。接口路径/api/v1/booking/create接收参数point_id检测点 IDdate预约日期格式YYYY-MM-DDperiod时段比如MORNING、AFTERNOON、EVENINGsample_type采样方式单人单管或者多人混管id_card、name受检人信息用于报告匹配。校验顺序非常重要顺序不对会出现逻辑漏洞参数基础校验Pydantic 自动完成日期是否在过去——会直接被拒绝检测点是否存在且当天开放schedule是否存在且statusOPEN当前用户是否已有该时段有效订单防重复名额是否充足Redis 预扣事务内创建订单、更新booked_count、写状态日志。这里分享一个经验第 5 步的防重复不能只查booking表还要把用户 30 分钟内取消过的订单考虑进去。我们上线后发现有个别用户反复预约再取消导致后台统计的有效预约量和真实体验严重不符。后来加了一条同一天同一检测点用户取消超过 3 次后当天不能再预约直接在前端置灰并提示今日预约操作过于频繁。3.3 检测报告生成方案报告这块我纠结了好久。最理想的方式是后端生成真 PDF但医院最终签字盖章用的其实是他们的业务系统我们这个小程序只是用来给用户查看。所以我把方案折中后端不生成 PDF而是返回结构化数据小程序端用页面模板渲染报告详情需要保存时通过uni.pageScrollTo生成整页截图保存到相册。这种做法对用户最轻量但对后端来说要注意一个问题报告数据一旦生成就不能让用户通过修改请求参数来伪造。所以报告详情接口不能只传booking_id必须同时校验当前用户是不是该订单的归属人返回的result字段直接用后端数据库值不信任前端任何传值。3.4 定时任务超时取消与数据统计系统里跑着两个定时任务都用 APScheduler 实现没有上 Celery——因为这个量级真没必要引入消息队列。第一个任务每分钟扫一次超过 15 分钟未支付的订单把 Redis 名额回补同时把订单状态置为EXPIRED并给用户推一条订阅消息告知预约已取消如需请重新预约。第二个任务每天凌晨统计前一天各个检测点的预约量、到场率、出报告时长。这些统计数据是医院运营方最关心的月底要拿去排班和下月资源规划的。统计结果直接写到一张daily_stat表里避免每天实时跑大查询。提示APScheduler 在多 worker 部署时会重复执行任务需要加 Redis 锁保证同一时刻只有一个进程在跑。我一开始没注意结果超时取消任务被两个 worker 同时跑导致用户订单被重复回补排查了半天。4. 前端 uniapp 页面实现与微信小程序适配4.1 项目创建与工程配置前端我用 HBuilderX 创建项目模板选择uniapp Vue3 TypeScript。项目结构里最重要的两个文件是pages.json和manifest.json。pages.json管理页面路由和 tabBar。我把首页、预约、订单、我的四个页面配成底部 tabBar其他页面报告详情、登录、个人资料作为普通页面。小程序顶部导航栏我一开始用默认的后来发现不同机型胶囊按钮位置差异很大干脆全部改成自定义导航统一风格。manifest.json里要重点配置微信小程序模块的appid以及权限声明。尤其要注意不要为了省事把所有权限都勾上。我见过不少小程序因为申请了过多的隐私权限比如位置信息、相册被审核驳回。核酸检测预约这个场景其实只需要相册保存报告截图和网络定位根本用不到申请了反而添麻烦。4.2 核心页面与交互流程实现首页是信息展示型页面顶部轮播图放医院公告中间是检测点状态卡片底部一个醒目的立即预约按钮。这个页面的关键点在于数据加载体验——首次进入时onLoad里拉取首页聚合接口用一个骨架屏组件占位实测下来用户跳出率明显比转圈加载低。预约页是业务最重的页面我拆成三个子模块检测点选择列表展示地址、营业时间、当前余量标签充足/紧张/约满日期选择自定义横向滚动日历可选日期范围是未来 7 天已过日期自动置灰时段选择每个日期对应上午、下午、晚上三个时段显示剩余名额。用户点提交预约后前端先用uni.showLoading防重复点击同时调预约接口。这里有一个很重要的交互细节提交按钮要加请求中状态我在上线前就遇到用户连点两下后端虽然做了防重但前端体验很差两次提交会拿到两条几乎一样的报错提示。订单列表页我用了onReachBottom做触底分页加载每次加载 10 条。这个页面有个隐藏问题微信小程序的onReachBottom触发频率受页面高度影响如果订单列表很短可能永远无法触发到底部加载。所以我在数据不足一屏时主动调一次加载直到数据填满或没有更多。4.3 微信登录与手机号授权微信登录在 uniapp 里很简单uni.login({ provider: weixin, success: async (loginRes) { const res await request.post(/api/v1/auth/login, { code: loginRes.code }) // 存储 token uni.setStorageSync(token, res.data.token) } })手机号授权要注意微信官方现在不允许通过uni.getUserInfo直接拿手机号了唯一合规的方式是用button open-typegetPhoneNumber用户点击后拿到code再通过后端调phonenumber.getPhoneNumber接口换取真实手机号。这个流程里踩过一个坑getPhoneNumber事件拿到的code是一次性的如果后端处理失败比如微信接口超时前端不能再次使用同一个 code必须让用户重新点击按钮。后来我在按钮文案上加了一句点击即代表同意获取手机号并且在失败时提示请重试。4.4 顶部导航栏与安全区适配自定义导航是微信小程序适配里最琐碎的部分。不同机型状态栏高度不一样iPhone 刘海屏约 44px普通安卓约 24px胶囊按钮的位置也各不相同。我是这样算的// 获取状态栏高度 const systemInfo uni.getSystemInfoSync() const statusBarHeight systemInfo.statusBarHeight // 获取胶囊按钮位置信息小程序端生效 const menuButton uni.getMenuButtonBoundingClientRect() // 导航栏高度 胶囊按钮高度 (胶囊顶部 - 状态栏高度) * 2 const navBarHeight menuButton.height (menuButton.top - statusBarHeight) * 2算出后动态设置导航栏的padding-top。如果你直接用 uview-plus 的Navbar组件它内部已经做了大部分适配但前提是custom模式下要正确传入statusBarHeight。这个适配代码建议抽成一个公共 mixin四人页面统一调用。4.5 列表加载更多与下拉刷新的实现对比预约记录和报告列表都涉及加载更多。微信小程序提供了原生onReachBottom和enablePullDownRefresh但我在实际使用中发现uniapp 的onReachBottom在部分安卓机上存在 100~200ms 的延迟。如果列表是弹窗里的嵌套滚动它更是完全不触发。解决办法有两种简单方案列表外层不做嵌套滚动页面滚动即列表滚动直接用onReachBottom稳妥方案自己监听scroll-view的scrolltolower事件但scroll-view需要显式设置高度在 tabBar 页面里高度要减去 tabBar 高度比较繁琐。我最终选了前者预约记录页和报告页都做成整页滚动底部分页加载条显示加载中……或没有更多了下拉刷新用onPullDownRefresh配合uni.stopPullDownRefresh()收尾。5. 常见问题与排查技巧实录5.1 微信小程序包体积超 2MBsource size 2612kb 的排查第一次上传微信小程序时后台直接报错source size 2612kb exceed max limit 2mb。主包超了 600 多 kb。这是 uniapp 项目最常见的坑主要原因有三个uview-plus 全量引入、本地图片资源过大、以及没有启用分包。我的处理顺序是开启分包加载pages.json里配置subPackages把报告详情、个人资料、帮助中心这些低频页面拆进分包主包只留 tabBar 四个页面和公共组件把 uview-plus 改成按需引入只注册用到的组件而不是在main.js里app.use(uviewPlus)全量安装图片统一压缩banner 图从 300KB 压到 50KB 以内logo 用 SVG检查代码里是否有误打的大 JSON 数据或 console 日志被打进生产包。一顿操作下来主包从 2.6MB 降到 1.4MB分包约 600KB总算顺利过审。5.2 uni-app 不打印日志信息真机调试时的排查思路有段时间我真机调试完全看不到console.log输出HBuilderX 控制台干干净净。后来排查清楚了开发运行模式下日志正常但一旦用发行模式打包代码会经过压缩混淆所有 console 日志默认被裁剪。所以不是日志没打而是日志被构建流程删了。如果是真机调试但用的是生产环境接口建议在manifest.json里临时开一下debug模式或者在代码里封装一个logger工具// utils/logger.js const isDebug import.meta.env.MODE ! production export const log (...args) { if (isDebug) { console.log(...args) } }另外还有一个场景微信开发者工具里能看到日志但手机预览看不到。这多半是因为vConsole没有启用。我当时直接在main.js里按环境变量动态引入vconsole生产环境自动移除开发环境在手机上也能看 console 和网络请求排查联调问题方便很多。5.3 HBuilderX 导入 uview-plus 后样式不生效这个问题的典型表现是组件能渲染但样式完全不对像是裸 HTML。罪魁祸首通常是easycom配置没生效。uview-plus 要求pages.json里配置easycom规则把^u-(.*)映射到组件目录。我用 HBuilderX 插件市场导入时它应该自动配好但如果你手动复制代码粘贴到项目里很容易漏掉。另外要注意主题配置uview-plus 的样式变量需要在uni.scss里引入它自带的变量文件否则很多组件的默认颜色会丢失。我一开始直接改了uni.scss里几个颜色变量导致部分组件颜色串了排查半天发现是变量名覆盖顺序的问题。建议先完全使用默认主题跑通再按需调整。5.4 预览二维码与体验版怎么发给别人试用开发阶段要把小程序发给同事或医院的人试用有两种方式预览微信开发者工具点预览生成一个二维码有效期只有 20 分钟用微信扫码就能进入开发版。适合快速验证单个功能体验版先把代码上传到微信公众平台然后在成员管理里添加体验成员体验成员在小程序里搜索版本管理进体验版。这个版本相对稳定适合让医院那边做正式验收。提示开发版二维码只有开发者本人能扫出调试包其他人扫会提示不在开发者名单中。要把体验权限放给外部人员必须在后台添加体验成员同时体验成员的微信号必须绑定手机号才能正常体验。这个小细节当时卡了不少同事。5.5 用 Charles 抓取小程序接口排查联调问题前后端联调时经常出现前端说接口通了、后端说没收到请求的尴尬局面。我直接用 Charles 抓包小程序查看真实请求。主要看三个地方请求 URL 是否正确、请求头是否带了 token、返回的 JSON 结构是否和前端解析逻辑匹配。需要注意 Charles 抓 HTTPS 需要在小程序端信任 Charles 的 CA 证书。我当时的坑是模拟器里信任了证书真机却忘了装导致只能抓到加密乱码。另外如果开了微信开发者工具代理Charles 的端口和开发者工具代理端口冲突时两者只能留一个否则请求直接失败。5.6 上线审核与隐私合规提醒医疗类小程序审核比普通小程序严主要有几个注意点类目选择如果涉及线上预约一般选医疗 - 就医服务类目需要提供医院相关的资质文件隐私协议小程序后台要配置用户隐私保护指引明确声明收集用户手机号、身份证号等信息域名要求所有接口域名必须是已备案的 HTTPS 域名而且要在小程序后台配置 request 合法域名。我当时忘了加socket合法域名导致某次线上问题排查时试 websocket 失败其实业务根本用不到 websocket内容审核页面里不能出现绝对化的医疗承诺用语比如保证当天出结果只能说预计 XX 时间可查询报告。6. 一点个人经验总结整个项目从立项到上线前后大约六周。回头看这套 Python uniapp 微信小程序的组合做中轻量级的预约挂号类系统是完全够用的而且后续扩展 App 端很顺手因为核心逻辑都在 uniapp 这一层。有几个教训想特别强调第一接口文档一定先定清楚FastAPI 的 Swagger 给了很好的基础但字段含义比如period是字符串还是枚举要写进注释前端和后端各写各的很容易踩雷第二预约系统的库存设计一定要同时考虑数据库和缓存两层千万不要只靠数据库一把梭高峰期一打就崩第三小程序端所有涉及用户隐私的能力申请都要从业务必要性出发宁可先不申请也别在审核时被拒。最后再送一个小技巧预约类小程序上线后一定要盯着创建订单成功率和支付回调成功率两个指标。我当时加了一个简单的埋点每天看这两个数字一旦下降基本能定位是微信接口问题还是后端服务问题比用户主动投诉来得快得多。