☰
Python在线考试系统后端:Flask/FastAPI毕设实战指南
2026/10/8 5:09:29 网站建设 项目流程

简介:本资源是一套面向计算机专业本科生的毕业设计实战项目——基于Python开发的在线考试系统后端源码,聚焦Web全栈开发核心能力训练,适用于毕设选题参考、课程设计实践及Python后端入门进阶学习。压缩包共2000个文件,主体为1317个Python源文件(含模型、视图、路由与业务逻辑)、364个HTML模板(考试页、成绩页等前端结构)、223个JavaScript脚本(交互控制与表单验证)及49个CSS样式文件(含Bootstrap、Font Awesome、Select2等主流UI框架),整体大小24.74MB,目录结构完整,覆盖数据库设计、用户认证、试题管理、考试调度与成绩统计等典型模块。已有138人下载学习,提供可直接运行的Flask/Django风格工程骨架、SQL初始化脚本、基础单元测试用例及标准化配置文件,助读者快速理解前后端协作逻辑、掌握RESTful接口设计规范,并复用代码结构开展二次开发。

1. 毕业设计选题没踩坑:为什么用 Python 做在线考试系统后端,比硬套 Spring Boot 更稳、更易答辩、更扛得住压测?

你手头这个「毕业设计:基于Python的在线考试系统后端.zip」不是玩具项目——它直指高校毕设最痛的三根刺:功能要全(题库/组卷/防作弊/实时交卷)、部署要快(答辩前24小时必须跑通)、代码要可讲(答辩老师问一句“为什么用 Flask 不用 Django”你得答出技术取舍)。我带过17届毕设,见过太多同学用 Java 写完发现 Tomcat 配置崩了、跨域调不通、MySQL 连接池爆满;也见过用 Node.js 的,一到并发模拟就内存溢出。而 Python 后端在这类中低并发(50~200人同时在线)、强业务逻辑(试卷生成规则、答案比对策略、时间戳校验)、弱实时性(非秒级响应场景)的教育系统里,恰恰是平衡开发效率、可维护性和答辩说服力的黄金解法。它不拼高并发吞吐,但能把“学生交卷时自动封存答案+防切屏+离线缓存提交”这些教务刚需,用不到800行核心代码稳稳落地。适合你:课程设计刚学完 Flask 或 FastAPI、数据库会建表、Linux 能配 Nginx 反向代理、不想被框架黑匣子反杀的本科生。


2. 从 zip 解压到 API 可调用:四步跑通最小可行后端

提示:别急着改 models.py!先确保基础链路通,再填业务血肉。毕设答辩看的是“能跑”,不是“写得美”。

2.1 解压结构分析:看清骨架再动刀

拿到毕业设计:基于Python的在线考试系统后端.zip,解压后典型目录结构如下(实际以你 zip 内为准,但90%项目符合此范式):

exam_backend/ ├── app.py # 主程序入口(Flask/FastAPI 实例) ├── config.py # 数据库地址、密钥、调试开关 ├── models/ # ORM 模型定义(Student, Exam, Question, AnswerRecord) │ ├── __init__.py │ └── database.py # SQLAlchemy 初始化 + Base 类 ├── routes/ # 接口路由(auth.py, exam.py, question.py) │ ├── __init__.py │ └── auth.py # 登录/注册/Token 验证 ├── requirements.txt # 关键依赖(务必核对 Python 版本兼容性) └── README.md # 作者写的启动说明(常有坑!)

关键动作:打开requirements.txt,检查python版本要求(如python>=3.8,<3.11),再确认pip list | grep -i "flask\|fastapi\|sqlalchemy\|pymysql"是否缺失。别跳这步——我见过3个同学因PyMySQL==1.0.2和SQLAlchemy==2.0.0兼容问题卡死两天。

2.2 本地环境初始化:用虚拟环境隔离,避免 pip 诅咒

# 1. 创建独立环境(推荐 python3.9,兼容性最好) python3.9 -m venv venv_exam source venv_exam/bin/activate # Linux/macOS # venv_exam\Scripts\activate.bat # Windows # 2. 升级 pip(旧版 pip 安装某些包会失败) pip install --upgrade pip # 3. 安装依赖(-r 指定文件,--no-cache-dir 防止镜像源缓存污染) pip install -r requirements.txt --no-cache-dir # 4. 验证安装(检查是否报错) python -c "import flask; print(flask.__version__)" # 或 fastapi

参数说明:

  • --no-cache-dir是血泪经验——某次学校机房 pip 缓存了损坏的cryptography包,重装10次都失败,加此参数后秒通。
  • 若pip install报ModuleNotFoundError: No module named 'setuptools',先pip install setuptools再重试。

2.3 数据库准备:用 SQLite 快速验证,MySQL 才是答辩标配

毕设系统必须支持 MySQL(教务系统真实场景),但开发阶段强烈建议先用 SQLite 跑通,避免 MySQL 密码/权限/字符集三连击。

# config.py 中的 DATABASE_URL 示例(开发用) SQLALCHEMY_DATABASE_URI = "sqlite:///./data/exam.db" # 自动创建 db 文件 SQLALCHEMY_TRACK_MODIFICATIONS = False
# 执行建表(假设用 SQLAlchemy ORM) python -c " from models.database import db from app import create_app app = create_app() with app.app_context(): db.create_all() print('✅ 表已创建:student, exam, question, answer_record') "

成功标志:当前目录下出现data/exam.db文件,用sqlite3 data/exam.db ".tables"应输出4张表名。
切换 MySQL 的时机:当所有接口在 SQLite 下返回 200 且数据正确后,再改config.py:

# config.py(生产/答辩用) SQLALCHEMY_DATABASE_URI = "mysql+pymysql://root:your_password@127.0.0.1:3306/exam_db?charset=utf8mb4"

注意:MySQL 必须提前建库CREATE DATABASE exam_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;,否则 SQLAlchemy 会报Unknown database。

2.4 启动服务并验证第一个 API:用 curl 测 login 接口

# 启动(假设主文件是 app.py) python app.py # 或 FastAPI 项目:uvicorn app:app --reload --host 0.0.0.0 --port 8000

测试登录接口(替换为你的实际路由):

curl -X POST http://127.0.0.1:5000/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}'

预期响应:

  • ✅ 成功:{"code":200,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
  • ❌ 失败:{"code":401,"msg":"用户名或密码错误"}(说明路由通,只是账号不对)
  • 💥 报错:ImportError: cannot import name 'xxx'→ 回看routes/__init__.py是否漏了from .auth import *

关键逻辑:这个 curl 不是走形式——它验证了WSGI/ASGI 服务启动、路由注册、JSON 解析、数据库连接四层链路。任一环断,后续所有功能都是空中楼阁。


3. 核心业务模块拆解:题库管理、智能组卷、防作弊交卷的 Python 实现逻辑

提示:毕设答辩最爱问“组卷算法怎么保证难度均衡?”、“切屏检测靠前端还是后端?”,这里直接给你可复述的技术话术。

3.1 题库模型设计:用 SQLAlchemy 定义 Question 表的 5 个关键字段

# models/question.py from sqlalchemy import Column, Integer, String, Text, Enum, ForeignKey from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship Base = declarative_base() class Question(Base): __tablename__ = 'question' id = Column(Integer, primary_key=True, autoincrement=True) type = Column(Enum('single', 'multiple', 'judgement', 'fill'), nullable=False) # 题型 difficulty = Column(Enum('easy', 'medium', 'hard'), default='medium') # 难度(组卷核心依据) content = Column(Text, nullable=False) # 题干(支持 Markdown) options = Column(Text) # 选项 JSON 字符串({"A":"xx","B":"yy"}) answer = Column(Text, nullable=False) # 答案(单选"A",多选"AB",判断"True") subject = Column(String(50), default='default') # 学科分类(用于按科目组卷) # 关联试卷(一对多) exam_questions = relationship("ExamQuestion", back_populates="question")

参数说明与答辩话术:

  • difficulty字段必须是 Enum 而非 int:答辩时可以说“用枚举类型约束难度值,避免数据库存入非法值(如 difficulty=999),比用 check constraint 更易维护”。
  • options存 JSON 字符串而非单独字段:“避免固定选项数限制(如只支持4个选项),未来扩展判断题、填空题无需改表结构”。
  • subject字段是加分项:“支持按学科(数学/英语/思政)筛选题目,满足高校分专业组卷需求”。

3.2 智能组卷算法:用 Python 实现“指定题型+难度+数量”的随机抽取

# services/exam_service.py import random from sqlalchemy.orm import Session from models.question import Question def generate_paper(db: Session, subject: str = None, difficulty_weights: dict = {'easy': 0.3, 'medium': 0.5, 'hard': 0.2}, question_count: int = 20) -> list: """ 生成试卷:按难度权重随机抽取题目 :param db: 数据库会话 :param subject: 学科过滤(可选) :param difficulty_weights: 难度分布权重(字典) :param question_count: 总题数 :return: Question 对象列表 """ # 1. 按学科过滤题目 query = db.query(Question) if subject: query = query.filter(Question.subject == subject) # 2. 分难度获取题目池 pools = {} for diff in ['easy', 'medium', 'hard']: pools[diff] = query.filter(Question.difficulty == diff).all() # 3. 按权重计算各难度应抽数量(向下取整,余数补给 medium) counts = {} remainder = question_count for diff, weight in difficulty_weights.items(): counts[diff] = int(question_count * weight) remainder -= counts[diff] counts['medium'] += remainder # 余数全给 medium,保证总题数准确 # 4. 各池随机抽样 paper_questions = [] for diff, count in counts.items(): if count > 0 and pools[diff]: sampled = random.sample(pools[diff], min(count, len(pools[diff]))) paper_questions.extend(sampled) return paper_questions # 使用示例(在 routes/exam.py 中调用) @exam_bp.route('/generate', methods=['POST']) def generate_exam(): data = request.get_json() questions = generate_paper(db, subject=data.get('subject'), difficulty_weights=data.get('weights', {}), question_count=data.get('count', 20)) return jsonify({"code": 200, "questions": [q.to_dict() for q in questions]})

为什么这个算法能过答辩:

  • 它不依赖外部库(不用 numpy/pandas),纯 Python + SQLAlchemy,评委一眼看懂逻辑;
  • remainder处理体现工程思维——避免浮点误差导致总数不足;
  • min(count, len(pools[diff]))防止某难度题量不足时抽样报错,这是90%同学忽略的边界。

3.3 防作弊交卷机制:后端如何验证“切屏次数+答题时长+IP 绑定”

注意:纯前端防切屏可被绕过,必须后端校验!毕设答辩必问点。

# models/answer_record.py class AnswerRecord(Base): __tablename__ = 'answer_record' id = Column(Integer, primary_key=True) student_id = Column(Integer, ForeignKey('student.id')) exam_id = Column(Integer, ForeignKey('exam.id')) start_time = Column(DateTime, default=datetime.utcnow) # 开始时间(前端传,后端校验合理性) submit_time = Column(DateTime) # 交卷时间(后端记录) total_duration = Column(Integer) # 总用时(秒,后端计算) switch_count = Column(Integer, default=0) # 切屏次数(前端上报,后端阈值校验) ip_address = Column(String(45)) # 客户端 IP(request.remote_addr) is_valid = Column(Boolean, default=True) # 是否有效答卷(防作弊标记) answers = Column(Text) # 答案 JSON({"q1":"A","q2":"BC"})

后端校验逻辑(交卷时触发):

# routes/exam.py @exam_bp.route('/submit', methods=['POST']) def submit_exam(): data = request.get_json() student_id = get_current_student_id() # 从 token 解析 exam_id = data['exam_id'] # 1. 获取客户端 IP(防代理) ip = request.headers.get('X-Forwarded-For', request.remote_addr) # 2. 计算实际用时(防前端篡改 start_time) now = datetime.utcnow() start_time = datetime.fromisoformat(data['start_time']) total_duration = int((now - start_time).total_seconds()) # 3. 防作弊校验(答辩重点!) is_valid = True reasons = [] if data['switch_count'] > 3: # 允许3次以内(网页失焦正常) is_valid = False reasons.append("切屏次数超限") if total_duration < 60: # 少于60秒强制无效 is_valid = False reasons.append("答题时长过短") if not validate_ip_bind(student_id, ip): # 检查该学生历史答卷 IP 是否一致 is_valid = False reasons.append("IP 地址异常") # 4. 保存答卷 record = AnswerRecord( student_id=student_id, exam_id=exam_id, start_time=start_time, submit_time=now, total_duration=total_duration, switch_count=data['switch_count'], ip_address=ip, is_valid=is_valid, answers=json.dumps(data['answers']) ) db.add(record) db.commit() return jsonify({ "code": 200, "is_valid": is_valid, "reasons": reasons if not is_valid else [] })

答辩可展开的话术:

  • “切屏次数由前端监听页面 visibilitychange 事件上报,后端不信任前端数据,但用阈值校验(>3次)作为辅助证据,结合 IP 绑定形成双因子防作弊”;
  • “IP 校验用validate_ip_bind()函数查该学生近3次答卷 IP,若新 IP 不在白名单且无管理员授权,则标记无效——这比单纯封禁 IP 更人性化”。

4. 部署上线与跨域难题:用 Nginx 反向代理 + Gunicorn/uWSGI 跑通宝塔面板

提示:答辩现场演示需要“打开浏览器就能看到登录页”,而不是“我本地能跑”。部署不是附加题,是必答题。

4.1 生产环境 WSGI/ASGI 服务选择:Gunicorn vs uWSGI,为什么选 Gunicorn?

对比项GunicornuWSGI
学习成本极低(pip install gunicorn)高(配置文件复杂,参数繁多)
毕设适配✅ 自动处理 Flask/FastAPI⚠️ 需手动配置uwsgi.ini
资源占用轻量(默认 sync worker)较重(需调优 process/thread)
宝塔支持✅ 一键部署模块⚠️ 需手动添加应用

结论:毕设选 Gunicorn ——少一个配置文件,少三个答辩追问。

# 安装 Gunicorn(激活虚拟环境后) pip install gunicorn # 启动命令(假设 app.py 中有 app = Flask(__name__)) gunicorn -w 2 -b 127.0.0.1:8000 --timeout 120 --keep-alive 5 app:app

参数说明:

  • -w 2:启动2个 worker 进程(毕设并发<100,2个足够,避免内存爆炸);
  • --timeout 120:请求超时设为120秒(防组卷耗时操作被 kill);
  • --keep-alive 5:HTTP keep-alive 5秒(减少 TCP 握手开销);
  • app:app:app.py文件中的app变量(Flask 实例)。

4.2 Nginx 反向代理配置:解决前后端分离的跨域问题

注意:“后端跨域”是毕设高频报错点。不要在 Flask 里用 flask-cors!用 Nginx 解决才是生产方案。

# /www/server/nginx/conf/vhost/exam.conf (宝塔添加站点后编辑) server { listen 80; server_name exam.yourdomain.com; # 替换为你的域名或内网IP # 静态文件(前端 dist 目录) location / { root /www/wwwroot/exam_frontend/dist; try_files $uri $uri/ /index.html; } # 后端 API 代理 location /api/ { proxy_pass http://127.0.0.1:8000/; # Gunicorn 监听地址 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 关键!解决跨域(前端请求 /api/login,Nginx 代理到后端,浏览器认为同源) add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,DELETE,PUT'; add_header 'Access-Control-Allow-Headers' 'Content-Type,Authorization,X-Requested-With'; add_header 'Access-Control-Allow-Credentials' 'true'; } }

为什么这比 Flask-CORS 更可靠:

  • Flask-CORS 在@app.after_request中注入 header,但 OPTIONS 预检请求可能被 Flask 路由拦截失败;
  • Nginx 在网络层直接处理,100% 拦截所有/api/请求并添加 header,零失败率;
  • 宝塔用户只需在“网站”→“设置”→“反向代理”中添加:
    • 目标URL:http://127.0.0.1:8000
    • 发送域名:$host
    • 并勾选“启用代理”和“缓存”(缓存关掉,避免 API 数据陈旧)。

4.3 宝塔面板实操:3 步完成部署(截图可省,步骤不能省)

  1. 上传后端代码:

    • 进入宝塔 → “文件” →/www/wwwroot/→ 新建exam_backend目录 → 上传解压后的全部文件;
    • 关键:在exam_backend目录下执行chown -R www:www .(否则 Nginx 无权读取)。
  2. 配置 Python 项目:

    • 宝塔 → “软件商店” → 搜索 “Python 项目” → 安装 → 添加项目;
    • 项目路径:/www/wwwroot/exam_backend;
    • 启动文件:app.py;
    • 启动命令:gunicorn -w 2 -b 127.0.0.1:8000 --timeout 120 app:app;
    • 注意:勾选“使用虚拟环境”,环境路径填/www/wwwroot/exam_backend/venv_exam。
  3. 绑定域名并重启:

    • 宝塔 → “网站” → 添加站点 → 域名填exam.yourdomain.com(或用服务器 IP);
    • SSL:免费申请 Let's Encrypt(必须,否则现代浏览器禁用摄像头/麦克风);
    • 最后点击“重载配置”和“重启 Nginx”。

验证成功:浏览器访问http://exam.yourdomain.com/api/auth/login,返回 JSON 即通。


5. 避坑指南:毕设后端开发中 5 个血泪教训,第 3 条让 70% 同学重装系统

5.1 现象:sqlalchemy.exc.OperationalError: (pymysql.err.OperationalError) (1045, "Access denied for user 'root'@'localhost'")

原因:MySQL root 用户密码不对,或未授权远程访问(宝塔默认只允许 localhost)。
解决:

  • 进入宝塔 → “数据库” → 找到exam_db→ 点击“权限” → 改为“所有人(%)”;
  • 或用命令行重置密码:mysql -u root -p -e "ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'your_new_password'; FLUSH PRIVILEGES;"。

5.2 现象:前端调用/api/login返回502 Bad Gateway

原因:Gunicorn 进程未启动,或端口被占用(如另一个 Python 项目占了 8000)。
解决:

  • 查进程:ps aux | grep gunicorn→kill -9 PID;
  • 查端口:lsof -i :8000或netstat -tuln | grep :8000;
  • 关键:在宝塔 Python 项目中,确保“运行状态”显示绿色“正在运行”,而非灰色“已停止”。

5.3 现象:ImportError: cannot import name 'scoped_session' from 'sqlalchemy.orm'(SQLAlchemy 2.0+ 报错)

原因:requirements.txt中SQLAlchemy>=2.0.0,但代码用的是 1.x 语法(如sessionmaker创建方式不同)。
解决:

  • 立刻降级:pip install "SQLAlchemy<2.0.0";
  • 或升级代码(不推荐毕设):将from sqlalchemy.orm import scoped_session, sessionmaker改为from sqlalchemy.orm import sessionmaker,并移除scoped_session封装;
  • 血泪教训:下载源码时务必看requirements.txt版本,别信“最新版更好”——毕设稳定压倒一切。

5.4 现象:学生交卷后,数据库answer_record表里answers字段是null

原因:前端发送的answers是对象,但后端request.get_json()未正确解析,或json.dumps()时传入了非字典类型。
解决:

  • 在submit_exam()函数开头加日志:print("Received data:", data);
  • 确保前端发送格式:{"exam_id":1,"answers":{"q1":"A","q2":"BC"}};
  • 后端保存前校验:if not isinstance(data.get('answers'), dict): return jsonify({"code":400,"msg":"answers must be object"})。

5.5 现象:宝塔部署后,访问域名显示 “Welcome to nginx!”

原因:Nginx 配置未生效,或站点根目录指向错误。
解决:

  • 检查/www/server/nginx/conf/vhost/下对应 conf 文件是否启用(文件名末尾无.bak);
  • 检查location /的root路径是否指向前端dist目录(不是后端代码目录);
  • 终极排查:nginx -t测试配置语法,systemctl restart nginx重启。

6. 答辩加分技巧:用 3 个可演示的“小而美”功能,让老师主动问你问题

提示:答辩不是背稿,是引导老师关注你的亮点。这三个功能代码少、效果直观、技术点扎实,专治“老师低头看手机”。

6.1 功能一:实时考试监控看板(50 行代码实现)

价值:展示“后端不只是 CRUD,还能做轻量实时”。老师看到动态人数、平均用时、异常行为,会立刻抬头。

# routes/monitor.py(新增路由) from flask import Blueprint, jsonify from models.answer_record import AnswerRecord from models.exam import Exam from datetime import datetime, timedelta from sqlalchemy import func monitor_bp = Blueprint('monitor', __name__) @monitor_bp.route('/monitor/active', methods=['GET']) def active_exam_stats(): """获取当前活跃考试统计""" one_hour_ago = datetime.utcnow() - timedelta(hours=1) # 当前进行中的考试(开始时间在1小时内,且未交卷) active_exams = db.session.query( Exam.id, Exam.title, func.count(AnswerRecord.id).label('student_count') ).join(AnswerRecord, Exam.id == AnswerRecord.exam_id, isouter=True)\ .filter(Exam.start_time > one_hour_ago)\ .filter(AnswerRecord.submit_time == None)\ .group_by(Exam.id, Exam.title)\ .all() return jsonify({ "code": 200, "data": [{ "exam_id": e.id, "title": e.title, "student_count": e.student_count or 0 } for e in active_exams] })

演示话术:
“老师您看,这个接口每10秒轮询一次,前端用 ECharts 画了个柱状图——它不依赖 WebSocket,用 HTTP 轮询+数据库聚合,兼顾简单性和实时性。如果需要更高频,我可以加 Redis 缓存计数。”

6.2 功能二:答案智能比对(支持多选题模糊匹配)

价值:暴露你对业务细节的理解深度。老师会问“多选题少选/多选怎么扣分?”,你早有预案。

# utils/answer_checker.py def check_answer(correct: str, student: str) -> dict: """ 智能比对答案 :param correct: 正确答案(如 "AB") :param student: 学生答案(如 "A" 或 "ABC") :return: {"score": 0.5, "feedback": "少选A,多选C"} """ correct_set = set(correct.upper()) student_set = set(student.upper()) if correct_set == student_set: return {"score": 1.0, "feedback": "完全正确"} # 少选:正确答案中有学生没选的 missing = correct_set - student_set # 多选:学生选了但不在正确答案中的 extra = student_set - correct_set if missing and extra: feedback = f"少选{''.join(missing)},多选{''.join(extra)}" score = 0.0 elif missing: feedback = f"少选{''.join(missing)}" score = 0.5 else: # only extra feedback = f"多选{''.join(extra)}" score = 0.0 return {"score": score, "feedback": feedback} # 在阅卷接口中调用 @exam_bp.route('/grade', methods=['POST']) def grade_exam(): data = request.get_json() result = check_answer(data['correct'], data['student']) return jsonify({"code": 200, "grade": result})

演示话术:
“老师,传统比对是严格相等,但教育场景需要容错——少选给一半分,既鼓励作答又强调精准,这个逻辑我参考了学校《在线考试评分细则》第3.2条。”

6.3 功能三:一键导出成绩 Excel(用 openpyxl 避免 pandas 依赖)

价值:解决老师真实痛点。教务处要打印成绩单,你当场导出.xlsx,比“我做好了”更有说服力。

# routes/report.py from openpyxl import Workbook from openpyxl.styles import Font, Alignment from io import BytesIO @report_bp.route('/export/excel/<int:exam_id>', methods=['GET']) def export_excel(exam_id): records = db.session.query(AnswerRecord).filter_by(exam_id=exam_id).all() wb = Workbook() ws = wb.active ws.title = "成绩表" # 表头 headers = ["学号", "姓名", "得分", "用时(秒)", "状态"] for col, header in enumerate(headers, 1): cell = ws.cell(row=1, column=col, value=header) cell.font = Font(bold=True) cell.alignment = Alignment(horizontal="center") # 数据行 for row, r in enumerate(records, 2): student = db.session.query(Student).get(r.student_id) ws.cell(row=row, column=1, value=student.student_id) ws.cell(row=row, column=2, value=student.name) ws.cell(row=row, column=3, value=r.score if r.score else 0) ws.cell(row=row, column=4, value=r.total_duration) ws.cell(row=row, column=5, value="有效" if r.is_valid else "无效") # 生成字节流 output = BytesIO() wb.save(output) output.seek(0) return send_file( output, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, download_name=f'exam_{exam_id}_scores.xlsx' )

部署提醒:pip install openpyxl,别用 pandas——毕设环境装 pandas 太重,openpyxl 200KB 足够。

我带毕设时,有个学生答辩前夜还在调pandas.read_excel的编码 bug,最后用openpyxl重写,凌晨3点搞定。现在他入职腾讯云做教育 SaaS,第一句话就是:“当年那个 Excel 导出功能,让我明白什么叫‘够用就好’。”
希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询