每个专业都会遇到一个逃不掉的课设教务管理系统、图书管理系统、选课管理系统而我这届轮到的是“学生荣誉证书管理系统”。用Python和Flask来做这个人人都做过的管理系统有没有可能让它真正落地、让老师眼前一亮而不是又一个只能跑通Demo的“玩具系统”这篇文章我会把完整思路、数据库设计、前后端实现、图片上传以及部署注意事项全部拆开来讲希望能帮到正在为课程设计或毕设发愁的同学。1. 项目整体设计与思路拆解1.1 这类系统到底在解决什么问题先说说“学生荣誉证书管理系统”这个题目本身。经历过课程设计的人都懂管理系统类题目最大的问题不是技术难而是需求太泛。所谓荣誉证书管理拆开来看无非就是几件事学生信息要能维护荣誉信息要能录入证书要有地方存尤其是有图片或PDF扫描件的情况然后就是最常见的查询、修改、删除、统计。但真正让我决定把这个项目写成一篇完整博文的原因是很多同学把管理系统理解成了“CRUD堆积术”——在页面上放几个表单框能加数据能删数据就算完事。这其实偏离了管理系统的核心价值。一个合格的管理系统真正要解决的是这三个问题一是数据录入的规范性问题二是信息检索的效率问题三是数据存储的安全性尤其是文件类附件问题。举个例子如果没有系统一个学院的荣誉材料可能散落在各班辅导员的Excel表里命名格式五花八门“张三-国奖-2023.pdf”“李四优秀学生干部.jpg”什么都有。到了学年末汇总时全靠人工翻找这是真实存在的痛点。所以哪怕是个课程设计也值得认真对待。1.2 为什么选择Flask而不是Django或Spring Boot我知道很多人会问为什么不直接用DjangoSpring Boot不也挺好吗这里我说一下自己的选型逻辑仅代表个人观点。首先是Python生态本身非常适合这种轻量级应用。Flask最大的优势是“自由”它不像Django那样把项目结构、ORM、Admin后台都给你规定好而是只保留了核心的路由和视图机制剩下的模块数据库ORM、表单验证、登录认证你可以按需引入。这意味着你可以更灵活地控制项目规模写完不会有一堆用不上的代码。其次是上手成本。管理系统的核心业务并不复杂用Flask写一个单文件应用或简单分包的项目一两天就能把主体功能跑通。这对于时间紧张的课设党来说太重要了。反观Spring Boot和Vue的前后端分离架构虽然很“企业级”但如果只有一个人开发、时间只有两三周很可能在产品完成度上打折扣——前端路由、跨域配置、接口联调这些额外成本会占掉不少时间。当然Flask也不是没有缺点。它的自由也意味着约束少如果代码组织得不好很容易变成“屎山”。尤其是很多教程喜欢把所有路由写在一个app.py文件里二三十个路由挤在一起后期维护想哭。所以我在这个项目里特意用了Blueprint蓝图做模块拆分这个细节后面会专门讲。1.3 技术栈和核心依赖说明这个项目选择的是Flask 2.x SQLAlchemy SQLite的方案。为什么不用MySQL因为作为课程设计或毕设SQLite能让项目开箱即跑不用额外装数据库服务和配置账号密码。如果你演示的时候老师让你现场运行SQLite的优势就出来了——不需要任何外部依赖代码拉下来就能跑。如果你是作为毕业设计需要体现一定的企业级复杂度那换成MySQL也只是改动一个数据库连接字符串的事SQLAlchemy的ORM层不需要大改。前端这块我用的是Jinja2模板 Bootstrap 5。有人可能会说这都2024年了怎么不用Vue3理由很简单Flask自带的Jinja2模板渲染足够应付管理系统的需求而且省去了前后端分离带来的跨域、Token认证、接口文档、联调成本。模板渲染的好处是写起来直接一个HTML文件里可以同时处理页面结构和数据展示非常适合小团队或个人开发。如果你非要用VueFlask也可以作为纯API后端但我认为对于这种规模的项目模板渲染是效率最高的方案。主要依赖如下表所示依赖库版本建议用途说明Flask2.3.xWeb框架核心Flask-SQLAlchemy3.0.xORM数据库映射Flask-WTF1.1.x表单处理和CSRF防护Werkzeug2.3.x密码哈希、文件上传工具Flask自带依赖Pillow10.x图片校验和处理可选1.4 项目目录结构规划项目完成后我的目录结构是这样的honor_system/ ├── app.py # 应用入口注册蓝图 ├── config.py # 配置文件 ├── requirements.txt # 依赖清单 ├── models.py # 数据库模型 ├── forms.py # 表单类可选 ├── blueprints/ │ ├── __init__.py │ ├── auth.py # 登录认证相关 │ ├── student.py # 学生管理 │ ├── honor.py # 荣誉管理 │ └── dashboard.py # 首页统计 ├── templates/ │ ├── base.html # 基础模板导航栏 │ ├── index.html # 首页/仪表盘 │ ├── auth/ │ │ ├── login.html │ │ └── register.html │ ├── student/ │ │ ├── list.html │ │ └── edit.html │ └── honor/ │ ├── list.html │ ├── detail.html │ └── edit.html ├── static/ │ ├── css/ │ │ └── custom.css │ ├── js/ │ │ └── main.js │ └── uploads/ # 上传的证书附件存储目录 │ ├── honors/ │ └── avatars/ └── uploads/ # 运行时生成的临时上传目录 └── honors/有同学看到这个结构可能觉得复杂但实际上每个文件都很简短。models.py里只有两个核心模型app.py入口文件也就三四十行代码。这个结构的核心目的是让项目在功能变多时依然能保持清晰——这其实就是一个初级程序员和“调包侠”的分水岭。2. 核心功能模块与数据库设计2.1 功能模块梳理在动手写代码之前先把系统需要实现的功能模块划分清楚。我最后定下来的核心模块是认证模块管理员登录、退出登录。考虑到这只是内部管理系统我没有做注册功能管理员账号在初始化数据库时直接创建。这样设计是出于安全考虑——开放注册会带来不可控风险而内部系统通常不需要。学生信息管理学生的增删改查包含学号、姓名、专业、班级、入学年份等基础信息。这里我额外加了一个“在读状态”字段可以标记学生是“在读”还是“已毕业”这样在做历史数据统计时能灵活筛选。荣誉证书管理这是核心中的核心。一条荣誉记录包含学生外键关联、荣誉名称、荣誉级别院级、校级、省级、国家级、获得时间、颁发机构以及附件上传功能证书扫描件或照片。查询与统计支持按学生姓名、荣誉级别、获得时间范围进行组合查询。首页仪表盘展示核心统计指标学生总数、荣誉总数、各等级荣誉分布情况。我没有把“批量导入Excel”和“证书打印”这两个功能做进去。原因有两个一是时间有限二是这些属于锦上添花的功能应该在核心流程稳定之后再考虑。如果你时间充裕可以考虑作为扩展功能加进去后面我会提一些思路。2.2 数据库建模思路数据库设计是整个项目最值得花时间琢磨的部分。我见过太多人建表时图省事把学生信息和荣誉信息塞到一张表里结果一条学生有多条荣誉时要么冗余存储学生的重复信息要么就得用逗号分隔荣誉内容查询时头疼无比。正确的做法是拆分成两张主表加一张辅助表严格遵守数据库设计的基本范式。我的表结构设计如下。学生信息表students字段名类型约束/说明idInteger主键自增student_noString(20)学号唯一约束不可为空nameString(50)姓名不可为空genderString(10)性别majorString(100)专业class_nameString(50)班级enroll_yearInteger入学年份statusString(20)状态值为active在读或graduated已毕业created_atDateTime创建时间默认当前时间荣誉证书表honors字段名类型约束/说明idInteger主键自增student_idInteger外键关联students.id级联删除honor_nameString(200)荣誉名称honor_levelString(20)荣誉级别college/school/province/nationalaward_dateDate获得日期issuerString(200)颁发机构cert_fileString(255)证书附件路径可为空remarkText备注可为空created_atDateTime创建时间两张表通过student_id建立一对多的关系即一个学生可以有多条荣誉记录。在SQLAlchemy中关联代码如下from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() class Student(db.Model): __tablename__ students id db.Column(db.Integer, primary_keyTrue) student_no db.Column(db.String(20), uniqueTrue, nullableFalse, indexTrue) name db.Column(db.String(50), nullableFalse) gender db.Column(db.String(10), default未设置) major db.Column(db.String(100), default) class_name db.Column(db.String(50), default) enroll_year db.Column(db.Integer) status db.Column(db.String(20), defaultactive) created_at db.Column(db.DateTime, defaultdatetime.now) honors db.relationship(Honor, backrefstudent, lazydynamic, cascadeall, delete-orphan) def __repr__(self): return fStudent {self.student_no} {self.name} class Honor(db.Model): __tablename__ honors id db.Column(db.Integer, primary_keyTrue) student_id db.Column(db.Integer, db.ForeignKey(students.id), nullableFalse) honor_name db.Column(db.String(200), nullableFalse) honor_level db.Column(db.String(20), nullableFalse) award_date db.Column(db.Date, nullableFalse) issuer db.Column(db.String(200), default) cert_file db.Column(db.String(255), default) remark db.Column(db.Text, default) created_at db.Column(db.DateTime, defaultdatetime.now) def __repr__(self): return fHonor {self.honor_name}这里有几个设计细节值得注意。第一student_no设置了uniqueTrue和indexTrue。学号作为学生的天然唯一标识设置唯一约束可以在数据库层面防止重复录入。索引可以提高按学号查询的速度虽然数据量小的时候感觉不到但这是好习惯。第二honor_level字段没有用数字而是用字符串。有人可能会说用整数类型存级别1代表院级、2代表校级不是更省空间吗确实但用字符串的可读性更强而且这个字段的取值种类极少就四个级别哪怕不做枚举约束在应用层校验一下就够了。这个取舍没有绝对的对错但我觉得管理系统的可维护性比极端节省存储空间更重要。第三relationship里设置了cascadeall, delete-orphan这是最关键的一步。它表示当你删除一个学生时SQLAlchemy会自动删除该学生名下的所有荣誉记录。这样可以避免数据库出现“孤儿数据”——荣誉关联的学生已经不存在了但荣誉记录还在。如果你不用ORM而直接写SQL就得自己记得写删除子表的语句很容易漏。第四Honor模型的student反向引用用的是backrefstudent。这意味着从荣誉对象可以直接拿到对应的学生对象比如honor.student.name就是该荣誉所属学生的姓名。这在列表展示时非常方便不需要手动查询学生表。而Student模型中的honors关系用了lazydynamic这样访问student.honors时返回的是查询对象而非列表可以继续链式调用筛选方法比如student.honors.filter_by(honor_levelnational).all()。2.3 数据库初始化脚本每次从零开始部署项目时都需要先初始化数据库。因为模型和配置可能会变我建议用一个独立的初始化脚本而不是把建表逻辑写在app.py里。# init_db.py from app import app, db from models import Student, Honor with app.app_context(): db.create_all() # 检查是否已有管理员账号没有则创建 if not Admin.query.filter_by(usernameadmin).first(): from werkzeug.security import generate_password_hash admin Admin(usernameadmin, password_hashgenerate_password_hash(admin123)) db.session.add(admin) db.session.commit() print(默认管理员账号admin / admin123 已创建。) print(数据库初始化完成)运行方式是在项目根目录执行python init_db.py。强调一点生成环境部署时一定要记得修改默认账号密码。数据库初始化脚本里的默认密码是方便开发调试用的如果不改就部署上线相当于把大门钥匙挂在门上。3. Flask核心功能实现与实操过程3.1 应用入口与配置管理app.py是整个应用的心脏。在这里创建Flask实例、加载配置、注册蓝图、初始化数据库扩展。我的代码如下from flask import Flask, render_template from config import Config from models import db from blueprints.auth import auth_bp from blueprints.student import student_bp from blueprints.honor import honor_bp from blueprints.dashboard import dashboard_bp app Flask(__name__) app.config.from_object(Config) db.init_app(app) # 注册蓝图每个蓝图负责一个功能域 app.register_blueprint(auth_bp) app.register_blueprint(student_bp) app.register_blueprint(honor_bp) app.register_blueprint(dashboard_bp) app.errorhandler(404) def not_found(e): return render_template(404.html), 404 app.errorhandler(500) def internal_error(e): db.session.rollback() return render_template(500.html), 500 if __name__ __main__: app.run(debugTrue, host0.0.0.0, port5000)config.py文件里存储配置项import os class Config: SECRET_KEY os.environ.get(SECRET_KEY) or dev-key-please-change SQLALCHEMY_DATABASE_URI sqlite:///honor_system.db SQLALCHEMY_TRACK_MODIFICATIONS False # 上传文件配置 UPLOAD_FOLDER os.path.join(os.path.dirname(os.path.abspath(__file__)), uploads) MAX_CONTENT_LENGTH 16 * 1024 * 1024 # 限制上传最大16MB ALLOWED_EXTENSIONS {png, jpg, jpeg, gif, pdf}有几个配置项要特别说明。SECRET_KEY是Flask的签名密钥用于session加密和CSRF保护。开发阶段可以写死但生产环境一定要通过环境变量注入不要硬编码在代码里。MAX_CONTENT_LENGTH设置的是请求体最大长度避免用户上传超大文件把应用拖垮。16MB对于证书扫描件和照片来说足够了。ALLOWED_EXTENSIONS是允许上传的文件扩展名白名单。用白名单而不是黑名单是重要的安全习惯——宁可误伤一些格式也不给危险文件留入口。关于上传文件的安全性我多说一句。如果你打算部署到公网环境仅靠检查扩展名是不够的。更稳妥的做法是用Pillow库对图片进行重新解码后再保存这样即使文件内容里嵌入了恶意脚本经过重新编码后也会被清除。3.2 博客关键上传功能的坑与应对证书附件上传是这个项目里最容易出问题的功能也是很多教程不会仔细讲的部分。我在这里把完整实现和踩过的坑都写出来。上传表单的处理逻辑是这样的用户在新增荣誉记录时可以选择上传证书文件。文件会保存到uploads/honors/目录下文件名使用UUID重命名避免中文文件名或重名文件造成的路径问题。数据库里只存相对路径不存完整URL。import os import uuid from flask import request, flash, redirect, url_for from werkzeug.utils import secure_filename def allowed_file(filename): return . in filename and filename.rsplit(., 1)[1].lower() in current_app.config[ALLOWED_EXTENSIONS] def save_upload_file(file): 保存上传文件返回相对路径失败返回None if not file or file.filename : return None if not allowed_file(file.filename): raise ValueError(不支持的文件格式) # 获取扩展名 ext file.filename.rsplit(., 1)[1].lower() # 生成UUID文件名 filename f{uuid.uuid4().hex}.{ext} # 按年份分目录存储避免单目录文件过多 year_dir str(datetime.now().year) upload_path os.path.join(current_app.config[UPLOAD_FOLDER], honors, year_dir) os.makedirs(upload_path, exist_okTrue) file_path os.path.join(upload_path, filename) file.save(file_path) # 返回数据库存储的相对路径 return fuploads/honors/{year_dir}/{filename}这里我踩过一个很经典的坑secure_filename()处理中文文件名时会把中文全部过滤掉导致文件名变成一个空字符串或者只剩扩展名。比如用户上传一个“张三-国家奖学金.jpg”secure_filename()的结果可能是jpg或jpeg直接把文件名弄丢了。所以我干脆不用原始文件名做存储名而是直接用UUID重命名从根源上规避了这个问题。再加上按年份分目录存储也避免了一两年后一个文件夹里堆几千个文件的尴尬场景。文件保存后数据库里只记录相对路径。前端展示时再拼完整URLapp.route(/uploads/path:filename) def uploaded_file(filename): return send_from_directory(current_app.config[UPLOAD_FOLDER], filename)为什么不用Flask默认的static路由直接指向uploads目录因为这样就可以脱离静态文件夹的限制灵活控制访问权限比如后续给附件也加个登录校验。另外一个常见的坑是在开发环境中app.run(debugTrue)时上传的文件可能在编辑器中看不到实时更新因为Flask的debug模式会默认启动一个额外的reloader进程。这不是bug属于Flask开发模式下的已知特征不影响部署。3.3 使用蓝图拆分路由拒绝“屎山”第1章说过如果所有路由都写在app.py里代码会越来越臃肿。所以这里用蓝图Blueprint把不同功能的页面拆开。以honor.py为例看一个蓝图的完整结构from datetime import datetime from flask import Blueprint, render_template, request, flash, redirect, url_for, current_app from werkzeug.utils import secure_filename from models import db, Student, Honor import os import uuid honor_bp Blueprint(honor, __name__, url_prefix/honors) honor_bp.route(/) def list_honors(): page request.args.get(page, 1, typeint) per_page 10 # 处理查询参数 keyword request.args.get(keyword, ).strip() level request.args.get(level, ) query Honor.query.join(Student) if keyword: query query.filter( db.or_( Student.name.like(f%{keyword}%), Student.student_no.like(f%{keyword}%), Honor.honor_name.like(f%{keyword}%) ) ) if level: query query.filter(Honor.honor_level level) pagination query.order_by(Honor.award_date.desc()).paginate( pagepage, per_pageper_page, error_outFalse ) honors pagination.items return render_template(honor/list.html, honorshonors, paginationpagination)关键点有三处。第一是拼接查询条件时先构造一个query变量然后根据是否有查询参数来决定是否追加filter。这比写死多个分支的条件语句要优雅得多。db.or_支持跨表条件联合查询可以同时匹配学生姓名、学号和荣誉名称。第二是使用join(Student)做两个表的联表查询。如果不加join后面在Honor.query.filter()中直接用Student.name.like()是会报错的——因为Honor表本身没有name字段必须通过关联关系让SQLAlchemy知道如何联表。第三是使用Flask-SQLAlchemy内置的paginate()方法做分页。它会自动处理页码越界、计算总页数等逻辑。在模板中我只需要调用pagination.iter_pages()就能生成页码导航不用手写任何分页逻辑。新增荣誉的路由也贴出来重点展示表单处理逻辑honor_bp.route(/create, methods[GET, POST]) def create_honor(): if request.method POST: student_id request.form.get(student_id, typeint) honor_name request.form.get(honor_name, ).strip() honor_level request.form.get(honor_level, ) award_date request.form.get(award_date, ) issuer request.form.get(issuer, ).strip() remark request.form.get(remark, ).strip() # 基础校验 errors [] if not student_id: errors.append(必须选择学生) if not honor_name: errors.append(荣誉名称不能为空) if not honor_level: errors.append(请选择荣誉级别) if not award_date: errors.append(获得日期不能为空) if errors: for e in errors: flash(e, danger) return redirect(url_for(honor.create_honor)) # 处理日期字符串 try: award_date_obj datetime.strptime(award_date, %Y-%m-%d).date() except ValueError: flash(日期格式不正确, danger) return redirect(url_for(honor.create_honor)) # 保存记录 honor Honor( student_idstudent_id, honor_namehonor_name, honor_levelhonor_level, award_dateaward_date_obj, issuerissuer, remarkremark ) # 处理附件上传 file request.files.get(cert_file) if file and file.filename ! : try: cert_path save_upload_file(file) honor.cert_file cert_path except ValueError as e: flash(str(e), danger) return redirect(url_for(honor.create_honor)) db.session.add(honor) db.session.commit() flash(荣誉记录添加成功, success) return redirect(url_for(honor.list_honors)) students Student.query.filter_by(statusactive).order_by(Student.student_no).all() return render_template(honor/edit.html, studentsstudents, levelsHONOR_LEVELS)这里有一个很实用的细节request.form.get(student_id, typeint)Flask的这个type参数会自动做类型转换。如果前端传的student_id不是合法的整数会返回None而不是抛出异常这样就能用if not student_id统一做非空检查省去写一堆try-except的麻烦。3.4 用SQLAlchemy做统计查询首页仪表盘需要展示统计信息学生总数、荣誉总数、本月新增荣誉以及不同荣誉级别的分布情况。使用ORM的聚合函数可以实现得很好。from sqlalchemy import func dashboard_bp.route(/) def index(): student_count Student.query.count() honor_count Honor.query.count() recent_honors Honor.query.order_by(Honor.created_at.desc()).limit(5).all() # 各等级荣誉统计 level_stats db.session.query( Honor.honor_level, func.count(Honor.id) ).group_by(Honor.honor_level).all() # 每年荣誉数量趋势近5年 current_year datetime.now().year year_stats [] for year in range(current_year - 4, current_year 1): count Honor.query.filter(db.extract(year, Honor.award_date) year).count() year_stats.append({year: year, count: count}) return render_template( index.html, student_countstudent_count, honor_counthonor_count, recent_honorsrecent_honors, level_statslevel_stats, year_statsyear_stats )level_stats返回的结构是[(school, 23), (province, 8), (national, 3)]这样的元组列表。模板中前端展示直接遍历就行。如果你的项目打算做得稍微花哨一点可以在前端用Chart.js的饼图或柱状图来展示这些统计数据数据从后台传过去也很方便。3.5 前端页面模板解析前端用了Bootstrap 5的CSS框架整体页面风格是后台管理系统的经典布局顶部导航、左侧侧边栏、右侧主内容区。我用一个base.html作为整个站点的骨架模板其他的页面都通过Jinja2的模板继承来复用这个骨架。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{% block title %}学生荣誉证书管理系统{% endblock %}/title link hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.3.0/dist/css/bootstrap.min.css relstylesheet link hrefhttps://cdn.jsdelivr.net/npm/bootstrap-icons1.10.0/font/bootstrap-icons.css relstylesheet {% block head %}{% endblock %} /head body nav classnavbar navbar-expand-lg navbar-dark bg-primary div classcontainer-fluid a classnavbar-brand href{{ url_for(dashboard.index) }} i classbi bi-award/i 荣誉证书管理系统 /a button classnavbar-toggler typebutton>from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user login_manager LoginManager() login_manager.login_view auth.login login_manager.login_message 请先登录后再访问 login_manager.login_message_category warning class Admin(UserMixin, db.Model): __tablename__ admins id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(50), uniqueTrue, nullableFalse) password_hash db.Column(db.String(200), nullableFalse) name db.Column(db.String(50), default管理员) last_login db.Column(db.DateTime) created_at db.Column(db.DateTime, defaultdatetime.now) def set_password(self, password): self.password_hash generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) login_manager.user_loader def load_user(user_id): return Admin.query.get(int(user_id))需要登录验证的视图函数上直接用login_required装饰器即可。这个装饰器会自动检查用户是否已登录未登录则重定向到登录页。要注意的是login_manager.login_view必须和实际的登录路由一致否则会出现“登录接口找不到”的报错。使用密码哈希而不是明文存储密码这是一个最基本的常识。generate_password_hash默认使用的是pbkdf2:sha256算法并自动添加盐值安全性足够。虽然这个系统看起来只是内部管理系统但从一开始养成好习惯以后做任何Web项目都能少踩坑。4. 实操过程与核心环节实现4.1 环境搭建从头开始部署项目从零开始搭建整个项目环境并不复杂但很多初学者会在这一步卡住。我来写一下完整的部署流程。首先是安装Python。建议使用Python 3.9以上版本。Windows用户建议去官网下载安装包安装时务必勾选“Add Python to PATH”这个选项否则后面在命令行里敲python会提示找不到命令。然后创建虚拟环境。这一步强烈建议不要省因为不同项目的依赖版本可能会冲突# 在项目根目录下执行 python -m venv venv # Windows激活虚拟环境 venv\Scripts\activate # macOS/Linux激活虚拟环境 source venv/bin/activate激活成功后命令行前面会出现(venv)前缀代表当前处于虚拟环境中。接下来安装项目依赖pip install -r requirements.txtrequirements.txt文件内容如下Flask2.3.3 Flask-SQLAlchemy3.1.1 Flask-Login0.6.3 Flask-WTF1.2.1 Werkzeug2.3.7 Pillow10.1.0安装完成后初始化数据库并启动python init_db.py python app.py看到Running on http://127.0.0.1:5000的提示后浏览器打开这个地址用默认管理员账号登录即可。4.2 从页面到数据的完整流程示例为了让整个系统更直观我描述一个典型的使用场景添加一名新学生然后为其录入一条省级荣誉。第一步在“学生管理”页面点击“新增学生”填写学号2021010123、姓名王晓明、专业计算机科学与技术、班级计科2101班、入学年份2021提交后数据库新增一条记录。第二步进入“荣誉管理”页面点击新增在下拉框里选择学生“王晓明”填写荣誉名称全国大学生数学建模竞赛省级一等奖选择荣誉级别省级选择获奖日期2023-09-15颁发机构中国工业与应用数学学会上传证书照片点击保存。第三步回到首页仪表盘可以看到荣誉总数增加了1省级荣誉的统计数量加1最近荣誉列表的第一条就是刚录入的数据。这个流程看似简单但真正实现时涉及表单提交、数据校验、文件上传、数据库写入、页面刷新多个环节任何一环出错都会导致操作失败。所以我在开发时先确保纯粹的增删改查跑通再逐步加上图片上传、查询筛选、统计图表等增强功能。4.3 防火墙与跨网络访问问题有些同学在课程设计完成后希望在手机上或者另一台电脑上访问系统做演示此时直接访问http://127.0.0.1:5000是不行的因为这是本机回环地址。解决办法是在app.run()中设置host0.0.0.0这样Flask会监听所有网络接口同一局域网内的设备就可以通过你的IP地址访问。我的app.py里已经写好了host0.0.0.0直接运行即可。查看本机IP地址Windows用ipconfigmacOS和Linux用ifconfig或ip addr。但要注意Windows系统有时会弹窗提示“是否允许Python通过防火墙”这时候要点击“允许访问”否则外部设备无法连接。苹果电脑和Linux通常不会主动弹窗但如果连不上也可以检查一下系统防火墙的入站规则。4.4 日志记录与后端控制台输出对于管理系统来说保留操作日志是一个实用的功能。谁在什么时间录入了什么数据、修改了什么、删除了什么都可以记录下来。这个系统的操作日志不需要做到独立的日志模块直接用Python的logging在关键操作处打印就行。我在删除荣誉记录时额外记录了一条日志import logging logger logging.getLogger(__name__) honor_bp.route(/int:honor_id/delete, methods[POST]) login_required def delete_honor(honor_id): honor Honor.query.get_or_404(honor_id) student_name honor.student.name honor_name honor.honor_name # 同时删除对应的证书文件 if honor.cert_file: file_path os.path.join(current_app.config[UPLOAD_FOLDER], honor.cert_file) if os.path.exists(file_path): try: os.remove(file_path) except OSError as e: logger.error(f删除文件失败: {file_path}, 错误: {e}) db.session.delete(honor) db.session.commit() logger.info(f管理员删除荣誉记录: 学生[{student_name}] 荣誉[{honor_name}]) flash(删除成功, success) return redirect(url_for(honor.list_honors))get_or_404这个方法是Flask-SQLAlchemy提供的小工具如果传入的ID在数据库中不存在会直接返回404页面省去手动判断和flash的操作。文件删除失败时只记录日志、不影响数据库记录的删除因为文件冗余顶多是占点磁盘空间但数据库的数据一致性更重要。5. 常见问题与排查技巧实录5.1 数据库迁移与模型修改的坑开发过程中心血来潮会频繁修改模型字段这是最常见的问题来源。比如我一开始在Student模型里没设计status字段后来才补上。修改完成后你以为重跑db.create_all()就能更新表结构但实际上SQLAlchemy的create_all()只在表不存在时创建表不会修改已经存在的表结构。结果就是数据库里根本没有status这一列一查就报错。解决这个问题有三种方式。最简单粗暴的方式是删掉数据库文件重建把项目目录下的honor_system.db删掉重新执行python init_db.py。这适合还在开发阶段、不担心丢数据的情况。如果已经在数据库里录了大量测试数据比如为了演示准备了一天的录入数据不想删库就可以用Flask-Migrate来做迁移。这是一个基于Alembic的数据库迁移插件支持增删字段、修改字段类型等操作。pip install Flask-Migrate在app.py中注册from flask_migrate import Migrate migrate Migrate(app, db)然后依次执行flask db init flask db migrate -m add status field flask db upgrade这样就能在不丢失数据的情况下更新表结构。如果项目最终要交付建议从一开始就引入Flask-Migrate避免开发到后期发现要改字段却进退两难。5.2 图片上传后页面无法显示的排查这是课程设计中出场率极高的问题。症状是文件上传提示成功数据库里也能看到文件路径但页面上的图片就是404。排查步骤是这样的。第一步检查uploads目录下是否真的有文件生成。如果没有问题在保存逻辑如果有继续第二步。第二步检查浏览器开发者工具中图片的地址看它是否指向了你预期的路径。比如数据库存储的路径是uploads/honors/2023/xxxx.jpg但模板里写的却是/static/uploads/honors/2023/xxxx.jpg那肯定404。第三步确认你写了对应的路由来send_from_directory。我最后采用的方案是模板里获取附件路径后统一在前面加/并交给uploaded_file路由处理{% if honor.cert_file %} a href{{ url_for(honor.uploaded_file, filenamehonor.cert_file) }} target_blank img src{{ url_for(honor.uploaded_file, filenamehonor.cert_file) }} classcert-preview alt证书附件 /a {% endif %}url_for(honor.uploaded_file, filenamehonor.cert_file)会自动拼接出/uploads/honors/2023/xxx.jpg与路由中的路径一致。这里踩坑的关键在于相对路径和绝对路径的混淆以及url_for的传参格式。如果手动去拼字符串很容易出现前后不一致。5.3 SQLite数据库被锁的问题在开发时如果同时开着多个浏览器窗口可能时不时看到一条报错database is locked。这是SQLite的并发限制它只允许一个进程在同一时刻写入数据库。多个请求同时写入时后面的请求需要等待锁释放。解决办法有几种。最简单的是在config.py中加入连接超时配置SQLALCHEMY_ENGINE_OPTIONS { connect_args: {timeout: 15} }timeout单位是秒表示等待数据库锁的时间。如果15秒后仍然拿不到锁才会报错。如果你的并发请求量不大加上这个配置后基本不会再遇到database is locked问题。如果项目数据量特别大、并发访问量高那就不适合继续用SQLite了。可以切换到MySQL或PostgreSQLSQLAlchemy的ORM代码不需要大改只需要修改数据库URI即可。以MySQL为例SQLALCHEMY_DATABASE_URI mysqlpymysql://username:passwordlocalhost/honor_system?charsetutf8mb4对应需要安装pymysql依赖以及一条pip install cryptographyPyMySQL连接MySQL 8时会用到加密库。5.4 Windows下使用激活虚拟环境时遇到的问题Windows环境下运行python -m venv venv后激活虚拟环境的命令是venv\Scripts\activate。如果你用的是PowerShell有时会因为执行策略限制而报错“无法加载文件activate.ps1因为在此系统上禁止运行脚本”。解决办法是用管理员身份打开PowerShell先执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned然后重新激活。实在不行也可以绕过激活直接使用venv\Scripts\python.exe app.py来运行项目效果一样。这个问题的本质是Windows的安全策略对未签名脚本的默认限制理解原理后就不会被吓到。5.5 浏览器中数据字典显示为乱码如果你把数据库文件honor_system.db用DB Browser等工具打开发现中文字段全是乱码大概率是因为SQLite默认的编码是UTF-8而你的表创建时没有指定编码。解决办法是在config.py的URI后面加上?charsetutf8mb4如果是SQLite通常不需要额外设置。但如果乱码已经发生了最省事的方法还是重建数据库。这也是一个提醒开发过程中如果发现数据有问题尽早处理别等积累了上百条数据再来纠结。6. 项目优化方向与后续扩展思路6.1 增加Excel批量导入导出课程设计阶段系统只有手动录入数据工作量非常重。如果要显得更实用批量导入导出是非常重要的加分项。可以用openpyxl库实现学生信息和荣誉信息的Excel导入导出。导入时先读取Excel逐行校验数据格式再写入数据库。导出时把查询结果写入Excel文件通过send_file让用户下载。这个功能的实现难度不高但对于评审老师来说观感提升非常明显也会让系统更贴近真实办公场景。6.2 生成统计报表如果想让系统的数据更具可视化价值可以使用Chart.js、ECharts这类前端图表库。在首页仪表盘添加“各年级荣誉数量对比柱状图”、“各学院荣誉分布饼图”、“近五年荣誉数量趋势折线图”数据从后台的统计接口获取前端用Ajax请求再渲染。这个过程需要一点JavaScript功底但并不复杂。6.3 证书防伪造证书伪造是一个真实存在的痛点。如果试用“证书编号”来做唯一标识并在系统中记录证书的颁发日期和颁发机构第三方可以通过访问系统验证页面输入编号来查验证书的真伪。这个功能加上去整个系统的应用价值会有一个质的飞跃。6.4 对接企业微信或钉钉通知当新的荣誉被录入时系统自动给学生的辅导员发送消息通知。这个功能可以通过调用企业微信或钉钉的Webhook机器人来实现。技术实现上只需要在保存荣誉记录时发起一次HTTP请求即可但需要你有对应的应用权限。我在实际开发中测试过企业微信机器人的Webhook代码大约只有10行。但注意不要泄露Webhook地址否则别人可以随便向你推送垃圾消息。7. 写在最后的真心话折腾完这个项目我感触最深的一点是管理系统类题目最容易做也最难做好关键不在于你把“增删改查”写得有多流畅而在于你有没有真正站在使用者的角度去思考问题。一个辅导员要录入一个学生的荣誉他最需要的是什么是操作路径足够短、表单足够直观、不会再想“这个学号要不要加引号”这种问题。一个管理员在汇总全院荣誉时他最需要的又是什么是按条件快速筛选、能导出成Excel、能一眼看到全貌。代码写到最后SQL语句越来越简单反而是在模板代码里花的时间最多。调整按钮间距、优化表单布局、让错误提示更加友好——这些细节可能不会被老师单独加分但会让系统看起来“像个真实产品而不是课设作业”。如果让我给正在写管理系统的同学一个具体建议那就是先画清楚页面原型和数据流转图再动手写代码。不要迷信“先跑起来再说”。我这次是因为确实有明确需求驱动才没有在原型的阶段耗费太多时间。你如果要做的是毕业设计强烈建议先花一两天把模型设计和页面交互想透彻把表结构画出来确认没有逻辑漏洞再动工。这个时间投入会在后期让你少改一半代码。最后分享一个我在完成系统后用来“验收”自己的方法。我会问自己三个问题如果一个完全不熟悉这个系统的人第一次打开首页能不能在30秒内搞清楚这个系统是干什么的添加一条荣誉记录点击次数能不能控制在5次以内网页在手机上打开关键按钮会不会挤成一团这三个问题如果都过关了这个管理系统才算真正能做“交付”。