某高校计算机学院的机房,以前基本靠一张纸质登记表加微信群接龙来安排使用,实验课、自由上机、社团培训经常撞时间,管理员一天要在好几份表格之间反复核对。后来我帮学院搭了一套“机房预约管理系统”,后端用Python Flask,前端用Vue,开发环境是PyCharm,面向学生和教师两类主要用户。标题里同时出现了Flask和Django,容易被误导,这里先把结论说清楚:这个项目最终选的是Flask,Django是早期框架对比阶段留下的备选记录,后文我会专门讲为什么放弃了Django。
如果你现在也在做课程设计、毕业设计,或者单纯想练一个前后端分离的Web系统,这篇文章能帮你少走很多弯路。我会从框架选型、数据模型设计、核心冲突检测、前后端联调,到部署排错,把整个项目的关键环节拆开讲,代码片段全部可直接抄走改改成你自己的业务。
1. 为什么选Flask而不是Django?这是90%的选手会纠结的第一个问题
1.1 两个框架的定位差异
Flask和Django经常被放在一起比较,但其实它们不是一个量级的工具。Django是“全家桶”,自带Admin后台、ORM、模板引擎、表单处理、认证体系,新建项目时就给你搭好了一整套框架,适合“一个框架搞定整个站点”的场景。Flask是“微框架”,核心只负责路由和WSGI,数据库、登录、表单、跨域这些能力都要自己挂第三方扩展,但好处是项目结构完全由你说了算。
对机房预约这种业务,两边都能做,但体验完全不一样。如果你用Django,最舒服的是自带Admin,登录后就能直接管理机房、用户、预约记录,几乎不需要自己写管理后台页面。但代价是Django的模板体系、中间件、App机制、settings里的各种配置项,对一个只想快速交付的学生项目来说,理解和讲清楚的成本非常高。答辩的时候老师问一句“你Django的中间件是怎么生效的”,很多人就卡住了。
1.2 机房预约系统需要的核心能力偏偏不是Django最擅长的
Django最强势的地方是“服务端渲染全家桶”:一个模板渲染HTML页面,Admin自动生成管理界面。但我们这个项目要的是前后端分离,Vue负责页面交互,Flask只提供JSON接口。这种情况下Django的Admin后台几乎用不上,模板引擎也用不上,反而多了一层你不需要学的东西。
Flask + Flask-SQLAlchemy + Flask-JWT-Extended + Flask-CORS,四个扩展就能覆盖数据库、登录鉴权、跨域请求,代码量少,逻辑集中,学生答辩时能从头到尾把每一行讲明白。真正需要一个系统的核心功能也就这些:
- 用户角色区分:学生、教师、管理员。
- 机房列表维护:新增、编辑、启用/停用。
- 按日期+时段查询空闲机房。
- 学生提交预约申请,教师/管理员审批。
- 预约记录查询和取消。
- 同一机房同一时段不能被重复预约。
这个业务范围,Flask完全够用,而且更轻。Django适合让你“少写代码”,但前提是你愿意接受它那一套固定玩法。如果项目要求必须用Django那另说,否则我建议优先Flask。
2. 开始写代码前最重要的设计:数据模型和预约冲突规则
2.1 先想清楚角色权限,再写表结构
很多同学拿到项目第一件事就是建表,这其实最容易翻车。机房预约系统的核心是“谁用什么资源、在什么时间用”,所以角色必须明确:
- 学生:注册/登录、查看空闲机房、提交预约申请、取消自己的预约、查看审批结果。
- 教师:登录、可以以班级或课程名义预约机房,也可以审批学生的预约申请。
- 管理员:维护机房信息、管理用户、查看全部预约记录、拥有审批和强制取消权限。
权限模型在代码里体现为JWT里的role字段,前端路由守卫再根据角色控制页面跳转。这样后端接口只需要校验“当前用户是否有权限做这个操作”,不用每处都写一遍完整权限逻辑。
2.2 核心表设计
整个系统我最终落地了4张核心表:
- user:id、username、password_hash、real_name、role(student/teacher/admin)、created_at。
- room:id、name、location、capacity、is_active。
- time_slot:id、label、start_time、end_time。例如“第1节”、“08:00-09:40”。
- reservation:id、user_id、room_id、time_slot_id、date、status、valid_flag、purpose、created_at。
其中status用字符串枚举更直观:pending(待审批)、approved(已通过)、rejected(已拒绝)、cancelled(已取消)、completed(已完成)。valid_flag这个字段是专门给并发冲突预留的,下面单独讲。
2.3 冲突检测:为什么一定要用固定时段模板
最初设计时,我也考虑过让用户自由选择开始时间和结束时间,比如“14:20到16:10”。但很快发现这会带来几个问题:日期时间计算复杂、跨时段判断麻烦、前端日期时间选择器体验差、后端冲突检测要用区间重叠判断。一个机房预约系统,本质上相当于课表管理,直接沿用学校“节次”概念是最稳的方案。
用固定时段后,判断冲突就变得非常简单:一个机房在某一天的某一个时段,最多只能有一条有效预约。比如:
- room_id = 3
- date = 2025-06-10
- time_slot_id = 2
如果数据库中已经存在一条pending或approved状态的记录,那么这个机房该时段就不可约。时间段模板维护好了,前端的“空闲查询”也变成一个简单的三条件查询,不用处理任何区间算法。
后端创建预约的接口核心代码大致长这样:
from datetime import datetime from flask import request, jsonify from models import db, Reservation from utils import login_required @app.post('/api/reservations') @login_required def create_reservation(): data = request.get_json() date_obj = datetime.strptime(data['date'], '%Y-%m-%d').date() room_id = data['room_id'] time_slot_id = data['time_slot_id'] conflict = Reservation.query.filter( Reservation.room_id == room_id, Reservation.date == date_obj, Reservation.time_slot_id == time_slot_id, Reservation.status.in_(['pending', 'approved']), Reservation.valid_flag == 1 ).first() if conflict: return jsonify({'message': '该机房这个时段已被预约'}), 409 reservation = Reservation( user_id=current_user.id, room_id=room_id, time_slot_id=time_slot_id, date=date_obj, status='pending', valid_flag=1, purpose=data.get('purpose', '') ) db.session.add(reservation) db.session.commit() return jsonify({'message': '预约成功,等待审批', 'id': reservation.id}), 201但只靠这段代码还不够,因为两个用户同时提交预约时,两个请求都可能在“查询冲突”这一步发现没有记录,然后同时插入成功,导致冲突数据落地。这个问题必须从数据库层兜底。
2.4 数据库唯一约束处理并发
我在reservation表上加了一个组合唯一约束,专门防并发冲突:
from sqlalchemy import UniqueConstraint class Reservation(db.Model): __tablename__ = 'reservation' __table_args__ = ( UniqueConstraint('room_id', 'date', 'time_slot_id', 'valid_flag', name='uq_room_time_valid'), ) # 其他字段...逻辑是这样的:valid_flag只有0和1,有效预约记录永远等于1,取消、拒绝、删除历史记录时把valid_flag置为0。数据库层面保证了“同一机房+同一天+同一时段”最多只有一条有效预约存在,即使两个请求同时进来,也只会有一条插入成功,另一条抛出IntegrityError。
这里需要说明一个常见误解:有人会在(room_id, date, time_slot_id)三个字段上直接建唯一约束,那样做会导致同一机房同一时段只能存在一条历史记录,取消预约后想重新预约就插不进去了。所以必须加valid_flag作为第四列,用记录状态区分“当前生效的预约”和“历史失效记录”。
3. Flask后端核心实现与Vue前端联调
3.1 项目结构怎么组织才不乱
Flask项目最怕的就是把所有路由写在一个app.py里,几百行之后自己都找不到接口。我用的结构是这样:
computer_room_system/ ├── app.py # Flask入口,初始化扩展、注册蓝图 ├── config.py # 配置项:数据库地址、秘钥、JWT过期时间 ├── models/ │ ├── __init__.py │ ├── user.py │ ├── room.py │ ├── time_slot.py │ └── reservation.py ├── api/ │ ├── __init__.py │ ├── auth.py # 登录、注册 │ ├── rooms.py # 机房管理 │ ├── reservations.py # 预约相关 │ └── admin.py # 管理员接口 ├── utils/ │ ├── __init__.py │ ├── decorators.py # 登录校验、角色校验装饰器 │ └── response.py # 统一返回格式 └── requirements.txt蓝图可以理解成把不同功能的URL前缀拆到不同文件里,app.py只负责注册。这样找接口、加功能都很有条理。
3.2 登录认证与接口清单
认证我用的是Flask-JWT-Extended,登录成功后返回一个token,前端每次请求放在Authorization头里。密码用werkzeug.security的generate_password_hash和check_password_hash,绝不存明文。
接口设计我列成了表,方便你对着抄:
| 接口 | 方法 | 说明 | 权限 |
|---|---|---|---|
| /api/auth/login | POST | 登录,返回token | 公开 |
| /api/auth/profile | GET | 获取当前用户信息 | 登录用户 |
| /api/rooms | GET | 机房列表 | 登录用户 |
| /api/rooms/available | POST | 查询某个日期的可用机房 | 登录用户 |
| /api/reservations | POST | 创建预约 | 学生/教师 |
| /api/reservations/my | GET | 我的预约记录 | 登录用户 |
| /api/reservations/ /approve | PUT | 审批通过 | 教师/管理员 |
| /api/reservations/ /reject | PUT | 审批驳回 | 教师/管理员 |
| /api/reservations/ /cancel | DELETE | 取消预约 | 本人/管理员 |
| /api/admin/users | GET | 用户管理 | 管理员 |
路由全部用蓝图,比如admin.py里定义一个带前缀的Blueprint:
from flask import Blueprint admin_bp = Blueprint('admin', __name__, url_prefix='/api/admin') @admin_bp.get('/users') def list_users(): # ...app.py里注册一下:
from api.auth import auth_bp from api.rooms import rooms_bp from api.reservations import reservations_bp from api.admin import admin_bp app.register_blueprint(auth_bp) app.register_blueprint(rooms_bp) app.register_blueprint(reservations_bp, url_prefix='/api') app.register_blueprint(admin_bp)3.3 查询空闲机房的接口实现
前端想知道“2025-06-10第2节课有哪些机房空着”,对应的接口逻辑是:先查出所有启用状态的机房,再排除掉当天该时段已经有有效预约的机房,剩下就是可用的。
from models import db, Room, Reservation from utils.decorators import login_required @app.post('/api/rooms/available') @login_required def get_available_rooms(): data = request.get_json() date_obj = datetime.strptime(data['date'], '%Y-%m-%d').date() time_slot_id = data['time_slot_id'] occupied_room_ids = [ r.room_id for r in db.session.query(Reservation.room_id).filter( Reservation.date == date_obj, Reservation.time_slot_id == time_slot_id, Reservation.status.in_(['pending', 'approved']), Reservation.valid_flag == 1 ).all() ] available_rooms = Room.query.filter( Room.is_active == True, ~Room.id.in_(occupied_room_ids) ).all() return jsonify({'rooms': [{'id': r.id, 'name': r.name, 'location': r.location, 'capacity': r.capacity} for r in available_rooms]})这套逻辑的查询量很小,一个机房列表加一个预约记录列表,不涉及复杂SQL,性能足够。
3.4 前端Vue侧的核心工作
前端我建议Vue3 + Vite + Element Plus + Axios,组件库明确,页面写起来快。如果没有用过Vue3,也可以直接用Vue2 + Element UI,代码思路一模一样。
前端最核心的几件事:
- 路由:/login、/rooms、/reservation、/my-reservations、/admin。
- 路由守卫:没登录跳转登录页;管理员页面校验角色。
- Axios请求拦截器:统一带Authorization头,收到401时自动跳回登录页。
- 页面数据流:用户登录拿到token和role,存入localStorage,刷新页面后从localStorage恢复登录状态。
预约提交流程的示例代码:
import request from '@/utils/request' const submitReservation = async (form) => { try { const res = await request.post('/reservations', { room_id: form.room_id, date: form.date, time_slot_id: form.time_slot_id, purpose: form.purpose }) ElMessage.success('预约成功,等待审批') return res.data } catch (error) { ElMessage.error(error.response?.data?.message || '预约失败') throw error } }这里最容易被忽略的是date字段格式。Element Plus的el-date-picker默认返回的是Date对象,直接提交到后端可能变成ISO字符串,后端的strptime '%Y-%m-%d'解析会报错。我是在表单提交前做的格式化:
const formatDate = (date) => { const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${year}-${month}-${day}` }类似这种细节,多写几回自然就有经验了。
4. PyCharm开发环境、联调部署与高频问题排查
4.1 PyCharm里怎么跑起来
开发阶段我直接在PyCharm里创建Flask项目,专业版有Flask模板,社区版也一样能跑,只是没有内置的Flask Server配置面板,手动创建运行配置也行。
推荐新建一个虚拟环境,在Terminal里执行:
python -m venv venv venv\Scripts\activate # Windows source venv/bin/activate # macOS / Linux pip install flask flask-sqlalchemy flask-jwt-extended flask-cors pymysql如果只想本地演示,数据库可以直接用SQLite,配置里把URI改成sqlite:///computer_room.db,连数据库服务都不用装。但如果你后面要接MySQL,连接串记得加上charset参数,否则中文容易乱码:
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:你的密码@localhost/computer_room?charset=utf8mb4'4.2 前后端联调时的跨域问题
前端Vite默认跑在5173端口,Flask跑在5000端口,两个端口不一样,浏览器的同源策略就会拦截请求。解决方式有两种,我建议开发时用Flask-CORS:
from flask_cors import CORS CORS(app, resources={r'/api/*': {'origins': '*'}})然后把前端axios的baseURL指向http://localhost:5000。另一种方式是配置Vite的proxy,把/api开头的请求转发到5000端口:
server: { proxy: { '/api': { target: 'http://localhost:5000', changeOrigin: true } } }用proxy的好处是浏览器只访问Vite的5173端口,代码里的请求地址可以写相对路径,部署后需要改的地方更少。
4.3 高频问题速查表
这套系统做完,我在调试过程中遇到过不少问题,挑出来几个最典型的:
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 前端提交日期后报“time data does not match format” | el-date-picker返回ISO字符串 | 提交前用formatDate格式化 |
| 登录后前端控制台显示Network Error | Flask没配CORS | 引入flask-cors并配置允许来源 |
| MySQL连接后中文显示乱码 | 连接串没指定charset | URI里加?charset=utf8mb4 |
| 两个用户同时预约成功,出现重复记录 | 只写了应用层判断,没有数据库唯一约束 | 给effective_flag加组合唯一索引 |
| 前端刷新页面后登录状态丢失 | token只存在内存变量里 | 用localStorage存储token,初始化时恢复 |
另外有一个前端容易踩的坑:JWT过期后,后端返回401,前端如果不统一处理,用户只觉得“点了没反应”。我在Axios响应拦截器里做了统一逻辑:
request.interceptors.response.use( response => response, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )4.4 并发预约冲突要讲清楚
这个项目如果拿去答辩,老师大概率会追问“你怎么保证同一时段不会被重复预约”。我的回答分两层:第一层是接口内部查询冲突,这是业务校验;第二层是数据库组合唯一约束,这是最终防线。两层配合,才能应对并发请求。
并发场景的实际处理是在创建预约的接口里捕获数据库异常:
try: db.session.add(reservation) db.session.commit() except IntegrityError: db.session.rollback() return jsonify({'message': '手慢了,这个机房该时段刚被别人预约'}), 409这样即使两个请求同时打进来,也只有一条能提交成功,另外一条会收到冲突提示。竞态问题的坑一定要在启动项目时就想清楚,否则上线后才发现数据重复,只能手动清库,很狼狈。
4.5 预约取消功能的一个细节
取消预约不能直接删记录,否则后面你没法统计历史数据,也没法追溯审核流程。正确做法是把状态改成cancelled,同时把valid_flag置为0。这样同一时段理论上就可以再被其他人预约了。
@app.delete('/api/reservations/<int:reservation_id>') @login_required def cancel_reservation(reservation_id): reservation = Reservation.query.get_or_404(reservation_id) if reservation.user_id != current_user.id and current_user.role != 'admin': return jsonify({'message': '无权操作'}), 403 reservation.status = 'cancelled' reservation.valid_flag = 0 db.session.commit() return jsonify({'message': '预约已取消'})valid_flag置0的操作很简单,但它解决了历史保留和复用时段的矛盾,这比直接删记录靠谱得多。
5. 做完这个系统之后,我想单独再说几句
整个项目从框架选型到最终跑通,最核心的工作量其实不在页面有多炫,而在数据状态的设计上。预约系统本质上是一个状态机,从pending到approved到completed,中间还要支持rejected和cancelled,任何一个状态转换考虑不周全,就会出现“明明取消成功了,时段还是被占用”或者“审批驳回后预约记录还在占用资源”这种怪问题。
我个人在实际操作中的体会是:先画一张状态流转图,把可能的路径全部列出来,再动手写代码,比把界面做完再去补逻辑省一半时间。
最后分享一个小经验:如果你想给这个系统加分,可以再做一层“公告”或者“机房设备报修”功能,数据上只需要加一张表,前端多两个页面,但对面试、答辩来说,展示出来的思维深度完全不一样。从一个单一预约功能扩展到完整的“机房使用闭环”,正是这类管理系统最有价值的地方。