带了三届毕业设计,医疗预约系统是被问得最多的一种选题。倒不是因为哪所学校指定这个题目,而是它太典型了:用户角色天然分成患者、医生、管理员三层,业务流程覆盖注册、挂号、排班、取消、就诊记录,数据模型也不复杂,但真要设计好号源、处理好并发预约,又确实能看出一个人对后端基本功的掌握程度。更关键的是,Python后端加Vue前端的组合,恰好是现在简历上最常出现的两行技术栈。前几天帮一个学弟从零搭了一套在线医疗预约系统,从PyCharm建虚拟环境开始,到后端分别用Django REST Framework和Flask写了两版,再到Vue页面联调、接口并发压测,整套走下来踩了不少坑。这篇文章把全过程完整复盘一遍,重点放在数据模型设计、预约冲突处理、角色权限、前端预约链路,以及PyCharm联调技巧上,适合正在做毕业设计、实训作业,或者想拿这个项目练手找工作的朋友。后端是选Django还是Flask,我会给出建议,两版的核心代码也都会贴出来。
1. 为什么"Python + Vue"组合会成为医疗预约选题的事实标准
1.1 这类系统的需求特征,决定了技术选型的方向
医疗预约系统不是电商秒杀系统,它不会遇到几十万QPS,但必须把业务逻辑做严谨。一个完整的需求清单大概是:患者能注册登录、按科室找医生、查看医生排班、提交预约、取消预约、查看历史记录;医生能维护自己的排班、查看预约名单;管理员要管理科室、医生信息,还要能补录诊断结果。所有操作加在一起,本质上就是一个"多角色CRUD + 一组有状态约束的业务流程"。
这样的需求特征给技术选型划了几条硬杠杠。第一,开发效率要高,单人完成整个项目(学生毕设场景尤其如此),框架必须能快速把CRUD铺开;第二,权限模型要清晰,因为涉及医疗数据的隐私边界,"医生只能看自己的号源"这类规则必须好落地;第三,要有能力处理一个关键并发场景——多个患者同时预约同一时段时,不能出现"超卖"。这三点分别对应框架全栈性、权限中间件、事务控制能力,Python生态里的Django和Flask都能满足,只是实现路径不同。
1.2 Django vs Flask:不是版本之争,是工期之争
很多人纠结Django和Flask选哪个,其实只要把它们当成两种不同哲学的选择题就清楚了。Django是"全家桶",ORM、Admin后台、Auth、Session、表单校验、迁移工具全都给你内置好了,再配上Django REST Framework出接口,速度极快。尤其那个Admin后台,几乎是白送的管理端,毕设里"管理员功能"这一大块直接就有了,这是我推荐Django的最重要理由。
Flask是"微框架",只带路由和模板,ORM要用SQLAlchemy、认证要接Flask-JWT-Extended、数据库迁移要配Alembic,每一个组件都要你自己拼。听起来麻烦,但换来的是完全透明:所有代码都是你写的,出问题好排查,面试时也能把每个环节讲清楚。对想锻炼自己、或者项目规模不大但想展示底层理解的人,Flask是很舒服的选择。
| 对比维度 | Django + DRF | Flask + SQLAlchemy |
|---|---|---|
| 后台管理 | Admin直接生成 | 需自己写管理页面 |
| 开发效率 | 高,内置多 | 中,需要手动组合 |
| 技术要求 | 理解框架约定 | 掌握各组件拼装 |
| 排错难度 | 封装多,报错层数多 | 透明,易定位 |
| 适合场景 | 毕设、快速交付 | 面试展示、深入学习 |
| 文档成熟度 | 官方文档很全 | 组件文档分散 |
1.3 我个人对这个项目的选型建议
如果时间紧、目标是论文答辩顺利通过,优先Django。别小看Admin后台这一个优势,它直接帮你覆盖了"管理员维护医生信息"这类页面,论文里还能多写一章"系统后台管理基于Django Admin的实现"。如果时间充裕、想在简历和面试里多聊几句,可以选Flask,因为你能把每个细节讲得头头是道,比如蓝图怎么拆、JWT怎么校验、SQLAlchemy会话怎么管理。
这次帮学弟的项目,主体用Django实现,同时把预约、排班这两个核心接口用Flask重写了一份做对照。这样论文里可以写"两套实现对比",面试时也可以说"我熟悉主流的两种Python Web框架",属于性价比最高的做法。
2. 数据模型先行:把医生、号源、预约和用户角色一次理清
2.1 六张核心表定下系统边界
数据模型是这类系统的地基。我见过很多半途而废的项目,都是因为后面发现表设计不对,改来改去把代码改乱了。这个系统我建议从六张表起步,不要一上来就堆字段。第一张是用户表,存账号、密码、真实姓名、手机号和角色,角色用patient、doctor、admin区分。第二张是患者扩展表,一个用户对应一条,存身份证号、出生日期、性别、既往病史。第三张是医生扩展表,关联用户和科室,存职称、擅长领域、个人简介。第四张是科室表,就三个字段:名称、位置、简介。第五张是排班表。第六张是预约单表。
Django项目里,用python manage.py startapp accounts和python manage.py startapp hospital创建两个应用,前者放用户表和两个扩展表,后者放科室、医生、排班、预约。注意配置AUTH_USER_MODEL时必须在第一次迁移前完成,否则后面改起来很痛:
# accounts/models.py from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): ROLE_CHOICES = ( ('patient', '患者'), ('doctor', '医生'), ('admin', '管理员'), ) role = models.CharField(max_length=10, choices=ROLE_CHOICES, default='patient') phone = models.CharField(max_length=20, blank=True) class Meta: db_table = 'user'2.2 号源表:把"库存"做成一眼能看懂的状态
排班表是整个预约系统的核心,说它是"号源库存"一点也不夸张。它记录了某个医生在某天某个时段放出了多少号,已经约出去多少号。字段设计上,work_date用来隔离具体日期,period用am和pm区分上午下午,start_time和end_time标记可预约时间段,total_slots是该时段的总号源数,booked_slots是已预约数,剩余的号源就是total_slots减booked_slots。
Django模型里我还加了一个unique约束,保证同一个医生同一天同一个半天的排班只有一条记录,这是防止重复排班的第一道防线:
# hospital/models.py class Schedule(models.Model): doctor = models.ForeignKey(DoctorProfile, on_delete=models.CASCADE, related_name='schedules') work_date = models.DateField(verbose_name='出诊日期') period = models.CharField(max_length=2, choices=(('am', '上午'), ('pm', '下午'))) start_time = models.TimeField() end_time = models.TimeField() total_slots = models.PositiveIntegerField(default=30) booked_slots = models.PositiveIntegerField(default=0) class Meta: db_table = 'schedule' unique_together = ('doctor', 'work_date', 'period') indexes = [models.Index(fields=['doctor', 'work_date'])]Flask那一版用SQLAlchemy写,模型逻辑基本一样,只要把db.Column字段类型对应上即可。
这里有个设计取舍要说明:为什么不用"每个时间段单独一条记录"的方式?比如08:00-08:30一条、08:30-09:00一条。那样做更直观,患者可以精确选到某一个30分钟时段,但排班数据量会大不少,而且预约冲突判断需要额外加时段重叠检查。对于常规门诊预约,半天一个号源池的做法更接近真实医院挂号流程,也方便后期写"剩余号源"的轮播展示和报表统计。如果论文里想多一个亮点,也可以把号源池拆成更细的时间片,但代价是前端时段选择器和后端约束校验都会复杂一档。
2.3 一个患者同一时段不能预约两次
预约单表是用户和排班的关联表,核心字段是患者、排班、就诊序号、状态、创建时间。有一个约束最容易忽略:同一个患者在同一时段只能有一条有效预约。Django里直接加unique约束,双保险:
class Appointment(models.Model): STATUS_CHOICES = ( ('pending', '待就诊'), ('completed', '已就诊'), ('cancelled', '已取消'), ) patient = models.ForeignKey(PatientProfile, on_delete=models.CASCADE, related_name='appointments') schedule = models.ForeignKey(Schedule, on_delete=models.CASCADE, related_name='appointments') appointment_no = models.PositiveIntegerField(default=1) status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='pending') created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'appointment' unique_together = ('patient', 'schedule')为什么unique_together能起作用?因为数据库唯一索引是在写入时强校验的,不管应用层代码怎么漏,只要两条记录的patient和schedule相同,数据库就会直接拒绝。这种"数据库层兜底+应用层业务判断"的写法,是生产级系统常用的双保险思路。
2.4 状态机:预约单从生成到完成只允许这几条路径
预约单的状态不能随便跳。一张单从创建开始只能走几条固定路径:pending(待就诊)可以变为completed(已就诊)或cancelled(已取消);completed和cancelled都是终态,不能再变。医生端如果允许"确认接诊",可以在中间加一个confirmed状态,但不要加太多,状态越多,前端拉数据和后端校验的代码越碎。
权限边界也和状态绑定:患者只能取消自己处于pending的预约,医生只能把排班范围内的预约标记为completed,管理员最好只做数据修正和统计,不做状态流转的日常操作。这个约束在后端代码里用角色+状态联合判断,前端只是隐藏按钮做体验辅助,不能作为安全手段。
3. 后端实现:Django/DRF 与 Flask/JWT 双版本并行
3.1 Django侧最省心的CRUD链路:Model-Serializer-ViewSet
Django REST Framework把大部分CRUD逻辑封装好了,我只写数据怎么过滤、怎么序列化,剩下的list、retrieve、create都由ViewSet生成。先在settings.py里把rest_framework和simplejwt接上,然后立刻建应用、写Model、写Serializer、写ViewSet、配路由,这是一条成熟的固定链路:
# hospital/serializers.py from rest_framework import serializers from .models import Schedule class ScheduleSerializer(serializers.ModelSerializer): doctor_name = serializers.CharField(source='doctor.user.real_name', read_only=True) department_name = serializers.CharField(source='doctor.department.name', read_only=True) remaining = serializers.SerializerMethodField() class Meta: model = Schedule fields = ('id', 'doctor', 'doctor_name', 'department_name', 'work_date', 'period', 'start_time', 'end_time', 'total_slots', 'booked_slots', 'remaining') def get_remaining(self, obj): return obj.total_slots - obj.booked_slots# hospital/views.py from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated from datetime import date from .models import Schedule from .serializers import ScheduleSerializer class ScheduleViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = ScheduleSerializer permission_classes = [IsAuthenticated] def get_queryset(self): queryset = Schedule.objects.select_related('doctor__user', 'doctor__department').all() department_id = self.request.query_params.get('department_id') doctor_id = self.request.query_params.get('doctor_id') if department_id: queryset = queryset.filter(doctor__department_id=department_id) if doctor_id: queryset = queryset.filter(doctor_id=doctor_id) return queryset.filter(work_date__gte=date.today())这里要注意select_related,作用是一次性把关联的医生和科室查出来,避免连续N次数据库查询。测试数据量小的时候感受不到差异,但到了答辩演示的时候,接口响应速度快慢表现会差很多。
3.2 Flask侧的蓝图与JWT实现:少一点封装,多一点掌控
Flask版我拆了四个蓝图:auth、department、schedule、appointment。应用初始化用一个工厂函数create_app,把扩展和蓝图统一注册进去,这样测试和部署都方便。认证直接上Flask-JWT-Extended,access_token默认一个小时过期,前端存在localStorage里,每次请求带上Authorization头。
# app/__init__.py from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_jwt_extended import JWTManager db = SQLAlchemy() jwt = JWTManager() def create_app(): app = Flask(__name__) app.config.from_object('config.Config') db.init_app(app) jwt.init_app(app) from .routes import auth, department, schedule, appointment app.register_blueprint(auth.bp) app.register_blueprint(department.bp) app.register_blueprint(schedule.bp) app.register_blueprint(appointment.bp) return app一个排班列表接口在Flask里是这样写的,很直白:
# app/routes/schedule.py from flask import Blueprint, jsonify, request from flask_jwt_extended import jwt_required from datetime import date from app.models import Schedule bp = Blueprint('schedule', __name__, url_prefix='/api/schedules') @bp.route('', methods=['GET']) @jwt_required() def list_schedules(): query = Schedule.query doctor_id = request.args.get('doctor_id') if doctor_id: query = query.filter(Schedule.doctor_id == doctor_id) schedules = query.filter(Schedule.work_date >= date.today()).all() return jsonify([{ 'id': s.id, 'doctor_id': s.doctor_id, 'work_date': s.work_date.isoformat(), 'period': s.period, 'start_time': s.start_time.strftime('%H:%M'), 'end_time': s.end_time.strftime('%H:%M'), 'total_slots': s.total_slots, 'booked_slots': s.booked_slots, } for s in schedules])Flask版本最大的优势就是调试直观。出问题的时候,从路由函数开始,每一步做了什么都在你眼前,不会有Django那一层"中间件帮你做了一堆你没写的事"的黑盒感。
3.3 预约扣减库存的并发安全写法
预约接口是整个系统里最需要动脑子的地方。如果写成"先查询还有没有号,有就插入预约单,再更新已预约数",在并发场景下必出问题。假设号源池剩余1个,两个患者同时发起请求,两个进程都读到booked_slots=29,都判断还有号,然后都插入预约单,最后booked_slots被更新成31,数据库里出现了第30、31条预约。这就是典型的超卖。
解决办法是在事务里给排班记录加行锁。Django用select_for_update:
from django.db import transaction from django.core.exceptions import ValidationError @transaction.atomic def create_appointment(user, schedule_id): schedule = Schedule.objects.select_for_update().get(pk=schedule_id) if schedule.booked_slots >= schedule.total_slots: raise ValidationError('该时段号源已约满') schedule.booked_slots += 1 schedule.save() return Appointment.objects.create( patient=user.patient_profile, schedule=schedule, appointment_no=schedule.booked_slots )select_for_update的意思是在数据库层面把这一行锁住,直到事务提交才释放。第二个请求进来后会等第一个请求提交,然后重新读,这时候booked_slots已经变成30了,就会走到"号源已约满"的分支。这段逻辑一定要放在@transaction.atomic里,否则锁不会按预期生效。
Flask加SQLAlchemy的写法更直接,用一条UPDATE语句做原子扣减,靠rowcount判断是否成功:
from sqlalchemy import update from app import db from app.models import Schedule def book_slot(schedule_id, patient_profile): result = db.session.execute( update(Schedule) .where(Schedule.id == schedule_id) .where(Schedule.booked_slots < Schedule.total_slots) .values(booked_slots=Schedule.booked_slots + 1) ) if result.rowcount == 0: return None, '号源已约满' schedule = db.session.get(Schedule, schedule_id) appointment_no = schedule.booked_slots db.session.commit() return appointment_no, None这种"条件更新"的写法比先查后写更稳,因为UPDATE语句本身在数据库引擎层面是原子的,where条件不满足就直接影响0行,不会出现并发插入。两种写法都是标准解法,选一种吃透就够了。
3.4 三类角色的权限边界怎么落到代码
权限是这类医疗系统的第二道生命线。Django里自定义一个角色校验的权限类:
from rest_framework.permissions import BasePermission class IsDoctor(BasePermission): def has_permission(self, request, view): return request.user.is_authenticated and request.user.role == 'doctor'然后在ViewSet里把permission_classes设成对应组合。Flask版可以用一个装饰器,在JWT里读出角色,然后判断:
from functools import wraps from flask_jwt_extended import get_jwt, jwt_required def role_required(*roles): def wrapper(fn): @wraps(fn) @jwt_required() def decorator(*args, **kwargs): if get_jwt().get('role') not in roles: return {'msg': '无权限访问'}, 403 return fn(*args, **kwargs) return decorator return wrapper患者、医生、管理员三种角色的接口边界,我的经验是宁可多分几个视图,也别写一个大接口做全部分支判断。比如"我的预约"写一个PatientAppointmentView,"医生的工作台"写一个DoctorScheduleView,代码清晰,权限也好配,后面改需求不至于牵一发而动全身。
4. Vue前端:围绕"选医生、选时间、看结果"构建核心页面
4.1 从路由到数据流的前端骨架
前端用Vue 3加Vite,UI库选Element Plus。如果环境里Node和npm还没装好,先装Node 18以上的LTS版本,然后npm create vue@latest就能得到一个带Vite的干净模板。路由结构我按"预约主流程"来设计:
/首页,科室列表/doctors/:departmentId科室下的医生列表/schedule/:doctorId医生排班与号源选择/book/:scheduleId确认预约信息/appointments我的预约记录/login和/register/doctor/workbench医生工作台
前端需要全局处理token。Axios实例里加请求拦截器,每次请求自动带上Authorization头;响应拦截器里遇到401就跳回登录页。这里有个容易踩的坑:Vue的localStorage存token是明文,XSS攻击下会被偷走,毕设层面够用,但论文里写了"安全性"就要注意这点,至少要用HttpOnly的Cookie方案或者把token有效期缩短加刷新机制。
4.2 时段选择器的组件级拆解
预约流程里最核心的组件是时段选择器。它接受医生排班列表,把今天和未来的日期分组展示,选一个时段后高亮。Element Plus自带的日历组件样式和交互比较固定,不如自己用flex布局排一个卡片列表来得直观。我一般把排班按日期分组,每组里上午、下午各一张卡片,卡片上显示时间段、号源剩余量,号源为0的置灰不可点。
<template> <div v-for="item in groupedSchedules" :key="item.date" class="schedule-day"> <h4>{{ item.date }}</h4> <div class="slot-grid"> <div v-for="s in item.schedules" :key="s.id" class="slot-card" :class="{ disabled: s.remaining <= 0, active: selectedId === s.id }" @click="handleSelect(s)"> <strong>{{ s.period === 'am' ? '上午' : '下午' }}</strong> <span>{{ s.start_time }} - {{ s.end_time }}</span> <span>剩余 {{ s.remaining }}</span> </div> </div> </div> </template> <script setup> import { ref } from 'vue' const props = defineProps({ schedules: { type: Array, required: true } }) const emit = defineEmits(['select']) const selectedId = ref(null) function handleSelect(s) { if (s.remaining <= 0) return selectedId.value = s.id emit('select', s) } </script>4.3 预约提交流程里的"反手逻辑"
前端提交预约时要注意处理三类后端返回结果:200表示成功;4xx表示业务错误,比如"号源已约满""你已预约过该时段";401表示token过期。很多初学者只在axios的then里写成功逻辑,错误全甩给catch,这样用户看到的是一堆看不懂的报错。我更建议在响应拦截器里统一把error.response.data里的message字段提出来,在页面里用ElMessage显示,同时按钮进入loading态,防止用户重复点击。
预约成功之后不要直接跳首页,而是要跳到"我的预约"页面,让用户立刻看到刚生成的那条记录,这是一个很小的产品细节,但对答辩演示的整体观感提升很明显。我还会在确认预约页展示一个红字提示:"请勿重复预约,医生看诊前需提前15分钟到科室候诊",这类文案也算系统功能的一部分。
4.4 医生端排班管理的一个快速实现
医生端不用做得很重,一个工作台页面就够:顶部显示当前医生的排班列表,中间是号源卡,点某一天某个时段可以看到已预约的患者名单。排班的新增和修改我用一个对话框搞定,日期选择器选日期,时间段下拉,号源总数填数字,提交后调排班接口。
这里有一个Vue列表刷新的细节:排班接口返回的日期是后端序列化后的字符串"2026-03-10",直接和前端Date()比较会出问题,因为Date()会创建带时区的对象。建议前端全程用字符串比较,或者用dayjs统一解析。这个问题在联调阶段特别容易踩,前端显示出来的日期总是差一天,排查半天发现是时区转换的锅,后文我会专门讲。
5. PyCharm环境搭建与前后端联调的实操记录
5.1 虚拟环境和依赖:一切坑的根源
PyCharm里建项目时,第一个动作就是指定虚拟环境。如果本机连Python都没装,去官网下载3.10或者3.11的安装包,注意勾选Add Python to PATH,然后命令行里python --version能看到版本号就说明装好了。接下来千万不要图省事用全局的Python解释器,不然三个月后你更新了某个包,项目和论文全崩了都不知道原因。在PyCharm设置里,Settings -> Project -> Python Interpreter,Add Interpreter -> Virtualenv Environment,新建一个venv目录就行。
后端依赖务必写进requirements.txt:
django==4.2.11 djangorestframework==3.15.1 django-cors-headers==4.3.1 djangorestframework-simplejwt==5.3.1 pymysql==1.1.0Flask版本对应是:
flask==3.0.3 flask-sqlalchemy==3.1.1 flask-jwt-extended==4.6.0 pymysql==1.1.0安装命令统一用pip install -r requirements.txt,不要一个一个敲,容易漏。
5.2 PyCharm运行配置:后端两个框架与前端分开跑
Django项目在PyCharm里的运行配置选"Django Server",Script填manage.py的绝对路径,Parameters填runserver 0.0.0.0:8000,Environment里把PYTHONUNBUFFERED设成1。Flask项目选"Python",Script填入口文件app.py,再在环境变量里加FLASK_APP=app.py,Flask自带的debug模式就能用了。
前端项目不用在PyCharm里建配置,直接用底部的Terminal面板敲npm run dev,Vite默认跑在5173端口。记得同时把PyCharm的Settings -> Tools -> Terminal改成"cmd"或系统的bash,不要用PowerShell在某些环境下的奇怪编码问题。
5.3 Vite代理和CORS:联调第一关
前后端联调最大的坎是跨域。开发阶段最简单的方式是让Vite做代理:前端请求的url用相对路径/api开头,Vite dev server把它转发到后端的8000端口。在vite.config.js里加:
export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true } } } })这样前端代码里请求写/api/schedules/就相当于请求http://127.0.0.1:8000/api/schedules/,浏览器看到的始终是同源的5173,不会触发CORS。等部署到线上,再把Vue打包成静态文件,用Nginx同时代理前端和后端/api路径,也就能规避跨域问题。如果非要在开发环境直接访问后端接口,那就得装django-cors-headers或者Flask-CORS,在配置里把allow_origins设成前端地址。
5.4 排查接口问题的三层定位法
联调阶段接口报错是日常,我的排查习惯分三层。第一层先打开浏览器开发者工具的Network面板,看请求有没有发出、HTTP状态码是多少、响应正文是什么。这一步能过滤掉80%的问题,比如404多半是路径写错了,500多半是后端代码报错,跨域报错则是浏览器控制台的红色CORS提示。第二层看后端控制台的日志,Django会把每个请求的行号和方法都打出来,Flask同样会打印Python Traceback,从堆栈里能直接定位到哪一行代码炸了。第三层才是打断点调试。PyCharm的断点、变量预览窗口很好用,在Django视图函数里打一个断点,前端点到预约按钮,请求就会停在后端代码里,可以一步步看数据是怎么被处理的。
我第一次带新手朋友做这个项目的时候,他就是卡在"前端点击没反应",结果查到最后是后端settings里忘记加app了,Django起动时就报错,根本没跑起来。这类"低级错误"在控制台日志里一眼就能看到,所以不要一上来就怀疑跨域,先老老实实看请求是否到达了后端。
6. 事务竞争、时区问题与部署前必做的检查项
6.1 并发预约"超卖"问题的底线解法
前文提过并发预约的行锁和条件更新两种写法,这里再补充一个最底层的兜底方案:数据库唯一约束。如果业务上能接受"一个患者对同一排班最多一条预约",那就在appointment表上加UniqueConstraint(patient, schedule),这样就算应用层代码写得有漏洞,数据库也会拒绝重复插入。三防线齐下,系统才敢说自己的预约逻辑是稳的。
毕设答辩或项目汇报时,这一块是必考技术点。面试官常会追问:select_for_update锁的是表还是行?答案是行锁。锁的粒度是数据库索引级别的,所以排班表的id主键索引一定要在,而且查询条件必须落到索引上,否则会退化成表锁,并发性能骤降。另一个追问是:如果MySQL事务隔离级别是Read Committed和高版本默认的Repeatable Read,行锁行为有什么区别。这一部分不用太深入,但你要能说出"锁住的行在事务提交前不可被其他事务修改"这一层就已经过关了。
6.2 日期与时区:一个"差一天"的坑能毁掉整个排班
这个坑我在好几个项目里都踩过。前端选择的日期是"2026-03-10",通过JSON传到后端,Python里用datetime.strptime('2026-03-10', '%Y-%m-%d')解析,得到的是naive datetime,没有时区信息。Django如果开启了USE_TZ=True,数据库存的DateTimeField会自动加上UTC时区偏移,导致你在代码里比较today和work_date时,结果总是差8小时。这不是逻辑错了,是系统把无时区的字符串当成了UTC时间。
我的建议是:和挂号日期相关的字段一律用DateField,不要用DateTimeField。只有创建时间、更新时间这种"时间点"才用DateTimeField。前端传日期就用YYYY-MM-DD字符串,后端解析后转成date对象,存到DateField里,全程不碰时区,问题就消失了。Flask版本同理,DateTime默认naive,但你如果部署在docker容器里,宿主机的时区不同也会让时间对不上,所以排班相关字段用Date是最省心的。
6.3 部署上线前的检查清单
不管最后是部署到云服务器还是只在本地答辩演示,提交前过一遍这个清单,能避免当场翻车。第一,Django把DEBUG改成False,把SECRET_KEY改成环境变量,ALLOWED_HOSTS填上服务器IP或域名。第二,Vue打完包之后,dist目录就是静态文件,用Nginx托管,同时把/api路径反向代理到Gunicorn或uWSGI监听的端口。第三,MySQL的字符集用utf8mb4,别用utf8,否则患者填个生僻字就插入失败。第四,给数据库做定时备份,crontab凌晨跑一个mysqldump就够了。第五,所有密码字段必须存哈希,Django的make_password和Flask的generate_password_hash都现成可用。
这里多说一句,如果对Linux运维不熟,纯新手又想在服务器上快速部署,可以试试宝塔面板。它提供可视化的Nginx、MySQL、Python项目管理,把Django项目传上去、绑定域名、开一个Python项目环境就能跑起来,比自己手写systemd和Nginx配置省事很多。不过论文里如果不需要涉及部署,本地跑通、录一段演示视频也完全够用。
6.4 给扩展功能留的接口余量
整个系统做稳定之后,还有几个方向可以扩展。一是加Redis缓存,排班查询接口数据变化不频繁,可以把当天的排班缓存到Redis,压力测试时性能提升明显。二是加消息通知,预约成功或取消时通过邮件、短信或者微信公众号模板消息通知患者,这个在答辩演示里特别加分,因为能展示你考虑到了"用户体验"层面。三是加每周排班模板,让医生设置固定的出诊习惯,前端一次生成一周的排班,省去手动一天天添加。
这三个扩展方向里,短信通知需要买第三方服务,邮件可以用免费的smtp服务实现,实现成本低、效果直观。如果论文篇幅不够,加一个"邮件通知预约成功"就是很好的一章。我的经验是,扩展功能不求多,一两个位置做精,比堆一堆半成品功能强很多。把这个项目的核心逻辑吃透之后,再去套其他类似的"预约"场景,比如疫苗预约、车辆年检预约、实验室设备借用,基本就是换汤不换药,数据库表结构微调,预约冲突校验规则稍微改改,前端换个主题色,又是一套能打的项目。