ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flask+uniapp诊所预约系统实战:从数据库设计到小程序发布

Flask+uniapp诊所预约系统实战:从数据库设计到小程序发布 这套系统其实是我给一个社区诊所做的实际项目当时对方提出来的需求很直白患者能拿微信里的小程序挂号不用现场排队医生能看得到预约记录和患者历史档案后台得有人管科室、管排班。整个需求听起来不复杂但真正落地的时候牵扯到的细节远比想象中多——Flask后端要处理号源并发、档案数据要设计得能支持后续体检报告接入、uniapp前端在微信开发者工具里跑和真机跑的行为又不一样。这篇文章我就把整套系统的实现思路完整拆开讲一遍从数据库表设计到前端页面渲染再到上线时踩过的坑全部记录在案希望能给正在做同类项目的朋友省一些弯路。1. 项目整体设计与技术选型思路1.1 核心需求拆解问诊、挂号、档案卡三者如何串联这个系统表面上是三个独立功能实际上核心是“患者身份”这条主线。挂号必须关联患者问诊必须调出历史档案档案卡又反过来记录每一次问诊的结果所以最先要做的不是写接口而是把数据关系画清楚。我把需求拆成五个角色视角患者端微信小程序里完成注册、登录、添加就诊人、按科室找医生、选号源、提交预约、查看电子档案卡、发起在线问诊。医生端查看当天排班、确认接诊、问诊结束后填写病历小结并归档。管理员端维护科室、医生信息、设置号源数量、查看整体预约数据。系统端自动处理号源扣减、预约状态流转待支付、已确认、已完成、已取消、过期未就诊自动释放号源。档案模块每个患者有一份独立的档案卡记录基础信息、既往病史、历次问诊记录支持医生端快速调阅。这里有一个容易忽略的设计点档案卡不能只存就诊记录还得把用户填写的“既往病史”“过敏史”这类信息结构化存好否则后续做慢病管理或者数据统计的时候要从文本里捞关键词就太痛苦了。我当时的做法是把这些信息拆成独立字段存到患者档案表里问诊记录单独一张表两张表通过patient_id关联。1.2 技术选型对比Flask和Django怎么选uniapp和原生小程序差在哪后端为什么选Flask而不是Django这个选择在那个场景下很明确。项目核心逻辑是预约流程和档案管理并不是内容管理型系统Django自带的admin后台、ORM、模板系统虽然很全但对于这种接口服务型项目来说有一半功能用不上反而把项目骨架撑大了。Flask足够轻自己组织蓝图Blueprint就能把用户、医生、预约、档案几个模块拆清楚而且Flask配SQLAlchemy之后ORM能力和Django区别不大部署也更灵活。再聊前端。用uniapp而不是微信原生小程序最大的理由是“一次编写多处运行”。当时诊所其实已经有计划要做支付宝小程序和抖音小程序如果拿原生写要维护三套代码成本直接翻倍。uniapp用Vue语法编写编译到微信小程序端之后页面渲染依赖的那套webview逻辑会被替换成小程序原生组件性能和原生写出来的差距不大。不过选择uniapp也意味着遇到坑得自己摸。最典型的一个uniapp里用view标签写习惯了web的div小程序端编译没问题但有些CSS属性在真机上行为不一致比如position: sticky在小程序里需要额外的兼容处理。这些细节后面我单独列一节讲。1.3 部署架构与开发环境准备整个系统的部署架构很简单前后端分离两部分都跑在同一台云服务器上。后端Python 3.9 Flask 2.2 SQLAlchemy 2.0数据库先用SQLite做开发生产环境换MySQL 8.0。前端HBuilderX 3.8Vue3语法UI库用uview-plus。联调工具微信开发者工具 Charles抓包。部署后端用gunicorn跑多进程前面挂Nginx做反向代理静态资源和接口请求分离。开发环境这里有一个建议Python环境一定用虚拟环境不要直接用系统Python。我在项目初期图省事直接用系统Python装依赖结果装到某个库的时候把系统环境搞乱了后面排查了很久。用python3 -m venv venv创建虚拟环境所有依赖都锁在requirements.txt里后面换机器部署的时候直接pip install -r requirements.txt就完事省心太多。2. 后端Flask系统设计与核心实现2.1 数据模型设计八张表撑起整个预约系统数据模型是整个系统的根基一开始设计不好后面写业务逻辑全是泪。我落地下来一共设计了八张核心表表名关键字段说明usersid, openid, nickname, avatar, phone小程序用户表openid是唯一标识patientsid, user_id, name, id_card, gender, birth_date, allergy_history, past_history就诊人档案表一个用户可添加多个就诊人departmentsid, name, description, sort_order科室表doctorsid, department_id, name, title, avatar, intro, is_available医生表关联科室schedulesid, doctor_id, work_date, start_time, end_time, total_slots, booked_slots医生排班表记录每天号源总量与已约数量appointmentsid, schedule_id, patient_id, doctor_id, appoint_date, time_slot, status, create_time预约记录表状态区分待确认/已确认/已完成/已取消medical_recordsid, appointment_id, patient_id, doctor_id, diagnosis, prescription, notes, create_time问诊病历记录表archive_cardsid, patient_id, card_no, blood_type, height, weight, vision_left, vision_right, chronic_diseases, created_at档案卡扩展信息表这里值得展开讲的是appointments表。预约不只是“存一条记录”那么简单它必须和排班表联动涉及号源扣减和状态流转。我当时在appointments表里加了一个索引组合索引是(schedule_id, appoint_date, time_slot)目的是防止同一时间段的号被并发抢走。这个索引在后面讲并发处理的时候会再提到。2.2 接口设计与JWT用户认证Flask后端提供的是纯RESTful API统一返回格式{ code: 0, message: success, data: {} }code为0表示成功非0表示业务错误。这个约定很简单前端拿到code之后统一做拦截处理不需要每个接口单独写错误分支。用户认证走JWT。微信小程序端登录的时候前端调用wx.login()拿到临时code注意是临时的不是openid传给后端的/api/auth/login接口后端拿这个code去微信服务器换openid和session_key查库确定用户是否存在不存在则自动注册然后签发JWT返回给前端。前端把JWT存到uni.setStorageSync(token, token)里后续请求通过Authorization: Bearer token带上。这里有一个关键的安全细节永远不要在服务端日志里打印JWT和openid。我早期调试时为了方便直接打印了请求头结果日志文件里全是对外敏感信息后来花了半天的功夫清理日志。2.3 号源扣减的并发问题数据库锁而不是应用锁预约系统的核心难点在号源扣减。当大量用户同时抢某一个医生的号如果代码逻辑是先查剩余号数判断大于0再执行更新就会出现“超卖”——两个人同时查到剩余1个号同时更新成功结果约了2个人。正确的做法是使用数据库原子操作from sqlalchemy import update from sqlalchemy.exc import SQLAlchemyError def book_slot(schedule_id): # 原子更新只有当已约数小于总数时才执行扣减 stmt ( update(Schedule) .where( Schedule.id schedule_id, Schedule.booked_slots Schedule.total_slots ) .values(booked_slotsSchedule.booked_slots 1) .returning(Schedule.id) ) result db.session.execute(stmt) if result.first() is None: raise ApiException(code4001, message号源已被抢完) # 扣减成功后再创建预约记录 db.session.commit()这段代码的精髓在where条件里的booked_slots total_slots。数据库执行这条update语句时会对这一行加锁即使两个请求同时进来也会被数据库的行锁串行化第二个请求执行时booked_slots已经比total_slots大条件不成立更新影响行数为0于是抛出“号源已被抢完”。这比先查后更安全也比在应用层加锁更可靠。业务上还可以做一层缓存。热门医生的号源数据在Redis里放一份扣减的时候先用Redis的DECR做预扣减然后异步同步到MySQL这样能承受更高的QPS。但诊所项目的量级没那么大MySQL原子更新已经完全够用没必要引入Redis增加复杂度。2.4 蓝图中模块化组织Blueprint拆分系统模块Flask要用好Blueprint蓝图是必经之路。我的项目结构是这样组织的medical_backend/ ├── app.py # 应用入口 ├── config.py # 配置 ├── requirements.txt ├── models/ │ ├── __init__.py │ ├── user.py │ ├── doctor.py │ ├── schedule.py │ ├── appointment.py │ └── medical_record.py ├── blueprints/ │ ├── __init__.py │ ├── auth_bp.py # 登录认证 │ ├── patient_bp.py # 就诊人档案 │ ├── doctor_bp.py # 医生与排班 │ ├── appointment_bp.py # 预约挂号 │ ├── record_bp.py # 病历记录 │ └── admin_bp.py # 管理端 └── utils/ ├── jwt_util.py └── response_util.py每个蓝图处理一个业务域。比如appointment_bp.py里只聚集了预约相关的路由逻辑包括创建预约、取消预约、查看预约列表、确认接诊。模板渲染完全用不到因为后端只返JSON。如果一开始就乱写所有路由堆在app.py里到后期改一个需求要翻几百行代码定位都是个问题。3. 前端uniapp开发实战3.1 从HBuilderX创建项目到集成uview-plus前端开发我用的是HBuilderX创建项目时直接选“uni-app”模板Vue3版本。为什么用HBuilderX而不是直接用命令行因为HBuilderX对uni-app的编译支持最完整可以一键运行到微信开发者工具而且它自带的插件市场集成很方便。UI库选的是uview-plus这是uview在Vue3时代的后继版本。安装流程是先从插件市场导入uview-plus然后在main.js里注册import uviewPlus from /uni_modules/uview-plus import { createSSRApp } from vue export function createApp() { const app createSSRApp(App) app.use(uviewPlus) return { app } }启用easycom组件模式这样u-button、u-input这些组件不需要手动引入自动按需加载编译出来的包体积会小很多。这里有个经验easycom配置要放到pages.json里但要注意easycom的匹配规则如果自定义组件命名不遵循u-前缀就不会被自动匹配上。3.2 全局请求封装聊一聊拦截器的正确写法小程序端的请求封装直接影响开发效率。我的做法是封装一个request.js模块统一管理请求头、超时时间、错误提示和登录态失效处理// utils/request.js const BASE_URL https://your-domain.com/api function request(options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) || }, success: (res) { if (res.data.code 0) { resolve(res.data.data) } else if (res.data.code 401) { // token过期跳转登录页 uni.navigateTo({ url: /pages/login/login }) reject(res.data) } else { uni.showToast({ title: res.data.message, icon: none }) reject(res.data) } }, fail: (err) { // 超时或断网 uni.showToast({ title: 网络异常请稍后重试, icon: none }) reject(err) } }) }) } export function get(url, data) { return request({ url, data, method: GET }) } export function post(url, data) { return request({ url, data, method: POST }) }这个封装写顺手之后页面里调接口只用一行代码const list await get(/api/doctors, { departmentId: 1 })这里要注意一件小事uni.request的success回调里拿到的是HTTP层面的响应不代表业务成功。必须判断res.data.code 0才对。我见过很多新手直接在success里写页面逻辑结果后端返回业务错误时也在走成功分支页面上数据莫名其妙就对不上了。3.3 核心页面实现首页、医生列表、预约与档案卡首页布局是典型的医疗类小程序风格顶部搜索框中间金刚区快速入口挂号、问诊、档案、缴费下面推荐医生卡片。这里用u-swiper做Banner轮播用u-grid做金刚区导航整体代码量不大。医生列表页是重点。要注意页面数据是通过调/api/doctors?departmentIdxx接口拿的根据顶部科室tab切换重新请求。列表项展示医生头像、姓名、职称、简介和“预约”按钮。按科室筛选时要加一个防抖不然用户快速切换tab时会同时发出多个请求响应顺序错乱展示的列表和当前选中的科室对不上。我的做法是记录最后一次请求的id请求返回后判断id是不是最新的不是最新就丢弃。预约页有几个关键交互选择日期、选择时间段、确认就诊人。日期数据来源于医生排班接口返回availableSlots时间段来自排班表的time_slot字段。提交预约时前端要再确认一次就诊人身份防止用户添加多个就诊人之后选错了人。提交成功之后跳转预约成功页展示预约单号这个单号是后端生成的格式是APP 年月日 随机序号。档案卡页面是这个系统里比较有特色的模块。展示就诊人基础信息、既往病史、过敏史下面是一个时间线列出历次问诊记录。时间线用uni-collapse实现点击某次记录可以展开看到诊断结果、医嘱和处方详情。这里我在前端做了一个小优化档案卡的数据接口返回的字段比较多如果每次打开页面都全量拉取首屏会慢所以加了缓存策略首次进入拉全量数据存到uni.setStorageSync后续进入先读缓存渲染再异步刷新。3.4 uniapp特有的平台兼容性处理uniapp开发微信小程序最大的坑不在写代码而在“写的时候想不到平台差异”。归纳一下我踩到的几个高发问题导航栏高度小程序状态栏高度和胶囊按钮高度在不同手机上不一样H5上直接用position: fixed就行小程序里需要动态获取uni.getSystemInfoSync().statusBarHeight来计算顶部安全距离。滚动加载小程序里监听页面是否到底部需要通过onReachBottom页面生命周期函数触达在H5里用的window.addEventListener(scroll)完全失效。所以列表加载更多必须写在页面级没法用公共组件一行代码搞定。CSS单位小程序里rpx是核心单位H5里可以正常用px/rem。设计稿按750宽写rpx时1:1对照设计稿但在H5端rpx换算会有细微的精度差异调试时发现布局偏了先看看是不是单位混用导致的。图片路径小程序本地图片不支持引用/static/xxx.png以外的远程路径也不能直接用background-image: url(...)加载本地路径需要转成base64或者换image组件。每个问题单独看都不难处理难的是你不知道什么时候会碰上。经验就是页面写完第一版先拿真机跑一遍最核心的流程再回PC端调样式顺序不能反。4. 微信小程序适配、打包与发布全流程4.1 manifest.json配置需要注意的细节manifest.json是uniapp项目的全局配置文件微信小程序相关的配置都在mp-weixin节点下设置。关键几个配置项{ mp-weixin: { appid: 你的小程序AppID, setting: { urlCheck: false, es6: true, postcss: true, minified: true }, usingComponents: true, permission: { scope.userLocation: { desc: 用于定位附近医院 } }, requiredPrivateInfos: [getLocation] } }urlCheck在开发环境下要关掉否则真机调试时所有请求都会因为HTTPS证书校验不通过被拦掉。requiredPrivateInfos是微信2022年后新增的要求如果你用了定位、相册这些能力但没声明审核直接被拒。特别注意小程序审核对医疗类目要求很严格如果涉及在线问诊需要医疗相关资质这在项目规划阶段就得确认别等做完了提审才知道缺证。4.2 运行到微信开发者工具与真机调试的完整流程开发调试流程是HBuilderX里点“运行到小程序模拟器”选择微信开发者工具路径编译完成后会自动打开微信开发者工具加载小程序代码。真机调试我推荐直接用微信开发者工具里的“真机调试”功能扫码后手机会打开小程序同时开发者工具里能看到实时日志。这个调试方式比HBuilderX内置的“真机运行”更稳因为HBuilderX的真机运行是通过局域网传输代码的如果手机和电脑不在同一网络或者防火墙拦了端口就会加载失败。调试时遇到白屏或接口报错先在开发者工具的“调试器”面板看console日志。我遇到过一次所有接口都返回401的问题排查了很久才发现是因为开发环境里后端没有正确配置跨域小程序请求被服务器跨域策略拦截浏览器里明明能访问小程序里就报错。这里提醒一下小程序不是也不存在跨域它是非浏览器环境压根没有CORS限制真正的原因是服务器端的安全策略把请求来源校验拦住了。仔细看返回信息别被误导。4.3 提审前必须检查的合规问题提审是很多开发者的噩梦微信审核对小程序的要求相当严格医疗类更是重点监管对象。根据我在这个项目上踩过的坑列一个自检清单用户隐私协议进入小程序时必须有明确的隐私协议弹窗说明收集哪些信息、用途是什么。没有这个审核直接拒。小程序类目选择医疗相关功能要选择“医疗-私立医疗机构”或“就医服务”类目需要提交对应的资质证明。备案要求2023年9月后新注册小程序要备案备案大概需要几天到两周提前做。页面空数据态用户搜索无结果、列表为空、网络异常这些状态必须设计明确的提示空页白屏会被判定为完成度不够。虚拟支付问题小程序内不支持虚拟支付如果涉及在线问诊费用要么走线下支付要么接入微信支付的医疗类目。这个限制在小程序里属于新规严查的别心存侥幸。发布上线之后建议先用体验版发给团队内部测试一周收集问题反馈确认没有重大问题再提审。我这次项目就是体验版阶段发现了两个问题一个是iPhone老机型上滑动列表卡顿另一个是档案卡时间线在部分安卓机上显示错乱都赶在提审前修掉了。4.4 安卓应用市场的打包发布除了微信小程序我还用uniapp打了一个安卓APK包。过程是HBuilderX中点击“发行-原生App-云打包”选择Android包名和证书等待云端打包完成。这里要注意几点包名要反向域名格式例如com.clinic.medical后续在应用市场备案时包名不能改动。首次创建一个jks签名证书妥善保管后续更新版本必须用同一个证书签名否则无法覆盖安装。部分应用市场要求提供软著证书或自检报告下载“App违法违规收集使用个人信息自评估报告”模板逐项填写提交。云打包默认生成的是通用APK如果追求极致体积可以选“AppLTOarm64”或“AppLTOx86”的分包策略但要注意32位和64位架构的选择。上架小米、华为这些市场前在各自开发者平台上传APK、填写隐私说明和功能截图审核周期一般几天。5. 项目实操中的高频问题与排查思路5.1 常见问题速查表问题现象可能原因排查思路小程序请求接口全部404后端未启动或Nginx路由配置错误先curl接口地址确认后端真实返回情况登录后接口返回401JWT过期或token没传到Authorization头检查前端request封装是否带token检查服务端token有效期预约时出现超卖未使用数据库原子更新查看排班表booked_slots是否出现大于total_slots的情况医生列表图片加载失败图片链接是http不是https小程序强制要求https改用云存储或后端转换页面滚动到底部不加载更多onReachBottom未在页面级实现确认配置了页面json里的onReachBottomDistance档案卡时间线显示错乱使用CSS float或flex嵌套问题换用uni-collapse或重写样式真机调试白屏开发机与手机网络不在同一局域网改用微信开发者工具真机调试5.2 线上故障排查实录数据库连接与慢查询上线稳定运行一个月后突然有用户反馈预约页面打开很慢。排查过程是这样的先从监控看到/api/doctors接口响应时间从平均200ms涨到2秒接着在MySQL里开启慢查询日志发现是一条查询排班表的SQL在外面条件缺失时全表扫了。问题出在代码里一个“方便”的写法查询医生排班时直接Schedule.query.filter_by(doctor_iddoctor_id).all()没有限定work_date。当排班数据量上来后这条SQL就要扫全表几万行。修法很简单组合索引(doctor_id, work_date)加上之后查询时间降到10ms以内。这个案例说明两个道理一是接口写完不是结束数据量上来之后SQL性能会暴露二是慢查询日志必须开否则出问题完全盲人摸象。5.3 安全加固SQL注入与请求校验医疗系统涉及患者隐私安全这条线不能松。几个基础但重要的加固点所有SQL查询必须用参数绑定不能字符串拼接。SQLAlchemy的ORM本身已经做了转义但如果后面有人图方便用text()直接传字符串就有注入风险。关键接口做请求频率限制。登录接口用Flask-Limiter限制每个IP每分钟最多20次防止暴力破解。管理员接口额外校验角色。JWT里带上role字段管理员接口用装饰器校验避免普通用户直接调管理API。敏感字段返回时做脱敏。身份证号、手机号只在必要时返回完整字段列表接口用mask_phone这样的工具函数处理后再返回前端。6. 一些真正后端到终端的经验总结最后这部分我不想写套路化的总结就纯分享几点在这个项目里反复验证过的体会。第一点项目的成功七成在需求拆解三成在编码。把这个预约系统的业务逻辑画清楚胜过先埋头写两百个接口。我见过太多团队数据库的第三张表还没建好就开始写页面了最后改表结构改到想哭。数据模型定下来后面的功能性开发都是在填空。第二点调试工具一定要备齐。Charles抓包工具在小程序开发里的作用被低估了。写一个wx.request的拦截脚本微信开发者工具里打开代理在电脑上就能看到小程序发出的所有请求和响应体排查问题上手快一倍。抓包时要配置好SSL证书否则只能看到加密后的乱码。第三点给自己留一个“技术债清单”。医疗项目里像身份证扫描识别、微信支付、体检报告OCR这类功能因为资质或者时间原因第一版可能不做。但要在代码结构里预留好位置——档案表里预留了id_card_image字段问诊记录表里预留了附件字段——这样后期加功能的时候不需要大改现有表结构直接顺着脉络接上去就行。第四点uniapp项目记得定期清理unpackage目录和缓存。这个目录是编译产物时间长了会有大量历史版本残留轻则占硬盘重则造成“改代码没生效”的假象。遇到页面改了但表现和之前一模一样先试试删除unpackage目录再重新编译很多时候问题瞬间解决。这套系统从立项到上线前后花了大约三周。第一周搭后端和数据模型第二周做前端的核心页面第三周联调上线。如果把代码量和踩坑的数量做成对比图我会发现写代码的时间占比不到60%大量时间都花在排查环境问题、兼容性问题和数据异常上。但做完整套流程你对Flask、uniapp和微信小程序生态的理解一定比单纯看书要深得多。希望这篇记录能帮正在做同类系统的朋友少走几步弯路有问题也欢迎留言交流。
返回列表