☰
宠物医院挂号预约系统实战:Django/Flask+Uniapp小程序开发全解析
2026/10/10 18:58:39 网站建设 项目流程

做宠物医疗这块也有一阵子了,从最初的纯后端管理端,到后来陆续加了移动端、小程序端,踩过的坑和攒下来的经验确实不少。今天拿一个比较典型的项目来聊:一个基于Django/Flask双后端选型、前端用Uniapp打包成微信小程序、面向宠物医院的挂号预约系统。这套系统不是那种炫技型demo,而是能真正跑在业务里、能应对早高峰同时抢号的预约体系,包含科室和医生管理、宠物档案、号源排班、预约就诊、支付对接这些核心链路。

如果你正准备做类似的小程序项目,或者刚接手一个医疗/服务类预约系统,又不太确定Django和Flask到底怎么选、Uniapp和微信小程序开发怎么配合,这篇内容可以给你一个相对完整的参考。我会从整体设计思路、数据库与接口设计、小程序端实现、核心流程和并发问题、再到实际开发中容易踩的坑,一层层说清楚。所有方案不是我凭空想的,都是在这个项目里实际跑过、测过、修过之后沉淀下来的,照着做基本能复现。

1. 项目背景与需求拆解

1.1 这个系统到底要解决什么问题

宠物医院的号源管理,跟人去医院挂号有很多相似点,但也有很不一样的地方。人在医院挂号,通常按科室、医生、时间段来选;宠物医院里,宠物主人往往更关心“我平时常找的那个大夫在不在”“能不能约到晚上下班后的时间段”“我家猫疫苗该打了,是不是一定要挂原来的科室”。这些需求表面上看是小功能,实际上会直接影响预约数据结构的设计。

我在做这个项目前,专门去两三家宠物医院观察过。他们之前的方式基本是微信上留言、电话预约、到店排队三合一,一个前台姑娘手写登记,忙的时候接电话、回微信、登记号牌、叫号全是一个人干。节假日前后,号源排重、医生休息、过号重排这些情况经常乱套,有的宠物主人提前一小时来等着,结果医生临时有事,整个上午的预约全部往后推。这就是最典型的痛点:缺少一个可靠的、可以让用户自助完成挂号预约、让医院统一管理号源的系统。

所以这个项目的核心定位其实是三件事:

  1. 让宠物主人能随时随地查看号源、提前预约,减少到店排队。
  2. 让医院前台和医生能轻松管理排班、停诊、预约状态,不用手抄本。
  3. 所有的预约记录有据可查,支持状态流转(待支付、已确认、已完成、已取消),方便结算和统计。

1.2 技术选型:为什么是Django/Flask + Uniapp

这个项目标题里既有Django又有Flask,其实这很常见。有的读者会疑惑,选一个不就行了?为什么要两个一起提?真实原因是,在实际团队或项目演进里,两种后端框架都有各自的适用场景。我自己的做法是用Django作为主后端完成核心业务,用Flask做一个轻量的支付回调或消息推送服务。听起来有点绕,我先说明为什么这样设计。

Django适合做业务逻辑重、表单多、权限多的系统。宠物医院挂号预约系统天然就有几个角色:用户(宠物主人)、前台、医生、可能还有管理员。Django自带的Admin后台和ORM在这类系统上是真的省事。用户表、宠物档案、预约记录这些模型,直接用Django模型定义,迁移、建表、后台管理一次到位。尤其是医院院长想看当天预约量、医生排班情况,直接用Django Admin配几个列表页就能搞定,不需要额外开发一套管理后台。

Flask则更适合做轻量独立服务。在这个项目里,微信支付回调、预约结束后的短信通知、定时清扫过期号源这类任务,我会拆成一个小服务,用Flask实现。它轻、启动快、和主业务解耦,出现问题也不拖累主系统。标题写成“Django Flask+Uniapp”,准确说就是这种“Django主业务 + Flask辅助服务”的组合,而不是在一个项目里同时两个框架处理同样的请求。

那前端为什么选Uniapp?因为它可以一套代码同时编到微信小程序、H5,甚至打包成App。宠物医院不只面向C端用户,前台在PC上也需要一个轻量操作界面,而H5或者小程序的web-view都能满足。用Uniapp就不用单独维护一个微信公众号H5端和一个微信小程序端了。尤其是预约成功后用户会收到订单详情页链接,H5形态方便在各种场景里打开。这是Uniapp给我最直接的效益。

如果你想快速起步,建议直接上手Uniapp + Vue3语法,配Django restframework,不要纠结太多框架间的花活。等业务跑通了,再考虑把Flask服务拆出来。

2. 数据库与后端接口设计

2.1 数据表设计与业务关系

这个系统的核心数据模型其实就围绕“谁在什么时间、由哪个医生、看哪只宠物”这条线索来展开。

首先,用户体系。这里的用户不是宠物,而是宠物主人的账号。用微信小程序登录,需要一个关联微信身份的用户表。我们项目里用UserProfile表,字段大致如下:

class UserProfile(models.Model): openid = models.CharField(max_length=64, unique=True, db_index=True) nickname = models.CharField(max_length=64, blank=True) avatar = models.URLField(blank=True) phone = models.CharField(max_length=11, blank=True) created_at = models.DateTimeField(auto_now_add=True)

注意,openid一定要加唯一索引,并且在后端做逻辑上的去重。小程序端每次静默登录,都会用code换一次openid,如果你不做幂等处理,用户重复启动小程序就会生成多条用户记录。

然后是宠物档案。因为挂号的不是人而是宠物,每一只宠物都有自己的基本信息。

class Pet(models.Model): owner = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name='pets') name = models.CharField(max_length=32) species = models.CharField(max_length=16) # 猫、狗、兔子… breed = models.CharField(max_length=32, blank=True) gender = models.CharField(max_length=4) birth_date = models.DateField(null=True, blank=True) weight = models.FloatField(default=0.0)

这里有个小点:宠物档案的用户外键用CASCADE还是PROTECT?我建议用CASCADE没问题,但要注意业务上不允许随意删除用户,一般做逻辑隐藏,所以真实项目里我更多用related_name和软删除字段。

科室和医生模型就相对常规了。科室可以有儿科、内科、外科、皮肤科、眼科等。医生归属于科室,还有职称、简介、头像以及一个重要的“排班优先级”字段(用于自动排班分配)。

接下来是号源表。设计号源表是整个预约系统的重中之重。我们最初失误过一次,把号源直接和预约记录一对一绑定,导致医生临时加号、停诊时非常难改。后面改成了“排班表 + 号码池”结构。

排班表:

class Schedule(models.Model): doctor = models.ForeignKey(Doctor, on_delete=models.CASCADE, related_name='schedules') clinic_date = models.DateField() start_time = models.TimeField() end_time = models.TimeField() avg_duration = models.IntegerField(default=15, help_text='单位分钟') max_count = models.IntegerField(default=12) # 这个时间段最大号数 status = models.CharField(max_length=16, choices=...) clinic = models.ForeignKey(Clinic, on_delete=models.CASCADE)

号源表可以不单独建,而是通过Schedule + Slot来表示。常见做法是,当某天排班创建后,系统自动按avg_duration生成号源时段,例如上午9:00-10:00,每个号15分钟,生成4个号。每个号对应Appointment的一条待预约记录,状态为空闲,或者有预约时状态为已分配。这种方案的好处是停诊时可以直接把排班状态置为停诊,所有未预约的号位都失效了。

预约记录表:

class Appointment(models.Model): order_no = models.CharField(max_length=32, unique=True) user = models.ForeignKey(UserProfile, on_delete=models.CASCADE) pet = models.ForeignKey(Pet, on_delete=models.CASCADE) doctor = models.ForeignKey(Doctor, on_delete=models.CASCADE) clinic_date = models.DateField() slot_time = models.TimeField() schedule = models.ForeignKey(Schedule, on_delete=models.CASCADE) status = models.CharField(max_length=16, choices=( ('pending', '待支付'), ('confirmed', '已确认'), ('finished', '已完成'), ('cancelled', '已取消'))) remark = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True)

从业务角度,我不建议把宠物、医生、时间直接嵌在一张超大表里,而是尽量字段化,这样以后统计报表会容易很多。

2.2 后端API怎么划分

无论用Django还是Flask,API设计思路是一致的:面向小程序端,提供JSON接口。这里我以Django REST Framework为例,接口大致划分成这几组:

  • 用户认证:POST /api/auth/login(微信登录)、GET /api/user/profile
  • 宠物管理:GET /api/pets、POST /api/pets、PUT /api/pets/{id}
  • 找医院/科室:GET /api/clinics、GET /api/departments
  • 找医生/排班:GET /api/doctors?department_id=xx、GET /api/schedules?doctor_id=xx&date=xx
  • 预约:POST /api/appointments、GET /api/appointments、POST /api/appointments/{id}/cancel
  • 支付:POST /api/payments/order、微信支付回调

接口设计时要注意一个原则:返回给前端的数据不要直接扔数据库对象,而是用序列化器控制输出字段。比如医生列表,前端只需要id、name、department_name、avatar、good_at,不要返回一堆内部字段。这样既省流量,也更安全。

Flask辅助服务上,我会单独挂一个/callback/payment路由来处理微信支付回调。这个接口接收微信服务器的通知,验签之后更新订单状态。用Flask写起来非常快:

@app.route('/callback/payment', methods=['POST']) def payment_callback(): data = request.get_data() # 验签逻辑... # 解析支付结果,更新预约状态 return {'code': 'SUCCESS'}

然后把回调服务独立部署,如果它挂了,不影响主业务继续访问,最多是支付状态没有自动回调通知。这种拆分对我这个项目来说是够用的。

3. 小程序端实现关键点

3.1 从Uniapp到微信小程序的构建过程

Uniapp做微信小程序开发,最舒服的是它保留了Vue单文件组件的写法。你在src/pages目录下写页面,每个页面是.vue文件,根目录下pages.json负责路由和页面配置。编译的时候,HBuilderX或者CLI可以一键打包成微信小程序项目,然后在微信开发者工具中打开dist/dev/mp-weixin这个目录。

这里要特别提醒一个习惯:不要直接在微信开发者工具里写页面,而应该把dist目录当作编译产物,像对待node_modules一样不去手动改它。我见过有同事图方便,直接在微信开发者工具里改代码,结果下一次Uniapp编译就把改动冲掉了。

项目的目录结构可以参考这样:

src/ pages/ index/index.vue // 首页 hospital/hospital.vue // 医院列表/科室 doctor/doctor.vue // 医生列表 schedule/schedule.vue // 排班日历 appointment/appointment.vue // 确认预约页 myAppointment/myAppointment.vue // 我的预约 pet/pet.vue // 宠物档案列表 profile/profile.vue // 个人中心 api/ request.js // 封装uni.request appointment.js user.js store/ index.js

首页核心是展示医院信息、热门科室、快捷入口。很多客户会要求首页放一个“立即预约”按钮,但真正的预约流程得先选择医院、科室、医生、时间,而不是直接跳到医生列表,所以页面层级在设计时就要提前跟需求方对齐,避免后期改了又改。

3.2 登录、预约、我的预约这几个核心页面

登录这块,微信小程序生态里最基础的做法是用uni.login拿到code,发给后端,后端再通过微信的code2Session接口换openid。我的前端封装大致是这样:

// api/request.js const BASE_URL = 'https://api.example.com/api' function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + path, method, data, header: { 'Content-Type': 'application/json', 'Authorization': uni.getStorageSync('token') || '' }, success: (res) => { if (res.data.code === 0) { resolve(res.data.data) } else { uni.showToast({ title: res.data.message, icon: 'none' }) reject(res.data) } }, fail: reject }) }) }

需要注意,小程序里uni.request本身不带身份信息,所以后端必须认证。我这里用的是自己签发的token(登录后生成一个随机字符串存Redis并返回前端),比直接拿openid当token要安全,也方便以后扩展手机号登录。

预约流程页面的数据流是这样的:选择医院 -> 选择科室 -> 选择医生 -> 选择日期 -> 选择时间段 -> 选择宠物 -> 提交预约(此时可能要求支付) -> 跳转支付页面/完成。这是一条交互链路,每一步都需要向后端拉取真实的数据。比如选了医生以后,要根据医生id请求/api/schedules?doctor_id=xx&date=xx,日历组件上显示哪天有余号,哪天约满了。

我在排班日历这里用的是官方uni-calendar改造版本,在dateCell中额外传一个hasSlot字段,有余号的可选,没号或已停诊的置灰。这样用户能一眼看到哪天能约。

“我的预约”页面相对简单,就是一个列表页,展示预约状态,提供取消预约按钮。取消时要在弹窗里让用户确认,因为取消后号源会释放回号池,不能无限次数随意取消。我们后端限制:同一用户当天取消超过3次,次日才能继续预约。

4. 核心流程与实操记录

4.1 预约挂号的完整时序

当用户选好时间、宠物后点击提交预约,这背后不是一个简单的insert。因为涉及号源占用、订单生成、支付通知,一定要用后端事务来保证数据一致性。我看很多初级代码会把状态直接写成多个独立接口:先创建订单、再更新号源状态、然后再减库存,中间任何一步失败都会出问题。

我这里推荐的做法是:后端用一个POST /api/appointments接口统一处理。

第一步,校验医生、日期、时间段是否存在且属于该医生的排班,号源是否还有空余。 第二步,用select_for_update()锁定排班记录或者号源记录,防止并发重复预约。 第三步,在数据库中插入Appointment记录,状态为pending。如果开启在线支付,先不回填排队号,等支付成功后再把状态改成confirmed。 第四步,如果是线下支付(到店支付),则直接生成confirmed状态,预约成功。 第五步,释放锁,返回order_no和支付参数。

有人会问,为什么支付之前就锁号?因为在热门宠物医院里,用户如果提交表单却最终没完成支付,号可能会被浪费。锁号五分钟内未支付,由定时任务自动释放号源,这样既给用户留了支付时间,又避免黄牛锁号。

伪代码逻辑如下:

# Django view 伪代码 with transaction.atomic(): slot = Slot.objects.select_for_update().get( schedule_id=schedule_id, time=slot_time, status='available') if slot.status != 'available': raise APIError('该号源已被预约,请选择其他时段') appointment = Appointment.objects.create( order_no=generate_order_no(), user=user, pet=pet, doctor=doctor, clinic_date=date, slot_time=slot_time, schedule=schedule, status='pending' ) slot.status = 'locked' slot.appointment = appointment slot.save()

这里generate_order_no()我建议用“时间戳 + 随机数 + 用户ID尾号”的方式,保证唯一,又方便排查订单。

4.2 号源管理与并发控制

宠物医院高峰期最怕的就是并发抢号。小规模场景下,比如一天几百个预约,用数据库行锁完全够。但如果医院确实很大,医生多、号源多,那么可以考虑把号源放到Redis里,用decr操作原子扣减库存。

我这个项目实际用的是“数据库锁 + 预约前Redis预扣”的组合。流程是:

  1. 用户点选时间段后,先调POST /api/appointments/occupy,在Redis中对对应的时段key执行DECR,如果结果小于0则说明已抢完,直接返回失败,并把key重置为0。
  2. 用户确认下单,创建预约记录,数据库操作成功后再把Redis里该时段的已占用数加回来(因为真正扣减以数据库为准,Redis只是挡并发)。
  3. 用户支付失败或超时,定时任务清理pending订单,同时把Redis里的占用数也释放掉。

这套方案能让前端的“秒杀”体验很平滑。当然,如果你的项目预算和运维能力有限,那就老老实实用select_for_update(),加上事务,对5000以下日预约量的医院完全足够。我在这个项目里就是先用数据库锁,后来日活涨到三万多预约量,才单独加了Redis预扣层。

其实对多数宠物医院来说,最要紧的不是几十万并发,而是保证不会出现“同一个号被两个人约到”。哪怕医院规模不大,一旦发生重复预约,处理起来就是客户投诉和医生排班双重麻烦。这也是我把数据库锁优先级放在最高的原因。

5. 常见问题与排查技巧实录

5.1 跨域与Session问题

小程序端请求后端接口,不存在浏览器跨域问题,但在调试阶段,如果你用Uniapp跑在H5上,就会遇到跨域。Django处理跨域一般用django-cors-headers,在settings.py里配置:

MIDDLEWARE = [ ... 'corsheaders.middleware.CorsMiddleware', ... ] CORS_ALLOW_ALL_ORIGINS = True # 开发环境 CORS_ALLOW_CREDENTIALS = True

但是注意,如果生产环境是全站HTTPS、域名固定,就不要设为允许所有来源。建议用CORS_ALLOWED_ORIGINS写死几个前端域名。

最容易踩的坑是:登录成功后,后端往Session里写用户信息,前端H5请求时没带Cookie,导致每次请求都未授权。所以小程序的开发模式,我建议统一用token放在Authorization头里,尽量不要依赖Session。而且微信小程序对Cookie支持并不顺手,用token最方便。

5.2 支付回调与预约状态不一致

支付回调是另一个让人掉头发的地方。微信支付会有异步回调,而且可能重复回调,如果你在回调里直接根据order_no把预约状态置为confirmed,就要小心重复消费问题。

正确做法是幂等处理:收到回调先查订单当前状态,如果已经是confirmed,直接返回成功,不再重复更新。否则再更新预约记录,同时释放掉定的锁号备注。我项目里Flask回调服务还做了签名校验,并且把所有回调原始报文记录到独立日志文件里,方便出问题时回溯。

@app.route('/callback/payment', methods=['POST']) def payout_callback(): # 1. 获取原始报文与签名 # 2. 用商户密钥计算签名对比 # 3. 查询订单,如果已完成直接返回SUCCESS # 4. 修改预约状态,并且触发短信通知 # 5. 记录回调日志

有一个容易被忽略的细节:小程序端调起支付并不是真正的支付完成标志。用户可能微信支付成功,但回调网络延迟或者后端服务重启了,还没收到回调。因此小程序端也要在拿到支付成功回调后,主动调用一次POST /api/payments/query?order_no=xx去后端查询支付结果。这个接口后端可以实时调用微信支付查询接口,也可以直接查本地订单状态。这样能最大限度减少“用户付了钱,但订单还是待支付”的投诉。

5.3 其他经验:排班生成、前端时间格式化、数据统计

排班生成这里,我最开始是让管理员每天手动录入,后来发现完全不可行。医院前台本来就要处理大量现场工作,再去手动录入一个月几十个医生排班,非常容易漏。后面改成“批量复制上周排班 + 手动微调”的方案。简单说,管理员先选好科室和日期范围,再选“复制某个已存在排班”,系统自动生成默认号源,有需要再修改个别医生的停诊标记。这个功能帮医院省了大量重复劳动。

时间格式化也是个容易被忽视的问题。Django默认使用UTC时间,小程序端使用本地时间,如果你直接返回2025-04-15T09:00:00Z给前端,前端解析的时候会转成当地时区,如果你的服务器不是UTC,就会出现预约时间差了8小时。我在这些项目里固定在后端将所有时间序列化为“年-月-日 时:分”字符串返回,前端不做额外转换,把时间显示和存储逻辑控制在服务端,问题少很多。

还有一个统计需求:院长会经常问“这个月哪个医生的预约率最高”“哪个科逾期未到最多”。在Django里写一个简单的统计视图并不难:

from django.db.models import Count, Q appointment_stats = Appointment.objects.values('doctor__name').annotate( total=Count('id'), confirmed=Count('id', filter=Q(status='confirmed')) )

但要注意统计的数据量级,如果预约记录百万条以上,这种group by可能在高峰期比较吃力。不过我服务的几家医院,数据量都还在十万级,Django的ORM配合索引完全够用。

6. 一些实操上的最终心得

做到后面我发现,宠物医院挂号预约系统真正难的不是技术选型,而是把业务流转想清楚。比如“医生临时停诊”这个需求,听起来很简单,但系统里要有应急处理:已预约用户要能收到停诊通知,号源自动释放,如果用户已支付,要能自动走原路退款,而且在用户端还要有一个醒目的提示告诉用户“该医生今天停诊,请改约其他医生”。这些异常流程如果没有提前设计,上线后一定是一团乱。

我在实际项目中坚持的另一个经验是:任何状态变更都保留记录。预约记录表里我加了一个AppointmentLog,用户在哪个时间把预约状态从pending改成confirmed、从confirmed改成cancelled,全部记录在案。这样出了问题可以追责,也能辅助运营分析用户取消原因。虽然这个表很小,但它的价值远超开发成本。

最后再分享一个小技巧:不要把所有逻辑都塞在小程序前端。很多页面上的“有没有号”状态,应该以后端返回为准,前端只做展示。比如医生列表页,用户不需要知道每个医生当前余几个号,只需要知道“有号”或“约满”。后端可以把号量状态直接算好,前端只渲染。这样代码更清晰,也为以后增加更多前端终端(比如支付宝小程序)打下基础。

如果你准备做类似项目,我的建议是从小切口开始:先做通一条“用户登录 -> 选科室 -> 选医生 -> 选时间 -> 创建预约 -> 查看预约”链路,把它打磨顺,再扩展支付、宠物档案、消息通知、统计报表。一上来就追求大而全是这个项目最容易翻车的地方。框架选型上,Django和Flask并不冲突,你可以按我前面说的方式,让Django负责主业务,Flask负责轻量辅助服务。只要数据边界和接口契约清楚,这套架构够你撑过很长一段业务增长期。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询