☰
Django+Flask+Vue实验室预约系统全栈开发实战
2026/10/9 3:29:43 网站建设 项目流程

高校或者科研单位里要搞实验室预约系统,第一步往往不是买设备,而是找技术方案。市面上成品的预约系统不少,要么收费贵,要么不符合学校自己的排课逻辑。我陆陆续续帮几个课题组和实验室做过类似的内部工具,最终沉淀下来一套比较顺手的组合:后端用 Django 做管理后台和核心业务,Flask 单独撑起预约入口与轻量 API,前端 Vue 负责页面交互,开发环境统一放在 PyCharm 里。这套方案跑了两三个年头,稳定性和开发效率都实测过,今天把整个系统的设计思路、核心代码和踩过的坑整理出来,给正在做类似课设、毕设或者真实项目的人做个参考。

先说清楚这套系统解决什么问题。第一是实验室排期冲突:公共实验室经常出现几个人同时申请使用的情况,一屏日历和一张Excel台账根本管不过来。第二是设备租赁管理:仪器、主机、传感器这些设备借出之后去了哪里、什么时候归还、损耗程度如何,都需要留痕。第三是审批流程:管理员需要看到申请、通过或者拒绝,并能导出统计。拆解下来核心就是“排期”和“台账”两件事,再配上用户权限与审批流,就是一个完整可交付的项目。

1. 系统整体设计与技术方案为什么这么选

1.1 预约与租赁的真实业务场景

我接触过的实验室预约,场景比想象中复杂一点。以某高校公共机房为例,每天有多个班级的课程会占用整间实验室,学生课余想进去做实验只能预约空闲时段;另有一部分老师团队长期使用特定设备,比如深度学习服务器、示波器、3D 打印机,这些设备的借用周期以天甚至周为单位。两类需求混在一起的时候,如果只做一个简单的“某人某天预约某个时段”的表单,完全不够。

真实的流程应该是:预约人提交“实验室 + 时间段 + 用途”,系统先做冲突检测,能过则生成待审核记录;管理员在后台确认后,预约单生效,使用人凭预约码开门或登记。设备租赁则单独成一条业务线:设备目录、库存数量、可用状态、借用周转单、归还与损耗登记,每一步都要有操作日志。这是我做这套系统时最先梳理清楚的问题,模型设计必须围绕“预约单”和“租赁单”两类核心实体展开,而不是围绕“实验室”和“设备”展开。系统和人的交互点,始终是各种“单子”。

1.2 为什么是 Django 加 Flask 的组合

很多人一听到“一个项目里用两个后端框架”就觉得怪,其实在真实开发中这种“双后端”结构并不少见。Django 的强项是全套件:ORM、Admin 后台、认证体系、表单处理都很成熟,特别适合做管理侧和核心数据层。Flask 的强项是轻,启动快、路由灵活,适合做对外的边缘服务,比如 H5 预约入口、设备状态推送、简化接口转发。

我这样分:核心业务全部放 Django,负责用户体系、数据模型、后台管理、预约与设备的核心 API。Flask 单独起一个轻量服务,放在预约入口与提醒模块的位置,比如扫二维码快速预约、微信公众号里打开的设备查询页面、按小时推送预约提醒。这个 Flask 服务可以直接复用 Django 的数据库表,做得薄一点,别接复杂逻辑。好处是两个框架各用擅长的部分,代码不容易互相污染;坏处是部署时多一个进程,但用 gunicorn 或者普通 systemd 也能跑得很稳。

如果只选一个框架,我推荐 Django 单一后端,性能完全够用。但既然标题里写了 django-flask 组合,说明业务里确实有这种“主后台 + 轻服务”的诉求。我的经验是:Flask 只做三件事以内的事情,不要让它变成第二个业务系统。

1.3 Vue 和 PyCharm 在这套组合里的位置

前端选 Vue,是团队熟悉度和生态决定的。Vue 上手曲线平缓,组件化写法直观,Vite 冷启动又快,非常适合内部管理系统这种表单密集、列表密集的中后台场景。PyCharm 则承担开发环境的核心作用:Python 解释器管理、Django/Flask 的项目结构识别、数据库控制台、断点调试,这些功能组合起来,比命令行加记事本高效太多。后面我会单独讲 PyCharm 的配置细节,连同那些比较好用的插件一起说。

2. 后端核心设计与数据模型拆解

2.1 Django 项目骨架与 App 划分

项目初始化我习惯这样处理。先建虚拟环境,再安装必要包,然后创建 Django 项目:

python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install django djangorestframework django-cors-headers django-admin startproject lab_system cd lab_system python manage.py startapp core python manage.py startapp reservation python manage.py startapp equipment

App 划分我是按业务域切,不是按技术层切。core 放用户、校区、学院、公共基础信息;reservation 只放预约相关逻辑;equipment 只放设备相关逻辑。这样后面的模块迭代不会互相踩脚。settings 里记得注册新 App 和 DRF、corsheaders:

INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'corsheaders', 'core', 'reservation', 'equipment', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # ... 其他中间件 ] CORS_ALLOW_ALL_ORIGINS = False CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", "http://127.0.0.1:5173", ]

关于热词里提到的“django创建app”,上面就是标准动作。很多人会忘记把新 App 注册进 INSTALLED_APPS,导致后续 makemigrations 时提示没有变化,排了半天才发现是这里漏了,这也是一个很常见的入门坑。

2.2 预约与设备租赁的模型设计细节

模型是整个系统的地基,我花最多时间的就是这一部分。以预约模块为例,核心模型是实验室、时段、预约单:

from django.db import models from django.contrib.auth.models import User from django.core.exceptions import ValidationError class Lab(models.Model): name = models.CharField(max_length=100, verbose_name="实验室名称") location = models.CharField(max_length=200, verbose_name="位置") capacity = models.IntegerField(default=30, verbose_name="容纳人数") open_time = models.TimeField(default="08:00", verbose_name="开放时间") close_time = models.TimeField(default="22:00", verbose_name="关闭时间") is_active = models.BooleanField(default=True, verbose_name="是否启用") def __str__(self): return self.name class Reservation(models.Model): STATUS_CHOICES = ( ('pending', '待审核'), ('approved', '已通过'), ('rejected', '已拒绝'), ('cancelled', '已取消'), ('completed', '已使用'), ) user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="预约人") lab = models.ForeignKey(Lab, on_delete=models.CASCADE, verbose_name="实验室") date = models.DateField(verbose_name="预约日期") start_time = models.TimeField(verbose_name="开始时间") end_time = models.TimeField(verbose_name="结束时间") purpose = models.TextField(verbose_name="用途说明") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-date', '-start_time'] verbose_name = "实验室预约单" def clean(self): if self.start_time >= self.end_time: raise ValidationError("开始时间必须早于结束时间")

这里有个关键点:模型里的 clean 方法做的是基础校验,但并发冲突检测得放到数据库层去做。我举例说明,两个用户几乎同时提交同一间实验室同一时段的预约,如果只靠 Python 逻辑里的 filter + count 判断,必然有概率两个请求都通过校验,然后写进两条冲突数据。解决办法有几种:加唯一约束、用事务加锁、或者在插入前用数据库行锁。最稳的做法是给 Reservation 增加一个条件唯一索引,不过跨时段的唯一约束在 Django 里写起来稍麻烦,一般用 select_for_update 配合事务处理核心冲突,或者直接在写入后做一次反向校验。

设备租赁模型比预约多一个关键字段:数量。

class Equipment(models.Model): name = models.CharField(max_length=100, verbose_name="设备名称") code = models.CharField(max_length=50, unique=True, verbose_name="设备编号") category = models.CharField(max_length=50, verbose_name="分类") total_count = models.IntegerField(default=1, verbose_name="总数量") available_count = models.IntegerField(default=1, verbose_name="可用数量") unit = models.CharField(max_length=20, default="台", verbose_name="单位") description = models.TextField(blank=True, verbose_name="设备说明") def __str__(self): return f"{self.name} ({self.code})" class EquipmentRental(models.Model): applicant = models.ForeignKey(User, on_delete=models.CASCADE) equipment = models.ForeignKey(Equipment, on_delete=models.CASCADE) quantity = models.IntegerField(verbose_name="借用数量") start_date = models.DateField(verbose_name="借出日期") end_date = models.DateField(verbose_name="计划归还日期") actual_return_date = models.DateField(null=True, blank=True, verbose_name="实际归还日期") status = models.CharField(max_length=20, choices=( ('borrowed', '借用中'), ('returned', '已归还'), ('overdue', '逾期未还'), ), default='borrowed')

数量控制是最容易出事故的地方。比如示波器一共 3 台,2 台被借走,available_count 应该是 1,但如果有 3 个并发请求同时申请,各自读到 1,然后都减去 1,可用数量就变成了 -2。我的做法是借用和归还都在事务里执行,并且用 F 表达式做原子更新:

from django.db import transaction from django.db.models import F with transaction.atomic(): equipment = Equipment.objects.select_for_update().get(pk=equipment_pk) if equipment.available_count < quantity: raise ValidationError("库存不足") Equipment.objects.filter(pk=equipment_pk).update( available_count=F('available_count') - quantity )

F 表达式能把更新下沉到数据库层完成,避免读改写三步之间的竞态条件,这是我在实际项目里踩过坑之后才学会的写法。

2.3 ORM 查询与对象删除的实战记录

热词里有一条“django执行查询-删除对象”,这个正好是新手高频操作。我一般建议直接在 PyCharm 的 Terminal 里跑python manage.py shell做调试,不用频繁写脚本。

python manage.py shell

比如我要查今天所有已审核通过的预约:

from reservation.models import Reservation from django.utils import timezone from datetime import date # 查询今天之后的预约 reservations = Reservation.objects.filter( date__gte=date.today(), status='approved' ) # 遍历输出 for r in reservations[:20]: print(r.user.username, r.lab.name, r.date, r.start_time, r.end_time)

删除对象分好几种场景。物理删除用 delete(),批量删除用 queryset 的 delete;但业务数据我一般不加物理删,而是用状态字段标记作废:

# 物理删除单个对象 reservation = Reservation.objects.get(pk=1) reservation.delete() # 批量删除 Reservation.objects.filter(status='cancelled').delete() # 软删除思路:把状态改为无效并保留记录 reservation.status = 'cancelled' reservation.save()

管理系统的数据是要承担责任追溯的,建议不要对预约单做物理删除。我见过一个团队做完了才发现用户点取消竟然把历史预约记录从数据库里干掉了,月底导出报表时怎么都对不上账。最后只能恢复备份,场面比较难看。

3. Flask 独立服务与接口实现

3.1 Flask 服务在这个项目里的真实角色

我设计 Flask 服务主要接三类需求:第一类是轻量 H5 页面,比如贴在实验室门口的二维码,扫码后直接打开一个简洁的预约页面,整个页面可能只有三个字段:实验室、时间段、姓名;第二类是设备状态提醒,定时任务按小时检查哪些设备即将到期,通过 Webhook 或者短信接口推送;第三类是与外部系统的数据对接,比如企业微信机器人通知、课程表系统同步等。这类周边服务逻辑不复杂、变化频繁,放在 Flask 里可以随时快速迭代,不用动 Django 主工程。

可能有人会问,这些功能 Django 不是也能做吗?确实能做,但会把主工程越撑越胖。预约核心代码、后台管理、前端 API、报表导出、消息推送、课程表同步全部揉在同一个项目里,改一处坏一处的概率大幅上升。拆一个 Flask 薄服务出来,等于给“非核心业务”划了一个隔离区,主工程的稳定性和周边功能的迭代速度都能兼顾。

3.2 Flask 与 Django 共库连接的配置方式

Flask 服务要复用 Django 的数据库,最简单的方式是直接用 SQLAlchemy 连接同一个 MySQL 或 PostgreSQL 数据库,不要通过 Django 再包一层。配置示例:

from flask import Flask, jsonify, request from flask_sqlalchemy import SQLAlchemy from datetime import datetime app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:password@localhost/lab_system?charset=utf8mb4' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) class Lab(db.Model): __tablename__ = 'core_lab' # 对应 Django 里 core_lab 表 id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100)) location = db.Column(db.String(200)) capacity = db.Column(db.Integer) @app.route('/api/labs') def list_labs(): labs = Lab.query.all() return jsonify([{ 'id': lab.id, 'name': lab.name, 'location': lab.location, 'capacity': lab.capacity } for lab in labs])

这里注意表名必须和 Django 里生成的一致。Django 默认表名规则是“app名_模型名”,比如 core 应用下的 Lab 模型对应 core_lab。跨框架共库的时候,统一规则非常重要,我用一个共享的数据库账号,只开放业务表权限,避免相互影响。

热词里有一条“flask 与 fastapi 比较”,在这类轻服务场景下我在实际项目里也对比过。FastAPI 性能更高,自动生成 OpenAPI 文档,适合重 API 服务;但 Flask 生态更老更稳、中间件和扩展多,部署到内网服务器上几乎没有学习成本。如果项目团队本来就会 Flask,我建议直接 Flask,少引入一种并行技术栈。

3.3 预约入口与设备状态查询接口实现

预约入口接口的核心是接收用户提交、做一次性冲突检查,然后写入预约记录。

from flask import request, jsonify from datetime import datetime def check_conflict(lab_id, date_str, start_str, end_str): conflict = db.session.execute( "SELECT id FROM reservation_reservation " "WHERE lab_id = :lab_id AND date = :date " "AND status IN ('pending', 'approved') " "AND start_time < :end AND end_time > :start " "LIMIT 1", {'lab_id': lab_id, 'date': date_str, 'start': start_str, 'end': end_str} ).fetchone() return conflict is not None @app.route('/h5/reserve', methods=['POST']) def h5_reserve(): data = request.get_json() lab_id = data.get('lab_id') date_str = data.get('date') start_str = data.get('start_time') end_str = data.get('end_time') username = data.get('username') # 查用户ID,最好直接使用 Django 里的 User 表 user = db.session.execute( "SELECT id FROM auth_user WHERE username = :username", {'username': username} ).fetchone() if not user: return jsonify({'code': 1, 'msg': '用户不存在'}), 404 if check_conflict(lab_id, date_str, start_str, end_str): return jsonify({'code': 1, 'msg': '该时段已被预约'}), 409 db.session.execute( "INSERT INTO reservation_reservation " "(user_id, lab_id, date, start_time, end_time, purpose, status, created_at) " "VALUES (:user_id, :lab_id, :date, :start, :end, :purpose, 'pending', :now)", {'user_id': user.id, 'lab_id': lab_id, 'date': date_str, 'start': start_str, 'end': end_str, 'purpose': data.get('purpose', 'H5快速预约'), 'now': datetime.now()} ) db.session.commit() return jsonify({'code': 0, 'msg': '提交成功,等待管理员审核'})

实测下来,这个方案最大的风险点是两个服务会同时往同一张表写数据,所以我强烈建议:对预约单的写入,要么全部收敛到 Django 主 API,Flask 只负责把数据传到 Django 接口;要么表里的写入操作全部走同一套 ORM 或 SQL 模板,不要一个地方用 Django ORM,另一个地方用 SQLAlchemy 却用了不同的字段处理逻辑,时间格式化、时区、状态枚举这些细节很容易出现不一致。

4. Vue 前端开发与 PyCharm 环境配置

4.1 Vue 项目的搭建和基础结构

前端我用 Vite 创建项目,基础的 Node 环境安装这里不铺开讲,假定你已经配置好了 Node.js 和 npm。

npm create vue@latest frontend cd frontend npm install npm install axios vue-router pinia npm run dev

模板选择时,我只勾选 Vue Router 和 Pinia,其他自定义选项先不引入。项目跑起来之后,在 PyCharm 里可以直接打开这个文件夹,PyCharm 新版对前端项目的支持已经相当完整,npm 脚本、Vue 单文件组件语法高亮、ESLint 都能识别。

前端目录我习惯分成几个模块:

  • views:页面级组件,比如实验室列表、预约申请、我的预约、设备租赁、后台管理
  • components:通用组件,包括表单弹窗、日历面板、状态标签
  • api:与后端接口对应的模块文件,比如 reservation.js、equipment.js
  • router:路由配置,包含登录守卫
  • stores:Pinia 状态,存用户信息、权限标识

组件化带来的好处是页面之间不互相纠缠。实验室列表页和后台管理页,分别负责读和写两类操作,只要能抽出统一的组件接口,改一处就能全盘生效。

4.2 路由、插槽与 axios 封装的实操写法

路由配置里最关键的是登录守卫。内部管理系统页面不能裸奔,得先判断用户是否登录,再看权限范围:

import { createRouter, createWebHistory } from 'vue-router' import { useUserStore } from '@/stores/user' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', redirect: '/dashboard' }, { path: '/login', name: 'login', component: () => import('@/views/Login.vue') }, { path: '/reservation', component: () => import('@/layouts/MainLayout.vue'), children: [ { path: '', name: 'reservation-list', component: () => import('@/views/reservation/ReservationList.vue') } ] } ] }) router.beforeEach((to, from, next) => { const userStore = useUserStore() if (to.name !== 'login' && !userStore.token) { next({ name: 'login' }) } else { next() } })

路由懒加载很重要,把页面按需拆开,首屏只加载必要代码,管理系统的“页面多、每页不复杂”的形态特别适合这种写法。

插槽主要解决组件扩展问题。比如历史预约记录表格,不同页面想对“操作列”做不同的处理,预约列表页想要“取消”按钮,设备租赁页想要“归还登记”按钮,我就在表格组件里留一个插槽:

<template> <table> <thead> <tr> <th>预约人</th> <th>实验室</th> <th>时间</th> <th>状态</th> <th>操作</th> </tr> </thead> <tbody> <tr v-for="item in items" :key="item.id"> <td>{{ item.user_name }}</td> <td>{{ item.lab_name }}</td> <td>{{ item.date }} {{ item.start_time }}-{{ item.end_time }}</td> <td><StatusTag :status="item.status" /></td> <td> <slot name="actions" :row="item"> <button disabled>无操作</button> </slot> </td> </tr> </tbody> </table> </template>

父组件这样用:

<ReservationTable :items="pendingList"> <template #actions="{ row }"> <button @click="approve(row.id)">通过</button> <button @click="reject(row.id)">拒绝</button> </template> </ReservationTable>

插槽灵活性的好处实际开发中体会特别深。一开始我把操作按钮写死在表格里,后来需求一改就得复制整个表格重写。改用插槽之后,表格组件只负责展示,操作逻辑全部由父组件控制,复用度一下子就上来了。

axios 封装我固定做三件事:配置 baseURL、统一塞 token、统一处理错误码。

import axios from 'axios' const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(error) } ) export default service

4.3 PyCharm 安装配置与插件推荐

热词里有大量关于 pycharm 安装、插件、激活的内容。激活部分我不展开,也不建议走破解渠道,直接使用社区版或者官方教育授权就能完成整个项目开发。PyCharm 社区版对 Python 开发完全够用,加上 Vite 前端开发也在新版里支持得很好。

我实际用下来,PyCharm 对这个项目最有帮助的几个功能:

  • 解释器管理。Project Interpreter 里直接选择虚拟环境 venv,所有依赖包版本都能可视化查看,装第三方库不用去终端敲 pip,点几下就行。
  • 运行配置。Django 项目一键运行管理命令,Flask 服务也建一个运行配置,Debug 模式可以设置断点,Django ORM 查询到哪一步取值不对一眼就能看清楚。
  • 数据库控制台。PyCharm Professional 内置 Database 工具,可以直接连 SQLite 或 MySQL 查表,调试预约冲突逻辑时不用在代码里打一堆 print。
  • 插件。我在用的有 Fitten Code,这个是免费的 AI 插件,补全 Django 模板代码和 Vue 组件都比较顺手;还有 Rainbow Brackets,层级括号一目了然;Chinese Language Pack 对新手友好,但看英文原版提示其实更能避免误解。热词里提到“pycharm好用的ai插件fitten”,确实好用,同类插件里它是少有的对 Python 和 Vue 支持都比较稳的。

有一点要提醒:PyCharm 里装包的按钮虽然方便,但项目依赖建议统一用 requirements.txt 管理。virtualenv 环境迁移到服务器时,直接 pip install -r requirements.txt 一把梭,不会出现本机跑得好好的、服务器缺包的情况。

5. 从开发到部署的完整复盘

5.1 功能实现的推进顺序

我开发这套系统的顺序不是从登录页开始的,而是按“数据 → 后台 → 接口 → 前端页面”推进。

第一阶段先建 Django 项目和模型,跑通迁移,模型字段和数据库表确认无误;第二阶段把 Admin 后台配好,用 Django admin 先手动录几条数据,验证管理操作流畅不流畅;第三阶段写 DRF 接口,把预约、设备、审批的 API 全部暴露出来;第四阶段做 Flask 轻服务,把 H5 入口和设备提醒接上;第五阶段才是 Vue 前端页面,先做列表页和表单页,再完善报表和图表。

这个顺序的优点是,每一步产出的东西都可以独立验证。模型建完就有表,后台配完就能录入真实数据,接口写完就能用 Postman 测,前端页面只是消费 API。如果一上来就埋头写 Vue 页面,往往会发现接口字段对不上,返工成本特别高。

5.2 SQLite 与 MySQL 的取舍实践

开发阶段我用的是 SQLite,零配置、单文件、复制即用。但生产环境我建议尽早切换到 MySQL。SQLite 处理中等并发读写时容易锁库,特别是预约操作集中在早晚高峰,高并发写入时可能会报 database is locked。

切换的核心工作是同步表结构和数据,Django 提供了比较顺滑的方式。先在开发环境执行 dumpdata 导出基础数据,再改 settings 切换到 MySQL,跑 migrate 重建表结构,最后 loaddata 导回数据:

python manage.py dumpdata --exclude auth.permission --exclude contenttypes -o data.json # 切换 settings 中的 DATABASES 配置 python manage.py migrate python manage.py loaddata data.json

如果是新项目,可以直接从第一天就用 MySQL,省得后面折腾。但小组开发或课设演示用 SQLite 完全没问题,关键在于不要在两种数据库之间反复横跳。

5.3 前后端联调与部署要点

联调阶段最麻烦的是跨域问题。开发模式下面我用 Vite 代理解决,本地前端跑 5173 端口,后端 API 跑在 Django 的 8000 端口,浏览器里是跨域请求。在 vite.config.js 里配:

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true } } } })

这样前端请求/api开头,浏览器以为在访问同源地址,实际由 Vite 把请求转发给后端。生产部署时我一般把 Vue build 之后的静态文件交给 Nginx 托管,同时把/api路径反向代理到 Django 或 Flask 服务上。

server { listen 80; server_name your-domain; location / { root /path/to/frontend/dist; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

部署完第一个要检查的就是静态文件路径和 try_files 配置。Vue Router 用 history 模式时,刷新子路由页面会先请求 Nginx 的对应路径,如果没配置 try_files 就会 404,这个坑我眼熟得很。

5.4 进度报告与权限控制

管理系统不能忽略权限。Django 自带的 User 模型只区分登录和非登录,预约角色和租赁管理员需要更细的权限。我的做法是在 core 应用里加一个 UserProfile 模型,存 role 字段,管理员、教师、学生三档。前端根据接口返回的 role 显示对应菜单和按钮,后端每个 API 加上 permission_classes 限制,不能只靠前端藏按钮。

from rest_framework.permissions import BasePermission class IsAdminOrReadOnly(BasePermission): def has_permission(self, request, view): if request.method in ['GET', 'HEAD', 'OPTIONS']: return True return request.user.is_authenticated and request.user.userprofile.role == 'admin'

这个设计确保前端页面再怎么改,也不影响后端安全性,权限的判断永远在后端。

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

6.1 高频问题速查表

我把这个项目里容易反复踩的问题整理成了一张表,给后来的人做个索引:

现象可能原因排查/解决思路
Django 迁移后数据库没有表新 App 未注册 INSTALLED_APPS检查 settings,执行 showmigrations 看是否生成了对应迁移文件
前端请求接口报 403CSRF 验证未关闭或 token 未带上Django 的 API 接口加上 csrf_exempt,或前端正确携带 CSRF token
预约重复提交却能成功并发冲突未处理改用 select_for_update 包事务,必要时加条件唯一索引
查询结果很慢关联表产生了 N+1 查询使用 select_related 和 prefetch_related 预加载关联对象
前端页面刷新就 404路由 history 模式和 Nginx 配置不匹配配置 try_files 回退到 index.html,或改用 hash 模式
Flask 和 Django 数据对不上表名或字段规则不一致统一表名规则,建议只读接口用 Django 侧验证
PyCharm 调试 Flask 不生效没有设置 app.run(debug=True)Flask 代码里需要 open debug 模式,PyCharm 运行配置选 Flask Server
设置了 CORS 仍报跨域请求头或预检请求未处理改用 Vite 代理,或者让后端处理 OPTIONS 预检请求
Vue 项目报 failed to load tsconfig@vue/tsconfig 依赖未安装安装 @vue/tsconfig 或修改 tsconfig 引用路径

N+1 问题值得单独说一句。Django ORM 默认懒加载,查询预约列表时如果循环里访问了每条的 lab 字段,就会额外触发一次 lab 表查询;100 条预约就额外打 100 条 SQL。解决办法很简单:

from reservation.models import Reservation reservations = Reservation.objects.filter(date__gte='2024-01-01').select_related('user', 'lab')

加上 select_related 之后,SQL 会通过 JOIN 把 user 和 lab 一并查出来,列表接口速度差距非常明显,尤其是后面数据量上到几百上千条时。

6.2 并发预约冲突的排查实录

有一次学生反馈说,同一时段同一个实验室明显只该有一条预约,但数据库里出现了两条。排查过程是先用 shell 查:

from reservation.models import Reservation dup = Reservation.objects.filter( date='2024-05-20', start_time='14:00', end_time='16:00', lab_id=3 ).values('user__username', 'status') print(list(dup))

发现两条记录 status 都是 approved,说明冲突校验在并发场景失效。原因是当时校验代码写的是:

count = Reservation.objects.filter(...).count() if count == 0: Reservation.objects.create(...)

count 和 create 之间有时间窗口,两个请求同时进来,各自 count 都读到 0,然后都写入。修复方案就是用 select_for_update 在事务里把目标记录锁住,或者引入冲突表的唯一约束。我最终采用的是“事务加锁 + 二次校验”的组合,既锁住了时间段,又在校验通过后重新检查和写入。

6.3 PyCharm 里安装依赖的三两事

热词里问到 pycharm 怎么安装 pandas、cv2、htmltestrunner。这些在 PyCharm 里操作确实方便,打开 Settings → Project → Python Interpreter,点加号搜索包名就能安装。但我更推荐直接用终端:

pip install pandas opencv-python htmltestrunner

至于为什么有些包在 PyCharm 点安装会报错,一半原因是没有切到正确的虚拟环境。记住一个原则:项目里所有 Python 操作都要在 venv 环境里进行,终端里执行前先激活虚拟环境,PyCharm 解释器下拉框选到 venv 路径,两者保持一致就不会出大问题。

6.4 实验数据回放需求的 m3u8 播放处理

这个点可能有点发散,但热词里反复出现 vue 播放 m3u8。实验室预约系统后期经常会加一个“实验录像回看”功能,导出的监控视频就是 m3u8 格式片段。Vue 里播放 m3u8 不需要复杂插件,hls.js 就够了:

npm install hls.js

组件里写:

<template> <video ref="videoRef" controls autoplay muted></video> </template> <script setup> import { ref, onMounted } from 'vue' import Hls from 'hls.js' const videoRef = ref(null) const src = ref('/media/lab1/20240520/14h00.m3u8') onMounted(() => { if (Hls.isSupported()) { const hls = new Hls() hls.loadSource(src.value) hls.attachMedia(videoRef.value) } else { videoRef.value.src = src.value } }) </script>

从“预约系统”延伸到“实验录像回看”其实是很自然的演进,实验室里摄像头录下的操作过程需要按预约单关联,学生提交预约时选“需要录制”,管理员后端一键生成回放链接,这就是这套系统后期的扩展方向。

结尾聊几句个人体会

做这个项目最大的体会是:写系统永远要把“数据一致性”放在第一位,功能做完之前先想清楚哪些字段不能冲突,哪些状态必须留痕。我刚接手类似需求时也喜欢先把页面做得花哨,结果后台数据和前端对不上,折腾半个月才把模型重构回来,代价很大。后来养成的习惯是:先列业务规则、画状态图、定表字段,再写一行代码。这套流程看着慢,实际总工期反而是最短的。

另一个体会是技术选型不需要盲目追新。Django、Flask、Vue 三件套算不上最新潮,但社区成熟、资料多、遇到问题能快速找到答案,这对内部管理系统这种“重业务、轻爆发”的项目来说就是最大的优势。我的做法是,把系统拆成 Django 主工程和 Flask 薄服务,前端 Vue 只做交互,Pycharm 负责把整个开发环境拢在一起,各归其位,反而比一个框架硬扛所有需求要舒服得多。

最后分享一个小技巧:开发中多利用 PyCharm 的数据库控制台和manage.py shell,写 ORM 查询之前先在 shell 里把数据和关系摸一遍,写出来的代码基本一遍过。这个习惯帮我省掉了大量调试时间,也推荐给你试试。

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

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

立即咨询