某高校计算机学院的机房以前基本靠一张纸质登记表加微信群接龙来安排使用实验课、自由上机、社团培训经常撞时间管理员一天要在好几份表格之间反复核对。后来我帮学院搭了一套“机房预约管理系统”后端用Python Flask前端用Vue开发环境是PyCharm面向学生和教师两类主要用户。标题里同时出现了Flask和Django容易被误导这里先把结论说清楚这个项目最终选的是FlaskDjango是早期框架对比阶段留下的备选记录后文我会专门讲为什么放弃了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张核心表userid、username、password_hash、real_name、role(student/teacher/admin)、created_at。roomid、name、location、capacity、is_active。time_slotid、label、start_time、end_time。例如“第1节”、“08:00-09:40”。reservationid、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 3date 2025-06-10time_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_idcurrent_user.id, room_idroom_id, time_slot_idtime_slot_id, datedate_obj, statuspending, valid_flag1, purposedata.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, nameuq_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/loginPOST登录返回token公开/api/auth/profileGET获取当前用户信息登录用户/api/roomsGET机房列表登录用户/api/rooms/availablePOST查询某个日期的可用机房登录用户/api/reservationsPOST创建预约学生/教师/api/reservations/myGET我的预约记录登录用户/api/reservations/ /approvePUT审批通过教师/管理员/api/reservations/ /rejectPUT审批驳回教师/管理员/api/reservations/ /cancelDELETE取消预约本人/管理员/api/admin/usersGET用户管理管理员路由全部用蓝图比如admin.py里定义一个带前缀的Blueprintfrom 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 mysqlpymysql://root:你的密码localhost/computer_room?charsetutf8mb44.2 前后端联调时的跨域问题前端Vite默认跑在5173端口Flask跑在5000端口两个端口不一样浏览器的同源策略就会拦截请求。解决方式有两种我建议开发时用Flask-CORSfrom 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 ErrorFlask没配CORS引入flask-cors并配置允许来源MySQL连接后中文显示乱码连接串没指定charsetURI里加?charsetutf8mb4两个用户同时预约成功出现重复记录只写了应用层判断没有数据库唯一约束给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任何一个状态转换考虑不周全就会出现“明明取消成功了时段还是被占用”或者“审批驳回后预约记录还在占用资源”这种怪问题。我个人在实际操作中的体会是先画一张状态流转图把可能的路径全部列出来再动手写代码比把界面做完再去补逻辑省一半时间。最后分享一个小经验如果你想给这个系统加分可以再做一层“公告”或者“机房设备报修”功能数据上只需要加一张表前端多两个页面但对面试、答辩来说展示出来的思维深度完全不一样。从一个单一预约功能扩展到完整的“机房使用闭环”正是这类管理系统最有价值的地方。