简介:本资源是一份面向高校计算机专业学生与Python初学者的学生成绩管理系统完整开发文档与配套代码,聚焦学业数据数字化管理场景,解决课程成绩录入、权限分级、统计分析与可视化展示等实际教学管理需求。压缩包共495个文件,约12.35MB,涵盖39个核心Python源码文件(含Django/Flask后端逻辑)、41个Vue前端组件(含多个.bak备份版,便于对比学习)、82个.zbak配置备份、161个SVG图标资源及批量bat运维脚本(如安装.bat、运行.bat),体现典型Web全栈项目结构与工程化实践路径。已有43人学习下载,读者可直接获取模块化系统架构设计说明、ER数据库模型图、RESTful API接口规范、部署运维指南及完整用户操作手册,尤其适合开展课程设计、毕业设计或Python Web开发能力进阶训练。 在学校里被这门课折磨过的朋友,对“学生成绩管理系统”这个名字一定不陌生。几乎每个计算机相关专业的课程设计、期末大作业里,都少不了它的身影。坦白说,这个项目虽然经典到有点“烂大街”,但正因为经典,它才特别适合用来检验你对Python基础语法、文件操作、数据库设计、界面开发甚至打包发布这一整条技术链路的掌握程度。
很多人拿到这个题目就开始闷头写代码,结果写完一个黑乎乎的、只能在命令行里加减数字的脚本,交上去被老师一句“没有界面、没有数据持久化、不符合工程规范”打回来。这篇文章我想从一个完整的“设计与实现”视角,把这个项目从头到尾拆开揉碎讲清楚。我会分享我实际做这个项目时采用的技术方案、数据库怎么设计、代码怎么组织、界面怎么布局,以及那些报告里不会写的坑和排查技巧。这篇文章适合正在做课程设计的学生、想练手Python综合项目的初学者,也适合想快速搭建一个带GUI的学生成绩管理工具的非计算机专业老师或教务人员。
1. 项目到底要做什么:需求分析先行
很多同学拿到题目就打开IDE写代码,写到一半发现功能缺了、数据存不住、逻辑乱了,最后全部推倒重来。这个项目的第一课其实不是Python,而是搞清楚“你到底要做什么东西”。
1.1 核心功能拆解
“学生成绩管理系统”这个名字听起来宽泛,但落到具体场景里,它要解决的问题其实非常明确:代替手工登记的Excel表格,让老师或教务人员可以高效地维护学生信息和成绩数据,同时能够快速完成查询、统计和排序。
我拆解功能时通常分成三个层次。基本层必须要有学生信息管理和成绩管理,就是增删改查,这是整个系统的地基。进阶层要有成绩的统计功能,比如计算平均分、总分、最高分最低分、不及格科目数,以及按单科或总分排名。加分项则是数据可视化,用图表直观展示成绩分布,以及把数据导出为Excel文件,这样老师可以拿着你的成果直接去存档或打印。如果你做的版本功能太单薄,答辩时很容易被老师追问到无话可说。
1.2 技术选型:为什么是Tkinter和SQLite
技术选型是设计文档里必须写清楚的部分,也是答辩时老师最爱问“为什么”的地方。我采用的方案是:Python + Tkinter + SQLite + Matplotlib。
先说Tkinter。它是Python标准库自带的GUI工具包。我知道很多人觉得Tkinter丑,但必须承认,在课程设计和快速开发这个场景下,它有三个不可替代的优势:不需要额外安装、打包体积可控、学习成本低。你写PyQt5当然更漂亮,但PyQt5的安装包几十兆,打包后exe体积会超过100MB,而且在别人的机器上运行时依赖问题一大堆。Tkinter做出来的界面虽然朴素,但通过合理的布局和配色,完全可以做到干净、整齐。
再说SQLite。我见过很多同学用文件或者列表来存数据,程序一关数据就没了,这是大忌。SQLite是Python内置的轻量级数据库,本质上就是一个文件,但支持完整的SQL语法。相比直接用CSV,SQLite最大的优势是支持并发读写和事务,而且数据量大时查询性能稳定。对于学生成绩这种单机、小数据量的场景,SQLite是绝对的主流选择。
至于Matplotlib,它是Python生态里最成熟的数据可视化库。虽然它默认的样式有点学术气,但设置中文字体和图表样式之后,效果完全能打。用它可以把成绩分布做成柱状图或饼图,这个功能会让你在答辩时加分不少。
1.3 系统整体架构设计
我设计这个系统时,没有把代码全部堆在一个文件里。虽然这种小程序写成一个单文件也能跑,但从“设计”的角度来看,拆分成模块化结构会显得你具备工程意识,也让后续维护和扩展轻松很多。
我的项目结构是这样的:
student_grade_system/ ├── main.py # 程序入口 ├── requirements.txt # 依赖清单 ├── database/ │ ├── __init__.py │ ├── db_helper.py # 数据库连接与初始化 │ └── student_dao.py # 学生数据访问对象 ├── models/ │ ├── __init__.py │ └── student.py # 学生、成绩实体类 ├── ui/ │ ├── __init__.py │ ├── login_window.py # 登录窗口 │ ├── main_window.py # 主窗口 │ ├── student_dialog.py # 学生信息编辑对话框 │ └── score_dialog.py # 成绩录入对话框 └── utils/ ├── __init__.py └── export_helper.py # 导出工具这里要说明一下,这样分层其实是在模仿MVC的思路。UI层只负责交互和显示,不直接操作数据库;数据访问层负责所有SQL操作;模型层定义数据结构和业务逻辑;工具层放一些与业务无关的通用函数。这样的好处是,如果之后你想换成MySQL,只需要改data层,UI完全不用动。这种“面向接口的设计”思维,在课程设计报告里写清楚,比堆功能更让老师认可。
2. 数据库与结构设计:先把地基打好
在写界面之前,我习惯先把数据库建好。数据结构决定了整个系统能做什么、不能做什么,也决定了后续很多功能实现起来是简单还是绕路。这个系统看起来只是“存成绩”,但实际上涉及学生基本信息、课程信息和成绩信息三类核心数据。
2.1 数据表设计思路
数据库设计的第一原则是“一张表只存一类实体”。我见过有人建一张大表,把学号、姓名、性别、专业、课程名、成绩全部塞进去。当时确实简单,但问题立刻就会暴露:如果这名学生选了5门课,他的姓名和性别就要重复存5次。如果有一天学生改了专业,你需要更新所有记录。这就是典型的“数据冗余”。所以,最标准的做法是把学生信息和成绩信息拆成两张表,通过学号进行关联。
我设计了三个核心实体:学生、课程、成绩。学生表存储学生的基本档案;课程表存储课程名称和学分;成绩表则记录某个学生修了某门课程得到了多少分,同时把每次考试的类型(比如平时、期中、期末)考虑进去。
还有一点容易被忽略,就是账户表。一个完整的系统应该有自己的登录功能,至少要有管理员账户的验证机制。我通常会在数据库里增加一张用户表,保存用户名和经过哈希加密的密码。
2.2 核心表结构定义
下面是本系统数据库初始化的SQL语句,我把它放在db_helper.py启动时执行,实现自动建表:
import sqlite3 import os DB_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'grade_system.db') def init_db(): # 确保数据目录存在 os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() # 用户表 cursor.execute(''' CREATE TABLE IF NOT EXISTS t_user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, role TEXT DEFAULT 'admin' ) ''') # 学生表 cursor.execute(''' CREATE TABLE IF NOT EXISTS t_student ( student_id TEXT PRIMARY KEY, name TEXT NOT NULL, gender TEXT, major TEXT, class_name TEXT, phone TEXT, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # 课程表 cursor.execute(''' CREATE TABLE IF NOT EXISTS t_course ( course_id TEXT PRIMARY KEY, course_name TEXT NOT NULL, credits REAL DEFAULT 0.0 ) ''') # 成绩表 cursor.execute(''' CREATE TABLE IF NOT EXISTS t_score ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, course_id TEXT NOT NULL, score REAL NOT NULL, exam_type TEXT DEFAULT '期末考试', UNIQUE(student_id, course_id, exam_type), FOREIGN KEY (student_id) REFERENCES t_student(student_id), FOREIGN KEY (course_id) REFERENCES t_course(course_id) ) ''') # 默认管理员,密码为 admin123 的SHA256哈希 cursor.execute( "INSERT OR IGNORE INTO t_user (username, password) VALUES (?, ?)", ('admin', '240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9') ) conn.commit() conn.close()这里我把默认管理员密码用SHA256进行哈希后再入库,而不是存明文。虽然系统复杂度不高,但稍微有一点安全意识的写法,写进文档里都是一个亮点。实际使用时建议登录后立刻修改密码。注意用户表、学生表、课程表和成绩表之间有明确的外键关联,这个关联关系是成绩统计和排名功能的基础。
2.3 数据访问层的封装技巧
很多初学者会在UI事件处理函数里直接写SQL语句。这样写确实跑得通,但一旦SQL语句变了,改起来很痛苦。我的习惯是单独写一个操作类,把对每一张表的增删改查封装成方法,UI层只调用方法,永远不关心SQL语句本身。
我把学生表的操作封装成StudentDao类,其中部分核心方法如下:
import sqlite3 from database.db_helper import DB_PATH class StudentDao: @staticmethod def _get_connect(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn @staticmethod def insert_student(data: dict) -> bool: conn = StudentDao._get_connect() try: conn.execute( "INSERT INTO t_student (student_id, name, gender, major, class_name, phone) " "VALUES (?, ?, ?, ?, ?, ?)", (data['student_id'], data['name'], data['gender'], data['major'], data['class_name'], data['phone']) ) conn.commit() return True except sqlite3.IntegrityError: return False finally: conn.close() @staticmethod def query_all_students() -> list: conn = StudentDao._get_connect() try: cursor = conn.execute( "SELECT * FROM t_student ORDER BY student_id" ) return [dict(row) for row in cursor.fetchall()] finally: conn.close() @staticmethod def delete_student(student_id: str) -> None: conn = StudentDao._get_connect() try: conn.execute("DELETE FROM t_score WHERE student_id = ?", (student_id,)) conn.execute("DELETE FROM t_student WHERE student_id = ?", (student_id,)) conn.commit() finally: conn.close()这里有几个容易踩的坑,我提前说一下。第一,row_factory设置为sqlite3.Row之后,查询结果就可以通过列名访问,代码可读性高很多,例如row['name']而不是row[1]。第二,delete_student方法里我同时删除了该学生关联的成绩记录,否则会留下悬空记录,导致后续统计时数据错乱。第三,所有连接用完必须close(),我利用try...finally保证这一点。虽然SQLite连接很轻量,但积累太多不关闭的连接会导致文件锁定,在Windows上尤其明显。
3. 核心功能模块的实现:从界面到数据
系统开发最核心的阶段就是功能模块落地。我按功能模块来组织代码,每个模块解决一个具体问题。这部分也是文章最长的部分,我会按学生信息管理、成绩录入修改、成绩统计排名、数据可视化四个模块来讲。
3.1 学生信息管理模块
学生信息管理本质上就是一张表格的增删改查。在Tkinter中,最合适的控件是ttk.Treeview,它能以表格形式展示数据,支持选中、滚动,并且操作起来很顺手。
我搭的主界面左侧是一个功能导航(用ttk.Notebook做页签切换),右侧是数据表格。查询区放一个关键字输入框和一个“查询”按钮,支持按照学号或姓名模糊查询。模糊查询用SQL的LIKE操作符实现:
@staticmethod def query_students_by_keyword(keyword: str) -> list: conn = StudentDao._get_connect() try: cursor = conn.execute( "SELECT * FROM t_student WHERE student_id LIKE ? OR name LIKE ? " "ORDER BY student_id", (f"%{keyword}%", f"%{keyword}%") ) return [dict(row) for row in cursor.fetchall()] finally: conn.close()添加和编辑学生信息,我用了单独的对话框窗口。为什么不直接在表格里改?因为表格直接编辑容易误操作,且没有输入校验。有同学说这样体验差,其实用一个干净的对话框反而让用户聚焦,出错率更低。对话框里用Entry接收输入,用ttk.Combobox提供性别选项,Button触发保存逻辑。在保存前必须完成几项校验:学号不能为空、学号格式必须合法(如纯数字且长度规范)、姓名不能为空、女生和男生性别只能二选一。
校验逻辑写起来不复杂,但很有必要。我遇到过一个典型的错误:有人输入了非数字的学号,插入数据库时因为主键类型不匹配直接报错崩溃。所以统一在UI层拦一道:
def validate_student_form(self): student_id = self.id_entry.get().strip() name = self.name_entry.get().strip() if not student_id: messagebox.showwarning("提示", "学号不能为空") return None if not name: messagebox.showwarning("提示", "姓名不能为空") return None if len(student_id) > 20: messagebox.showwarning("提示", "学号长度不能超过20个字符") return None return { 'student_id': student_id, 'name': name, 'gender': self.gender_combo.get(), 'major': self.major_entry.get().strip(), 'class_name': self.class_entry.get().strip(), 'phone': self.phone_entry.get().strip() }3.2 成绩录入与修改模块
成绩管理是系统的核心,这里的设计比学生管理稍微复杂一点,因为涉及“给哪个学生的哪门课录多少分”三要素。
我的方案是做一个独立的成绩管理页签。页签顶部是学生列表,点击某一行后,下方表格显示该学生的所有成绩记录。操作按钮有“录入成绩”“修改成绩”“删除成绩”。
录入成绩对话框需要选择课程、输入分数、选择考试类型。课程如果不在列表里,系统要支持现场输入新课程,这个功能很多同学的版本里没有,但它其实很实用。成绩分数需要校验必须是0到100之间的数字,否则数据库里会出现负分这种脏数据。
更新成绩的SQL要注意,它不能只是简单UPDATE,因为成绩表中有UNIQUE(student_id, course_id, exam_type)约束,表示同一学生同一门课程同一考试类型只能有一条记录。所以录入时我会先查是否存在,存在则更新,不存在才插入,这叫做“upsert”逻辑:
@staticmethod def upsert_score(student_id: str, course_id: str, score: float, exam_type: str) -> None: conn = ScoreDao._get_connect() try: conn.execute( "INSERT INTO t_score (student_id, course_id, score, exam_type) " "VALUES (?, ?, ?, ?) " "ON CONFLICT(student_id, course_id, exam_type) " "DO UPDATE SET score = excluded.score", (student_id, course_id, score, exam_type) ) conn.commit() finally: conn.close()SQLite从3.24版本开始支持ON CONFLICT DO UPDATE语法,这个语法简洁且具有原子性,比先查再插的写法更可靠。Python自带的sqlite3版本通常都很新,可以直接使用,这条方案在Windows、macOS、Linux上都能正常运行,我在多个环境下都验证过。
3.3 成绩统计与排名模块
有了数据基础,统计模块就能做得有模有样。这个模块的价值在于把原始数据变成“信息”,是系统最出彩的部分。
排名逻辑我按“总分优先、同分时再按学号排序”来处理。最核心的统计SQL是按学生分组汇总:
SELECT s.student_id, s.name, s.class_name, COUNT(sc.id) AS course_count, ROUND(SUM(sc.score), 2) AS total_score, ROUND(AVG(sc.score), 2) AS avg_score, MIN(sc.score) AS min_score, MAX(sc.score) AS max_score FROM t_student s LEFT JOIN t_score sc ON s.student_id = sc.student_id GROUP BY s.student_id ORDER BY total_score DESC为什么用LEFT JOIN而不是INNER JOIN?因为LEFT JOIN会把没有成绩的学生也查出来,哪怕他所有课程都是空的,至少保证界面上不会出现学生凭空消失的情况。MIN和MAX可以直接拿来算“偏科情况”,AVG计算平均分。有些人把平均分存到学生表里,我强烈不建议这样做。因为它是可以实时计算出来的派生数据,一旦成绩更新而平均分没有同步更新,数据就不一致了。
排名之后,我还做了“分数段统计”功能。将成绩划分为优秀(90分以上)、良好(80到89分)、中等(70到79分)、及格(60到69分)、不及格(60分以下)五个区间,分别统计人数。这份数据直接对接后面的可视化模块。
3.4 数据可视化:让图表说话
Matplotlib嵌入Tkinter窗口的方法是使用FigureCanvasTkAgg。它可以把Matplotlib的Figure对象渲染到Tkinter的容器里,实现图表和界面的良好结合。
我实现了一个简单的柱状图,展示各分数段人数分布:
import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg def draw_score_distribution(self, frame, stats: dict): # 解决中文显示问题 plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] plt.rcParams['axes.unicode_minus'] = False fig = plt.Figure(figsize=(6, 4), dpi=100) ax = fig.add_subplot(111) levels = ['优秀', '良好', '中等', '及格', '不及格'] values = [stats.get(level, 0) for level in levels] colors = ['#4CAF50', '#8BC34A', '#FFC107', '#FF9800', '#F44336'] ax.bar(levels, values, color=colors, alpha=0.8) ax.set_title('成绩分布统计') ax.set_xlabel('分数段') ax.set_ylabel('人数') for i, v in enumerate(values): ax.text(i, v + 0.05, str(v), ha='center') for widget in frame.winfo_children(): widget.destroy() canvas = FigureCanvasTkAgg(fig, master=frame) canvas.draw() canvas.get_tk_widget().pack(fill='both', expand=True)有个细节提醒一下:Matplotlib默认字体不支持中文,不设置中文字体会导致图表里出现方块。解决办法就是代码里的前两行,设置字体为SimHei或Microsoft YaHei。这个坑几乎每个人都会遇到,提前处理了能省很多事。然后,每次刷新图表前我会销毁旧控件再重新绘制,不然多次点击按钮会导致多个图叠加在一起,内存也会越占越多。
4. 界面设计与交互流程:让程序有一个好皮囊
一个系统光有功能还不够,界面布局和交互体验决定了它是否“能用”。很多人觉得界面设计很虚,但其实有清晰的设计原则和流程,做起来并不难。答辩时老师一眼看过去,第一印象很重要。
4.1 整体布局设计
我采用了经典的“上下+标签页”结构。顶部是一个工具栏,放系统标题。下面用ttk.Notebook分成几个页签:学生管理、成绩录入、成绩统计、数据图表。
这种设计的好处是功能分类清晰,用户一眼就知道去哪里操作。导航放在页签上,点击即可切换,不需要单独做侧边栏菜单。对于学生成绩管理这种简单工具型应用,我始终觉得“简洁、直白”比“花哨、复杂”重要得多。
主窗口的尺寸我设置为1024x680,这个尺寸对大部分笔记本屏幕都友好,不会超出屏幕。表格用ttk.Treeview配合滚动条,支持双击行触发编辑,支持单击行高亮显示。
4.2 交互逻辑细节与状态处理
界面交互有一个地方特别容易被人忽略,就是“按钮状态管理”。比如未选中表格行时,“修改”和“删除”按钮应该是灰色的,选中之后才可点击。我通过绑定Treeview的选中事件<<TreeviewSelect>>来切换按钮的state属性。
另外,所有数据变更操作(增、删、改)完成后,都要立即刷新表格内容。如果你等到用户下次打开界面再刷新,用户会以为操作失败。我在每次调用DAO完成数据变更后,都会统一调用一次refresh_student_table()或refresh_score_table()。
还有一个细节是关于Entry控件的焦点处理。用户输完学号后,按Enter键应该能直接跳到下一个输入框,或者触发查询。我给查询输入框绑定了<Return>事件,这样用户查询时不需要移动鼠标点按钮。看起来是小功能,但实际使用中体验提升非常明显。
4.3 打包发布与依赖管理
程序写完之后,需要把它打包成一个可执行的exe文件。我用的是PyInstaller,它在Windows下打包Tkinter应用比较成熟。
安装方式很简单:pip install pyinstaller。打包时需要注意这样几个小细节。
第一,给主程序配一个图标文件,不然生成的exe是默认的PyInstaller图标,看起来很不专业:
pyinstaller --onefile --windowed --icon=app.ico --name=学生成绩管理系统 main.py第二,因为我的数据库文件是运行时创建的,所以打包时不包含数据库文件。程序首次启动会在data目录下自动建库,这样用户拿到手直接用,不用手动配置环境和文件位置。把init_db()放在程序启动后的第一步执行,就是这个目的。
第三,--onefile模式生成单个exe文件体积会大一些,Tkinter+Matplotlib打包后大概50到70MB。如果你觉得太大,可以去掉Matplotlib模块打包,或者改用--onedir模式。对于学生作业来说,--onefile更方便提交,双击即用。
打包过程中如果遇到报错,多半是缺少matplotlib.backends.backend_tkagg模块,此时可以在打包命令中加入--hidden-import matplotlib.backends.backend_tkagg,这是比较常见的坑。
5. 常见问题与排查技巧实录
写这个系统的过程中,我遇到过各种各样的问题,尤其是跨平台运行时的问题。整理成清单分享出来,希望能帮你少走弯路。
5.1 常见问题速查表
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 界面中文全部显示为方块 | 系统缺少中文字体或Matplotlib未配置中文字体 | 设置plt.rcParams['font.sans-serif']=['SimHei'];Windows下可安装微软雅黑 |
| 插入数据时报INTERGRITY ERROR | 学号重复或外键关联的记录不存在 | 先查询是否已存在;使用INSERT OR IGNORE或先做合法性检查 |
| 程序退出后数据丢失 | 未提交事务或数据库路径错误 | 每次写操作后调用conn.commit();检查DB_PATH是否为绝对路径 |
| 打包后的exe无法运行或闪退 | 动态依赖文件丢失 | 用--onedir模式打包检查,加入--hidden-import,在命令行运行exe查看报错信息 |
| SQLite数据库文件被锁定 | 数据库连接未关闭 | 确保每次数据库操作都在finally中关闭连接 |
ttk.Treeview刷新后数据重复 | 未清空旧数据 | 刷新前调用tree.delete(*tree.get_children()) |
| 图表中文出现方框 | Matplotlib字体问题 | 执行plt.rcParams['font.sans-serif']=['Microsoft YaHei']并设置axes.unicode_minus=False |
5.2 一个隐蔽的坑:SQLite事务与自动提交
很多初学者在操作SQLite时,只记得执行SQL语句,常常忘记conn.commit()。在插入、更新、删除之后,如果没有提交事务,数据只在当前连接内生效,程序退出后事务回滚,数据就丢了。这会在测试时造成“明明运行成功但重启后数据不见了”的诡异现象。
我的处理习惯是:所有写操作统一在方法内部完成commit(),调用方完全不感知事务细节。这样UI层不需要关心数据库的事务状态,集中管理,减少遗漏。
5.3 关于环境与依赖的补充
有同学问自己电脑上跑通的系统拿到别人电脑上就报“No module named 'tkinter'”。这里要说明一下,Tkinter在Windows官方安装包中通常自带,但有些精简Python发行版或不带GUI的Linux服务器上可能没有。跨机器运行时最简单的办法就是打包成exe,用户不需要额外安装Python解释器,自然就不会遇到模块缺失问题。
如果你在自己机器上运行代码,建议先创建虚拟环境,从需求文件安装依赖,避免污染全局环境:
python -m venv venv venv\Scripts\activate pip install -r requirements.txt python main.py6. 写在最后的一些实践体会
做完了这个项目,再回头看你可能会发现,学生成绩管理系统真正的难点不在于某个单独的技术点,而在于如何把界面、数据库、业务逻辑这些零散的部分整合成一个整体。我见过太多人写了一堆代码,但连一个完整的学生信息添加流程都不能顺畅跑通,原因就是“只管局部,不管全局”。
根据我个人实际操作中的体会,建议你写这个项目时不要急于打开IDE,先把设计文档里的功能模块列表、数据库ER图、界面草图画出来。哪怕手绘也行,想清楚了再动手,效率远高于边写边改。你甚至可以先把所有DAO的方法搭好空架子,再回过头来写界面,这样每完成一个小模块就能测试一个小模块,排查范围小,问题定位快。
最后再分享一个小技巧:写完核心功能之后,可以为你这个系统增加一条“数据备份”功能,也就是在每次关闭程序前自动把grade_system.db复制成带时间戳的备份文件。别小看这一两行代码,它让你从“能跑”进阶为“可靠”,答辩时也多了一个可以讲实际场景的应用点。这个项目后续还可以往多用户权限管理、年级/班级维度统计、成绩单PDF导出等方向扩展,都是现成架构上比较容易增加的能力。希望这篇文章能帮到你,少踩几个我当年踩过的坑。
本文还有配套的精品资源,点击获取