
简介Python毕业生信息审核系统源码是一套面向Python学习者与Web开发初学者的完整项目聚焦高校毕业生信息的录入、查询、审核与审批全流程亦可迁移至教育管理或人力资源招聘等需要批量信息审核的场景。项目基于Python Web框架构建涉及后端业务逻辑、接口测试、学生信息管理模块及中间件设计能够较为完整地展示信息管理系统从数据存储到审核流转的实现思路。压缩包共54个文件以45个py源文件为主辅以xml配置、gitignore、说明文档与项目配置文件整体约51KB目录结构清晰便于按模块拆解学习。目前已有123人学习下载。通过阅读和运行这份源码读者可以理解manage.py启动、接口调用、数据库建模、权限校验与日志记录等关键环节也可在Django/Flask框架及SQLite/MySQL等数据库配合使用中提升Web开发实操能力项目还包含api_test.py等测试入口便于对照数据流进行调试与二次开发适合作为课程设计或入门项目的参考资料。1. 毕业生信息审核系统先判断它值不值得你投入每年六月初教务和学工的老师就开始头疼几百个毕业生的信息核对表发下去收回来的Excel五花八门有些人改了一版再发一版到底哪一版是最终稿全靠人工记忆出了错也没法追溯是谁改的。毕业生信息审核系统源码解决的就是这个场景——把“盖章式审核”变成“流程化线上审核”学生自己提交个人信息与材料辅导员初审院系复审学校终审每一步都留操作日志。对Python入门或正在找课程设计题目的同学来说这个项目非常合适它有明确的业务边界、有权限模型、有文件处理技术栈又是最主流的Flask加SQLAlchemy能讲到答辩评委听得懂的深度。对高校信息化团队这套源码也可以作为通用审核流程的参考骨架。下文按一个能跑通的Flask实现来讲重点落在数据模型、状态机、权限守卫和五个高频踩坑点上。2. 毕业生信息审核系统的数据模型先设计表再写代码2.1 五张核心表把业务拆干净毕业生审核业务上其实就三类对象人、材料、动作。人包括学生和各级审核者材料是成绩单、就业协议、实习证明这类附件动作则是谁在什么时间做了什么决定。我一般不建一张大宽表硬塞而是拆成五张表users用户表、students学生主表、materials材料表、audit_logs审核日志表、dict_options字典表。dict_options放专业列表、材料类型这类可枚举值后期学校加专业时不用改代码。这里给出核心的models.py精简版class User(UserMixin, db.Model): __tablename__ users id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(60), uniqueTrue, nullableFalse, indexTrue) password_hash db.Column(db.String(128), nullableFalse) role db.Column(db.String(20), nullableFalse) # student/counselor/dean/admin real_name db.Column(db.String(50), nullableFalse) college_id db.Column(db.Integer, db.ForeignKey(colleges.id)) class Student(db.Model): __tablename__ students id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, db.ForeignKey(users.id)) college_id db.Column(db.Integer, db.ForeignKey(colleges.id)) student_no db.Column(db.String(20), uniqueTrue, nullableFalse, indexTrue) name db.Column(db.String(50), nullableFalse) major db.Column(db.String(80), nullableFalse) grade db.Column(db.String(10), nullableFalse) # 如 2025 届 phone db.Column(db.String(20)) email db.Column(db.String(120)) status db.Column(db.String(20), defaultdraft, indexTrue) class Material(db.Model): __tablename__ materials id db.Column(db.Integer, primary_keyTrue) student_id db.Column(db.Integer, db.ForeignKey(students.id), nullableFalse) type db.Column(db.String(30), nullableFalse) # score_sheet/agreement/internship filename db.Column(db.String(255), nullableFalse) stored_path db.Column(db.String(255), nullableFalse) uploaded_at db.Column(db.DateTime, defaultdatetime.now) class AuditLog(db.Model): __tablename__ audit_logs id db.Column(db.Integer, primary_keyTrue) student_id db.Column(db.Integer, nullableFalse, indexTrue) operator_id db.Column(db.Integer, nullableFalse) action db.Column(db.String(20), nullableFalse) # submit/approve/reject/revise comment db.Column(db.Text) old_status db.Column(db.String(20)) new_status db.Column(db.String(20)) created_at db.Column(db.DateTime, defaultdatetime.now)几点经验。student_no用String不用Integer是因为学号可能含前导零或字母后缀用数字类型会丢掉这些信息等数据导进去才发现主键对不上就晚了。status列加索引是给列表筛选用的毕业生审核列表页最常见的查询就是“按状态过滤”没有索引时数据量大起来会走全表扫描。password_hash字段存的是werkzeug生成的哈希串原始密码直接落库是这个系统里最不能碰的写法——源码一旦泄露就是所有账号裸奔。审核日志表单独建而不是在students表上加一堆时间字段原因在于一次审核记录可能被驳回多次再重新提交。用old_status和new_status把变更前后的状态都记下来比只记新状态好排查得多。出问题时可以精确回放这个学生的完整流转历史不用去猜“他是不是从草稿直接跳到了通过”。2.2 审核状态机为什么不干脆用布尔值很多第一版实现审核结果就是一个is_approved布尔字段。当时看着省事后面全是坑驳回原因写在哪学生改了又重新提交怎么记院系看到的过程和学校看到的过程不一样更关键的是布尔字段没法表达“待审核”“草稿”这些中间态。我一般把状态定为六个draft、pending、approved、rejected、revised、archived。revised表示被驳回后修改重新提交archived表示毕业归档。状态机的作用是给流转加约束把非法跳转挡在写数据库之前。有人觉得这是过度设计实际上这一小段代码能让你的统计口径从此不再靠猜。ALLOWED_TRANSITIONS { draft: [pending], pending: [approved, rejected], rejected: [revised, approved], # 驳回后修改重交也允许审核直接通过 revised: [approved, rejected], approved: [archived], # 终态只允许归档 } def transition(student, new_status): if new_status not in ALLOWED_TRANSITIONS.get(student.status, []): raise ValueError(f非法状态流转: {student.status} - {new_status}) old_status student.status student.status new_status return old_status这里我把驳回后的再次提交设成revised而不是直接回到pending。收益是审核员在列表里能一眼看出“这人是被驳回过又修改交来的”优先看修改说明不用点进去和第一次提交的混在一起。如果你觉得状态多把revised并入pending也可以但列表页就要靠最近一条audit_logs去推断“是否被驳回过”查询变复杂且慢。状态机里的非法流转检查要放在事务里执行校验、更新、写日志三步要么一起成功要么一起失败避免出现状态改过去了、日志没写上的半截数据。3. 跑通审核主流程三个Flask路由与批量操作3.1 学生提交与辅导员审核最小可跑通的两条链路表结构定了之后主流程其实就是两个核心动作学生提交、审核员决定通过或驳回。做成两个JSON接口前端页面只要在这两个接口上套表单就能跑。先看学生提交app.route(/api/students/int:student_id/submit, methods[POST]) login_required def submit_student(student_id): student Student.query.get_or_404(student_id) if student.user_id ! current_user.id: abort(403) try: old transition(student, pending) db.session.add(AuditLog( student_idstudent.id, operator_idcurrent_user.id, actionsubmit, old_statusold, new_statuspending, comment学生提交审核, )) db.session.commit() except ValueError as e: db.session.rollback() return {error: str(e)}, 400 return {status: student.status}, 200逻辑说明先检查归属再走状态机最后把“谁在什么时间提交了”写入审计日志。注意abort(403)和返回400的边界归属错误属于权限问题用403让前端明确感知“你没有权限操作这条数据”状态非法属于业务冲突返回400携带错误信息前端可以直接把error信息弹给用户。db.session.rollback()很关键事务失败后如果不回滚连接上挂着半截事务下一个请求复用连接时会出现莫名其妙的“改动还在事务里”。再看辅导员审核app.route(/api/audit/int:student_id, methods[POST]) login_required role_required(counselor, dean, admin) def audit_student(student_id): student Student.query.get_or_404(student_id) # 数据级权限审核员只能审本院系学生 if student.college_id ! current_user.college_id: abort(403) data request.get_json() action data.get(action) # approve / reject comment data.get(comment, ) if action not in (approve, reject): return {error: action 只能是 approve 或 reject}, 400 if action reject and not comment.strip(): return {error: 驳回必须填写原因}, 400 try: new_status approved if action approve else rejected old transition(student, new_status) db.session.add(AuditLog( student_idstudent.id, operator_idcurrent_user.id, actionaction, commentcomment, old_statusold, new_statusnew_status, )) db.session.commit() except ValueError as e: db.session.rollback() return {error: str(e)}, 400 return {status: student.status}, 200这里有两道检验角色是辅导员、院系归属一致。很多系统只做了第一道结果辅导员能审到别的院系去。归属校验拿当前登录用户的college_id和学生college_id比对是典型的数据级权限第4章细讲。驳回必填原因这个规则不只是体验问题——驳回不留痕学生看不懂为什么被打回第二天会重新提交一模一样的材料审核循环空转。3.2 批量审核与Excel导出毕业季效率的来源毕业季的审核是按批量算的一个辅导员手上有几十上百条待审记录一条条点再提交会点疯掉。常见做法是提供一个批量接口前端勾选一批学生统一执行通过或驳回。app.route(/api/audit/batch, methods[POST]) login_required role_required(counselor, dean, admin) def audit_batch(): data request.get_json() ids data.get(ids, []) action data.get(action) comment data.get(comment, ) if not ids or action not in (approve, reject): return {error: 参数不合法}, 400 done, skipped [], [] try: with db.session.begin(): for sid in ids: student Student.query.get(sid) if not student: skipped.append({id: sid, reason: 记录不存在}) continue if student.college_id ! current_user.college_id: skipped.append({id: sid, reason: 非本院系数据}) continue new_status approved if action approve else rejected old transition(student, new_status) db.session.add(AuditLog( student_idstudent.id, operator_idcurrent_user.id, actionaction, commentcomment, old_statusold, new_statusnew_status, )) done.append(sid) except ValueError as e: return {error: str(e)}, 400 return {done: done, skipped: skipped}, 200db.session.begin()把整个循环包进一个事务要么全部成功要么一处非法就整体回滚。这避免了尴尬场景500条里第37条状态异常前面36条已经提交审核员二次点击时发现数据变化了却不知道哪几条成功了。done和skipped分开返回前端能展示“成功XX条、跳过XX条、跳过原因”这个反馈很重要——不是黑匣子式的“操作完成”。导出Excel是审核系统的另一个标配功能学校层面要的是各专业通过率、未通过名单、材料缺失情况。统计汇总交给pandas很方便# 顶部 import pandas as pd from flask import send_file from io import BytesIO app.route(/api/export/approved, methods[GET]) login_required role_required(dean, admin) def export_approved(): students Student.query.filter_by(statusapproved).all() rows [{ 学号: s.student_no, 姓名: s.name, 专业: s.major, 班级: s.grade, 联系方式: s.phone, } for s in students] df pd.DataFrame(rows) summary df.groupby(major).size().reset_index(name通过人数) output BytesIO() with pd.ExcelWriter(output, engineopenpyxl) as writer: df.to_excel(writer, sheet_name通过名单, indexFalse) summary.to_excel(writer, sheet_name专业汇总, indexFalse) output.seek(0) return send_file(output, as_attachmentTrue, download_nameapproved_graduates.xlsx)pandas的groupby比手写SQL再拼Excel快得多。如果环境里没装一次性把pandas和numpy装齐——pip install pandas numpy openpyxl。numpy在这个项目里不是主场但它是pandas的地基漏装后启动时必报ImportError。导出接口记得挂dean/admin权限这类文件包含全部学生联系方式属于敏感数据给辅导员导出全院的名单在多数高校是不合规的。4. 三种角色的权限控制越权是这个系统最容易漏的地方4.1 角色守卫装饰器让没权限的人拿不到接口权限控制第一个层次是接口级控制哪些角色能调这个接口。最通用的方案是装饰器from functools import wraps from flask import session, abort def role_required(*roles): def decorator(fn): wraps(fn) def wrapper(*args, **kwargs): if session.get(role) not in roles: abort(403) return fn(*args, **kwargs) return wrapper return decorator用法就是在路由上挂一行role_required(counselor, dean, admin)。好处是权限逻辑集中在路由声明处读代码时一眼就知道接口的准入边界。session里存的是登录时写入的用户信息建议把user_id、role、college_id都放进去避免每个请求都去查一次数据库。配合登录接口app.route(/api/login, methods[POST]) def login(): data request.get_json() user User.query.filter_by(usernamedata.get(username)).first() if not user or not check_password_hash(user.password_hash, data.get(password)): return {error: 用户名或密码错误}, 401 session[uid] user.id session[role] user.role session[college_id] user.college_id return {role: user.role, name: user.real_name}, 200注意装饰器顺序login_required要放在role_required的上面外层因为role_required要读session里的role得先确保已经登录。顺序反过来未登录用户会先撞上403而不是401前端会误判成“已登录但无权限”跳转逻辑就乱了。这个细节特别容易被忽略等前后端联调时就会发现登录失效的提示一路通不到登录页。这里还有个常见误区有人把admin挂到所有接口上认为“管理员是超级角色什么都能干”。在审核系统里admin应该是配置管理员不是业务操作员。admin能建账号、配置流程但具体审核动作应该由辅导员和院系管理员操作否则审核记录里的operator_id是admin追溯起来说不清是哪个环节的人做的。4.2 数据级权限只有角色还不够的4个场景角色控制解决“能不能调接口”数据级权限解决“接口内部能不能操作这条数据”。最典型的四个越权场景。第一个学生A修改学生B的信息。路由参数是student_id实现里没做归属校验访问/api/students/3/submit就能改别人的档案。校验方式是提交接口里比对student.user_id和current_user.id或者干脆拒绝一切非draft状态的修改请求。第二个辅导员审核本院系以外的学生。这是跨院系越权。必须拿当前用户的college_id和待审学生的college_id做比对批量接口里也要逐个过滤而不是只看前端勾选结果。前端不传college_id后端自己查这一点很重要。第三个管理员篡改已归档记录。毕业归档后students记录应当不可变只能在archived之后走人工订正流程。这个约束靠状态机兜底但如果审核接口里没有校验“终态不可修改”仍然可能被直接改回pending。建议在更新前统一加一个断言if student.status in (approved, archived): abort(403)。第四个纵向越权比如辅导员试图直接终审通过自己带的班级。多数高校流程是辅导员初审、院系终审两级审核不能是同一个人。这靠状态机不够要按角色限制可流转方向counselor只能把pending变成rejected或revised后的继续驳回不能直接变成approvedapproved必须由dean角色来掉。数据级权限的检查位置放在路由函数内部、状态机调用之前而不是装饰器里。原因很简单装饰器拿不到具体的资源对象强行查库会把代码写得很绕。把归属校验写在具体接口里逻辑顺序一目了然也方便写单元测试。5. 避坑指南这套审核源码最常翻车的5个地方5.1 坑1SQLite并发写导致“database is locked”现象审核高峰时多个辅导员同时批量操作页面不定期报OperationalError: database is locked。原因SQLite的写锁是数据库级别的一次只允许一个写事务Flask默认连接池在并发写时容易超时报错。毕业季几十个辅导员同时在线操作这种场景很常见。解决课设演示用SQLite没问题真实部署第一件事就是把数据库换成MySQL或PostgreSQL改一下连接串SQLAlchemy的模型代码几乎不用动。如果坚持用SQLite开WAL模式能缓解在engine创建时执行PRAGMA journal_modeWAL配合短事务能扛住小规模并发。自测方法开两个终端同时跑批量审核脚本看哪个先抛锁就能判断你的并发模型是否安全。5.2 坑2日期格式不统一统计口径全乱现象导出Excel后发现部分学生的毕业年份变成数字串或空值专业汇总里人数对不上。原因前端表单里有的传2025-06-30有的传2025/6/30还有人传“2025年6月”。入库时SQLAlchemy能解析一部分解析不了的直接抛错或存成NULL。解决前端统一用date input后端接收时强制解析一次解析失败直接给400不放脏数据入库。解析函数不要自己手写正则用datetime.strptime白名单里只允许ISO8601一种格式。我见过最离谱的一版字段类型是String把“六月底”这种中文都存进去了。审核系统一旦涉及统计报表日期字段必须是日期类型字符串存日期是给自己上刑。5.3 坑3文件上传不校验类型路径穿越带来安全隐患现象上传材料后文件名变成一串乱码下载时文件名不对严重时有人上传了可执行文件。原因信任了客户端传来的filename。直接把filename拼路径遇到../这类内容就会被路径穿越利用。我在代码评审里见过真实案例上传接口可以覆盖服务器上任意路径的文件。解决不用原始文件名做存储路径用uuid重命名保存扩展名用白名单校验。安全片段如下import os from uuid import uuid4 ALLOWED_EXTENSIONS {pdf, jpg, jpeg, png, zip} file request.files[file] ext os.path.splitext(file.filename)[1].lower().lstrip(.) if ext not in ALLOWED_EXTENSIONS: return {error: f不支持的文件类型: {ext}}, 400 stored_name f{uuid4().hex}.{ext} file.save(os.path.join(UPLOAD_DIR, stored_name))uuid生成新文件名原始文件名只留存在数据库的filename字段下载时按需读取。既防路径穿越又避免重名覆盖。下载时用send_file取stored_pathdownload_name取数据库里存的原始filename不要信任用户新传的参数。exe、py这类文件一概不进白名单后端校验必须有前端校验只是体验。5.4 坑4批量审核事务边界不清处理到一半状态已改现象批量操作500条记录第37条状态不合法接口报错但前面36条已经改了。原因循环里每条commit一次事务边界切得太碎。解决把整个批量循环包进db.session.begin()任何一条失败就整体回滚。代价是“部分成功”无法保留但审核操作本来就该保持原子性。如果业务上确实要“部分成功”必须明确返回done和skipped列表并且前端明确提示“部分记录未处理”不能只报一个错误就完事。这个场景里事务边界不是一个性能问题是一个数据一致性问题优先级完全不同。注意db.session.begin()块内抛出的异常会让上下文管理器自动回滚不需要在except里再手动rollback否则事务状态会混乱。5.5 坑5状态流转没有日志出问题无法回放现象发现某个学生审核通过了但不知道是谁、什么时候、依据什么材料通过的。原因只更新students.status没有写audit_logs或者写了日志但不记录old_status。解决每次状态变更都写一条audit_logsold_status和new_status都存。这条日志既是审计追溯的依据也是统计分析的数据源。留存了完整日志后你可以算出平均驳回次数、驳回原因分布、哪些材料最容易缺失这些指标对教务优化流程非常有价值。如果没做这一步系统跑三个月后就是一团谁也说不清楚的黑匣子。6. 从“能跑通”到“能答辩/能交付”验证清单与两个进阶方向6.1 一份可以直接照做的验证清单交出去之前按这个清单过一遍。用三个浏览器分别登录学生、辅导员、管理员验证完整流程学生提交、辅导员驳回、学生修改再提交、辅导员通过、管理员归档。然后测权限学生访问审核接口返回403辅导员审别的院系返回403未登录用户访问导出文件返回401或302。最后做数据检验导出Excel后抽查三个人的信息和数据库里逐一对照核对统计口径。性能不用过度压测但至少用1000条测试数据跑一遍列表页确认状态筛选没有明显卡顿。6.2 两个进阶方向值得投入第一个方向是解耦审核规则。把“哪些材料必传、哪些专业需要实名认证、哪些情况必须走人工复核”做成可配置规则用字典表或JSON配置驱动而不是硬编码在路由里。学校调整政策时不用改代码这个点答辩时讲出来会让评委眼前一亮。第二个方向是换前后端分离架构Flask只出API前端用Vue。对毕设来说这是加分项能让老师看到你对接口设计和前后端协作的理解。注意跨域和登录态要早做设计不要等前后端联调时再翻车。跨域方案用Flask-CORS能快速解决登录态继续用session加Cookie如果走JWT则要处理token刷新逻辑。做这套系统时我自己栽过的坑就是一开始省事务、省状态机用一堆is_开头的布尔字段硬顶最后统计口径乱成一团回放审核记录全靠翻聊天记录。后来老老实实把状态机补上把每步变更写进日志系统的稳定性才真正立住。如果你也是从这类“看着简单”的项目起步希望这条路子能让你少走一点弯路也希望帮到你。本文还有配套的精品资源点击获取