☰
Django+Vue 3 前后端分离实战:实验室设备预约系统设计与实现
2026/10/26 14:35:49 网站建设 项目流程

1. 项目缘起与整体设计思路

实验室设备预约这件事,说大不大,说小也真不小。我所在的课题组有十几台套仪器,从高效液相色谱到GPU工作站,以前全靠一张Excel表在群里传来传去,谁先谁后全凭手速和人情。最离谱的一次,两位师兄同时出现在同一台设备前,各自拿着打印出来的预约表,场面一度非常尴尬。后来我花了大概三周时间,用Django加Vue 3搭了一套智能预约系统,跑了大半年,基本没再出过冲突。这篇文章就把整个设计和实现过程拆开来讲,包括我踩过的坑和后来补上的优化。

这套系统核心解决三个问题:第一,设备时段的可视化与冲突检测;第二,用户信用与违规记录的管理;第三,基于历史数据的智能推荐时段。技术栈选的是Django作为后端框架、Vue 3作为前端框架,前后端完全分离,通过RESTful API通信。为什么这么选?Django自带ORM和Admin后台,对于实验室这种需要快速迭代、又不想在数据库操作上花太多精力的场景非常合适;Vue 3的组合式API写起来灵活,配合Element Plus组件库,表格和日历视图的开发效率很高。如果你也在做类似的管理系统,这套组合基本是当前性价比最高的方案之一。

适合谁来读?如果你有Python基础、了解Django的基本概念(比如Model、View、URLconf),同时想找一个完整的前后端分离项目练手,那这篇内容会非常对口。即使你之前只写过单体Django应用,没碰过Vue,跟着思路走也能把骨架搭起来。我会把关键配置和代码逻辑都写清楚,参数选择也会解释原因,尽量让你能直接抄作业。

2. 技术选型背后的取舍与考量

2.1 为什么是Django而不是Flask或FastAPI

很多人做小项目喜欢用Flask,轻量、灵活。但实验室预约系统有一个隐藏需求:后台管理。设备信息要维护、用户要审核、预约记录要导出,这些如果自己写页面,工作量直接翻倍。Django的Admin后台开箱即用,注册一下Model就能得到一个功能完善的管理界面,省下来的时间可以花在核心业务逻辑上。

FastAPI性能确实好,异步支持也漂亮,但它的生态在权限管理和后台方面不如Django成熟。Django的auth模块、权限系统、中间件机制都是现成的,对于预约系统这种需要区分普通用户、设备管理员、系统管理员三种角色的场景,Django的Group和Permission体系可以直接用,不需要自己造轮子。

还有一个实际因素:Django的ORM对新手友好。预约系统涉及的核心查询——比如“查某设备在某时间段内是否已被预约”——用ORM写起来很直观,filter(start_time__lt=end, end_time__gt=start)一行就能表达清楚。如果换成手写SQL,调试成本会高不少。

2.2 Vue 3组合式API带来的开发体验变化

Vue 2的Options API写多了会发现,一个组件的逻辑被拆散在data、methods、computed、watch里,改一个功能要跳好几个地方。Vue 3的组合式API允许把相关逻辑写在一起,比如预约日历组件,所有跟日期计算、时段渲染、冲突判断相关的代码可以放在一个setup()里,维护起来清晰很多。

另外Vue 3对TypeScript的支持更好,虽然我这个项目没有全面上TS,但在关键的数据结构定义上用了类型标注,配合Volar插件,写代码时提示很准确,减少了低级错误。Element Plus对Vue 3的支持也已经很稳定,日历、表格、表单这些组件直接拿来用,样式统一,省去了大量CSS调试时间。

2.3 前后端分离的利与弊

前后端分离最大的好处是职责清晰。后端只负责数据存取和业务规则校验,前端只负责展示和交互。这样当我想换一套UI风格时,后端完全不用动;当业务规则调整时,前端也不需要重新打包。

但分离也带来了额外成本。首先是跨域问题,开发阶段前端跑在5173端口,后端跑在8000端口,必须配置CORS。其次是认证方式,不能用Django传统的Session认证,得换成JWT(JSON Web Token)。JWT的好处是无状态,服务器不用存Session,适合前后端分离;坏处是Token一旦签发,在过期前无法主动作废,需要配合黑名单机制或者设置较短的过期时间。我采用的是Access Token加Refresh Token的方案,Access Token有效期2小时,Refresh Token有效期7天,基本平衡了安全性和用户体验。

3. 核心功能模块的详细拆解

3.1 数据模型设计:从设备到预约记录

整个系统的数据模型围绕四个核心实体展开:用户(User)、设备(Equipment)、预约记录(Reservation)、信用记录(CreditLog)。用户直接复用Django的AbstractUser,扩展了学号、所属课题组、信用分三个字段。设备表包含名称、型号、位置、状态(可用/维护中/停用)、单次最长预约时长等字段。预约记录表是核心,字段包括用户外键、设备外键、开始时间、结束时间、状态(待审核/已通过/已取消/已完成/已违约)、创建时间。

这里有一个设计决策值得展开:预约状态是用枚举还是用单独的状态表?我选择了枚举,因为状态数量有限且不会频繁变动,用Django的TextChoices定义清晰直观。如果状态需要支持自定义流程,那才需要考虑状态表。

信用记录表用来记录每次违约或违规操作,比如预约后未按时使用、超时未归还等。每条记录关联用户和预约,扣分分值根据违规类型不同而不同。信用分低于阈值时,系统自动限制该用户的预约权限。这个机制上线后,设备空置率明显下降。

3.2 冲突检测算法:时间区间重叠判断

预约系统最核心的逻辑就是冲突检测。两个时间段是否重叠,判断条件其实很简单:假设已有预约的区间是[A_start, A_end],新预约的区间是[B_start, B_end],那么重叠的条件是A_start < B_end and A_end > B_start。这个逻辑在数据库查询里直接翻译成filter(start_time__lt=new_end, end_time__gt=new_start)即可。

但实际场景比这复杂。首先,设备可能有维护时间,维护时间段内不允许预约。其次,有些设备允许连续预约但需要间隔15分钟用于清理或校准。第三,跨天预约需要特殊处理,比如晚上10点到第二天早上6点。我的做法是在设备表里增加一个buffer_minutes字段,冲突检测时把新预约的开始时间往前推buffer分钟、结束时间往后推buffer分钟,再去和已有记录比较。这样逻辑统一,不需要为每种设备写不同的判断代码。

还有一个边界情况:如果用户取消了一个预约,这个时间段应该立即释放。我的实现是取消操作直接把状态改为“已取消”,冲突检测时只查询状态为“待审核”和“已通过”的记录。这样已取消的记录不会影响新预约,同时保留了历史数据用于审计。

3.3 智能推荐时段的实现思路

所谓“智能”,其实没有那么玄乎。我用的是一种基于历史数据的简单推荐策略:统计某台设备在过去30天内各个时段的预约频率,找出相对空闲的时间段推荐给用户。具体做法是,把一天划分为48个半小时槽,统计每个槽的历史预约次数,然后对次数做归一化,生成一个“繁忙度”分数。用户选择设备后,系统展示未来7天的日历,每个时段用不同颜色表示繁忙程度,绿色表示空闲、黄色表示一般、红色表示繁忙。

这个推荐不需要机器学习模型,用Django的聚合查询就能实现。Reservation.objects.filter(equipment=eq, start_time__gte=thirty_days_ago).annotate(hour=ExtractHour('start_time')).values('hour').annotate(count=Count('id')),拿到每个小时的预约次数后,在前端用颜色映射展示。实测下来,用户对这个功能的评价不错,尤其是新来的同学,不用再问“什么时候人少”这种问题了。

4. 实操过程与关键环节实现

4.1 后端环境搭建与项目初始化

先确保Python版本在3.9以上,我用的3.11。创建虚拟环境这一步不能省,否则依赖冲突会让你怀疑人生。

python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate pip install django djangorestframework djangorestframework-simplejwt django-cors-headers

安装完成后创建Django项目和应用:

django-admin startproject lab_booking cd lab_booking python manage.py startapp booking

在settings.py里注册应用和中间件。这里有一个容易漏的点:corsheaders中间件必须放在CommonMiddleware之前,否则跨域请求会被拦截。具体配置如下:

INSTALLED_APPS = [ # ... 其他默认应用 'rest_framework', 'corsheaders', 'booking', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', # ... 其他中间件 ] CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", # Vue开发服务器地址 ] REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ), }

数据库我用的PostgreSQL,因为预约系统涉及时间区间查询,PostgreSQL的tstzrange类型和GiST索引对这类查询优化很好。如果只是练手,SQLite也够用,但生产环境建议上PostgreSQL。

4.2 预约接口的完整实现

预约创建接口需要处理几个关键逻辑:权限校验、时间格式校验、冲突检测、信用分校验。我用DRF的APIView来实现,代码结构清晰。

from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from django.utils.dateparse import parse_datetime from .models import Equipment, Reservation from .serializers import ReservationSerializer class CreateReservationView(APIView): def post(self, request): user = request.user if user.credit_score < 60: return Response( {"error": "信用分不足,暂时无法预约"}, status=status.HTTP_403_FORBIDDEN ) equipment_id = request.data.get('equipment_id') start_time = parse_datetime(request.data.get('start_time')) end_time = parse_datetime(request.data.get('end_time')) if not all([equipment_id, start_time, end_time]): return Response( {"error": "参数不完整"}, status=status.HTTP_400_BAD_REQUEST ) if start_time >= end_time: return Response( {"error": "结束时间必须晚于开始时间"}, status=status.HTTP_400_BAD_REQUEST ) equipment = Equipment.objects.get(id=equipment_id) buffer = timedelta(minutes=equipment.buffer_minutes) conflicts = Reservation.objects.filter( equipment=equipment, status__in=['pending', 'approved'], start_time__lt=end_time + buffer, end_time__gt=start_time - buffer ) if conflicts.exists(): return Response( {"error": "该时段已被预约,请选择其他时间"}, status=status.HTTP_409_CONFLICT ) reservation = Reservation.objects.create( user=user, equipment=equipment, start_time=start_time, end_time=end_time, status='pending' ) serializer = ReservationSerializer(reservation) return Response(serializer.data, status=status.HTTP_201_CREATED)

这段代码里,buffer的处理是重点。假设设备需要15分钟清理时间,那么新预约的开始时间往前推15分钟、结束时间往后推15分钟,再去和已有记录比较,就能保证两笔预约之间至少有15分钟间隔。这个逻辑比在数据库里存“实际可用结束时间”要灵活,因为buffer可以随时调整而不影响历史数据。

4.3 前端日历组件的关键实现

Vue 3这边,我用Element Plus的el-calendar组件做基础,自定义了日期单元格的内容。核心逻辑是:用户选择设备后,调用后端接口获取该设备未来7天的预约情况,然后在前端计算每个时段的繁忙度。

const fetchAvailability = async (equipmentId) => { const res = await axios.get(`/api/equipment/${equipmentId}/availability/`) const data = res.data // data格式: [{ date: '2024-01-15', hour: 9, count: 3 }, ...] const heatmap = {} data.forEach(item => { const key = `${item.date}-${item.hour}` heatmap[key] = item.count }) return heatmap } const getCellClass = (date, hour) => { const key = `${formatDate(date)}-${hour}` const count = heatmap.value[key] || 0 if (count === 0) return 'cell-free' if (count <= 2) return 'cell-normal' return 'cell-busy' }

样式上,cell-free用浅绿色背景,cell-normal用浅黄色,cell-busy用浅红色。用户一眼就能看出哪些时段空闲。点击某个时段后,弹出预约表单,自动填充开始时间和结束时间,用户只需确认即可。

这里有一个体验优化点:预约表单的结束时间默认比开始时间晚1小时,但不超过设备的最大预约时长。如果用户手动调整开始时间,结束时间会自动跟着变。这个联动逻辑用Vue的watch实现,代码量不大但体验提升明显。

4.4 JWT认证的完整配置

前后端分离项目里,认证是绕不开的坎。我用的djangorestframework-simplejwt,配置如下:

from datetime import timedelta SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(hours=2), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, 'AUTH_HEADER_TYPES': ('Bearer',), }

前端在axios拦截器里统一处理Token。请求时自动带上Authorization: Bearer <token>,响应如果返回401,则尝试用Refresh Token换新的Access Token,换成功后重发原请求。如果Refresh Token也过期了,就跳转到登录页。

axios.interceptors.response.use( response => response, async error => { const originalRequest = error.config if (error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true const refreshToken = localStorage.getItem('refresh_token') if (refreshToken) { try { const res = await axios.post('/api/token/refresh/', { refresh: refreshToken }) localStorage.setItem('access_token', res.data.access) originalRequest.headers['Authorization'] = `Bearer ${res.data.access}` return axios(originalRequest) } catch (e) { localStorage.removeItem('access_token') localStorage.removeItem('refresh_token') window.location.href = '/login' } } } return Promise.reject(error) } )

这个拦截器逻辑我调试了很久,关键点是_retry标记,防止无限循环重试。另外,Refresh Token的存储位置也有讲究,放在localStorage里方便但存在XSS风险,放在httpOnly Cookie里更安全但需要后端配合设置。我选择了localStorage,因为实验室内部系统风险可控,而且实现简单。

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

5.1 跨域请求被拦截的排查思路

跨域问题几乎是前后端分离项目的必修课。症状是浏览器控制台报Access-Control-Allow-Origin相关的错误,但后端日志里看不到请求记录。这说明请求被浏览器拦截了,根本没到后端。

排查步骤:第一,确认django-cors-headers已安装并注册在INSTALLED_APPS里;第二,确认CorsMiddleware放在中间件列表的最前面;第三,确认CORS_ALLOWED_ORIGINS里包含了前端的确切地址,注意端口号不能错;第四,如果前端用了代理,检查代理配置是否正确转发。

我遇到过一个坑:开发时前端地址是http://localhost:5173,但有时候浏览器会解析成http://127.0.0.1:5173,这两个被视为不同的Origin。解决办法是把两个地址都加到CORS_ALLOWED_ORIGINS里,或者统一使用一个。

5.2 时间区间查询的性能优化

预约记录表数据量上来之后,冲突检测查询会变慢。我一开始没加索引,当记录超过5000条时,每次预约请求要等1秒以上。后来在start_time和end_time上加了联合索引,查询时间降到50毫秒以内。

class Reservation(models.Model): # ... 其他字段 class Meta: indexes = [ models.Index(fields=['equipment', 'start_time', 'end_time']), models.Index(fields=['user', 'status']), ]

索引的顺序也有讲究。equipment放在最前面是因为冲突检测总是先按设备过滤,然后才是时间范围。如果反过来,索引效果会打折扣。另外,status字段的选择性不高(只有几个值),单独建索引意义不大,但和user组合起来用于查询“某用户的待审核预约”就很有效。

5.3 预约状态流转的边界情况处理

预约状态有五种:待审核、已通过、已取消、已完成、已违约。状态流转的规则必须严格定义,否则会出现数据不一致。比如,已取消的预约不能再改为已完成;已违约的预约不能重新激活。

我的做法是在Model层加一个clean()方法做校验,同时在View层也做一次检查。虽然有点冗余,但能防止通过Admin后台直接修改数据时绕过校验。另外,我写了一个定时任务,每天凌晨检查前一天已通过但未完成的预约,自动标记为违约并扣信用分。这个任务用Django的management command实现,配合系统的crontab执行。

# booking/management/commands/check_overdue.py from django.core.management.base import BaseCommand from django.utils import timezone from booking.models import Reservation, CreditLog class Command(BaseCommand): def handle(self, *args, **options): yesterday = timezone.now() - timedelta(days=1) overdue = Reservation.objects.filter( status='approved', end_time__lt=yesterday ) for res in overdue: res.status = 'violated' res.save() CreditLog.objects.create( user=res.user, reservation=res, points=-5, reason='预约后未按时使用' ) res.user.credit_score -= 5 res.user.save()

这个命令我设置成每天早上6点执行,运行了大半年,处理了大概几十条违约记录,逻辑稳定。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
前端请求返回401Token过期或未携带检查请求头是否有Authorization配置axios拦截器自动刷新Token
预约时提示冲突但实际空闲buffer时间设置过大检查设备的buffer_minutes值根据设备实际情况调整buffer
日历组件不显示数据接口返回格式与前端解析不匹配在浏览器Network面板查看响应统一前后端日期格式为ISO 8601
信用分扣减不生效未保存用户对象检查是否调用了user.save()确保修改后调用save()
跨域请求失败CORS配置遗漏查看浏览器控制台错误信息检查CORS_ALLOWED_ORIGINS配置
数据库查询慢缺少索引使用explain分析查询计划添加联合索引

6. 上线部署与后续扩展方向

6.1 部署方案的选择与配置

开发完成后,部署到实验室的一台闲置服务器上。操作系统是Ubuntu 22.04,用Nginx做反向代理,Gunicorn跑Django,前端打包后的静态文件也由Nginx托管。这种方案成熟稳定,配置也不复杂。

Nginx配置的关键部分:

server { listen 80; server_name lab-booking.local; location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /var/www/lab-booking/dist; try_files $uri $uri/ /index.html; } }

try_files那一行很重要,否则Vue Router的history模式刷新页面会404。Gunicorn用systemd管理,开机自启,进程挂掉自动重启。数据库PostgreSQL设置每天凌晨自动备份,备份文件保留最近30天。

6.2 后续可以扩展的功能点

系统跑稳之后,我陆续加了一些小功能。比如企业微信机器人通知,预约审核通过或即将开始时推送提醒;比如设备使用统计报表,按课题组维度统计使用时长,方便年底算账;比如扫码签到,用户到现场后扫设备上的二维码确认使用,减少“预约了不来”的情况。

再往远了想,可以接入实验室的门禁系统,预约成功后自动授权对应时间段的门禁权限。这个需要和门禁厂商的接口对接,目前还在调研阶段。另一个方向是用历史数据做更精细的推荐,比如根据用户的研究方向推荐相关设备,或者预测未来一周的繁忙时段提前提醒用户错峰预约。

6.3 一些个人体会

这套系统从构思到上线,前后大概花了三周,其中一半时间在调试认证和跨域。如果重来一次,我会在项目初期就把JWT认证和CORS配置搞定,而不是等到前端页面写了一半才发现请求发不出去。另外,数据库索引一定要提前规划,不要等到数据量上来才补,那时候改表结构会有停机成本。

还有一点,预约系统的核心是规则,不是技术。技术只是实现规则的工具。在写代码之前,最好把预约规则、违约判定、信用分计算这些逻辑用文字写清楚,和实验室管理员确认一遍。我当初就是没确认清楚“超时多久算违约”,导致代码改了两版。规则明确了,代码写起来就是翻译工作,效率高很多。

最后分享一个小技巧:在Django Admin里给预约记录加一个导出CSV的action,管理员可以一键导出某段时间的所有预约数据,方便做统计和审计。这个功能实现起来很简单,但管理员用得很开心,算是花小钱办大事。

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

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

立即咨询