要不要拿点东西练手?很多初学者在学完 Python 基础语法之后,都会有这么一段迷茫期:变量、列表、字典、文件操作、函数、类,单拎出来各个都看得懂,可一关掉教程就不知道这些东西到底怎么串起来用。教务系统这种项目,是练手最划算的选择之一。功能明确、逻辑环环相扣,但又不会复杂到让新手劝退。把“学生管理、课程管理、成绩管理、选课”这些传统业务在控制台里跑起来,写完之后你会发现,之前学的那些零散知识点全都有了下落。
这篇文章就把我实际写的一个“简单版教务系统”完整拆开给你看,从需求分析、表结构设计到具体的 Python 代码实现、踩过的坑、排查思路,全部复盘。不搞花哨的图形界面,只做命令行交互,把核心逻辑吃透。整体代码量控制在 300 行到 400 行,无论你是刚入门想找项目练手,还是想看看一个完整系统是怎么组织代码的,都能在这里找到点有用的东西。
1. 系统需求与整体设计思路
1.1 需求拆解:一个“教务系统”到底需要哪些功能
教务系统听起来是个很宏大的概念,但落到“简单版”这个定位上,核心业务就四件事:管学生、管课程、管成绩、管选课。围绕这四件事,最基础的功能需求如下:
- 学生管理:新增学生、查看学生列表、按学号查询、修改学生信息、删除学生。
- 课程管理:新增课程、查看课程列表、按课程号查询、删除课程。
- 选课管理:学生选课、查看某学生已选课程、退选课程。
- 成绩管理:录入学生某门课的成绩、按学号或课程查看成绩单。
- 数据存储:程序重启之后数据不丢失。
一开始很容易犯的毛病是“功能越加越多”,今天想加个教师端,明天想加个排课模块。但既然是练手项目,把上面这几条做扎实了,比堆砌一堆半成品功能有用得多。我最终给这个系统划定了边界:不做GUI、不做Web、不做权限分级,只有管理员一个角色,交互方式就是命令行菜单。
选这个边界是有原因的。控制台交互能让你把注意力集中在数据结构和业务流程上。你一旦上了 Flask 或 Tkinter,注意力就会分散到路由、模板、组件布局上,反而淡化了“训练 Python 核心能力”这个目标。
1.2 数据存储方案选型:为什么不直接写文件
第二个需要做决定的地方是数据怎么存。最简单的选择是把数据写进文本文件或 JSON 文件,高级一点的是用 SQLite,再往上就是 MySQL 之类的数据库服务器。
我的建议是直接用 sqlite3。它是 Python 标准库自带的模块,不需要安装任何第三方包,却能让你写出真正的 SQL 语句,体验完整的数据库操作流程。JSON 文件方案在数据量小的时候确实能跑,但你要自己处理“读文件—改数据—写回文件”的全过程,还要考虑并发写入时数据损坏的问题。SQLite 把这些都封装好了,你只需要用标准 SQL 语句就能完成增删改查。
用 SQLite 还有一个隐藏好处:将来如果你要把这个系统升级成 Web 版,或者把数据迁移到 MySQL,你的 Data Access 层代码基本是通用的,改动成本极低。用 JSON 文件实现的数据层,到了那时几乎要全部重写。
1.3 代码结构规划:别把全部代码堆在一个文件里
初学者很容易把所有代码都堆进一个 main.py,刚开始写的时候挺爽,写到后面就会头疼:函数之间互相调用,变量作用域混乱,找一个 bug 要翻半天。
我在动手之前就把代码规划成了三个模块:
| 模块 | 职责 | 对应文件 |
|---|---|---|
| 数据库层 | 负责建表、连接数据库、执行 SQL 的通用函数 | db.py |
| 业务层 | 学生、课程、选课、成绩的具体操作函数 | models.py |
| 交互层 | 控制台菜单、用户输入、调用业务函数 | main.py |
这样分层之后,每一层的代码都只关注自己的事情。比如你想调整菜单交互逻辑,只需要改 main.py;想让课程号自动生成而不用手输,只需要改 models.py 里的相应函数。“低耦合,高内聚”不是喊口号,这种小项目里照做,收益立竿见影。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库表结构设计与初始化
2.1 四张表的关系模型
在设计数据库表之前,先把业务实体梳理清楚:学生、课程、成绩、管理员账号。其中成绩表并不是独立存在的,它是“学生”和“课程”之间的关联表,专门用来存“某学生选了某门课”以及“这门课考了多少分”。
表结构如下:
-
students 表
id:主键,自增(其实学号本身可以做主键,但用自增 id 更符合数据库规范)student_id:学号,设置 UNIQUE 约束name:姓名major:专业
-
courses 表
id:主键,自增course_id:课程号,UNIQUE 约束course_name:课程名credit:学分teacher:授课教师
-
scores 表
id:主键,自增student_id:学号(外键指向 students.student_id)course_id:课程号(外键指向 courses.course_id)score:成绩,默认 NULL 表示还没考试
-
users 表
id:主键,自增username:用户名password:密码
为什么学号、课程号不用外键约束?在 SQLite 里,外键约束默认是关闭的,而且成绩表里的 student_id 和 course_id 如果加了真实的外键约束,删除学生或课程时会遇到外键冲突,处理起来会麻烦一点。这个项目作为练手,我选择在业务逻辑层手动维护一致性,也就是删除学生时“手动”把成绩表里对应的记录一起删掉。真实项目里,用外键加上 ON DELETE CASCADE 是更稳妥的方案,这一点你可以作为扩展方向自己去验证。
2.2 数据库初始化代码
我用一个独立的 db.py 处理数据库的创建和连接。这个文件里只放数据库层的逻辑,不掺任何业务判断。
python复制import sqlite3
DB_NAME = "edu_system.db"
def get_connection():
conn = sqlite3.connect(DB_NAME)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_connection()
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS students (
id INTEGER PRIMARY KEY AUTOINCREMENT,
student_id TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
major TEXT DEFAULT ''
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS courses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
course_id TEXT UNIQUE NOT NULL,
course_name TEXT NOT NULL,
credit REAL DEFAULT 0,
teacher TEXT DEFAULT ''
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS scores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
student_id TEXT NOT NULL,
course_id TEXT NOT NULL,
score REAL DEFAULT NULL,
UNIQUE(student_id, course_id)
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL
)
""")
cursor.execute("""
INSERT OR IGNORE INTO users (username, password)
VALUES ('admin', '123456')
""")
conn.commit()
conn.close()
几个容易忽略的细节:
conn.row_factory = sqlite3.Row这行很关键。它让查出来的结果可以通过row[0]或row["name"]两种方式访问,极大的改善了代码可读性。UNIQUE(student_id, course_id)约束保证了同一个学生不会重复选同一门课。这个约束比你在代码里用if判断要可靠得多。INSERT OR IGNORE用于初始化管理员账号。如果 users 表里已经有 username 为 admin 的记录,这条插入操作会被自动忽略,不会导致程序崩溃。
2.3 关于 SQLite 连接的最佳实践
写 SQLite 代码,有几个连接管理的教训值得拿出来说说:
第一,不要在每个函数里都新建连接。虽然代码看起来简单,但 SQLite 对于写入操作会锁定数据库文件,多个连接同时写入时容易报 database is locked 错误。像这种单用户的控制台程序,完全可以在主程序中创建一个全局连接,所有函数都用同一个连接对象操作。我最初的版本就是每个函数 get_connection() 一下,结果偶发锁库问题,后来统一成了一个连接,问题就消失了。
第二,写完要记得 commit()。SQLite 默认是手动提交事务,用 SELECT 查询不提交没问题,但 INSERT、UPDATE、DELETE 之后如果不提交,数据不会真正写入到磁盘。这个 bug 非常隐蔽,因为程序不报错,重启之后数据却全没了。
第三,用完连接要关闭。尤其是写好写坏的调试阶段,连接没关闭会导致 .db 文件被占用,在某些操作系统上还会阻止你删掉文件重来。
3. 核心业务模块:增删改查的实现细节
3.1 学生管理模块:把“输入校验”当成头等大事
学生管理是最基础的 CRUD,涉及的 SQL 无非是 INSERT、SELECT、UPDATE、DELETE。但既然是给“教务系统”用的,输入校验就必须严格,不然一堆脏数据会把后面的选课和成绩模块一起带偏。
我实现了这几个函数:add_student()、list_students()、find_student()、update_student()、delete_student()。拿新增学生举例,核心逻辑如下:
python复制def add_student(conn, student_id, name, major=""):
if not student_id.strip() or not name.strip():
return False, "学号和姓名不能为空"
cursor = conn.cursor()
try:
cursor.execute(
"INSERT INTO students (student_id, name, major) VALUES (?, ?, ?)",
(student_id.strip(), name.strip(), major.strip())
)
conn.commit()
return True, "学生添加成功"
except sqlite3.IntegrityError:
return False, "学号已存在"
这里必须说两个关键点。第一个是参数化查询,第二个是 IntegrityError 异常捕获。
参数化查询就是 SQL 语句里的 ? 占位符。千万不要用字符串拼接去构造 SQL,比如 f"INSERT INTO students VALUES ('{student_id}', '{name}')"。这种写法非常危险,一旦输入值里含有单引号,要么 SQL 执行报错,要么会构造出恶意 SQL 语句,造成所谓的 SQL 注入。虽然这是个本地练手项目,但养成好习惯要从第一天开始。
捕获 sqlite3.IntegrityError 是为了处理学号冲突。如果没有这个异常捕获,当用户输入一个已存在的学号时,程序会直接崩掉。捕获异常之后把提示信息返回给交互层,用户就能看到“学号已存在”而不是一堆堆栈信息。这个模式就是“防御性编程”,实际项目里到处都用得上。
删除学生的函数有一个地方容易漏。直接写 DELETE FROM students WHERE student_id = ? 当然简单,但如果这个学生的成绩表里还有记录,就会留下“无主”的数据残渣。我在业务层手动处理了这个引用的完整性:
python复制def delete_student(conn, student_id):
cursor = conn.cursor()
cursor.execute("DELETE FROM scores WHERE student_id = ?", (student_id,))
cursor.execute("DELETE FROM students WHERE student_id = ?", (student_id,))
conn.commit()
return cursor.rowcount > 0
cursor.rowcount 可以拿到上一条 SQL 影响的行数。如果删除的学生不存在,rowcount 会是 0,这样交互层就知道要提示“学生不存在”而不是“删除成功”。
3.2 课程管理模块:考虑学分的数值约束
课程管理整体逻辑和学生管理类似,但有一个独立的问题是学分(credit)必须是正数。这里我用了一个简单校验:如果输入值看起来不是数字或小于等于 0,就直接拒绝写入,返回提示信息,不让脏数据进入数据库。
python复制def add_course(conn, course_id, course_name, credit, teacher=""):
if not course_id.strip() or not course_name.strip():
return False, "课程号和课程名不能为空"
try:
credit = float(credit)
except ValueError:
return False, "学分必须是数字"
if credit <= 0:
return False, "学分必须大于 0"
cursor = conn.cursor()
try:
cursor.execute(
"INSERT INTO courses (course_id, course_name, credit, teacher) VALUES (?, ?, ?, ?)",
(course_id.strip(), course_name.strip(), credit, teacher.strip())
)
conn.commit()
return True, "课程添加成功"
except sqlite3.IntegrityError:
return False, "课程号已存在"
注意这里的 float(credit) 转换放在 try...except 里。用户可能输入 “abc”“3.5学分”这类无法转换的值,转换过程会抛出 ValueError。在业务函数里就地把异常转化成人话返回,交互层的代码就能非常干净。很多新手没这层处理,用户输入一个错误格式,整个程序就带着一堆红色报错退出了,体验很差。
3.3 选课模块:把“是否可选”的逻辑讲清楚
选课是整个系统里业务逻辑最密集的地方。一个学生能不能选某门课程,需要同时满足多个条件,我逐条列出来:
- 学生必须存在。
- 课程必须存在。
- 学生没有选过这门课。
- 课程没有选满(可选扩展,这里先不做容量限制)。
只有所有条件都通过,才执行 INSERT 写入 scores 表。这套逻辑用代码实现起来并不复杂,但关键在于“先查后写”的顺序,缺一不可。
python复制def choose_course(conn, student_id, course_id):
cursor = conn.cursor()
student = cursor.execute(
"SELECT * FROM students WHERE student_id = ?", (student_id,)
).fetchone()
if not student:
return False, "学生不存在"
course = cursor.execute(
"SELECT * FROM courses WHERE course_id = ?", (course_id,)
).fetchone()
if not course:
return False, "课程不存在"
existed = cursor.execute(
"SELECT * FROM scores WHERE student_id = ? AND course_id = ?",
(student_id, course_id)
).fetchone()
if existed:
return False, "该学生已选此课程"
cursor.execute(
"INSERT INTO scores (student_id, course_id, score) VALUES (?, ?, ?)",
(student_id, course_id, None)
)
conn.commit()
return True, f"选课成功:{student['name']} 选择了 {course['course_name']}"
注意 scores 表插入选课记录时,把 score 字段设为 None,也就是 SQL 里的 NULL。这代表“选了课但还没成绩”,语义上非常清楚。等考试结束,再把分数更新进去。
SELECT * 返回的是一个 sqlite3.Row 对象,因为启动连接时设置了 row_factory,所以可以直接用 student["name"] 访问字段,这个写法在后面拼接提示信息时非常舒服。
3.4 成绩录入模块:更新操作与 NULL 值的边界
成绩录入本质上是 UPDATE 操作:根据学号和课程号找到 scores 表里的记录,把 score 字段更新为用户输入的值。核心代码如下:
python复制def set_score(conn, student_id, course_id, score_value):
cursor = conn.cursor()
existed = cursor.execute(
"SELECT * FROM scores WHERE student_id = ? AND course_id = ?",
(student_id, course_id)
).fetchone()
if not existed:
return False, "该学生未选此课程,不能录入成绩"
try:
score_value = float(score_value)
except ValueError:
return False, "成绩必须是数字"
if score_value < 0 or score_value > 100:
return False, "成绩必须在 0 到 100 之间"
cursor.execute(
"UPDATE scores SET score = ? WHERE student_id = ? AND course_id = ?",
(score_value, student_id, course_id)
)
conn.commit()
return True, "成绩录入成功"
这里“该学生未选此课程”的判断特别重要。有些学生还没选课,你却硬要给他录成绩,这在业务上是不允许的。这个判断直接把问题拦在入口处,数据库层不需要任何额外逻辑。
成绩范围校验(0 到 100)同样是业务层的职责。虽然真实世界有的课程会用到百分制以外的计分方式,但在这个简单的系统里,明确把范围限定在百分制,既符合常规又减少了异常情况。
4. 交互层设计:让整个系统“能用”起来
4.1 主菜单设计与输入循环
业务逻辑全部就绪后,剩下来的是交互层。很多人不重视交互层,觉得它无非是 print 加 input。但交互层的代码质量直接决定了程序好不好用,尤其是“输入死循环”这个问题,交互层很常见。
下面是我对主菜单循环的处理方式:
python复制def main_menu():
print("\n========== 教务管理系统 ==========")
print("1. 学生管理")
print("2. 课程管理")
print("3. 选课管理")
print("4. 成绩管理")
print("5. 查看学生选课情况")
print("0. 退出系统")
print("==================================")
while True:
main_menu()
choice = input("请选择操作:").strip()
if choice == "1":
student_menu()
elif choice == "2":
course_menu()
elif choice == "3":
choose_menu()
elif choice == "4":
score_menu()
elif choice == "5":
list_choices()
elif choice == "0":
print("感谢使用,再见!")
break
else:
print("无效选择,请重新输入")
.strip() 这一步很关键。用户输入时很可能随手敲了个空格,比如输入“1 ”,如果不 strip,程序就把它当成无效选择了。这个小细节能让程序的容错性提高一大截。
子菜单的写法类似,这里以学生管理子菜单为例:
python复制def student_menu():
while True:
print("\n----- 学生管理 -----")
print("1. 添加学生")
print("2. 查看所有学生")
print("3. 按学号查找学生")
print("4. 修改学生信息")
print("5. 删除学生")
print("0. 返回主菜单")
choice = input("请选择:").strip()
if choice == "1":
student_id = input("请输入学号:")
name = input("请输入姓名:")
major = input("请输入专业(可回车跳过):")
ok, msg = add_student(conn, student_id, name, major)
print(msg)
elif choice == "2":
show_students()
# ... 其他分支
elif choice == "0":
break
else:
print("无效输入,请重新选择")
4.2 展示函数:查询结果要让人一眼看懂
有一次我在测试程序时,直接用循环把 SELECT 的结果一行行打出来,效果惨不忍睹:没有表头,没有对齐,数据多了根本看不出哪一列是什么。后来我统一写了两个展示辅助函数,把“格式化打印”这件事集中处理。
python复制def show_students(students):
if not students:
print("暂无学生数据")
return
print(f"{'学号':<12} {'姓名':<10} {'专业':<16}")
print("-" * 40)
for s in students:
print(f"{s['student_id']:<12} {s['name']:<10} {s['major']:<16}")
def show_courses(courses):
if not courses:
print("暂无课程数据")
return
print(f"{'课程号':<10} {'课程名':<16} {'学分':<8} {'教师':<10}")
print("-" * 48)
for c in courses:
print(f"{c['course_id']:<10} {c['course_name']:<16} {c['credit']:<8} {c['teacher']:<10}")
Python 的 f-string 支持对齐符号 <,后面跟一个正整数就表示左对齐并填充到指定宽度。中文字符的宽度问题和英文字符不一样,实际打印中可能达不到严格对齐的效果,但至少比胡乱 print 要清晰得多。真要处理中文字符对齐,可以考虑引入第三方库 wcwidth,但作为练手项目,现在这种程度已经够用了。
4.3 查看学生选课情况的连表查询
查看选课情况是这个系统里唯一需要用到 JOIN 查询的地方。你不仅要知道学生选了《高等数学》这门课,还想知道教师是谁、学分多少、考了多少分,这些信息分别存在 courses 表和 scores 表里,光查 scores 表拿不到完整信息。
这条 SQL 的写法如下:
python复制def get_student_courses(conn, student_id):
cursor = conn.cursor()
rows = cursor.execute("""
SELECT c.course_id, c.course_name, c.credit, c.teacher, s.score
FROM scores s
JOIN courses c ON s.course_id = c.course_id
WHERE s.student_id = ?
""", (student_id,)).fetchall()
return rows
注意 FROM 子句的写法:FROM scores s JOIN courses c ON s.course_id = c.course_id,这里给表起了别名(s 和 c),让 SQL 语句短一些。JOIN 的语义可以这样理解:先把 scores 表和 courses 表按课程号拼成一张大表,再从这个大表里筛选指定学生在的行。
连表查询对于初学者来说往往是一个分水岭,一旦跨过去,你对数据库的理解就能上一个台阶。这个教务系统恰好提供了一个足够自然的使用场景,让你有动力去理解 JOIN。
5. 异常处理与系统健壮性分析
5.1 入口处的全局异常兜底
程序再小心,也难免有预料之外的运行错误。为了不至于一遇到意外就“崩得很难看”,我在主循环外层包了一层 try...except。发生异常时,程序不会直接退出,而是打印错误信息后让用户重新选择:
python复制if __name__ == "__main__":
init_db()
conn = get_connection()
try:
login(conn)
while True:
main_menu()
choice = input("请选择操作:").strip()
if choice == "0":
print("感谢使用,再见!")
break
handle_choice(conn, choice)
except Exception as e:
print(f"程序出现异常:{e}")
finally:
conn.close()
初学者可能觉得这层 try...except 有点多余,但它其实提供了一个“安全网”。真实开发中,程序面对的用户输入五花八门,谁能想到用户会输入一个特殊符号导致编码报错?有了这层兜底,至少程序不会直接崩溃退出,而是给用户留下一个可读的错误提示。
5.2 用户输入类型转换的统一处理
输入“成绩”这类必须转成数字的值时,很容易在 float() 转换时抛异常。我的处理方式是在每个业务函数内部单独校验,而不是在交互层统一转换。这两个位置各有利弊:
- 在交互层转换:可以让业务函数强制接收 float 类型,类型更干净,但每个子菜单都要重复写 try-except 代码。
- 在业务函数内转换:代码有重复,但保证了业务函数的独立性,将来调用方换成 GUI 或 Web 框架时,校验逻辑仍然有效。
考虑到这个项目规模,我选择了后者。你就算把 main.py 整个换掉,业务层的安全边界依然稳稳的。
5.3 无限循环和选择菜单的边界
有几次我在测试“返回主菜单”功能时,点进子菜单后输入 “0”,结果不仅没有退出,反而继续打印了子菜单,怎么也回不到主菜单。排查之后发现是子菜单里“0”分支的判断写错了,把 break 写成了 continue。continue 是跳过本次循环,等于直接回到 while 循环开头,自然又打印了一遍子菜单。
这种错误非常典型:不是语法不会,而是循环控制语句的语义没吃透。break 结束整个循环,continue 跳过本次迭代。在菜单逻辑里,“返回上一层”应该用 break。如果你在写菜单时遇到了类似的“怎么选都回不去”的问题,先检查是不是把这两个关键字搞混了。
6. 常见问题速查与排错思路
6.1 数据库文件被占用或无法删掉的解决方式
如果你在 Windows 上反复运行程序,可能会遇到数据库文件被锁定的报错。原因通常是上一次程序没有正常退出,连接没有关闭。排查时先关掉所有正在运行的 Python 窗口,再检查是不是有后台进程占用 .db 文件。如果还是不行,直接删掉 .db 文件重新跑一次也不失为一个快速重置的方式,毕竟这是开发调试阶段,数据丢了也不心疼。
6.2 显示中文出现乱码怎么办
在 Windows 命令行跑这个程序,如果出现中文乱码,大概率是编码问题,也就是编码不匹配。最简单的解决办法是在 Python 文件的第一行加上 # -*- coding: utf-8 -*-,并且在程序入口处把标准输出重定向到 UTF-8。可以用一句固定代码处理:
python复制import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
这段代码在不同的 PyCharm、VSCode 等编辑器里可能需要根据实际环境调整。如果你是在 PyCharm 里运行,通常只用在“File > Settings > Editor > File Encodings”里把全局编码和项目编码都改成 UTF-8 就能解决。
6.3 表已经存在却提示 no such table
这个问题的典型原因是初始化函数没有先于业务操作执行。程序启动时先调用 init_db() 建表,之后所有业务函数才能操作这些表。如果你在写代码的时候单独调试某个业务函数,跳过了初始化,就会遇到 sqlite3.OperationalError: no such table: students。
另一种可能是 CREATE TABLE IF NOT EXISTS 被误写成了 CREATE TABLE,导致第二次运行时因为表已存在而报错。把 SQL 语句加回 IF NOT EXISTS 就能解决。
6.4 系统学号重复但程序不报错
学号重复不报错的原因非常隐蔽:你手动往数据库里插入了重复学号的数据,绕过了业务层的校验。你可以在 SQLite 的表中增加 UNIQUE 约束来防止这个问题,这也是我在建表时给 student_id 加上 UNIQUE 的原因。双保险总比单保险稳。已经出现重复数据的补救办法是直接删掉数据库文件重建,或者执行一条手动去重的 SQL。
6.5 修改了代码但运行时好像没变化
运行代码时经常出现“我明明改了代码,但运行结果还是老样子”的困惑。这大概率是 Python 进程缓存了旧模块,或者你看错了运行入口。排查方法很直接:先把所有正在运行的 Python 进程全部停掉,再从主文件重新跑一遍,并确认当前工作目录是对的。另外,如果你用的是普通编辑器的“Run”按钮,注意它可能运行的不是 main.py 而是你上次打开的文件。
7. 项目扩展方向:别让这个系统止步于此
这个简易教务系统功能完整、结构清晰,非常适合作为继续加功能的起点。以下几个方向我亲测过,难度递增,但每一个都能实打实地提升你的编程水平:
增加课程容量限制。给 courses 表增加一个 capacity 字段,在选课时统计该课程的选课人数,达到上限就提示“课程已满”。这会迫使你去接触 COUNT 聚合函数和事务的处理,对 SQL 理解有很大帮助。
增加登录防暴力破解机制。现在登录模块只是简单地查用户名和密码是否匹配,你可以增加一个登录失败次数的计数器,连续失败三次就锁定一段时间。这个功能会涉及时间戳和临时状态管理,是理解“会话”概念的一个微缩模型。
把数据层切换成 MySQL。SQLite 的所有操作你换成 pymysql 库之后,基本逻辑不变,但你会遇到连接参数、事务隔离、字符集等一系列新问题。走一遍这个流程,比单纯看教程学数据库要深刻得多。
增加简单的统计报表。比如按课程统计平均分、最高分、及格率,或者按专业统计平均绩点。写 SQL 时要用到 GROUP BY、HAVING、多重 JOIN,这些可都是面试题里的常客。
从控制台升级到 Web 界面。用 Flask 把现有的 models.py 业务函数包一层路由,就能做成一个浏览器访问的教务系统。因为业务逻辑层已经分离得很干净,所以这个升级过程会异常顺畅。你只需要新增一个 app.py,把之前交互层的 print(input) 换成 render_template 就行。大约半天时间,你就能得到一个真正能给别人演示的 Web 项目。
我个人在实际操作中的体会是:做完这个系统,最大的收获不是那几百行代码,而是你终于体验了一遍“从需求到实现”的完整闭环。你不再是一个只会写函数和循环的初学者,而是能自己分析需求、设计数据表、编写业务逻辑、排查 Bug 的人。建议你也亲手敲一遍,边敲边想每一步的意义,遇到卡壳就回头翻这篇文章,比复制粘贴全代码要学到的多得多。整个项目认真做完,你的 Python 实战能力会有一个肉眼可见的跃升。
