资讯动态

Flask+SQLAlchemy教务系统源码:选课成绩与权限控制实战

发布时间:2026/10/29 11:19:53 来源:尧图企业网站定制
简介这是一套面向 Python Web 初学者与课程设计学习者的学校教务系统项目包围绕学生信息、课程安排、成绩录入与查询等教务流程提供可运行源码、数据库脚本和设计文档适合课程实践、毕业设计参考与功能拆解学习。压缩包共 9 个文件包含 7 个 sql 数据库脚本、1 个 py 主程序源码和 1 个 docx 设计论文整体约 467KB体量轻便便于快速导入与本地运行。已有 594 人学习下载具备一定参考基础。项目覆盖学生、教师、课程、成绩等核心数据表读者可借此理解 Python 后端逻辑、Web 页面交互、数据库表结构设计及增删改查实现并熟悉基础语法、模块组织与数据导入导出论文部分还可帮助梳理需求分析、架构设计、技术选型与功能实现思路适合按模块阅读、复现与二次改造。1. 教务排课不是表格堆砌一份 Python 教务系统源码能省掉多少手工活开学前两周教务老师在群里甩出一张 Excel让几百名学生填选修课收回来一堆命名各异的文件再一份份合并、去重、查时间冲突——这套流程每年重演一次。这份 python 学校教务系统源码想干掉的就是这个循环把学生、教师、课程、选课、成绩这几件事收进一个带角色登录的 Web 后台约束交给数据库页面只负责展示。它不是什么大型教务中台而是一套 Python 后端 关系型数据库 模板页面的完整课程资源课程设计、毕业设计起步能直接用想拿真实业务练手 CRUD、事务和权限控制的开发者也能拆得动。压缩包里是能跑起来的代码不是几张 ER 图。2. 技术栈与目录结构Flask 蓝图 SQLAlchemy 的分层骨架解压之后第一眼看到的往往是一堆 .py 和一个 templates 文件夹很多人卡在这一步不知道从哪个文件开始读。我的习惯是先看 requirements.txt 和入口文件五分钟就能判断这套代码是「能改的工程」还是「交完作业就不管的一次性脚本」。这份教务系统属于前者它用了蓝图分模块、模型独立成包、配置分环境的写法结构上留了二次开发的口子。2.1 选型判断为什么是 Flask 而不是 Django教务系统的页面数量其实不多登录、学生选课列表、学生课表、教师授课列表、教师成绩录入、管理员的学生/课程/班级维护加起来二十个路由出头。这种体量上 Django 不是不行但它自带的 admin 和 ORM 封装太厚初学者改着改着就分不清哪部分是框架给的、哪部分是自己写的。Flask 的好处是把「路由—视图—模型—模板」四件事摆得很直白出错时栈信息也短排错成本低。依赖清单大致长这样装在 requirements.txt 里Flask2.3.3 Flask-SQLAlchemy3.1.1 Flask-Login0.6.3 Flask-WTF1.2.1 PyMySQL1.1.0 openpyxl3.1.2 python-dotenv1.0.0逐行说下各自管什么。Flask 是主框架Flask-SQLAlchemy 把数据库会话绑到应用上下文上视图里直接db.session就能用Flask-Login 负责登录态current_user在整个请求周期里可读权限判断基本靠它Flask-WTF 主要用来做 CSRF 保护和表单校验选课、成绩录入这类写操作没有它容易被浏览器缓存坑PyMySQL 是 MySQL 的纯 Python 驱动省掉编译 mysqlclient 的麻烦Windows 上尤其明显openpyxl 撑起成绩模板导出和批量导入这两块教务场景里离不开 Excel。数据库这块我一般推荐 MySQL 8字符集设成 utf8mb4。学生姓名里偶尔出现的生僻字、课程名里的特殊符号用 utf8 三字节存会直接报错这个坑挪到上线后才发现就得改表改连接串代价不小。本地想快速看效果也可以先用 SQLite 把流程跑通但要注意 SQLite 对行级锁和并发写的支持很弱选课抢名额这种场景必须在 MySQL 上验证。2.2 目录结构一个请求从 URL 走到模板把压缩包解开主干目录大致是这样组织的school_edu/ ├── app.py # 应用工厂创建 Flask 实例 ├── config.py # 分环境配置开发/测试/生产 ├── requirements.txt ├── models/ │ ├── __init__.py # 初始化 db 对象避免循环导入 │ ├── user.py # 用户、班级 │ ├── course.py # 课程、选课记录 │ └── score.py # 成绩 ├── views/ │ ├── auth.py # 登录、登出、改密 │ ├── student.py # 选课、退课、查课表、查成绩 │ ├── teacher.py # 授课列表、成绩录入、成绩导出 │ └── admin.py # 学生/教师/课程/班级维护 ├── utils/ │ ├── decorators.py # role_required 角色校验装饰器 │ └── excel.py # 导入导出封装 ├── templates/ │ ├── base.html # 公共骨架含导航和 flash 消息区 │ ├── auth/login.html │ ├── student/ # course_list.html、timetable.html │ └── teacher/ # score_entry.html ├── static/ │ ├── css/、js/ └── data/ └── init_db.py # 建表 灌初始数据这个分层的关键在于models/__init__.py里只放一个db SQLAlchemy()各个模型文件from models import db再定义类最后在app.py里db.init_app(app)。很多人写 Flask 时把 db 放在 app.py模型又反过来 import app循环导入报错能折腾一下午这是最常见的翻车点之一。一个请求走完的链路是这样的浏览器访问/student/coursesFlask 按注册的蓝图前缀匹配到student_bp的course_list视图先过login_required确认会话存在再过role_required(student)确认角色然后视图里用Course.query.filter_by(statusopen)拿数据把对象列表丢给render_template(student/course_list.html, coursescourses)Jinja2 渲染成 HTML 返回。整条链路没有中间层读代码时可以直接从 URL 跳到函数这也是我推荐新手从这个项目入手的原因。2.3 启动入口与配置create_app 工厂函数配置单独抽在 config.py按环境分三个类公共部分放基类import os from urllib.parse import quote_plus class BaseConfig: SECRET_KEY os.environ.get(SECRET_KEY, dev-only-change-me) SQLALCHEMY_TRACK_MODIFICATIONS False SESSION_COOKIE_HTTPONLY True SESSION_COOKIE_SAMESITE Lax PER_PAGE 20 # 列表分页每页条数 class DevConfig(BaseConfig): DEBUG True # 密码里的 # 等字符必须转义否则连接串会被截断 SQLALCHEMY_DATABASE_URI ( mysqlpymysql://edu_user:%s127.0.0.1:3306/school_edu?charsetutf8mb4 % quote_plus(os.environ.get(DB_PWD, 123456)) ) class ProdConfig(BaseConfig): DEBUG False SQLALCHEMY_DATABASE_URI os.environ[DATABASE_URL] SESSION_COOKIE_SECURE True几个参数值得单独说。SECRET_KEY打死都不能用硬编码的默认值上线它决定 session 签名泄露等于任何人可以伪造登录态SQLALCHEMY_TRACK_MODIFICATIONS关掉是为了消除一条警告开着还会带来不必要的对象追踪开销连接串里的charsetutf8mb4是中文不乱码的前提quote_plus处理密码特殊字符这行省掉的话密码里带个就会连到错误的主机上报的错还特别玄学。入口文件用工厂模式方便测试时创建不同配置的实例from flask import Flask from models import db from views.auth import auth_bp from views.student import student_bp from views.teacher import teacher_bp from views.admin import admin_bp from config import DevConfig, ProdConfig CONFIG_MAP {dev: DevConfig, prod: ProdConfig} def create_app(envdev): app Flask(__name__) app.config.from_object(CONFIG_MAP[env]) db.init_app(app) # 蓝图统一在这里挂载改前缀只动这一处 app.register_blueprint(auth_bp) app.register_blueprint(student_bp, url_prefix/student) app.register_blueprint(teacher_bp, url_prefix/teacher) app.register_blueprint(admin_bp, url_prefix/admin) return app app create_app() if __name__ __main__: app.run(host0.0.0.0, port5000)蓝图前缀在这里集中注册以后想把/admin改成/manage只需要动一行不用去每个视图函数上改路由。host0.0.0.0是为了让同一局域网内的同学能访问你的开发机做联调只在开发环境这么写生产环境交给 WSGI 服务器。3. 数据模型与建表六张表撑起选课与成绩链路模型设计是这个项目最值得细看的部分。教务系统的业务复杂度几乎全部压在表关系和约束上页面只是把约束的结果显示出来。表建对了很多「同一个人选了两遍同一门课」「成绩录到了别人的课上」的问题从根上就不会发生。3.1 六张核心表的关系梳理剥离掉日志表和字典表核心是六张用户表、班级表、课程表、选课记录表、成绩表加上一张教师授课关联或者直接用课程表的 teacher_id 外键。关系是这样咬合的表名主键关键字段与其他表的关系sys_useridusername、password_hash、role、class_id多对一关联班级role 区分 admin/teacher/studentsys_classidname、grade、major一对多关联学生t_courseidcode、name、credit、teacher_id、capacity、semester多对一关联授课教师t_enrollmentidstudent_id、course_id、status学生与课程的多对多中间表t_scoreidenroll_id、value、updated_by一对一挂在选课记录上t_termidname、start_date、end_date课程与成绩按学期归档这里有个容易忽略的设计取舍成绩表为什么不直接存 student_id course_id而是挂到选课记录 enroll_id 上因为学生退课之后成绩记录应该一起失效挂在选课记录上删选课记录时级联删除成绩语义上更干净如果直接存学生和课程退课再重选旧成绩就会变成孤儿数据期末统计平均分时会被算进去。3.2 SQLAlchemy 模型代码逐段拆解先看用户和班级密码用 werkzeug 自带的哈希函数不存明文from datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash from models import db class SysClass(db.Model): __tablename__ sys_class id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(64), nullableFalse) grade db.Column(db.String(16), nullableFalse) # 如 2022 major db.Column(db.String(64)) students db.relationship(User, backrefclazz, lazydynamic) class User(db.Model): __tablename__ sys_user id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(32), uniqueTrue, nullableFalse, indexTrue) password_hash db.Column(db.String(256), nullableFalse) real_name db.Column(db.String(32), nullableFalse) role db.Column(db.Enum(admin, teacher, student, defaultstudent), nullableFalse, indexTrue) class_id db.Column(db.Integer, db.ForeignKey(sys_class.id)) is_active db.Column(db.Boolean, defaultTrue) # 软删除不物理删用户 def set_password(self, raw): # 默认 pbkdf2:sha256不要用 md5密码表泄露时 md5 基本等于明文 self.password_hash generate_password_hash(raw) def check_password(self, raw): return check_password_hash(self.password_hash, raw) property def is_authenticated(self): return Trueusername上加 unique 和 index 两件事登录时按用户名查唯一索引同时承担了「不允许重名」和「加速查询」两个职责。is_active用软删除而不是db.session.delete()因为用户一旦被物理删除他名下的选课记录和成绩会变成悬空外键要么报错要么留下脏数据教务数据是不允许这么玩的。课程与选课class Course(db.Model): __tablename__ t_course __table_args__ ( db.UniqueConstraint(code, semester, nameuq_course_code_term), ) id db.Column(db.Integer, primary_keyTrue) code db.Column(db.String(32), nullableFalse) # 课程编号 name db.Column(db.String(128), nullableFalse) credit db.Column(db.Numeric(3, 1), default2.0) # 学分保留一位小数 teacher_id db.Column(db.Integer, db.ForeignKey(sys_user.id), nullableFalse) capacity db.Column(db.Integer, default50) semester db.Column(db.String(16), nullableFalse) # 如 2024-2025-1 status db.Column(db.Enum(open, closed, defaultopen)) teacher db.relationship(User, foreign_keys[teacher_id]) enrollments db.relationship(Enrollment, back_populatescourse, cascadeall, delete-orphan) class Enrollment(db.Model): __tablename__ t_enrollment # 同一个学生在同一学期不能重复选同一门课靠数据库兜底 __table_args__ ( db.UniqueConstraint(student_id, course_id, nameuq_enroll_student_course), ) id db.Column(db.Integer, primary_keyTrue) student_id db.Column(db.Integer, db.ForeignKey(sys_user.id), nullableFalse, indexTrue) course_id db.Column(db.Integer, db.ForeignKey(t_course.id), nullableFalse, indexTrue) status db.Column(db.Enum(selected, dropped, defaultselected)) created_at db.Column(db.DateTime, defaultdatetime.now) course db.relationship(Course, back_populatesenrollments) student db.relationship(User, foreign_keys[student_id]) score db.relationship(Score, back_populatesenrollment, uselistFalse)credit用Numeric(3,1)而不是 Float学分 2.5 这种值如果用浮点存累加一学期之后会出现12.299999的尾数导出成绩单时很难看。cascadeall, delete-orphan挂在课程到选课的关系上删课程时自动清掉选课记录但生产环境里我不建议真删课程把status改成 closed 更稳妥历史成绩才留得住。成绩表class Score(db.Model): __tablename__ t_score __table_args__ ( db.UniqueConstraint(enrollment_id, nameuq_score_enrollment), db.CheckConstraint(value 0 AND value 100, nameck_score_range), ) id db.Column(db.Integer, primary_keyTrue) enrollment_id db.Column(db.Integer, db.ForeignKey(t_enrollment.id), nullableFalse) value db.Column(db.Numeric(5, 1)) # 允许空表示未录入 updated_by db.Column(db.Integer) # 哪个教师最后改的 updated_at db.Column(db.DateTime, defaultdatetime.now, onupdatedatetime.now) enrollment db.relationship(Enrollment, back_populatesscore)CheckConstraint限制 0 到 100uq_score_enrollment保证一条选课记录只有一条成绩。这两条约束写在数据库里比写在视图函数里靠谱得多——视图可能被绕过直连数据库改数据的管理员不会走你的校验但约束永远生效。3.3 建表脚本与初始数据建表和灌数据放在data/init_db.py通过 Flask CLI 注册成命令比直接python init_db.py更规范import click from flask.cli import with_appcontext from models import db from models.user import User, SysClass from models.course import Course click.command(init-db) with_appcontext def init_db_command(): 清库重建仅限开发环境使用 db.drop_all() db.create_all() admin User(usernameadmin, real_name管理员, roleadmin) admin.set_password(admin123) cls SysClass(name计科一班, grade2022, major计算机) db.session.add_all([admin, cls]) db.session.commit() click.echo(初始化完成) def register_commands(app): app.cli.add_command(init_db_command)db.drop_all()这行是双刃剑开发时爽误连生产库时是灾难。我自己的习惯是在这个函数第一行加一句if not current_app.debug: raise RuntimeError(生产环境禁止重建)算是给自己留的后悔药。跑完建表后记得手动补两个 MySQL 侧的东西一是确认库和连接都是 utf8mb4SHOW CREATE TABLE t_course能看到DEFAULT CHARSETutf8mb4二是给t_enrollment(course_id, status)建复合索引选课列表统计已选人数时按课程 ID 加状态过滤没这个索引几千条数据就开始卡。4. 核心业务代码拆解选课、退课、成绩录入的完整链路模型是静态的真正决定系统能不能用住的是这三条写链路。教务系统的特点写多读少、并发集中在选课开放的那几个小时任何一处没考虑到表现都是「名额超了」或者「数据对不上」。4.1 选课接口容量校验与唯一约束双保险最典型的问题是两个人同时点最后一个名额。只做「查一下人数没满就插入」在并发下必翻车两个请求都能查到 49/50。正确的做法是把课程行锁住再统计from flask import Blueprint, jsonify, request from flask_login import login_required, current_user from sqlalchemy.exc import IntegrityError from models import db from models.course import Course, Enrollment from utils.decorators import role_required student_bp Blueprint(student, __name__) student_bp.post(/course/int:course_id/enroll) login_required role_required(student) def enroll(course_id): # SELECT ... FOR UPDATE锁住课程行其他事务排队等 course db.session.execute( db.select(Course).where(Course.id course_id).with_for_update() ).scalar_one_or_none() if course is None or course.status ! open: return jsonify(code404, msg课程不存在或已关闭), 404 picked db.session.execute( db.select(db.func.count()).select_from(Enrollment).where( Enrollment.course_id course_id, Enrollment.status selected ) ).scalar() if picked course.capacity: db.session.rollback() return jsonify(code409, msg名额已满), 409 # 同一学期已选过这门课含退课后重选的情况 exists Enrollment.query.filter_by( student_idcurrent_user.id, course_idcourse_id ).first() if exists: if exists.status selected: db.session.rollback() return jsonify(code409, msg已选过该课程), 409 exists.status selected # 退课后重选复用原记录 db.session.commit() return jsonify(code0, msg选课成功) try: db.session.add(Enrollment(student_idcurrent_user.id, course_idcourse_id)) db.session.commit() except IntegrityError: # 唯一约束兜底并发下极少数请求会走到这里 db.session.rollback() return jsonify(code409, msg请勿重复提交), 409 return jsonify(code0, msg选课成功)with_for_update()是这段代码的核心它会生成SELECT ... FOR UPDATE把这一行锁到事务提交为止并发请求在数据库层面排队容量判断才成立。代价是锁竞争所以锁的范围必须小——只锁课程行不能锁整张表。IntegrityError那个分支是双保险。就算锁没拦住比如有人绕过接口直接写库uq_enroll_student_course也会让重复插入失败捕获后返回友好提示而不是把 500 抛给前端。很多人省掉这个 try压测时偶尔冒一个 500 出来日志里只有一句 IntegrityError查起来很费劲。退课走的是状态更新同时要拦一个业务规则成绩已经录入的课不允许退student_bp.post(/enrollment/int:eid/drop) login_required role_required(student) def drop_course(eid): en db.session.get(Enrollment, eid) if en is None or en.student_id ! current_user.id: return jsonify(code403, msg无权操作), 403 if en.score and en.score.value is not None: return jsonify(code409, msg成绩已录入无法退课), 409 en.status dropped db.session.commit() return jsonify(code0, msg退课成功)en.student_id ! current_user.id这行是越权防护。前端传过来的 enrollment id 是可枚举的不做这层判断改个数字就能退别人的课这种漏洞在课程设计里特别常见。4.2 教师端成绩录入单条与批量两条路径成绩录入页的做法是表格里直接编辑提交时整行带上来。单条保存用于改错批量保存用于期末统一提交。批量接口要注意两点一是全成功或全失败不能改一半二是教师只能改自己教的课。teacher_bp.post(/score/batch) login_required role_required(teacher) def save_scores(): payload request.get_json(silentTrue) or {} rows payload.get(rows, []) if not rows: return jsonify(code400, msg没有需要保存的数据), 400 for row in rows: en db.session.get(Enrollment, row[enrollment_id]) # 关键校验这门课的授课教师必须是当前登录人 if en is None or en.course.teacher_id ! current_user.id: db.session.rollback() return jsonify(code403, msg存在无权修改的记录), 403 try: val round(float(row[value]), 1) except (TypeError, ValueError): db.session.rollback() return jsonify(code400, msg成绩格式不正确), 400 if not 0 val 100: db.session.rollback() return jsonify(code400, msg成绩超出 0-100 范围), 400 if en.score is None: db.session.add(Score(enrollment_iden.id, valueval, updated_bycurrent_user.id)) else: en.score.value val en.score.updated_by current_user.id db.session.commit() # 全部校验通过才提交一次事务 return jsonify(code0, msg保存成功)round(float(...), 1)是为了跟数据库Numeric(5,1)对齐。前端传88.55进来MySQL 会四舍五入成88.6如果视图里已经按原值做过平均分计算就会出现页面上显示的分数和统计结果对不上的情况学生来问的时候很难解释。批量导入 Excel 用 openpyxl 读逻辑跟批量保存一样区别是先整体解析成 list 再进事务from openpyxl import load_workbook def parse_score_excel(file_stream, course_id): wb load_workbook(file_stream, read_onlyTrue, data_onlyTrue) ws wb.active result [] # 第一行是表头学号 | 姓名 | 成绩 for row in ws.iter_rows(min_row2, values_onlyTrue): if not row or not row[0]: continue result.append({ username: str(row[0]).strip(), value: row[2], }) return resultread_onlyTrue对几百行的表影响不大但学生数上千的学校一次导入几千行内存占用差得很明显。data_onlyTrue是为了拿到公式的计算结果而不是公式字符串成绩表里经常有老师用公式算总评不加这个参数读进来就是B2*0.3C2*0.7。4.3 权限装饰器一处写对处处生效角色校验如果每个视图里手写if current_user.role ! teacher早晚有一处漏掉。抽成装饰器顺序上注意login_required必须在外面from functools import wraps from flask import abort from flask_login import current_user def role_required(*roles): def decorator(fn): wraps(fn) def wrapper(*args, **kwargs): if not current_user.is_authenticated: abort(401) if current_user.role not in roles: abort(403) return fn(*args, **kwargs) return wrapper return decorator装饰器顺序写反成role_required(teacher)在外、login_required在内未登录时current_user是匿名对象role属性不存在直接 AttributeError。这种错误只在没登录访问时出现自己测试时通常已经登录了很容易漏掉上线后才会被人踩出来。5. 运行与排错从本地跑通到多角色登录的常见问题排查代码能看懂和能跑起来是两件事。这一章按真实启动顺序走一遍再把几个高频报错摊开讲。5.1 从零启动的完整步骤先建虚拟环境别把依赖装进系统 Pythoncd school_edu python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt接着准备数据库用 root 登进去建库和账号注意字符集CREATE DATABASE school_edu DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER edu_user% IDENTIFIED BY 你的密码; GRANT ALL PRIVILEGES ON school_edu.* TO edu_user%; FLUSH PRIVILEGES;把密码写进环境变量别硬编码到 config.py 里跟着代码提交export SECRET_KEY$(python -c import secrets;print(secrets.token_hex(16))) export DB_PWD你的密码最后初始化数据并启动export FLASK_APPapp flask init-db flask run --host0.0.0.0 --port5000浏览器打开http://127.0.0.1:5000用初始化脚本里的管理员账号登录先建班级、再建教师和学生、最后开课程顺序不能反——课程要绑教师学生要绑班级倒着来会一路报外键错误。5.2 常见问题排查清单现象一启动就报ModuleNotFoundError: No module named MySQLdb。原因是 SQLAlchemy 连接串里写的是mysql://默认去找 MySQLdb 这个驱动而 requirements 里装的是 PyMySQL。解决方式是把连接串改成mysqlpymysql://显式指定驱动。别去装 mysqlclientWindows 上编译需要 Visual C 构建工具为一个驱动折腾半小时不值。现象二录入中文课程名之后页面显示成问号或者插入时报Incorrect string value。根因是库、表、连接三处字符集不一致。检查顺序是SHOW VARIABLES LIKE character_set%看服务端SHOW CREATE TABLE t_course看表再看连接串有没有?charsetutf8mb4。只改一处不够三层必须都对齐到 utf8mb4。老库里已经有数据的得ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4才能救回来。现象三登录成功后又跳回登录页无限循环。八成是SECRET_KEY每次重启都变。用secrets.token_hex()写在启动脚本里每次进程重启都是新值之前签发的 session 全部失效浏览器带着旧 cookie 来服务端验不过就当你没登录。开发时把它固定在一个 .env 文件里生产环境用环境变量注入且保持不变。另一种可能是SESSION_COOKIE_SECURETrue但你在 http 下访问浏览器不会回传 cookie本地调试记得关掉这项。现象四选课提交后报 500日志里是IntegrityError: Duplicate entry。说明撞上了uq_enroll_student_course唯一约束通常是用户连点了两下提交或者退课后重选时没走「复用原记录」那条分支直接又插了一条。处理方式是在视图里 try 住 IntegrityError 返回友好提示同时前端提交按钮点击后置灰两层都做。现象五页面上 CSS 全丢只剩裸 HTML。模板里引静态文件用了写死的/static/css/app.css而应用挂在子路径下部署时路径就错了。统一改成{{ url_for(static, filenamecss/app.css) }}让 Flask 自己拼。顺带检查一下templates/base.html里的导航链接是不是也用 url_for 生成的硬编码的href在换前缀后全会 404。5.3 上线前必须改的几处配置开发能跑不等于能对外。DEBUGTrue必须关调试模式下出错页会把源码片段和变量值直接吐给访问者等于把数据库密码送到人眼前。app.run()换成 gunicorn 之类的 WSGI 服务器gunicorn -w 4 -b 0.0.0.0:8000 app:create_app(prod)多进程下要注意 session 别用内存存储。再给登录接口加个频率限制同一 IP 一分钟超过若干次失败就暂时拒绝教务系统里弱口令账号不少裸奔的登录接口很容易被撞库。6. 二次开发把通用教务系统改成自己学校能用的样子拿到这套代码之后最省事的改法不是推倒重写而是沿着已有的模型和蓝图往里加东西。我自己常做的第一个改造是学分上限校验学生一学期最多选 24 学分超了直接拒绝。这个规则放视图里最合适因为要读当前学期的所有选课记录MAX_CREDIT 24 def check_credit_limit(student_id, semester, add_credit): total db.session.query(db.func.coalesce(db.func.sum(Course.credit), 0)) \ .join(Enrollment, Enrollment.course_id Course.id) \ .filter(Enrollment.student_id student_id, Enrollment.status selected, Course.semester semester).scalar() return float(total) float(add_credit) MAX_CREDIT放在enroll视图里容量校验之后调用返回 False 就回滚并提示当前已选学分。coalesce那句不能省学生一门课都没选时sum返回 NULLfloat(None)直接抛异常第一条选课就失败这个坑我踩过。第二个改造是课表视图。现在的实现大概率只是一个按课程列出来的清单看不出时间冲突。想做得像样一点得给t_course加weekday、start_section、section_len三个字段选课时做时间区间重叠判断然后前端用一个 7 列 × 12 行的表格渲染每个格子按rowspan合并。冲突判断的核心就是两段区间是否相交not (a_end b_start or b_end a_start)写在 Python 里比写 SQL 清楚。第三个方向是往前后端分离上挪。把视图里的render_template换成jsonify登录态从 session 换成 JWT前端用任意框架接。工作量集中在两处一是所有login_required要换成 JWT 校验二是 CSRF 保护要去掉JWT 放 Header 里本身就不受 CSRF 影响。数据库层完全不用动模型和事务代码可以原样搬过去这也是当初把模型独立成包的好处。改造方向改动文件风险点学分上限views/student.pysum 为 NULL 的兜底时间冲突检测models/course.py、student.py旧数据缺时间字段需补默认值成绩单导出 PDFutils/excel.py 新增模块中文字体需显式注册换 JWTviews/auth.py、utils/decorators.py令牌过期与刷新策略验证改造有没有改坏我的做法是先跑一遍手工回归管理员建课、学生选课到满员、再选报满错误、教师录成绩、学生查成绩、导出 Excel 对一遍数字。这套流程十分钟能走完但每次改完模型或事务相关代码我都会重走一遍——教务系统的数据错一次期末核对就是一场灾难。最后说个习惯。这套代码我每次部署到新环境都会先SELECT COUNT(*)数一遍五张核心表的行数再跑一遍选课和录成绩确认写链路通了才敢交给别人用。数据库这东西不像页面样式错了肉眼能看出来数据错了往往要等到期末统计才对不上那时候已经很难追溯是哪一步出的问题。希望你改这套代码的时候也把这条验证流程固定下来别嫌麻烦。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑