
简介基于Python与MySQL的学生管理系统提供可视化图形界面面向学校教务人员或个人开发者用于学生档案、选课及成绩信息的统一管理可有效替代手工Excel登记流程。压缩包共2个文件包含Python主程序与SQL数据库脚本整体体积仅7KB解压即可查看完整代码与建表语句。系统采用客户端/服务器模式Python端内置学生信息管理、课程信息管理、成绩信息管理和系统管理四个功能模块支持数据的增删改查与界面交互其中学生信息模块可维护学号、姓名、班级等基础档案课程与成绩模块则支持选课记录和考试成绩的批量管理。SQL脚本负责构建MySQL数据库表结构便于快速初始化学习环境。目前已有6765人学习下载适合Python初学者理解GUI开发与MySQL联动也可作为计算机课程设计或毕业设计的参考蓝本。1. 学生管理系统Python 和 MySQL 的第一次完整握手如果你是刚把 Python 语法啃完、正准备找个项目练手的学生或者是想给某个小团队做内部工具的一线开发者这个标题基本就是你绕不开的经典组合。学生管理系统听起来老套但它恰好覆盖了 Python 操作数据库、GUI 事件绑定、数据增删改查这一整条闭环链路做完它你就有能力去碰任何“表单 表格 数据库”类型的业务系统。可视化界面这一步最容易被初学者轻视——以为就是拖几个控件真正写起来才发现线程、刷新、编码处处是坑。我见过太多人卡在“代码能跑但界面一操作就崩”或者“MySQL 中文全变问号”这种问题上最后不得不放弃可视化。这篇文章就从选型开始把你从零带到能独立完成一个带登录窗口、学生信息维护、成绩查询的桌面系统并把几个高频翻车现场提前拆给你看。整篇文章的核心观点是这个项目的难点不在 Python也不在 SQL而在两者交界处的数据类型、编码和刷新时机。2. 可视化界面选型Tkinter 够用但你要知道它和 PyQt5 的边界2.1 为什么多数课程设计选 Tkinter零依赖和够用的控件可视化界面在 Python 里最常走三条路Tkinter、PyQt5/PySide6、Web 前端框架。对于“学生管理系统”这种典型桌面 CRUD 应用Tkinter 是默认首选原因只有一个它是标准库装好 Python 就有不需要额外 pip 安装也不需要处理 Qt 的许可证和打包体积问题。对于绝大多数课程设计或内部小工具Tkinter 自带 Label、Entry、Button、Treeview、Messagebox 这几个控件已经完全够覆盖学生信息的展示、录入、编辑和删除操作。但 Tkinter 的边界也很明显。它没有现成的表格控件Treeview 勉强能当表格用但样式古板列宽调整和单元格编辑都很别扭。如果你要做的界面包含复杂布局、富文本、图表联动这一类需求Tkinter 会让你写布局代码写到怀疑人生。PyQt5 的优势在于 QTableWidget 和信号槽机制做复杂界面的效率高很多但代价是你需要多学一套框架的 API而且打包出来的 exe 体积通常超过 80MB对“交个作业”来说有点重量级。我个人的判断标准是数据量不超过几百条、界面只有两三个窗体、不需要复杂样式——选 Tkinter要做多标签页、嵌入式图表、或者带树形导航的完整业务系统——直接上 PyQt5。学生管理系统通常属于前者所以后面全部代码基于 Tkinter。2.2 用 pymysql 还是 mysql-connector-python驱动选型的真实差异连接 MySQLPython 侧最常遇到两个驱动库pymysql 和 mysql-connector-python。前者是纯 Python 实现安装没有任何依赖绝大多数教程里都用它兼容性最好后者是官方驱动性能理论上更优但安装时容易附带依赖冲突在一些精简版 Linux 环境里还会遇到 lib 缺失的问题。做学生管理系统这种小型应用性能差异完全感觉不到用 pymysql 就够了。连接参数的坑比驱动选择更值得注意。host、port、user、password、database 这五个参数里最容易出错的是 database 这个字段——如果你在创建连接时没指定库名后面每条 SQL 都得写成SELECT * FROM 库名.表名不仅啰嗦还容易在改库名时漏改。另外pymysql 的 connect 方法默认不会自动提交事务你执行完 INSERT 或 UPDATE 之后必须手动conn.commit()否则数据看起来“执行成功”一刷新又没了。这个点我会在后面的代码里给你标清楚。2.3 最小骨架验证 Python、MySQL、驱动三者连通在写任何界面代码之前先确认底层链路是通的。下面这段代码是环境自检脚本你新建一个check_env.py文件逐个环节验证。import pymysql # 连接参数按你自己的 MySQL 配置修改 conn pymysql.connect( host127.0.0.1, port3306, userroot, password123456, databasestudent_db, charsetutf8mb4 ) try: with conn.cursor() as cursor: cursor.execute(SELECT VERSION()) version cursor.fetchone() print(MySQL 版本:, version[0]) cursor.execute(SELECT 11) print(基础查询正常) finally: conn.close()这段脚本里最关键的是charsetutf8mb4这个参数。如果你漏掉它或写成utf8后面插入中文姓名时大概率会出现Incorrect string value错误因为 utf8 在 MySQL 里是 utf8mb3 的别名存不了 emoji 和部分生僻字。另外注意with conn.cursor()只负责关闭游标不负责关闭连接所以conn.close()要放在 finally 块里。如果你把数据库名写错了这里会直接抛OperationalError这就是排查问题的第一道关卡。跑通这段脚本后环境就算准备好了。3. 建库建表与登录验证把 MySQL 设计从第一版就做对3.1 把学生表拆成三张表为什么推荐课程表和成绩表独立很多初学者第一反应是建一张大表把学号、姓名、班级、电话、课程、成绩全塞进去。这在演示数据少的时候确实“看起来能用”但你一旦插入第二条成绩记录就面临数据冗余同一个学生每多一门课就要复制一遍姓名电话。更麻烦的是一旦学生改名或换电话你得更新多条记录漏一条就是数据不一致。我建议至少拆成三张表学生表、课程表、成绩表。学生表只存学生的固定属性课程表只存课程编号和课程名成绩表通过学号和课程编号关联前两张表。这样做的好处是增删改查的边界非常清晰而且后面如果要做“查某门课的平均分”“统计某个学生的总学分”这类聚合查询SQL 写起来反而更简单。下表是三张表的结构参考。表名字段类型说明studentstu_idVARCHAR(20) PRIMARY KEY学号业务主键studentnameVARCHAR(50) NOT NULL学生姓名studentgenderCHAR(2)性别studentclass_nameVARCHAR(50)班级名studentphoneVARCHAR(20)联系方式coursecourse_idINT PRIMARY KEY AUTO_INCREMENT课程编号coursecourse_nameVARCHAR(100) UNIQUE课程名scorestu_idVARCHAR(20)学号外键scorecourse_idINT课程编号外键scorescore_valueDECIMAL(5,2)成绩保留两位小数如果你只是为了完成一个演示系统上面这个结构足够。但有一点要提前想好score 表要不要设置联合主键(stu_id, course_id)如果同一个学生同一门课只能有一条成绩记录那就应该加联合主键从数据库层面杜绝重复录入。加联合主键的代价是后续“补考成绩覆盖”的逻辑要用INSERT ... ON DUPLICATE KEY UPDATE来实现稍麻烦但值得。3.2 初始化数据库的 SQL 脚本字符集与自增列的两个注意点建表脚本里最容易踩的坑是字符集没指定。如果你的 MySQL 服务端默认字符集是 latin1即使连接参数写了 utf8mb4建表时不指定 DEFAULT CHARSET中文照样报错。我习惯每张表都显式加上字符集并把排序规则写成utf8mb4_unicode_ci保证后续查询时中文排序稳定。CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE student_db; CREATE TABLE IF NOT EXISTS student ( stu_id VARCHAR(20) PRIMARY KEY COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(2) DEFAULT 男 COMMENT 性别, class_name VARCHAR(50) COMMENT 班级, phone VARCHAR(20) COMMENT 联系电话 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS course ( course_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 课程ID, course_name VARCHAR(100) NOT NULL UNIQUE COMMENT 课程名称 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS score ( stu_id VARCHAR(20) NOT NULL COMMENT 学号, course_id INT NOT NULL COMMENT 课程ID, score_value DECIMAL(5,2) NOT NULL COMMENT 成绩, PRIMARY KEY (stu_id, course_id), CONSTRAINT fk_score_student FOREIGN KEY (stu_id) REFERENCES student (stu_id) ON DELETE CASCADE, CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course (course_id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;上面脚本里我刻意加入了两个外键约束并且让外键在父表删除时级联删除成绩记录。有些教程不推荐外键理由是影响插入性能但对学生管理系统这种低并发场景外键带来的数据一致性保护远大于性能损耗。实操时要注意如果你后续要按“班级批量删除学生”外键约束可能会导致 DELETE 多表时报错那时你需要先删成绩表再删学生表顺序不能反。还有一个很多人踩的坑是自增列。course_id 用INT AUTO_INCREMENT没问题但如果你在界面里做了“导入课程”功能批量插入后想拿到刚插入的 ID需要调用cursor.lastrowid而不是自己查 MAX(id)——后者在高并发下会拿错值这个我们到代码部分细说。3.3 连接池还是单连接为什么小项目不要过度设计学生管理系统的并发量通常是一个老师或管理员在用顶多同时开三五个窗口。这种情况下连接池是多余的每次操作新建连接、用完关闭反而更简单可靠。你只需要写一个公共的get_connection()函数统一管理连接参数即可。import pymysql DB_CONFIG { host: 127.0.0.1, port: 3306, user: root, password: 123456, database: student_db, charset: utf8mb4 } def get_connection(): 统一获取数据库连接所有模块都从这里拿连接 return pymysql.connect(**DB_CONFIG)这里把配置集中在一个字典里是刻意的。等到你的代码写了五个模块都用数据库时改密码只需要动这一处而不是全局搜索替换。另外pymysql.connect(**DB_CONFIG)这种写法比逐个参数传入更清晰新增参数时也不容易漏。有人会在登录功能里用“管理员表”存账号密码我建议如果只是课程设计直接在代码里写死一个 admin 账号就可以了。真正做系统时密码要 hash 存储不能明文入库这个安全意识从一开始就要有。3.4 登录窗口的实现绑定回车键与状态切换的细节登录窗口一般就两个输入框加一个按钮但细节里藏着三个坑密码框要show*、点击登录时要禁用按钮防止重复提交、按回车要能触发登录。第三个坑最容易忘因为用户习惯输完密码直接按 Enter你只绑了鼠标点击事件就会觉得“软件不好用”。import tkinter as tk from tkinter import messagebox def do_login(username_var, password_var): username username_var.get().strip() password password_var.get() if username admin and password 123456: messagebox.showinfo(成功, 登录成功) # 这里写跳转到主窗体的逻辑 else: messagebox.showerror(失败, 账号或密码错误) def create_login_window(): win tk.Tk() win.title(学生管理系统 - 登录) win.geometry(360x220) tk.Label(win, text用户名).pack(pady10) username_var tk.StringVar() tk.Entry(win, textvariableusername_var, width28).pack() tk.Label(win, text密码).pack(pady10) password_var tk.StringVar() tk.Entry(win, textvariablepassword_var, show*, width28).pack() login_btn tk.Button(win, text登录, width20) login_btn.pack(pady15) # 绑定鼠标点击和回车键两种触发方式 login_btn.bind(Button-1, lambda e: do_login(username_var, password_var)) win.bind(Return, lambda e: do_login(username_var, password_var)) win.mainloop() if __name__ __main__: create_login_window()注意strip()的调用位置用户名要做空白字符清理密码不能做——万一某个用户的密码设计成首尾有空格你 strip 掉反而对不上。win.bind(Return)是绑定整个窗体的回车事件如果你在多个 Entry 之间切换焦点这个绑定始终生效这点比单独给 Entry 绑定事件更省心。登录成功后跳转主窗体的标准做法是先win.destroy()销毁登录窗再创建主窗体对象。很多人会直接用win.withdraw()隐藏登录窗这样主窗体结束后程序还残留一个隐藏进程任务栏图标消失但 Python 进程不退出。我建议用 destroy 干净利落。4. 主界面与学生信息管理从 Treeview 到 CRUD 的完整代码4.1 主窗体的布局菜单栏、工具栏、状态栏三类区域主窗体不需要花哨建议按“顶部菜单/按钮、中间表格、底部状态栏”三段式结构划分。这样用户操作路径最短也符合大多数管理系统的操作习惯。顶部放“新增、编辑、删除、刷新、查询”五个按钮中间放 Treeview 表格展示学生列表底部放一个 Label 显示当前记录数。下面这个函数构建了基底骨架。import tkinter as tk from tkinter import ttk def create_main_window(): root tk.Tk() root.title(学生信息管理) root.geometry(800x500) # 顶部工具栏 toolbar tk.Frame(root) toolbar.pack(sidetk.TOP, filltk.X) btn_add tk.Button(toolbar, text新增学生) btn_add.pack(sidetk.LEFT, padx5, pady5) btn_edit tk.Button(toolbar, text编辑) btn_edit.pack(sidetk.LEFT, padx5, pady5) btn_delete tk.Button(toolbar, text删除) btn_delete.pack(sidetk.LEFT, padx5, pady5) btn_refresh tk.Button(toolbar, text刷新) btn_refresh.pack(sidetk.LEFT, padx5, pady5) # 中间表格区 columns (stu_id, name, gender, class_name, phone) tree ttk.Treeview(root, columnscolumns, showheadings) for col, text in zip(columns, [学号, 姓名, 性别, 班级, 联系电话]): tree.heading(col, texttext) tree.column(col, width120, anchortk.CENTER) tree.pack(sidetk.TOP, filltk.BOTH, expandTrue) # 底部状态栏 status_label tk.Label(root, text共 0 条记录, anchortk.W) status_label.pack(sidetk.BOTTOM, filltk.X) return root, tree, status_label这里把root, tree, status_label都返回出去是因为后面的操作函数都要用到这些控件对象。Treeview 的showheadings参数容易被漏掉不写的话表格最左侧会多出一列空白列那是用来显示树形层次关系的对纯表格场景没用一定要去掉。tree.column的 width 参数是按像素算的如果你的电脑屏幕分辨率特别高或字体缩放比例不是 100%130 像素可能会显示不全中文内容建议后续接一个横向滚动条兜底。性能方面Treeview 一次渲染几百行没问题如果学生人数上万你需要考虑分页后面我会提到。4.2 刷新表格数据的正确姿势先清空再填充游标用完必须关刷新是 CRUD 里最核心的函数所有增删改操作完成后都要调用它。它做的事情就三步查全表、清空 Treeview、逐行插入。最容易出 bug 的是顺序——必须先tree.delete再循环插入如果先插入再清空新数据会被一起清掉。def refresh_student_list(tree, status_label): 从数据库读取学生列表并刷新表格显示 conn get_connection() try: with conn.cursor() as cursor: cursor.execute(SELECT stu_id, name, gender, class_name, phone FROM student ORDER BY stu_id) rows cursor.fetchall() # 清空 Treeview 现有数据 for item in tree.get_children(): tree.delete(item) # 逐条插入新数据 for row in rows: tree.insert(, tk.END, valueslist(row)) status_label.config(textf共 {len(rows)} 条记录) except pymysql.Error as e: messagebox.showerror(数据库错误, f查询失败: {e}) finally: conn.close()注意tree.get_children()返回的是 Treeview 内部的项目 ID 列表删除时逐个删。如果你清空后不刷新status_label删除学生后状态栏数字不会变给用户造成“删了又没删”的错觉。另外fetchall()返回的是元组列表list(row)是为了确保 values 参数是列表类型有些 Tkinter 版本传元组也能行但显式转列表能避免奇怪的兼容性问题。conn.close()在 finally 里是铁律。初学者容易只关游标不关连接在循环操作场景下会导致 MySQL 连接数暴涨最终报Too many connections。如果你发现程序用着用着变卡先检查这里。4.3 新增与编辑表单一个弹窗函数复用两种场景新增和编辑表单的字段完全一样区别只在编辑时要回填旧值。我习惯写一个统一的show_student_form函数通过stu_id参数区分模式和数据库操作。传None走新增逻辑传具体学号走更新逻辑。import tkinter as tk from tkinter import messagebox def show_student_form(parent, stu_idNone, on_successNone): 打开新增或编辑学生信息的对话框 form tk.Toplevel(parent) form.title(编辑学生 if stu_id else 新增学生) form.geometry(320x280) form.grab_set() # 模态窗口禁止操作主窗体 vars_dict {} labels [学号, 姓名, 性别, 班级, 联系电话] entry_keys [stu_id, name, gender, class_name, phone] for i, (label, key) in enumerate(zip(labels, entry_keys)): tk.Label(form, textlabel).grid(rowi, column0, padx10, pady8) var tk.StringVar() tk.Entry(form, textvariablevar).grid(rowi, column1, padx10, pady8) vars_dict[key] var # 如果是编辑模式回填数据 if stu_id: conn get_connection() try: with conn.cursor() as cursor: cursor.execute(SELECT stu_id, name, gender, class_name, phone FROM student WHERE stu_id%s, (stu_id,)) row cursor.fetchone() if row: for key, value in zip(entry_keys, row): vars_dict[key].set(value) finally: conn.close() def save(): data {key: var.get().strip() for key, var in vars_dict.items()} if not data[stu_id] or not data[name]: messagebox.showwarning(提示, 学号和姓名不能为空) return conn get_connection() try: with conn.cursor() as cursor: if stu_id: sql UPDATE student SET name%s, gender%s, class_name%s, phone%s WHERE stu_id%s cursor.execute(sql, (data[name], data[gender], data[class_name], data[phone], stu_id)) else: sql INSERT INTO student (stu_id, name, gender, class_name, phone) VALUES (%s, %s, %s, %s, %s) cursor.execute(sql, (data[stu_id], data[name], data[gender], data[class_name], data[phone])) conn.commit() except pymysql.IntegrityError: messagebox.showerror(错误, 该学号已存在) return finally: conn.close() form.destroy() if on_success: on_success() tk.Button(form, text保存, commandsave).grid(rowlen(labels), column0, columnspan2, pady15)这个函数有三个值得学习的细节。第一是form.grab_set()它让弹窗变成模态用户不关掉弹窗就无法操作主窗体避免因为误触主窗体导致数据状态不一致。第二是编辑模式下用WHERE stu_id%s的占位符写法而不是直接把变量拼进 SQL——后者会被 SQL 注入攻击虽然学生管理系统风险低但这个习惯必须养成。第三是捕获pymysql.IntegrityError当你插入重复主键时MySQL 会抛这个异常如果你不捕获程序会直接崩溃退出。一个常见的增强需求是“性别”字段用下拉框而不是输入框。你可以把性别对应的 Entry 换成ttk.Combobox(form, values[男, 女], statereadonly)这样用户就不会输入“男 ”这种带空格的内容。但要注意Combobox 的默认值要手动 set不像 Entry 那样空着就行。4.4 删除学生先确认再执行处理外键约束的两种思路删除操作是 CRUD 里最危险的一步一定要弹确认框。逻辑不算复杂但如果直接执行 DELETE 语句遇到 score 表有外键约束时会报错或者级联删除。前面建表时我用了ON DELETE CASCADE所以这里直接删除学号就行子表数据自动清理。如果当初没建级联代码里就要先删成绩再删学生。def delete_student(tree, status_label): 删除选中学生删除前弹确认框 selected tree.selection() if not selected: messagebox.showwarning(提示, 请先选择要删除的学生) return item_id selected[0] values tree.item(item_id, values) stu_id values[0] confirm messagebox.askyesno(确认删除, f确定删除学号为 {stu_id} 的学生吗\n其成绩记录将同步删除。) if not confirm: return conn get_connection() try: with conn.cursor() as cursor: cursor.execute(DELETE FROM student WHERE stu_id%s, (stu_id,)) conn.commit() messagebox.showinfo(成功, 删除成功) refresh_student_list(tree, status_label) except pymysql.Error as e: conn.rollback() messagebox.showerror(删除失败, str(e)) finally: conn.close()注意tree.selection()返回的是元组即使单选也返回含一个元素的元组。如果你让 Treeview 支持多选需要遍历所有选中的 item 逐个删除并且包在一个事务里。conn.rollback()放在 except 里是很好的习惯——当 SQL 执行出错时如果没有 rollback连接关闭时 MySQL 会自动回滚但显式 rollback 可以更早释放锁资源。这里还有一个用户体验的细节删除后刷新表格但刷新后 Treeview 的选中状态会丢失。如果你希望删除后自动选中下一条记录可以在刷新前记录当前选中行的索引这个功能的实现留给读者自己思考。5. 避坑实录从编码错乱到界面假死的六个典型问题5.1 中文乱码现象是“姓名”列显示问号这个问题出现次数最多。现象看起来是 INSERT 执行成功但 SELECT 出来全是问号。原因可能出在三个层面数据库表字符集、连接字符集、Python 端编码。排查顺序是先看建表语句有没有DEFAULT CHARSETutf8mb4再看连接参数有没有charsetutf8mb4最后看 Python 文件头部有没有# -*- coding: utf-8 -*-。把这三层都统一成 utf8mb4问题基本能解决。还有一个隐蔽的可能性MySQL 的 my.ini 里character_set_server设成了 gbk这种情况下建库时要显式带字符集不能被默认值坑了。5.2 查询结果乱码表和数据都是对的界面显示却乱码这种情况通常是 Treeview 的编码问题。Tkinter 内部用的是 Unicode正常情况下不会乱码。如果你用了json.dumps或str()做数据类型转换可能把原本正确的字符串转成了带\u转义的形式。我的建议是从数据库取出来是什么类型就直接传给 Treeview不要做多余的类型转换。尤其是不要把Decimal类型转成 float 再转成 str直接str(row[2])用原始值。5.3 界面假死查询大数据量时窗口无响应当你的查询语句执行时间超过一两秒Tkinter 主循环会被阻塞窗口拖不动也关不掉用户会以为程序死了。学生管理系统数据量小通常不会触发但如果你加了模糊查询LIKE %关键词%且没给表加索引数据量到几万条时就会明显卡顿。解决的思路有两个一是给经常查询的字段加索引比如ALTER TABLE student ADD INDEX idx_name (name)二是把查询放到线程里执行查询完成后通过after方法把结果回传到主线程。线程方案代码量会多一倍学生管理系统不建议上加索引足够了。5.4 删除学生时外键报错Cannot delete or update a parent row这个报错说明你的 score 表外键没设置ON DELETE CASCADE。如果你建的是一对多关系删除主表记录时子表记录没地方去。两种解决方式一是修改外键约束ALTER TABLE score DROP FOREIGN KEY fk_score_student然后重新加带级联删除的外键二是在删除按钮的逻辑里先执行DELETE FROM score WHERE stu_id%s再删除学生表记录。第一种治本第二种适合不愿意改动表结构的场景。5.5 刷新按钮点击后无变化可能不是代码错是视图缓存有些时候你明明 UPDATE 成功了但界面上数据还是旧的。这通常是因为你的查询用了 SQL 视图而非直接查表MySQL 的视图在特定隔离级别下会有缓存语义。解决办法很简单不用视图直接查表。如果一定要视图可以查完之后FLUSH或重连数据库。5.6 打包 exe 后找不到 MySQL依赖和路径两个坑如果你用 PyInstaller 打包项目最容易出的问题是 MySQL 驱动找不到。pymysql 是纯 Python 的打包通常没问题但如果你换用了 mysql-connector-python它的动态链接库在打包时经常丢失。解决方式是打包命令里加上--hidden-import参数。另一个坑是配置文件路径——打包后 exe 所在的目录不一定能写入文件如果你在代码里用了相对路径读写配置打包后大概率报错。我建议代码里所有资源路径都用sys._MEIPASS和os.path.dirname(sys.executable)动态拼接。6. 提升使用体验的三个实战技巧批量导入、成绩统计与打包发布到这里一个能跑的学生管理系统已经成形但距离“别人愿意用”还有一段距离。我建议你优先做下面三件事它们投入不大收获非常明显。第一件事是批量 Excel 导入。做成一个按钮选择 xlsx 文件后用 openpyxl 读取逐行插入数据库。这里要特别注意的是重复学号的处理逻辑——遇到重复是跳过、覆盖还是中断做个单选提示框让用户自己选才是完整的产品逻辑。def import_from_excel(path, tree, status_label): 从 Excel 导入学生数据xlsx 格式学号、姓名、性别、班级、电话 import openpyxl wb openpyxl.load_workbook(path) ws wb.active conn get_connection() inserted 0 for row in ws.iter_rows(min_row2, values_onlyTrue): stu_id, name, gender, class_name, phone row[:5] try: with conn.cursor() as cursor: cursor.execute( INSERT INTO student (stu_id, name, gender, class_name, phone) VALUES (%s, %s, %s, %s, %s), (str(stu_id).strip(), name.strip(), gender, class_name, phone) ) inserted 1 except pymysql.IntegrityError: pass # 跳过重复学号也可以改成更新逻辑 conn.commit() conn.close() messagebox.showinfo(完成, f成功导入 {inserted} 条数据) refresh_student_list(tree, status_label)这段代码里values_onlyTrue这个参数是从 openpyxl 读 Excel 最简洁的写法返回的是元组而不是 cell 对象省去后续逐个取.value的麻烦。注意跳过重复的异常处理放在每行的内层循环里这样一条出错不会中断整个导入过程——关键的生产级设计。第二件事是成绩统计功能。在 score 表上做AVG(score_value) GROUP BY course_id查询把结果展示到 Treeview 或直接导出。这个功能能体现你对 SQL 聚合函数的掌握在答辩或写文档时比单纯的学生信息管理更容易出彩。第三件事是打包发布。使用 PyInstaller 时我建议用pyinstaller -F -w main.py --hidden-import pymysql。-F打包成单文件-w隐藏控制台窗口。打包完成后把 main.spec 文件留着后续更新代码后直接pyinstaller main.spec重新打包效率比反复敲命令高。如果打包后杀毒软件误报通常是因为程序是单文件且加了-F参数换目录或者改用-D目录模式能解决。做完这三个功能这个系统就已经超出了“作业水平”达到了“能交给非技术同事用”的程度。我当年给学生管理系统加完 Excel 导入功能后某位辅导员直接拿去用了三个学期唯一提的修改需求是“成绩导入时也跳过空行”——这就是真实使用的反馈比任何答辩老师的评分都有价值。这个项目虽然简单但它把 Python 语法、数据库设计、GUI 事件驱动三条知识线串在了一起。如果你做完后想继续进阶建议把这个系统的界面换成一到两个网页面用 Flask 或 FastAPI 包一层 JSON 接口——你会立刻明白桌面端和 Web 端在数据流上的本质区别那是另一个值得投入的方向。希望这篇文章能帮你把第一个完整项目跑起来也希望你踩坑时能想起这里写过的某一行代码。本文还有配套的精品资源点击获取