ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Flask+SQLite学生荣誉证书管理系统:从数据库设计到部署

Flask+SQLite学生荣誉证书管理系统:从数据库设计到部署 每年奖学金评定季各个学院的办公室基本都要经历一场Excel表格带来的灾难。几百条证书记录在多个文件里来回复制粘贴名字写法不统一、发证日期格式混乱、按条件统计时只能靠肉眼数行数。我去年帮一个学院做了这套基于Flask的学生荣誉证书管理系统用Python写成的轻量级Web应用部署在本地就能跑彻底治好了这个老毛病。这套系统适合谁用高校辅导员、院系教务老师、学生工作负责人以及所有想用Flask练手做web开发的初学者。它能完成学生信息维护、荣誉证书录入、多条件查询、级别筛选、Excel导出、打印预览这些核心功能。整个项目代码量不大但一个小型管理系统该有的东西几乎都覆盖了而且完全本地部署数据不出办公室不依赖任何外部服务。我做这类项目有一个原则先解决真实场景里的痛点再谈技术选型。下面就把这套系统的完整实现思路、数据库设计、核心代码和部署过程中的坑都摊开来讲希望你能少走弯路。1. 为什么放着现成系统不用非要自己写一个市面上成熟的学生管理系统并不少但落到学院内部管理荣誉证书这个具体场景时往往都不合适。要么是收费的商业系统功能大到根本用不完要么部署在云端数据安全和管理责任都说不清更多的其实是学工系统里附带的一个模块字段固定、无法自定义想按校级、省级筛选还得找开发方提需求一等就是几个月。Excel虽然大家都在用但多人协作、数据校验、按条件筛选这些事做起来非常痛苦。证书一多、年份一长表格就失控了。还有人把数据存在微信聊天记录里换个人就断档。我的需求其实非常朴素就六条单机或局域网内直接运行不要复杂的部署流程能录学生、录证书删改要方便支持按姓名、学号、证书级别、年份组合查询评奖评优时要能快速导出名单打印证书清单时页面要好看、格式要整齐数据不能丢备份要简单选型时我没怎么纠结直接定了Python Flask SQLite这套组合。原因很直接Flask足够轻一个小项目几十个路由写完不需要框架替你决定代码结构SQLite零配置数据库就是一个本地文件备份时直接拷贝走人。这套组合对学院级的数据量——几千名学生、上万条证书记录——性能绰绰有余。我也对比过其他方案。Django功能全自带Admin后台和完整ORM但项目体积和学习曲线都偏重杀鸡用牛刀。Node.js和PHP也能做不过对非计算机背景的维护者来说Python的语法和生态更友好后面想改需求拉个懂Python的人来也更容易。Electron桌面应用方案我也考虑过打包体积大而且本质还是Web技术不如直接用浏览器访问来得轻量自然。这里必须说一个核心判断这套系统的最终用户大概率不是程序员。辅导员、班主任不会敲命令行也不懂环境变量怎么配。所以所有设计都要围绕非技术用户也能操作来展开——界面要直观、操作要少、反馈要明确。这也直接影响了我后面写一键启动脚本、简化部署步骤的方式。2. 数据库怎么设计才够用又不返工很多初学Flask的人上手就写路由、写页面数据库设计拖到最后随便弄这是典型的本末倒置。数据库是整个系统的地基字段没想清楚后面改表结构会让你改到怀疑人生。我在这套系统上第一步就是定表结构前后用了两个晚上反复推演才把表设计敲定。我最后的学生表设计如下字段类型说明idINTEGER 主键自增IDstudent_noVARCHAR(20)学号唯一索引nameVARCHAR(50)姓名class_nameVARCHAR(50)班级majorVARCHAR(80)专业enrollment_yearINTEGER入学年份证书表设计如下字段类型说明idINTEGER 主键自增IDstudent_idINTEGER 外键关联学生表idcert_nameVARCHAR(120)证书名称cert_levelVARCHAR(20)级别国家级/省级/市级/校级/院级/其他issue_dateDATE发证日期issuing_unitVARCHAR(120)发证单位cert_noVARCHAR(80)证书编号remarkTEXT备注这个设计有什么讲究每一点都是我实际踩过坑之后换来的。学生和证书必须拆成两张表。这是最基本的表结构规范化。如果为了省事全塞进一张表同一个学生有三张证书他的姓名学号就得重复三行将来改一次班级信息要同时改三行甚至更多漏改一处数据就矛盾了。拆开后学生的公共信息只存一份证书通过student_id外键关联这是关系型数据库最经典的建模思路。学号必须唯一。现实中哪怕学生重名学号也一定不同所以学号是比姓名可靠得多的身份标识。在SQLAlchemy里给student_no加uniqueTrue录入时重复会直接报错拦截。这个约束必须在数据库层面做不能只靠应用层判断不然并发提交时很容易绕过检查。级别字段用固定下拉框。评奖评优时按证书级别加权计分是刚需如果允许自由填写国家国家级国奖三种写法都会出现在库里统计时头都大。我在表单里直接用下拉框限定几个选项国家级、省级、市级、校级、院级、其他。宁可损失一点灵活性也要保住数据的干净整齐。发证日期用DATE类型而不是字符串。SQLite对类型约束相对宽松但业务代码里用日期类型能保证后续按年份筛选、按月排序时不翻车。前端表单用input typedate用户直接在日历控件里选后端统一格式化成标准日期字符串存储。如果图省事存成2024年3月这种字符串后面你想按年份分组统计就等着自己写解析逻辑吧。这套设计是够用且不返工的平衡点。我没有做单独的权限表因为使用场景就是办公室局域网内几个人用信任度较高不需要角色体系。也没有做证书图片上传因为打印证书清单所需的文字信息已经全覆盖了。如果你要把系统放到全校范围用那就必须加用户表、登录验证、操作日志和备份策略这是后话。3. Flask项目骨架与实践目录结构、配置和启动这一节直接给你一套我验证过可直接复用的项目结构照着搭就行。cert-system/ ├── app.py # Flask应用入口所有路由 ├── models.py # SQLAlchemy模型定义 ├── requirements.txt # 依赖清单 ├── start.bat # Windows一键启动脚本 ├── certs.db # SQLite数据库文件首次运行自动生成 ├── static/ │ ├── css/ │ │ ├── style.css │ │ └── print.css │ └── js/ │ └── main.js └── templates/ ├── base.html ├── index.html ├── students.html ├── student_form.html ├── certs.html ├── cert_form.html ├── cert_detail.html └── stats.html依赖清单精简到极致就三样flask2.3.3 flask-sqlalchemy3.1.0 openpyxl3.1.2flask我锁了2.3.3版本因为这是我长期使用的稳定版本插件兼容性都已经验证过。openpyxl是后面Excel导出功能要用的先装好。models.py里定义两个模型代码是这样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, indexTrue) class_name db.Column(db.String(50)) major db.Column(db.String(80)) enrollment_year db.Column(db.Integer) certificates db.relationship(Certificate, backrefstudent, lazydynamic) def __repr__(self): return fStudent {self.student_no} {self.name} class Certificate(db.Model): __tablename__ certificates id db.Column(db.Integer, primary_keyTrue) student_id db.Column(db.Integer, db.ForeignKey(students.id), nullableFalse, indexTrue) cert_name db.Column(db.String(120), nullableFalse) cert_level db.Column(db.String(20), nullableFalse) issue_date db.Column(db.Date) issuing_unit db.Column(db.String(120)) cert_no db.Column(db.String(80)) remark db.Column(db.Text) created_at db.Column(db.DateTime, defaultdatetime.now) def __repr__(self): return fCertificate {self.cert_name} {self.cert_level}这里有两个容易忽略但很重要的点。一是db.relationship的backrefstudent这个反向引用让我们能通过cert.student.name直接拿到学生的姓名省去手动join查询。二是indexTrue加在student_no、name、student_id这些常用作查询条件的字段上数据量上来之后查询性能会明显更好——几千条数据时感觉不到区别但养成良好的加索引习惯对任何规模的项目都有价值。app.py入口的初始化方式from flask import Flask, render_template, request, redirect, url_for, flash, send_file from models import db, Student, Certificate import io from datetime import datetime from openpyxl import Workbook from openpyxl.styles import Font from openpyxl.utils import get_column_letter app Flask(__name__) app.config[SECRET_KEY] please-change-me-to-a-random-string app.config[SQLALCHEMY_DATABASE_URI] sqlite:///certs.db app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False app.config[SQLALCHEMY_ENGINE_OPTIONS] { connect_args: {timeout: 10} } db.init_app(app) with app.app_context(): db.create_all()db.create_all()必须放在app.app_context()里这是flask-sqlalchemy 3.x的标准初始化方式。老版本可以直接在模块级别调用新版本就必须显式创建应用上下文这是初学者最容易踩的坑——运行后一直报No such table排查半天才发现是因为create_all()根本没执行。路由的写法遵循标准的CRUD模式。以证书录入为例我贴一段完整代码app.route(/certs/add, methods[GET, POST]) def add_cert(): if request.method POST: student_id request.form.get(student_id) cert_name request.form.get(cert_name) cert_level request.form.get(cert_level) issue_date_str request.form.get(issue_date) issuing_unit request.form.get(issuing_unit) cert_no request.form.get(cert_no) remark request.form.get(remark) if not student_id or not cert_name or not cert_level: flash(学生、证书名称、级别为必填项) return redirect(url_for(add_cert)) issue_date None if issue_date_str: try: issue_date datetime.strptime(issue_date_str, %Y-%m-%d).date() except ValueError: flash(日期格式不正确) return redirect(url_for(add_cert)) cert Certificate( student_idint(student_id), cert_namecert_name, cert_levelcert_level, issue_dateissue_date, issuing_unitissuing_unit, cert_nocert_no, remarkremark ) db.session.add(cert) db.session.commit() flash(证书添加成功) return redirect(url_for(cert_list)) students Student.query.order_by(Student.student_no).all() return render_template(cert_form.html, studentsstudents)后端校验这段一定要写。虽然前端HTML里已经加了required标记但后端必须再校验一次——用户不一定通过页面提交直接构造请求绕过前端校验的事太常见了。后端校验做得好不好直接影响数据质量。日期处理是个隐蔽的坑。表单如果传了空字符串datetime.strptime(, %Y-%m-%d)会直接抛异常所以我先判断if issue_date_str再转换。很多人一眼觉得这逻辑多余直到自己提交空日期时页面直接500才反应过来。这类细节就是开发经验值在起作用。证书列表页的查询我实现了三维度组合筛选app.route(/certs) def cert_list(): keyword request.args.get(keyword, ).strip() level request.args.get(level, ) year request.args.get(year, ) query Certificate.query.join(Student) if keyword: like f%{keyword}% query query.filter(db.or_( Student.name.like(like), Student.student_no.like(like), Certificate.cert_name.like(like) )) if level: query query.filter(Certificate.cert_level level) if year: query query.filter(db.extract(year, Certificate.issue_date) int(year)) certs query.order_by(Certificate.issue_date.desc()).all() total query.count() return render_template(certs.html, certscerts, totaltotal, keywordkeyword, levellevel, yearyear)db.or_把多个LIKE条件合并成一个逻辑或让用户输入一个关键词就能同时匹配学生姓名、学号或证书名称。年份筛选用的db.extract(year, ...)是跨数据库安全的标准写法不要在SQLite里用字符串拼接函数来做换个数据库就失效了。开发时我用的是VSCode Python插件配好virtualenv后F5就能启动调试非常顺手。如果你是新手建议先花半小时把VSCode的Python环境配置调好再开始动手写代码能省下后面大量排错时间。4. 前端页面骨架与体验细节不需要前端框架也很能打这个系统的使用者是办公室老师不是专业前端所以我的原则是界面简单、反馈清晰、操作最少。我选了Bootstrap 5的CDN来搭页面框架没有引入Vue、React或任何工程化工具。为什么不用前端框架因为这类内部管理系统的核心是表单和表格全是服务端渲染的Jinja2模板就够用了。你用Vue ElementUI当然更强但项目复杂度会指数级上升——打包、路由、状态管理、跨域每样都要学最后你可能花了三天搭环境一天写业务性价比太低。base.html是模板继承的根基我把它当作全站的公共壳!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 href{{ url_for(static, filenamecss/style.css) }} relstylesheet {% block head_extras %}{% endblock %} /head body nav classnavbar navbar-expand-lg navbar-dark bg-primary div classcontainer-fluid a classnavbar-brand href{{ url_for(index) }}荣誉证书管理系统/a div classnavbar-nav a classnav-link href{{ url_for(student_list) }}学生管理/a a classnav-link href{{ url_for(cert_list) }}证书管理/a a classnav-link href{{ url_for(stats) }}统计看板/a /div /div /nav div classcontainer mt-4 {% with messages get_flashed_messages() %} {% if messages %} {% for message in messages %} div classalert alert-success alert-dismissible fade show rolealert {{ message }} button typebutton classbtn-close>link relstylesheet href{{ url_for(static, filenamecss/print.css) }} mediaprintprint.css的内容media print { .no-print { display: none !important; } .container { width: 100%; max-width: none; } body { background: #fff; } .cert-box { border: 2px solid #333; padding: 48px; margin: 0 auto; max-width: 780px; font-family: SimSun, 宋体, serif; } .cert-box h2 { text-align: center; margin-bottom: 24px; } .cert-meta { line-height: 2.2; } }页面显示时证书详情照常展示所有内容用户点打印预览按钮后浏览器会套用media print的规则把导航栏、操作按钮全部隐藏只保留证书信息和样式化的外框。这就是最轻量的生成PDF方案——用户自己选择另存为PDF就得到了电子版。证书详情模板里打印按钮我加在.no-print容器里div classno-print mb-3 button onclickwindow.print() classbtn btn-primary打印预览/button a href{{ url_for(cert_list) }} classbtn btn-secondary返回列表/a /div div classcert-box h2荣誉证书/h2 div classcert-meta p证书名称{{ cert.cert_name }}/p p证书级别{{ cert.cert_level }}/p p学生姓名{{ cert.student.name }}/p p学号{{ cert.student.student_no }}/p p班级{{ cert.student.class_name }}/p p发证日期{{ cert.issue_date.strftime(%Y年%m月%d日) if cert.issue_date else — }}/p p发证单位{{ cert.issuing_unit or — }}/p p证书编号{{ cert.cert_no or — }}/p /div /divExcel导出的实现我用了openpyxl实际代码app.route(/certs/export) def export_certs(): # 复用与cert_list相同的查询逻辑获取certs keyword request.args.get(keyword, ).strip() level request.args.get(level, ) query Certificate.query.join(Student) if keyword: like f%{keyword}% query query.filter(db.or_( Student.name.like(like), Student.student_no.like(like), Certificate.cert_name.like(like) )) if level: query query.filter(Certificate.cert_level level) certs query.order_by(Certificate.issue_date.desc()).all() wb Workbook() ws wb.active ws.title 证书汇总 headers [学号, 姓名, 班级, 证书名称, 级别, 发证日期, 证书编号, 发证单位] ws.append(headers) for cell in ws[1]: cell.font Font(boldTrue) for c in certs: ws.append([ c.student.student_no, c.student.name, c.student.class_name, c.cert_name, c.cert_level, c.issue_date.strftime(%Y-%m-%d) if c.issue_date else , c.cert_no or , c.issuing_unit or ]) for i, header in enumerate(headers, 1): ws.column_dimensions[get_column_letter(i)].width max(12, len(header) * 2 4) buf io.BytesIO() wb.save(buf) buf.seek(0) return send_file( buf, as_attachmentTrue, download_name证书汇总.xlsx, mimetypeapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet )这段代码看着不难但里面藏着一个我排查了很久的坑第一次导出时Excel文件下载下来是损坏的。原因出在buf.seek(0)这行——openpyxl保存后文件指针停在末尾send_file从当前位置开始读读出来的自然是空内容。所以凡是把数据写入内存缓冲区之后再发送的用户都要记得先seek(0)。这个坑不写出来不知道还会坑多少人。导出文件名用中文也完全没问题Flask 2.x以后会自动处理download_name的编码不需要手动做URL编码或特殊转义。在统计看板页面我按证书级别做了汇总统计直接展示条形图。图形我用的是Bootstrap的进度条组件没引入图表库——几条色块就能直观看出哪个级别的证书占比高。需要更精细的图表时再接Chart.js但现阶段真没必要。6. 本机部署与局域网访问从开发机到辅导员电脑系统开发完成后我在自己电脑上跑得很欢轮到我交付给真正的用户时遇到的问题数量远超预期。下面每一条都是真金白银换来的经验。第一关就是Windows环境的Python安装问题。辅导员办公室的电脑完全没有Python环境。装Python本身不难但安装器里那个Add Python to PATH复选框默认是不勾的一旦漏勾命令行里敲python就报不是内部或外部命令写好的脚本完全跑不起来。这是我远程协助时遇到的最多的报错场景几乎可以确诊为Python环境配置第一坑。我的解决办法是给非技术用户做一键启动脚本。start.bat内容如下echo off cd /d %~dp0 python -c import flask 2nul if errorlevel 1 ( echo 正在安装依赖库请稍候... pip install -r requirements.txt ) python app.pycd /d %~dp0是切换到脚本所在目录避免用户从别的路径双击运行导致文件找不到。脚本先检查flask有没有装没装就自动装依赖装完再启动应用用户全程只需要双击一下。这里我把判断是否已安装用python -c import flask的退出码来判断比挨个检查包名清爽得多。应用启动时监听地址要改一下if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)host0.0.0.0意味着监听所有网络接口局域网内其他电脑可以通过这台主机的IP访问。假设办公室电脑的IP是192.168.1.8那别的老师浏览器里输入http://192.168.1.8:5000就能打开系统。注意debugTrue在这种交付场景必须关掉——调试模式开着等于把远程执行入口暴露在局域网里还有修改代码后自动重启的行为会干扰正常使用。第二个常见坑是SQLite数据库并发写。多个老师同时录入数据时SQLite会报database is locked。这个系统的并发量很小我用一个连接超时配置来缓解就是前面SQLALCHEMY_ENGINE_OPTIONS里那个timeout: 10。它表示如果数据库被占用最多等待10秒再尝试而不是立刻失败。对办公室场景实测这个配置非常稳完全够用。第三个问题是备份。所有数据都在certs.db这一个文件里我写了个说明让老师每周把数据库文件拷贝到共享盘或U盘就算备份。这方案听起来原始但比Excel散落在各个聊天记录里强太多。有些学院的老师用Excel管理学期初误操作覆盖掉上个学期的文件几个星期的录入全没了的例子我是亲眼见过的。数据库文件单文件备份反而是最简单可靠的方案。如果你的需求是要在更大范围内使用比如整个学院同时几十人操作那可以考虑用gunicorn Nginx部署在专用服务器上数据库换MySQL。好消息是SQLAlchemy的ORM层把这些都抽象好了业务代码几乎不用改只需要把SQLALCHEMY_DATABASE_URI换成MySQL的连接串再处理一下并发事务就行。这也是当初坚持用ORM而不是裸SQL的最大回报。部署完后建议做一次完整验证清空数据库文件重启应用、插入几条测试数据、导出Excel、打印预览、断网重连。全部跑通后再正式录入真实数据。这个验证步骤能提前发现大部分环境问题别等数据录了一半再折腾。7. 我对这类小型管理系统的几条实在建议写完这套系统我复盘出了几条经验。它们不一定适合所有项目但对Flask小项目来说应该挺有参考价值。第一功能一定要做减法。统计大屏、微信消息推送、批量图片导入、自动生成评语——这些功能都能做但第一版千万不要碰。内部管理系统的需求有一个规律用户想象中的很多功能真正上线后根本没人用。正确节奏是先做录、查、改、删、导出这五件事跑起来让用户实际试用根据真实反馈再加功能。我见过太多项目死在增强管理功能的路上。第二模型层一定要用ORM。即使只是SQLite也别裸写SQL。ORM的价值不仅在于省几行代码更在于你想换数据库、想加字段、想改关联关系时改造成本极低。我之前用裸SQL写过一个类似的工具加了几个条件的查询后SQL拼得像天书重构时才体会到ORM的甜头。第三模板继承是必须的。base.html把导航栏、公共CSS、flash提示、脚本引用都包好每个页面只写自己独有的{% block content %}全站一致性和后期维护都会轻松很多。我看到不少新手把同样的导航栏复制到七八个页面里后面改个链接要改七八处这种苦真没必要吃。第四字符编码问题要前置处理。从Python源码到数据库再到HTML模板全程统一UTF-8基本不会遇到乱码。开发工具、编辑器的默认编码都设成UTF-8数据库里已有的乱码数据基本都是源头导入时编码不对造成的导入前先确认原始文件的编码。这个习惯能让你少掉一大半头发。最后说一下后续扩展方向。这套系统目前没有登录功能如果你部署的环境网络不安全可以考虑加一层简单的登录验证——Flask-Login是现成的方案往现在的代码里接入也不算复杂。证书图片上传、批量录入、学院内多部门数据隔离也都可以做。但我的建议还是那句话先把当前这套用顺用户真的提了明确需求再去动代码否则你会一边写一边替用户设计他根本不需要的功能。对我个人来说这类内部工具的技术含量不算高但它解决了一个具体场景里反复出现的真实痛点——从用Excel翻找一下午到输入一个名字点回车所有证书记录一秒出来这种变化是实实在在的。你的学院或部门如果有类似的需求照着这套思路搭一个出来花的时间一定比你想象中少得多。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表