ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Flask+Vue的高校机房管理系统设计与实现

基于Flask+Vue的高校机房管理系统设计与实现

如果你接手过任何一个高校机房的管理需求,应该对下面这几件事不陌生:课表更新靠人工通知,机房空闲状态全凭现场碰运气,学生上机了没有签到记录,设备坏了维修进度没人跟踪。项目名称“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 也能做但属于“过度装备”。

两者的差别在选型时很直观:

对比维度FlaskDjango
项目结构自由组织,蓝图按模块拆分约定优先,自动生成项目骨架
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 app

run.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 装饰器是通用方案。新增管理功能时,只要把装饰器加上,就不会出现“普通学生直接调用设备删除接口”的低级漏洞。

第四,前端界面隐藏某个入口不等于安全。菜单可以按角色渲染,但后端接口仍然要逐一验证。安全的关键永远在后端,不在前端隐藏了几个路由。

如果项目后续还要继续迭代,我大概率会从三个方向入手。第一,把预约粒度做得更细,支持按教师课表批量抢占整个机房,而不是让学生逐个座位抢;第二,增加一台“签到终端”概念,学生在机房门口的大屏上输入学号即可完成自助签到,签到时自动分配一个空闲座位;第三,把统计报表做成定时任务,每天凌晨自动生成机房利用率日报,推送给管理员。机房管理系统最值钱的部分其实是沉淀下来的使用数据,有数据之后做排课优化、座位分配策略才有依据。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进