如果你接手过任何一个高校机房的管理需求,应该对下面这几件事不陌生:课表更新靠人工通知,机房空闲状态全凭现场碰运气,学生上机了没有签到记录,设备坏了维修进度没人跟踪。项目名称“python基于flask的高校机房管理系统的设计与实现-vue pycharm django”,其实把整条技术路线都写在了脸面上——后端用 Flask 提供接口,前端用 Vue 搭建界面,PyCharm 作为主力开发环境,数据库用 MySQL 存储,组成一套前后端分离的管理系统。这篇文章打算从头讲清楚:高校机房管理系统到底要设计哪些模块,数据库怎么建,接口怎么写,前端怎么配合,开发环境怎么搭,以及我在实际联调中踩过的坑。无论是准备拿它做毕业设计,还是单位里真的要落地一个机房管理平台,这套思路都可以直接套用。
1. 机房管理的真实业务场景与功能边界
1.1 角色与核心诉求
高校机房和网吧式机房最大的不同在于:使用人群不只是“来上网的”,而是有明确教学任务和自习需求的学生和老师。所以这个系统的核心不是计时收费,而是围绕“课表、预约、设备、维修”做闭环管理。我接触过的需求里,角色基本固定为三类。
| 角色 | 典型诉求 | 系统需要提供的功能 |
|---|---|---|
| 学生 | 找空闲机房、预约座位、签到上机、故障报修 | 机房空闲查询、座位预约、个人上机记录、报修提交 |
| 教师 | 实验课排课、临时占用机房、查看上机名单 | 机房预约、课程时段占用、学生签到统计 |
| 管理员 | 维护机房和座位信息、管理账号、掌握全盘使用数据 | 机房/设备管理、用户管理、预约审核、数据报表 |
很多第一次做这类系统的人会犯同一个错误:把普通网吧计费系统的功能直接搬过来。实际上高校机房更看重的是“教学时段隔离”和“设备生命周期管理”,而不是计费。比如一个机房早上第1-2节有计算机网络实验,第3-4节可能被开放成自由自习,这两个时段的可用状态必须由课表驱动,而不是管理员手动开关。
1.2 两条主线业务流程
整个系统拆开来看就两条主线:一条是预约上机流,一条是设备维护流。
预约上机流是:学生查看机房空闲时段,选择座位提交预约,管理员或系统自动审批,到机房后用学号和密码登录签到,开始上机,离开时签退,系统记录上机时长。如果预约了没来,超过时间窗口自动取消,并把座位释放回资源池。
设备维护流是:学生发现某台电脑开不了机,在系统里提交报修工单,附带座位号和故障描述;管理员看到后把设备状态改成“维修中”,指派维修人员处理;修好后更新状态并把工单标记为已完成,设备重新进入可预约状态。
这两条线的状态变化,几乎就是后端所有接口的骨架。想清楚这两条流,再去做数据库设计和代码开发就顺了。
1.3 系统边界:什么该做、什么不该做
这里必须提醒一句:机房管理系统和机房硬件控制系统是两回事。系统可以做到“记录什么时间的哪台设备被谁使用了”,但做不到“远程开机、远程锁定屏幕”。如果你要对接还原卡、电子教室或者硬件的定时开关机,那属于另一套硬件集成方案,已经不是 Web 后端的范畴了。
我建议第一版系统不要碰硬件控制,先把纯业务闭环跑通。等数据都沉淀下来了,如果机房里有智能插座或者支持 API 的管理系统,再扩展硬件对接也不迟。过早引入硬件对接会让项目复杂度急剧上升,而且校园网络的设备多样性很容易让排错变成体力活。
2. Flask 和 Vue 这套组合是怎么定下来的(顺带聊 Django)
2.1 Flask 与 Django:选型时的实际比较
项目标题里出现过 Django,说明你大概率在纠结过“到底是 Flask 还是 Django”。我两个框架都写过,这么说吧:这个项目用 Flask 更合适,Django 也能做但属于“过度装备”。
两者的差别在选型时很直观:
| 对比维度 | Flask | Django |
|---|---|---|
| 项目结构 | 自由组织,蓝图按模块拆分 | 约定优先,自动生成项目骨架 |
| ORM | 通过 Flask-SQLAlchemy 集成 | 内置完整 ORM 和迁移工具 |
| Admin 后台 | 需要自己写 | 自带成熟 Admin |
| 认证体系 | 无内置,用扩展实现 | 内置用户认证和权限框架 |
| 学习曲线 | 平缓,写一个接口成本很低 | 相对陡峭,概念较多 |
| 适合场景 | 中小型 API 服务、前后端分离项目 | 内容丰富、后台系统密集的整体 Web 应用 |
高校机房管理系统本质上就是一个给 Vue 前端提供 JSON 的接口服务,核心工作量在查库、写库、状态校验。Flask 路由短、依赖少、部署简单,一个文件就能把服务拉起来。Django 自带的那套模板、Admin、表单,在这个项目里根本用不到,反而给新手增加了概念负担。
带高年级学生做过一次实训,有人非要用 Django,结果卡在 DRF 配置上花了两天。换回 Flask 之后,半天就把用户登录和机房列表两个接口跑通了。不是说 Django 不好,而是做人选型要对得上项目复杂度。
2.2 前端为什么要用 Vue 和 Element
后端只管出 JSON 接口,前端需要一套趁手的 UI 框架,最好的搭配就是 Vue 加组件库。Vue 对国内开发者友好,中文资料多,招聘市场上会的人也遍地都是。组件库我推荐 Element Plus,它提供的表格、表单、对话框、日期选择器,做管理后台效率极高。
学生预约页可以做成机房卡片列表加座位布局图,管理员后台用表格加抽屉详情,这些都是 Element Plus 开箱即用的东西。如果你用 Vue 的时候非要自己手写 CSS 去做日期选择和分页,那等于自己把组件库的轮子重新造了一遍,完全没有必要。
前端工程化这里建议直接 Vite,不要再用 Vue CLI 的旧方案。Vite 冷启动速度快,配置简单,代理跨域也直接在配置文件中解决。Vite 搭配 Vue 3 已经是目前的主流,网上资源也多,遇到报错直接搜基本都有答案。
2.3 PyCharm 在这个项目里承担什么
PyCharm 在这个项目里扮演的是“开发环境总管”的角色。它负责三件事:虚拟环境管理、运行配置、断点调试。
虚拟环境是很多新手最容易忽略的。Python 项目如果不做环境隔离,这台机器装 Flask 3.0,那台机器装 Flask 2.0,依赖全堆在全局环境里,早晚会出问题。PyCharm 里创建项目时可以直接选择新建 venv,后续所有依赖都装在这个独立环境里,换电脑或交付给别人的时候只要导出一份 requirements.txt 就能复现。
调试方面,PyCharm 的断点调试比单纯 print 定位问题效率高很多。需要在某个接口返回异常时查看局部变量,直接在路由函数里打断点即可。这是纯文本编辑器很难比的体验。
3. 数据库建模:把机房、设备、预约、上机串成一条链
3.1 核心表梳理:一张强关联清单
数据库是这类管理系统的地基。表设计得不好,后面接口越写越痛苦。我从实际需求出发拆成了下面这些表,这张清单第一次想清楚,后面就顺了。
- user:用户表,存储学生、教师、管理员的账号信息,用 role 字段区分身份;
- room:机房表,存储机房名称、位置、座位数、开放时段和当前状态;
- computer:设备表,存储每台电脑属于哪个机房、座位号、IP、当前状态;
- course_schedule:课表专项表,记录哪个机房在什么时间被哪门课占用;
- reservation:预约表,学生或教师对某机房的某台座位进行预约;
- on_record:上机记录表,保存签到、签退时间和实际使用时长;
- repair:报修工单表,记录故障设备、提交人、故障描述、处理状态;
- notice:公告表,发布机房开放调整、系统维护等通知。
这里最容易漏掉的表是 course_schedule。如果没有课表隔离,预约系统根本不知道某个机房“其实一直被占用”。很多功能不完善的系统就是死活不安排课表表,结果一到上课时间,预约系统还在疯狂给学生分配座位,现场必然混乱。
3.2 SQLAlchemy 模型代码:先跑起来再去优化
模型定义我用 Flask-SQLAlchemy 写,这里给出几个关键模型作为参考。
from flask_sqlalchemy import SQLAlchemy from datetime import datetime db = SQLAlchemy() class User(db.Model): __tablename__ = 'user' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), unique=True, nullable=False, index=True) password_hash = db.Column(db.String(256), nullable=False) role = db.Column(db.String(16), nullable=False) # student / teacher / admin real_name = db.Column(db.String(64)) student_no = db.Column(db.String(32)) phone = db.Column(db.String(20)) created_at = db.Column(db.DateTime, default=datetime.now) class Room(db.Model): __tablename__ = 'room' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), nullable=False) location = db.Column(db.String(128)) seat_count = db.Column(db.Integer, default=40) open_sections = db.Column(db.JSON) # 开放节次,如 ["1-2", "3-4", "5-6", "7-8"] status = db.Column(db.String(16), default='open') # open / maintenance / closed class Computer(db.Model): __tablename__ = 'computer' id = db.Column(db.Integer, primary_key=True) room_id = db.Column(db.Integer, db.ForeignKey('room.id')) seat_no = db.Column(db.String(16)) ip_address = db.Column(db.String(32)) status = db.Column(db.String(16), default='normal') # normal / broken / maintenance room = db.relationship('Room', backref='computers') class Reservation(db.Model): __tablename__ = 'reservation' __table_args__ = ( db.UniqueConstraint('date', 'start_time', 'computer_id', name='uq_reservation_slot'), ) id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) room_id = db.Column(db.Integer, db.ForeignKey('room.id')) computer_id = db.Column(db.Integer, db.ForeignKey('computer.id')) date = db.Column(db.Date) start_time = db.Column(db.String(8)) # 如 "14:00" end_time = db.Column(db.String(8)) # 如 "16:00" status = db.Column(db.String(16), default='pending') # pending / approved / rejected / checked_in / completed / canceled user = db.relationship('User', backref='reservations') computer = db.relationship('Computer', backref='reservations') class OnRecord(db.Model): __tablename__ = 'on_record' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) computer_id = db.Column(db.Integer, db.ForeignKey('computer.id')) reservation_id = db.Column(db.Integer, db.ForeignKey('reservation.id')) checkin_time = db.Column(db.DateTime) checkout_time = db.Column(db.DateTime) duration_minutes = db.Column(db.Integer, default=0)有几个设计细节值得说。第一,Reservation 表加了联合唯一约束,这非常重要,它从数据库层面保证“同一日期、同一开始时间、同一座位”不会出现两条预约记录,业务代码即使漏判,数据库也能兜底。第二,用户表不拆成学生表和教师表,而是用单表加 role 字段,是因为学生和教师的共同属性远远多于差异,拆表会在认证和关联查询时平添麻烦。第三,设备表和机房表分开,是因为一个机房内设备数量多且状态动态变化,如果把这些信息全塞进机房表,那机房表会变得又笨又难更新。
3.3 机时/计费与后续扩展设计
高校机房系统要不要计费,看学校的具体规定。有些学校给每个学生分配每月机房使用机时,超出部分收费;也有学校完全免费开放。我的建议是,数据库里不要提前把“金额”写死,而是预留可配置区域。比如在 room 表加一个 use_policy 字段,值为 free 或 quota,quota 时就按用户的已用时长和机房预设的限时规则来校验。
后续如果要对接一卡通,也尽量通过学号字段做中间映射,不要重复采集个人信息。这样系统将来扩展的时候,核心表结构不用大改,只需要加一张关联表把学号和校园卡卡号关联起来即可。
4. 后端接口设计:登录鉴权、预约签到、设备报修的实现细节
4.1 登录认证、JWT 与基于角色的权限装饰器
后端采用前后端分离,认证方案我选 JWT。原因很简单:Flask 后端无状态,Vue 前端拿到 Token 后存在 localStorage,请求时放进请求头,服务器解析即可,不需要维护 Session 表。
先写登录接口:
from flask import Blueprint, request, jsonify from werkzeug.security import check_password_hash from flask_jwt_extended import create_access_token, jwt_required, get_jwt, get_jwt_identity from app.models import db, User bp = Blueprint('auth', __name__) @bp.route('/api/auth/login', methods=['POST']) def login(): data = request.get_json(force=True) username = data.get('username', '').strip() password = data.get('password', '') user = User.query.filter_by(username=username).first() if not user or not check_password_hash(user.password_hash, password): return jsonify(code=1, msg='用户名或密码错误'), 401 token = create_access_token( identity=str(user.id), additional_claims={ 'role': user.role, 'real_name': user.real_name } ) return jsonify(code=0, data={ 'token': token, 'role': user.role, 'real_name': user.real_name })JWT 生成时把角色写进 claims,后面做权限控制就不用每次查库。但这里有个坑要注意:前端传来的 role 一律不能信,后端必须从 JWT 解析出角色来做判断。有人为了省事,把 role 放在请求参数里让后端判断,这等于给攻击者留了后门。
写一个统一的角色权限装饰器,比在每个路由里重复判断靠谱得多:
from functools import wraps from flask_jwt_extended import verify_jwt_in_request, get_jwt def role_required(*roles): def wrapper(fn): @wraps(fn) def decorator(*args, **kwargs): verify_jwt_in_request() claims = get_jwt() role = claims.get('role') if role not in roles: return jsonify(code=1, msg='没有权限访问该接口'), 403 return fn(*args, **kwargs) return decorator return wrapper之后在管理接口上直接标注:
@bp.route('/api/admin/room', methods=['POST']) @role_required('admin') def create_room(): pass这个装饰器是整套后端权限控制的核心,比在每个函数里抄一遍 if 判断干净多了。
4.2 预约-签到-签退的状态流转实现
预约接口的重点不是插入记录,而是状态校验。学生提交预约时,要确保座位存在、设备正常、时段未被占用。
核心逻辑如下:
from datetime import date from flask_jwt_extended import get_jwt_identity @bp.route('/api/reservation', methods=['POST']) @jwt_required() def create_reservation(): user_id = int(get_jwt_identity()) data = request.get_json(force=True) computer_id = data.get('computer_id') start_time = data.get('start_time') end_time = data.get('end_time') use_date = data.get('date') computer = Computer.query.filter_by(id=computer_id, status='normal').first() if not computer: return jsonify(code=1, msg='该设备不可用'), 400 conflict = Reservation.query.filter_by( computer_id=computer_id, date=use_date, start_time=start_time ).filter( Reservation.status.in_(['pending', 'approved', 'checked_in']) ).first() if conflict: return jsonify(code=1, msg='该座位在该时段已被预约'), 400 booking = Reservation( user_id=user_id, room_id=computer.room_id, computer_id=computer_id, date=use_date, start_time=start_time, end_time=end_time, status='pending' ) db.session.add(booking) db.session.commit() return jsonify(code=0, data={'reservation_id': booking.id})这里状态判断有一个容易忽略的点:pending的预约也必须算作冲突。如果只检查“已通过”的预约,学生连续点两次提交按钮就可能生成两条 pending 记录,座位就被同时锁住两次。把 pending、approved、checked_in 全部纳入冲突检查,再从数据库唯一约束兜底,双保险。
签到接口我建议做自动校时,不允许学生自己选时间,而是由服务器时间判断当前是否处于允许窗口。比如允许预约开始前 15 分钟签到,预约开始后 30 分钟内未签到则自动作废,释放座位给其他人使用。
@bp.route('/api/reservation/<int:reservation_id>/checkin', methods=['POST']) @jwt_required() def checkin(reservation_id): booking = Reservation.query.get_or_404(reservation_id) now = datetime.now() if booking.status not in ('approved', 'pending'): return jsonify(code=1, msg='当前状态不允许签到'), 400 start = datetime.strptime(f"{booking.date} {booking.start_time}", "%Y-%m-%d %H:%M") if now < start - timedelta(minutes=15): return jsonify(code=1, msg='尚未到签到时间'), 400 if now > start + timedelta(minutes=30): booking.status = 'canceled' db.session.commit() return jsonify(code=1, msg='已超过签到时间,预约自动取消'), 400 booking.status = 'checked_in' record = OnRecord( user_id=booking.user_id, computer_id=booking.computer_id, reservation_id=booking.id, checkin_time=now ) db.session.add(record) db.session.commit() return jsonify(code=0, data={'record_id': record.id})签退逻辑相对简单,读取对应的上机记录,计算当前时间和 checkin_time 的差值分钟数,写入 duration_minutes 和 checkout_time,并把预约状态改成 completed。
4.3 设备报修与统计报表接口
设备报修的核心是状态联动。学生提交报修工单后,对应 Computer 的 status 要同步变成 broken,避免其他学生继续预约故障机器。管理员维修完成时,更新 repair 表状态,再把 Computer 状态改回 normal。
统计报表接口不需要太复杂,把基础查询写清楚就好。比如统计每个机房最近一周的上机人次:
from sqlalchemy import func report = db.session.query( Room.name, func.count(OnRecord.id).label('total_count') ).select_from(OnRecord).join( Computer, OnRecord.computer_id == Computer.id ).join( Room, Computer.room_id == Room.id ).filter( OnRecord.checkin_time >= datetime.now() - timedelta(days=7) ).group_by( Room.id ).all()前端拿到这个二维数组之后渲染成柱状图或者表格都行。这类聚合查询在 SQLAlchemy 里看着绕,其实翻译成 SQL 就是常规的 JOIN + GROUP BY,理解了之后就顺手了。
5. 前端 Vue 项目怎么搭:请求封装、权限路由与核心页面
5.1 工程初始化与目录组织
前端我用 Vue 3 加 Vite 初始化,命令如下:
npm create vite@latest computer-room-web -- --template vue cd computer-room-web npm install npm install vue-router axios element-plus创建完以后,把 src 下面的目录整理成下面这个样子。这种组织方式对一个中后台项目来说完全够用。
src/ api/ # 接口请求模块,按业务拆分 router/ # 路由配置 stores/ # 状态管理(用户信息、Token 等) views/ # 页面组件 login.vue student/ booking.vue my_booking.vue admin/ room_manage.vue computer_manage.vue repair_manage.vue teacher/ schedule_manage.vue utils/ request.js # Axios 封装目录定下来后再写功能,思路会清晰很多。很多新手拿到需求直接往 App.vue 里堆代码,项目一旦超过三个页面就没办法维护了,这也是我为什么反复强调先定目录。
5.2 Axios 封装与 Vue Router 权限守卫
Axios 封装有两个关键点:请求时自动携带 Token,响应时统一处理后端返回的错误码。
import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( resp => resp.data.code === 0 ? resp.data.data : Promise.reject(resp.data), err => { const msg = err.response?.data?.msg || '请求失败' ElMessage.error(msg) if (err.response?.status === 401) { localStorage.removeItem('token') localStorage.removeItem('role') window.location.href = '/login' } return Promise.reject(err) } ) export default request路由守卫的作用是,没有 Token 的用户一律不能进系统页面,登录后的用户访问登录页自动跳回首页:
import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/login', component: () => import('../views/login.vue') }, { path: '/', component: () => import('../views/layout.vue'), children: [ { path: '', redirect: '/booking' }, { path: 'booking', component: () => import('../views/student/booking.vue') }, { path: 'admin/room', component: () => import('../views/admin/room_manage.vue') }, { path: 'admin/computer', component: () => import('../views/admin/computer_manage.vue') } ] } ] }) router.beforeEach((to) => { const token = localStorage.getItem('token') if (to.path !== '/login' && !token) return '/login' if (to.path === '/login' && token) return '/' }) export default router更细的按角色控制菜单,可以在 layout 侧边栏根据 localStorage 里的 role 字段动态渲染,这比在路由配置里写 meta 然后逐个判断要直观。管理员看到完整菜单,学生只看到“座位预约”和“我的预约”,体验更干净。
5.3 学生预约页和管理员后端的实现思路
学生预约页是前端工作量最大的部分,核心是一个座位选择交互。我的实现思路是:先请求机房列表,每个机房渲染成卡片;进入机房详情后,请求该机房全部设备的状态;设备状态有三种,正常可选、已占用、故障不可选。用 Element Plus 的按钮或者自定义 div 排列成网格模拟座位布局图。
点击设备后弹出日期和时段选择,推荐时段时间隔两小时,这样可以有效减少碎片化时段。提交预约的接口就是 4.2 里描述的那个。签到时,页面显示一个按钮,点击后调用 checkin 接口,接口内部统一处理校时逻辑。
管理员后端页面相对常规,机房管理用表格加弹窗表单,设备管理用表格加批量导入。批量导入功能建议预留 CSV 导入,几十台设备一台一台录入实在不现实。报表页用 ECharts 画条形图和折线图,数据来源是统计接口返回的聚合数组。
6. PyCharm 开发环境和 Flask 工程组织的落地细节
6.1 使用 Flask 工厂模式组织项目结构
Flask 项目如果全部写在一个 app.py 里,前期跑通很快,但一旦有十来个接口,文件就会膨胀到几百行。我建议直接用工厂模式,这样结构清晰,也方便后续测试。
computer_room_server/ app/ __init__.py # 工厂函数 create_app config.py # 配置 models.py # 数据库模型 api/ auth.py # 登录接口 room.py # 机房接口 reservation.py # 预约接口 user.py # 用户管理接口 repair.py # 报修接口 stats.py # 统计接口 migrations/ # Flask-Migrate 迁移目录 requirements.txt run.py工厂函数的核心代码:
from flask import Flask from flask_cors import CORS from flask_migrate import Migrate from flask_jwt_extended import JWTManager from app.config import Config from app.models import db def create_app(): app = Flask(__name__) app.config.from_object(Config) db.init_app(app) Migrate(app, db) JWTManager(app) CORS(app, resources={r"/api/*": {"origins": "*"}}) from app.api import auth, room, reservation, user, repair, stats app.register_blueprint(auth.bp) app.register_blueprint(room.bp) app.register_blueprint(reservation.bp) app.register_blueprint(user.bp) app.register_blueprint(repair.bp) app.register_blueprint(stats.bp) return apprun.py 里直接创建应用并启动:
from app import create_app app = create_app() if __name__ == '__main__': app.run(host='0.0.0.0', port=5001, debug=False)6.2 依赖清单和配置项说明
requirements.txt 是项目交付的一部分,换环境一键安装:
flask>=3.0.0 flask-sqlalchemy>=3.1.0 flask-migrate>=4.0.0 flask-jwt-extended>=4.6.0 flask-cors>=4.0.0 pymysql>=1.1.0 gunicorn>=21.2.0配置项里最需要注意的是数据库连接串的编码参数。不加 charset=utf8mb4,后期存储前端传来的中文就会变问号。配置文件大概这样:
import os from datetime import timedelta class Config: SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-change-me') SQLALCHEMY_DATABASE_URI = ( 'mysql+pymysql://root:your_password@127.0.0.1:3306/' 'computer_room_system?charset=utf8mb4' ) SQLALCHEMY_TRACK_MODIFICATIONS = False JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=12)MySQL 从命令行创建库的时候也要指定字符集,否则表创建出来编码是 latin1 或者旧的 utf8,存 emoji 和生僻字直接报错。
6.3 PyCharm 的虚拟环境、运行与调试配置
在 PyCharm 里新建项目时,选择虚拟环境类型为 venv,Python 解释器就绑定到虚拟环境。之后在 Terminal 里执行:
pip install -r requirements.txt确认依赖装好之后,配置运行方式。推荐在 PyCharm 的 Run Configuration 里选择 Module name,值为flask,Parameters 填run --port=5001,Environment variables 里设置FLASK_APP=run.py。这样启动方式和命令行 Flask 行为一致,日志输出也更干净。
调试时直接在函数内部打断点,点击 Debug 运行,请求发过来后就能看到每一步的变量值。接口返回 500 时先看 PyCharm 控制台里的异常栈,绝大多数问题在栈信息里就能定位到具体行,不需要瞎猜。
7. 前后端联调踩坑记录与安全基线
7.1 跨域问题:前端代理与后端 CORS 怎么配合
前后端分离最经典的问题就是跨域。Vue 开发服务器跑在 5173 端口,Flask 跑在 5001 端口,浏览器直接请求就会被同源策略拦住。
我的处理方式是开发环境用 Vite 代理,前端不直接访问后端地址,而是访问同源的/api前缀,由 Vite 代理转发到 Flask。这是最接近生产环境的表现形式。
// vite.config.js export default { server: { port: 5173, proxy: { '/api': { target: 'http://127.0.0.1:5001', changeOrigin: true } } } }后端再加一层 CORS 兜底,防止有请求绕过代理直接打到后端。Flask-Cors 的配置我在工厂函数里已经写过。两个都配上之后,开发环境几乎不会再碰到跨域报错。如果还报跨域,多半是请求头里写了自定义字段但后端没有允许对应 Header,检查一下 Access-Control-Allow-Headers 是否包含 Authorization 即可。
7.2 时间字段序列化和 JSON 编码器处理
Flask 的 jsonify 在遇到 datetime 对象时会直接报“Object of type datetime is not JSON serializable”。这个问题第一次联调必然遇到。解决办法是自定义 JSON 编码器,把所有 datetime 统一转成字符串。
from datetime import datetime from flask.json import JSONEncoder class CustomJSONEncoder(JSONEncoder): def default(self, obj): if isinstance(obj, datetime): return obj.strftime('%Y-%m-%d %H:%M:%S') return super().default(obj)然后在工厂函数里挂载:
app.json_encoder = CustomJSONEncoder还有一种省事的做法是,在所有模型序列化时手动把时间字段格式化。但这样每个 serializer 都要写一遍,容易漏。全局挂编码器一劳永逸,强烈推荐。
7.3 预约并发冲突:从业务校验到数据库兜底
预约冲突校验那一段代码看着没问题,但假如两个学生同时点击提交同一个座位,后端会先后读到“无冲突”,然后都插入成功。这就是典型的并发竞态。
解决思路有两个,第一层是代码里用数据库锁处理,查询时加上行锁禁止其他事务同时读到空闲状态:
conflict = Reservation.query.filter_by( computer_id=computer_id, date=use_date, start_time=start_time ).with_for_update().first()第二层就是我在 3.2 里讲的联合唯一约束。即使第一条没拦住,第二条记录插入时数据库会抛 IntegrityError,后端捕获后返回“该座位已被预约”提示即可。
try: db.session.commit() except IntegrityError: db.session.rollback() return jsonify(code=1, msg='该座位已被预约,请重新选择'), 400小系统并发没那么夸张,但预留这个兜底能避免很多莫名其妙的数据异常。这两个机制配合起来,预约流程的真实可靠性才有保证。
7.4 安全基线:密码、令牌与接口权限的最小集
机房管理系统的安全要求不高,但有几条线不能破。首先,密码必须用 werkzeug.security 里的哈希函数处理,登录时用 check_password_hash 校验,数据库绝对不允许存明文密码。
其次,JWT 的过期时间要合理。设置太短学生频繁被踢下线,设置太长泄露风险大。12 小时是一个比较合适的折中。管理员的 Token 过期时间可以再短一点,比如 2 小时,减少账号待在浏览器里的窗口期。
第三,权限验证必须在后端接口完成,前面写的 role_required 装饰器是通用方案。新增管理功能时,只要把装饰器加上,就不会出现“普通学生直接调用设备删除接口”的低级漏洞。
第四,前端界面隐藏某个入口不等于安全。菜单可以按角色渲染,但后端接口仍然要逐一验证。安全的关键永远在后端,不在前端隐藏了几个路由。
如果项目后续还要继续迭代,我大概率会从三个方向入手。第一,把预约粒度做得更细,支持按教师课表批量抢占整个机房,而不是让学生逐个座位抢;第二,增加一台“签到终端”概念,学生在机房门口的大屏上输入学号即可完成自助签到,签到时自动分配一个空闲座位;第三,把统计报表做成定时任务,每天凌晨自动生成机房利用率日报,推送给管理员。机房管理系统最值钱的部分其实是沉淀下来的使用数据,有数据之后做排课优化、座位分配策略才有依据。