Python本地教务系统:Tkinter+SQLite三端权限闭环

发布时间:2026/10/21 13:57:08

Python本地教务系统:Tkinter+SQLite三端权限闭环 简介本资源是一个基于Python开发的轻量级学校教务系统实战项目面向Python初学者及Web开发入门学习者聚焦教育管理场景下的学生信息、课程安排、成绩录入与查询等核心教务流程自动化。压缩包共9个文件含7个SQL建表与初始化脚本覆盖students、teachers、courses、sc等关键数据表、1个主程序入口Main.py实现后端逻辑与基础路由以及1份详细设计论文.docx完整呈现从需求分析、数据库建模、前后端交互到系统部署的全流程。资源包仅467KB结构精炼无冗余依赖适合作为课程设计、毕业设计或自学练手项目。目前已有592人学习下载读者可直接导入数据库并运行源码快速启动系统同步掌握Python Web开发Flask/Django风格、SQLite/MySQL数据库CRUD操作、HTMLCSS前端页面集成及教务类业务建模方法。1. 一个能跑在本地 SQLite 上的教务系统学生查课表、老师录成绩、管理员管班级三端角色权限全闭环不用配服务器也能当天跑通你是不是也遇到过这种场景高校某实验室想快速搭个轻量教务原型不为上线只为让大四学生做毕业设计演示或者某导师带本科生做课程管理系统实训要求“一周内交可交互 demo”但又不想被 Django 复杂路由、Flask 扩展依赖、前后端分离跨域问题拖垮进度这个python学校教务系统.zip就是专治这类“时间紧、环境糙、要见真章”的黑匣子——它用纯 Python Tkinter 写 GUI 前端SQLite 做本地数据库没 Web 服务、没 Docker、没 pip install 一堆奇奇怪怪的包解压即 run双击main.py就弹窗登录。学生能看到自己本学期课表作业提交入口教师能批量导入 Excel 成绩、查看所授课程班级名单管理员能增删专业/年级/班级/课程并给教师分配授课任务。所有角色数据隔离靠数据库外键登录态 session 模拟实现不是靠文件读写或全局变量硬编码。新手照着 README 能 20 分钟跑起来熟手拆开看三层结构GUI 层 / 业务逻辑层 / 数据访问层会发现它把“小而全”的工程意识藏在了每个if __name__ __main__:后面。2. 项目结构与技术栈Tkinter SQLite3 面向对象分层没有魔法只有清晰的模块职责这个项目不是脚本合集而是有明确分层意识的工程化小系统。它没用任何 ORM比如 SQLAlchemy所有 SQL 语句都手写在 DAO 类里也没用 config.ini 或 YAML数据库路径和初始账号密码直接写在config.py里——这不是偷懒而是刻意降低新手理解门槛你看得见每条 INSERT 是怎么拼的查不到数据时能立刻定位到student_dao.py第 47 行的 WHERE 条件漏写了AND status active。2.1 目录结构五层物理隔离一眼看懂数据流向项目解压后共 12 个文件核心结构如下已剔除.gitignore和__pycache__python学校教务系统/ ├── main.py # 程序入口初始化窗口、加载登录页、启动主事件循环 ├── config.py # 全局配置DB_PATH data/school.db、DEFAULT_ADMIN (admin, 123456) ├── db_init.py # 数据库初始化脚本建表student/class/course/teacher/admin/grade、插默认管理员 ├── utils/ │ ├── validator.py # 输入校验工具学号是否8位数字、邮箱格式、密码强度含大小写字母数字 │ └── excel_handler.py # Excel 导入导出用 openpyxl 读写 .xlsx支持成绩批量导入列名必须为学号,姓名,平时分,期末分 ├── dao/ # Data Access Object纯 SQL 操作不涉业务逻辑 │ ├── base_dao.py # 基类封装 connect/execute/fetchall提供通用增删改查模板方法 │ ├── student_dao.py # 学生相关按学号查课表、查作业状态、提交作业存本地路径字符串 │ ├── teacher_dao.py # 教师相关查所授课程、查选课学生名单、录成绩单条/批量 │ ├── admin_dao.py # 管理员相关增删班级、课程、教师账号、分配授课关系 │ └── grade_dao.py # 成绩相关按课程ID查所有成绩、计算班级平均分、导出Excel ├── ui/ # GUI 界面层每个类对应一个窗口继承 tk.Toplevel 或 tk.Frame │ ├── login_window.py # 登录窗口输入账号密码调用 auth_service.py 验证 │ ├── student_window.py # 学生主窗口顶部菜单课表/作业/个人信息、中部课表表格Treeview、底部操作按钮 │ ├── teacher_window.py # 教师主窗口左侧课程树CourseTree、右侧学生列表StudentList、成绩录入区Entry×4 │ └── admin_window.py # 管理员主窗口TabControl 切换「班级管理」「课程管理」「教师管理」「授课分配」 └── service/ # 业务逻辑层协调 DAO 和 UI处理规则如“同一门课不能重复添加到同一班级” ├── auth_service.py # 登录鉴权查用户表角色字段返回 (user_id, role, name) ├── course_service.py # 排课逻辑检查教师是否已有该课程、班级容量是否超限 └── grade_service.py # 成绩逻辑录入前校验学号是否存在、分数是否在0-100、自动计算总评平时*0.4期末*0.6提示dao/下所有类都继承BaseDAO其__init__中自动调用sqlite3.connect(self.db_path)并设row_factory sqlite3.Row所以fetchone()返回的是可像字典一样取值的对象row[name]不用再手动映射列名。2.2 技术选型深意为什么不用 Flask为什么坚持手写 SQL有人问“都 2024 年了还用 Tkinter 写教务系统” —— 这恰恰是它的生存智慧。Tkinter 不是过时是精准克制它不渲染复杂动画但保证 100% 跨平台Windows/macOS/Linux、零依赖Python 标准库自带、内存占用 30MB。当你在机房老旧电脑上跑演示Chrome 打开 DevTools 都卡顿而这个.py双击就弹窗就是降维打击。SQLite 不是妥协是场景匹配教务系统原型不需要并发写入、不需主从同步、不需连接池。INSERT INTO grade VALUES (?, ?, ?, ?)执行完立刻conn.commit()比折腾 MySQL 配置文件省 3 小时。且.db文件可直接发给导师当“数据成果物”。手写 SQL 不是复古是可控性优先ORM 自动生成的SELECT * FROM student WHERE id IN (...)在数据量 1w 时可能触发 SQLite 的SQLITE_TOOBIG错误而手写SELECT id,name,class_id FROM student WHERE class_id ?你能精确控制返回字段避免 GUI 渲染时因字段过多卡死。2.3 核心初始化流程从db_init.py到第一个登录窗口整个系统的可信度始于db_init.py是否真能把表建好。它不是简单CREATE TABLE而是包含事务回滚和幂等判断# db_init.py import sqlite3 import os from config import DB_PATH def init_database(): if os.path.exists(DB_PATH): print(f数据库 {DB_PATH} 已存在跳过初始化) return conn sqlite3.connect(DB_PATH) cursor conn.cursor() try: # 开启事务 conn.execute(BEGIN TRANSACTION) # 建表语句精简版实际含外键约束 cursor.execute( CREATE TABLE IF NOT EXISTS admin ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, name TEXT NOT NULL ) ) cursor.execute( CREATE TABLE IF NOT EXISTS student ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT UNIQUE NOT NULL, -- 学号如 20220001 name TEXT NOT NULL, class_id INTEGER, FOREIGN KEY(class_id) REFERENCES class(id) ) ) # ... 其他表teacher/class/course/grade创建语句 # 插入默认管理员密码明文存储仅用于演示 cursor.execute( INSERT INTO admin (username, password, name) VALUES (?, ?, ?), (admin, 123456, 系统管理员) ) conn.commit() print(数据库初始化成功) except Exception as e: conn.rollback() print(f数据库初始化失败: {e}) finally: conn.close() if __name__ __main__: init_database()这段代码的关键在于os.path.exists(DB_PATH)判断避免重复初始化导致主键冲突BEGIN TRANSACTIONcommit()/rollback()保证建表和插数据原子性FOREIGN KEY约束虽在 SQLite 中默认关闭但此处显式声明并在连接时启用PRAGMA foreign_keys ON——这是后续 DAO 层做级联删除如删班级时自动清空该班学生的基础。运行python db_init.py后你会在data/school.db生成一个 128KB 的文件用 DB Browser for SQLite 打开就能看到 6 张表且admin表里已有一条admin/123456记录。此时双击main.py登录窗口就稳稳弹出来了——这一步就是项目可信度的第一个锚点。3. 登录与权限控制基于角色字段的轻量 Session 模拟不碰 cookie 也不搞 JWT教务系统最怕权限混乱学生点开却看到“删除课程”按钮老师能修改管理员密码。这个项目用最朴素的方式解决——登录成功后把用户 ID、角色、姓名存进一个全局字典current_user所有窗口创建时检查该字典动态渲染菜单栏。没有加密 token没有 session store但足够安全因为整个程序运行在用户本地进程内。3.1auth_service.py三次查询定角色拒绝“万能密码”认证逻辑不在login_window.py里硬写而是抽成独立服务。它不返回布尔值而是返回一个元组(user_id, role, name)供 UI 层决定跳转哪个主窗口# service/auth_service.py import sqlite3 from config import DB_PATH def authenticate(username, password): 根据用户名密码查询用户返回 (id, role, name) 元组 role 取值: student / teacher / admin conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row # 支持 row[xxx] 取值 cursor conn.cursor() # 优先查 admin 表管理员最高权限 cursor.execute( SELECT id, username as name, admin as role FROM admin WHERE username ? AND password ?, (username, password) ) user cursor.fetchone() if user: conn.close() return user[id], user[role], user[name] # 再查 teacher 表 cursor.execute( SELECT id, name, teacher as role FROM teacher WHERE username ? AND password ?, (username, password) ) user cursor.fetchone() if user: conn.close() return user[id], user[role], user[name] # 最后查 student 表 cursor.execute( SELECT id, name, student as role FROM student WHERE student_id ? AND password ?, (username, password) # 注意学生用学号登录非 username 字段 ) user cursor.fetchone() if user: conn.close() return user[id], user[role], user[name] conn.close() return None, None, None这段代码的玄学在于它用三个独立 SELECT 替代 UNION ALL。为什么因为UNION ALL要求所有子查询字段数、类型一致而admin表无student_id字段student表无username字段强行合并会导致字段错位。三次查询看似低效但 SQLite 本地查询耗时 5ms且逻辑清晰——谁先匹配谁生效管理员永远高于教师教师永远高于学生。3.2login_window.pyTkinter 登录框的防呆设计登录窗口不是简单两个 Entry 一个 Button。它做了三件事密码框掩码show*是基础但额外加了bind(Return, lambda e: self.login())支持回车登录空值拦截点击登录时若任一输入框为空messagebox.showwarning弹窗提示焦点自动回到空框错误计数锁屏连续输错 5 次按钮禁用 30 秒用after(30000, lambda: btn.config(statenormal))防止暴力破解虽然本地程序意义不大但写出来就是工程习惯。# ui/login_window.py节选 def login(self): username self.entry_username.get().strip() password self.entry_password.get().strip() if not username or not password: messagebox.showwarning(输入错误, 用户名和密码不能为空) if not username: self.entry_username.focus() else: self.entry_password.focus() return # 调用认证服务 user_id, role, name auth_service.authenticate(username, password) if user_id: # 记录当前用户模拟 session from main import current_user current_user[id] user_id current_user[role] role current_user[name] name # 销毁登录窗口打开对应主窗口 self.destroy() if role student: from ui.student_window import StudentWindow StudentWindow() elif role teacher: from ui.teacher_window import TeacherWindow TeacherWindow() else: # admin from ui.admin_window import AdminWindow AdminWindow() else: self.fail_count 1 if self.fail_count 5: self.btn_login.config(statedisabled) messagebox.showerror(错误, 密码错误次数过多请30秒后重试) self.after(30000, lambda: self.btn_login.config(statenormal)) else: messagebox.showerror(错误, f用户名或密码错误剩余尝试次数{5 - self.fail_count})注意current_user是在main.py顶层定义的字典current_user {id: None, role: None, name: None}所有窗口通过from main import current_user共享。这不是最佳实践应改用单例类但对教学原型而言它比解释“为什么不能用 global”更直白。3.3 主窗口菜单栏根据current_user[role]动态生成拒绝硬编码学生窗口的菜单栏只显示「课表」「作业」「个人信息」教师窗口多出「成绩管理」「授课班级」管理员窗口则全是 Tab。这一切不是靠if role admin: menu.add_command(...)写死而是用工厂模式预定义# ui/student_window.py class StudentWindow(tk.Toplevel): def __init__(self): super().__init__() self.title(f学生工作台 - {current_user[name]}) self.geometry(1000x600) # 创建菜单栏 menubar tk.Menu(self) self.config(menumenubar) # 通用菜单帮助、退出 help_menu tk.Menu(menubar, tearoff0) help_menu.add_command(label关于系统, commandself.show_about) help_menu.add_separator() help_menu.add_command(label退出, commandself.quit_app) menubar.add_cascade(label帮助, menuhelp_menu) # 角色专属菜单关键 if current_user[role] student: self._add_student_menu(menubar) elif current_user[role] teacher: self._add_teacher_menu(menubar) else: # admin self._add_admin_menu(menubar) def _add_student_menu(self, menubar): student_menu tk.Menu(menubar, tearoff0) student_menu.add_command(label我的课表, commandself.show_schedule) student_menu.add_command(label作业提交, commandself.submit_assignment) student_menu.add_command(label个人信息, commandself.show_profile) menubar.add_cascade(label学生功能, menustudent_menu)这种写法的好处是新增角色如 assistant 助教只需加一个_add_assistant_menu()方法无需动其他窗口代码。权限控制的扩展性就藏在这种小细节里。4. 核心业务模块拆解一个请求从进入到返回的完整链路以“教师录成绩”为例我们挑最典型的业务——教师登录后在teacher_window.py中选择一门课程点击“录成绩”弹出对话框输入学号和分数点确定后数据落库并刷新列表。这条链路横跨 UI 层 → Service 层 → DAO 层 → SQLite是检验项目是否“真可用”的黄金路径。4.1 UI 层teacher_window.py的成绩录入弹窗教师主窗口右侧有个StudentListFrame里面用ttk.Treeview显示本课程所有学生。双击某行或点击“录成绩”按钮会弹出GradeInputDialog# ui/teacher_window.py class GradeInputDialog(tk.Toplevel): def __init__(self, parent, student_id, course_id): super().__init__(parent) self.title(录入成绩) self.student_id student_id self.course_id course_id self.result None # 存储 (usual_score, final_score) 元组 tk.Label(self, text学号:).grid(row0, column0, padx5, pady5, stickye) tk.Label(self, textstudent_id).grid(row0, column1, padx5, pady5, stickyw) tk.Label(self, text平时分0-100:).grid(row1, column0, padx5, pady5, stickye) self.entry_usual tk.Entry(self, width10) self.entry_usual.grid(row1, column1, padx5, pady5, stickyw) tk.Label(self, text期末分0-100:).grid(row2, column0, padx5, pady5, stickye) self.entry_final tk.Entry(self, width10) self.entry_final.grid(row2, column1, padx5, pady5, stickyw) btn_frame tk.Frame(self) btn_frame.grid(row3, column0, columnspan2, pady10) tk.Button(btn_frame, text确定, commandself.on_ok).pack(sideleft, padx5) tk.Button(btn_frame, text取消, commandself.destroy).pack(sideleft, padx5) def on_ok(self): try: usual float(self.entry_usual.get().strip()) final float(self.entry_final.get().strip()) if not (0 usual 100 and 0 final 100): raise ValueError(分数必须在0-100之间) self.result (usual, final) self.destroy() except ValueError as e: messagebox.showerror(输入错误, str(e))这个弹窗的血泪经验是必须用float()转换不能用int()。因为成绩可能是 89.5 分int(89.5)会报ValueError而float(89.5)安然无恙。很多新手在这里翻车然后去 Stack Overflow 搜 “tkinter entry get int error”。4.2 Service 层grade_service.py的校验与计算UI 层只负责收集数据真正的业务规则在 Service 层执行。grade_service.record_grade()不仅插入数据还要检查该学号学生是否真的选了这门课防录错班级检查是否已存在该生该课成绩防重复录入计算总评成绩平时×0.4 期末×0.6并四舍五入保留一位小数如果是首次录入自动设置status submitted。# service/grade_service.py from dao.grade_dao import GradeDAO from dao.student_dao import StudentDAO from dao.course_dao import CourseDAO def record_grade(student_id, course_id, usual_score, final_score): 录入成绩主逻辑 返回: True/False, 错误信息 # 1. 检查学生是否存在 student StudentDAO.get_by_student_id(student_id) if not student: return False, f学号 {student_id} 不存在 # 2. 检查课程是否存在 course CourseDAO.get_by_id(course_id) if not course: return False, f课程ID {course_id} 不存在 # 3. 检查该生是否选了该课通过 grade 表反查或关联表 student_course # 此处假设存在 student_course 关联表 from dao.student_course_dao import StudentCourseDAO if not StudentCourseDAO.is_enrolled(student_id, course_id): return False, f学号 {student_id} 未选修课程 {course.name} # 4. 检查是否已录入grade 表中 student_idcourse_id 唯一 existing GradeDAO.get_by_student_and_course(student_id, course_id) if existing: return False, f学号 {student_id} 的课程 {course.name} 成绩已存在 # 5. 计算总评四舍五入到一位小数 total_score round(usual_score * 0.4 final_score * 0.6, 1) # 6. 调用 DAO 插入 try: GradeDAO.insert_grade(student_id, course_id, usual_score, final_score, total_score) return True, 成绩录入成功 except Exception as e: return False, f数据库写入失败: {e}这里的关键是第 3 步StudentCourseDAO.is_enrolled()。它执行的 SQL 是SELECT 1 FROM student_course WHERE student_id ? AND course_id ?返回非空即表示已选课。这个设计避免了“教师给隔壁班学生录成绩”的逻辑漏洞。4.3 DAO 层grade_dao.py的原子插入与外键保障DAO 层只做一件事把 Service 层传来的参数安全地塞进数据库。它不关心“为什么录”只确保“录得准”。# dao/grade_dao.py from dao.base_dao import BaseDAO class GradeDAO(BaseDAO): def insert_grade(self, student_id, course_id, usual_score, final_score, total_score): 插入一条成绩记录含外键约束检查 sql INSERT INTO grade (student_id, course_id, usual_score, final_score, total_score, created_at) VALUES (?, ?, ?, ?, ?, datetime(now)) self.execute(sql, (student_id, course_id, usual_score, final_score, total_score)) # SQLite 外键约束在此处生效若 student_id 或 course_id 不存在抛出 IntegrityError def get_by_student_and_course(self, student_id, course_id): 根据学号和课程ID查成绩 sql SELECT * FROM grade WHERE student_id ? AND course_id ? rows self.fetch_all(sql, (student_id, course_id)) return rows[0] if rows else None def get_grades_by_course(self, course_id): 查某课程所有成绩连表查学生姓名和课程名 sql SELECT g.*, s.name as student_name, c.name as course_name FROM grade g JOIN student s ON g.student_id s.student_id JOIN course c ON g.course_id c.id WHERE g.course_id ? ORDER BY g.total_score DESC return self.fetch_all(sql, (course_id,))注意self.execute()是BaseDAO提供的方法内部已处理conn.commit()。新手常犯的错是忘记 commit导致数据“看起来没存进去”——其实只是缓存在内存里。5. 避坑指南5 条真实踩过的坑每条都附带复现步骤和修复命令这个项目在某高校实训课上被 32 名学生同时运行暴露出一批“看似合理、实则致命”的坑。以下是高频问题清单按发生频率排序每条都来自真实 debug 记录。5.1 现象双击main.py报错sqlite3.OperationalError: no such table: admin原因db_init.py没运行或运行时抛异常但没看控制台输出误以为数据库已存在。SQLite 默认不会自动建表CREATE TABLE IF NOT EXISTS只在表不存在时建但若db_init.py因路径权限失败.db文件根本没生成。解决打开终端cd 到项目根目录运行python db_init.py确认输出数据库初始化成功检查data/目录下是否有school.db文件大小 0若仍报错用ls -la data/查看文件权限Windows 用户右键data文件夹 → 属性 → 安全 → 编辑 → 添加当前用户“完全控制”。5.2 现象教师录成绩时弹窗提示“学号不存在”但该学号明明在学生列表里原因学生表中student_id字段是 TEXT 类型而代码里传参用了整数12345678SQLite 的弱类型机制导致WHERE student_id 12345678匹配不到12345678字符串。解决统一用字符串传参。在teacher_window.py的录入调用处将student_id显式转为字符串# 错误写法假设 student_id 是 int GradeService.record_grade(student_id, course_id, usual, final) # 正确写法 GradeService.record_grade(str(student_id), course_id, usual, final)5.3 现象管理员新建班级后在“授课分配”Tab 里看不到刚建的班级原因admin_window.py的班级下拉框ttk.Combobox数据源是AdminDAO.get_all_classes()该方法在窗口初始化时只执行一次。新建班级后ComboBox 缓存未刷新。解决在“新建班级”按钮的回调函数末尾强制刷新 ComboBox# admin_window.py def add_class(self): # ... 新建班级逻辑 AdminDAO.add_class(name, grade, major) # 刷新班级下拉框 classes AdminDAO.get_all_classes() self.combo_class[values] [c[name] for c in classes] self.combo_class.set() # 清空当前选中5.4 现象导出成绩 Excel 时文件生成了但打不开提示“文件损坏”原因excel_handler.py用openpyxl.Workbook()创建新工作簿但未调用wb.save(filename)而是错误地用了wb.close()。close()不保存文件save()才写磁盘。解决检查export_grades_to_excel()函数确保最后一行是# 错误 wb.close() # 正确 wb.save(filename) wb.close() # close 仍建议保留释放资源5.5 现象学生查课表时Treeview 显示空白但数据库里有数据原因student_window.py的课表表格用Treeview.insert(, end, valuesrow)插入数据但row是sqlite3.Row对象values参数需要元组或列表。直接传row会报TypeError: expected sequence。解决将Row对象转为元组# 错误 tree.insert(, end, valuesrow) # 正确 tree.insert(, end, valuestuple(row)) # 或更安全指定字段 tree.insert(, end, values(row[course_name], row[teacher_name], row[time], row[location]))6. 二次开发实战如何接入 MySQL、增加作业附件上传、改造为 Web 版Flask 极简迁移方案现在你已经跑通、读懂、避过坑下一步就是让它真正为你所用。我不会讲“理论上可以怎么做”而是给你三条马上能动手的路径每条都附验证方法和一句血泪教训。6.1 路径一把 SQLite 换成 MySQL只需改 3 个文件 2 行代码很多同学问“能不能连公司 MySQL 服务器” 答案是肯定的且改动极小。核心思路是保持 DAO 层接口不变只替换底层连接驱动。步骤安装pymysqlpip install pymysql修改config.py新增 MySQL 配置# config.py DB_TYPE mysql # sqlite or mysql if DB_TYPE mysql: DB_HOST 192.168.1.100 DB_PORT 3306 DB_NAME school_db DB_USER app_user DB_PASS secure_password else: DB_PATH data/school.db修改dao/base_dao.py的__init__方法# dao/base_dao.py import sqlite3 import pymysql # 新增导入 from config import DB_TYPE, DB_PATH, DB_HOST, DB_NAME, DB_USER, DB_PASS class BaseDAO: def __init__(self): if DB_TYPE mysql: self.conn pymysql.connect( hostDB_HOST, portDB_PORT, userDB_USER, passwordDB_PASS, databaseDB_NAME, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor # 返回字典兼容 sqlite3.Row ) else: self.conn sqlite3.connect(DB_PATH) self.conn.row_factory sqlite3.Row修改db_init.py为 MySQL 补全建表语句主要是AUTO_INCREMENT和ENGINEInnoDB运行python db_init.py它会自动连 MySQL 并建表。验证方法在 MySQL 客户端执行USE school_db; SHOW TABLES;应看到 6 张表再查SELECT * FROM admin;应有默认管理员。血泪教训MySQL 的cursorclasspymysql.cursors.DictCursor必须加上否则row[name]会报错因为默认返回元组。从那以后我每次换数据库驱动第一件事就是查文档确认返回值类型。6.2 路径二给作业模块增加附件上传用filedialog.askopenfilename 本地路径存储学生提交作业时常需传 PDF/Word。项目原版只存“已提交”状态现在升级为存文件路径。改动点student_window.py在“作业提交”按钮回调中加文件选择from tkinter import filedialog def submit_assignment(self): filepath filedialog.askopenfilename( title选择作业文件, filetypes[(PDF files, *.pdf), (Word files, *.docx), (All files, *.*)] ) if not filepath: return # 存路径到数据库假设作业表新增 attachment_path 字段 from service.assignment_service import AssignmentService success, msg AssignmentService.submit_assignment( current_user[id], self.selected_course_id, filepath )service/assignment_service.py新增方法将filepath存入assignment表ui/student_window.py在作业列表中为每行加“下载”按钮点击后用os.startfile(filepath)Windows或subprocess.run([open, filepath])macOS打开。验证方法提交后查数据库SELECT attachment_path FROM assignment WHERE student_id 20220001;应返回绝对路径点击下载按钮对应文件应被系统默认程序打开。血泪教训绝对路径在不同电脑上无效生产环境必须存相对路径或用文件服务器。但教学原型先跑通再说。6.3 路径三用 Flask 快速包装成 Web 版保留 90% 业务逻辑如果你需要部署到服务器Tkinter 就不适用了。但别重写service/和dao/目录里的代码90% 可直接复用。极简迁移方案新建app.py用 Flask 暴露 API# app.py from flask import Flask, request, jsonify from service.auth_service import authenticate from service.grade_service import record_grade from service.course_service import get_student_schedule app Flask(__name__) app.route(/api/login, methods[POST]) def api_login(): data request.json user_id, role, name authenticate(data[username], data[password]) if user_id p a hrefhttps://download.csdn.net/download/a1234567822/89001931 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
延伸阅读

更多相关文章

2026/10/21 13:57:08

Servlet+JSP集成ECharts实现成绩统计可视化

简介:这是一套基于ServletJSPMySQL开发的JavaWeb学生成绩管理系统实战项目,面向Java初学者与Web开发入门学习者,聚焦教学管理场景下的成绩录入、查询、统计与可视化需求。资源包共344个文件,包含86个Java源码(如ScoreD…

2026/10/21 13:57:08

Servlet+JSP+MySQL学生成绩系统:增删改查与ECharts图表实战

简介:这是一套基于ServletJSPMySQL实现的JavaWeb学生成绩管理系统升级版,面向Java初学者与Web开发入门学习者,聚焦教学管理场景下的成绩录入、查询、统计与可视化需求。资源包共344个文件,包含86个Java源码(如ScoreDao…

2026/10/21 13:57:08

个人技能管理体系搭建实战:从技能树到复盘

把“skills”当成一个项目标题来拆,其实是件很有意思的事。它不是一个具体功能,也不是一个系统名称,它指的是——你身上那堆能力,到底是怎么被组织起来的。我见过太多人,简历上写“掌握十几种软件”,真上手…

2026/10/21 15:03:50

agent-skills:把智能体能力沉淀为可复用、可编排的技能库

agent-skills:把智能体能干的活,沉淀成一套可复用、可编排的技能库做Agent项目时间长了,你会发现一个挺扎心的规律:单看一个智能体的Demo,跑通并不难,难的是让它稳定地干很多不同类型的活。今天聊的这个项目…

2026/10/21 15:03:50

交通信号灯识别数据集:三种标注格式与82.88% mAP复现

前段时间整理了一个带标注信息的交通信号灯识别数据集,核心能力是识别左转灯、红绿黄灯,实测识别率82.88%,并且同步导出了YOLO、COCO JSON、Pascal VOC XML三种标注格式。这个数字不是拍脑袋报的,是我在自己环境里完整跑训练和验证…

2026/10/21 15:03:50

股票期权适当性考试考点拆解:先建立风险边界再谈开户

简介:面向拟参加上交所股票期权适当性考试的投资者、从业者与备考学员,尤其能让对期权规则不熟悉的读者快速入门;这套题库资料以单选题形式系统梳理期权交易核心考点,适合备考时间紧张、需要反复刷题校验的人群。内容涵盖期权买方…

2026/10/21 15:03:50

IntelliJ IDEA Git高效实战:图形化操作深度指南

1. 这不是Git教程,而是我在IntelliJ IDEA里“摸爬滚打”五年攒下的真功夫你点开这个标题,大概率不是想学Git基础命令——git clone、git commit、git push这些,官网文档写得比谁都清楚。你真正卡住的地方,是当一个分支在IDEA里突然…

2026/10/21 14:58:43

AI证件照制作API全流程:人脸检测、人像分割与背景替换实战

1. 项目背景:为什么证件照不能靠"随手拍 随便裁"解决先说个我自己的经历。前阵子有个朋友着急办证,手头只有一张在咖啡馆拍的侧身自拍,问我能不能帮忙"裁一下"当证件照用。我当时就跟他讲,证件照真不是把脑袋…

2026/10/21 4:53:08

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/21 0:00:35

交通流仿真实战:IDM跟驰与MOBIL换道模型参数调优与避坑指南

简介:这份资源是面向交通工程学习者与Python开发者的微观交通流仿真项目,核心实现智能驾驶模型(IDM)下的车辆跟驰与换道行为,可用于理解速度、密度与流量之间的动态关系,适合具备Python基础、希望深入交通模…

2026/10/21 0:00:35

揭穿智能体表面顺从:用追踪代码重建任务执行确定性

1. 表面顺从不是智能体的“礼貌”,而是任务执行链路上的隐性断点最近在调试一个跨平台长周期任务调度系统时,我反复遇到一种令人困惑的现象:智能体在用户指令发出后,几乎秒级响应“已收到”“正在处理”“请稍候”,UI上…

2026/10/21 0:00:35

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事:AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里,按宣传口径,本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”,而是这话背后的含金量——它把过去只能…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑