简介:面向学校后勤管理场景,这是一套基于Python语言、SQL Server数据库和Tkinter图形界面库的学生宿舍管理系统源码包,适合Python初学者及桌面应用开发者参考,可解决学生信息登记、管理员权限控制、核酸检测记录管理等日常需求。系统划分为学生信息、管理员信息、核酸信息三大功能模块,后端通过pyodbc或pymssql库连接SQL Server完成增删改查及权限控制,前端使用Tkinter实现窗口布局与事件交互,并附带数据库备份和README说明,便于理解整体架构。压缩包共21个文件,包含2个Python主程序、4个pyc编译文件、5个png界面截图、5个xml项目配置、1个bak数据库备份以及md说明文档,整体大小仅1.12MB,目录结构清晰,学习成本低。目前已有1732人学习下载,可用来完成课程设计、毕业设计或作为C/S架构项目的练手范例,帮助读者快速掌握从数据库设计到界面开发的完整流程。
1. 用 python+SQLsever+tkinter 做宿舍管理系统:这东西值不值得自己写
大二下学期的课设清单里,十个有八个会看到“学生宿舍管理系统”,要求不外乎 python + SQLsever + tkinter。我当年也是从这行字入的门,后来帮人改过好几版类似的代码。先说结论:这个组合能完整跑通“界面—业务—数据库”三层,很适合练手,但照着网上的源码抄,十有八九会在数据库连接和事务提交上翻车。这套系统的真实价值在于:表结构设计决定了业务能不能扩展,连接层决定了程序会不会动不动崩,tkinter 的布局决定了老师/宿管愿不愿意用。本文把这三块拆开讲透,照着做能避开多数新手坑,新手能跟完,熟手可以拿其中的连接层和查询思路做参考。
2. 拆清业务边界:宿舍管理的核心流程与三张关键表
2.1 宿舍管理的闭环:从入住登记到退宿结算
宿舍管理系统听起来简单,但“管理”两个字背后是一套完整的业务闭环:学生入住时登记个人信息并分配床位,住宿期间要支持查询、调换宿舍、登记维修和来访记录,退宿时要结算水电费或检查物品。如果只做一张“学生表”加一张“宿舍表”,后期想加任何功能都得推倒重来。
我一般建议按照状态流转来拆业务,而不是按界面功能来拆。一个学生从入学到毕业,状态依次是“未入住”“已入住”“已退宿”,中间可能穿插“调宿”。宿舍房间的状态则是“空闲”“部分入住”“已满”“维修中”。这两个状态机是所有查询统计的根基。
很多网上的课设代码把“宿舍号”直接写进学生表,看起来省事,但一旦调宿就要改学生记录,历史数据就丢了。正确做法是引入一张独立的入住记录表,每次入住、调宿、退宿都插入一条新记录,学生表只保留学号和姓名等静态信息。这样无论是统计当前住宿人数,还是回溯某学期谁住过哪间房,都有一条干净的时间线。
2.2 建表脚本:宿舍表、学生表与入住记录表
用 SQL Server 建库建表,先把数据库建好再执行建表语句。我习惯在 SSMS(SQL Server Management Studio)里新建查询,贴入以下脚本执行。注意 SQLsever 安装配置时默认排序规则选 Chinese_PRC_CI_AS,否则中文存进去显示乱码的问题会一直纠缠你。
-- 创建宿舍楼表:每栋楼有楼号、楼层数、房间总数 CREATE TABLE DormBuilding ( BuildingID INT IDENTITY(1,1) PRIMARY KEY, BuildingName NVARCHAR(50) NOT NULL, Floors INT NOT NULL, RoomsPerFloor INT NOT NULL ); -- 创建宿舍房间表:归属楼栋、房间号、床位容量、当前状态 CREATE TABLE DormRoom ( RoomID INT IDENTITY(1,1) PRIMARY KEY, BuildingID INT NOT NULL FOREIGN KEY REFERENCES DormBuilding(BuildingID), RoomNo NVARCHAR(20) NOT NULL, BedCount INT NOT NULL DEFAULT 4, Status INT NOT NULL DEFAULT 0 -- 0空闲 1部分入住 2已满 3维修中 ); -- 创建学生表:只存静态基本信息 CREATE TABLE Student ( StudentID NVARCHAR(20) PRIMARY KEY, -- 学号,不设自增 Name NVARCHAR(50) NOT NULL, Gender NVARCHAR(2) NOT NULL, ClassName NVARCHAR(50), Phone NVARCHAR(20) ); -- 创建入住记录表:每次入住/调宿/退宿都写一条 CREATE TABLE StayRecord ( StayID INT IDENTITY(1,1) PRIMARY KEY, StudentID NVARCHAR(20) NOT NULL FOREIGN KEY REFERENCES Student(StudentID), RoomID INT NOT NULL FOREIGN KEY REFERENCES DormRoom(RoomID), CheckInDate DATE NOT NULL, CheckOutDate DATE NULL, -- 退宿前为 NULL ReasonCode INT NOT NULL DEFAULT 0 -- 0入住 1调宿 2退宿 );这段脚本有几个关键设计点。StayRecord 表用CheckOutDate IS NULL来表示“该学生当前正在住宿”,这个条件会被频繁用于查询当前住宿名单,所以在 CheckOutDate 上建索引后,几万条记录也不会慢。Status 字段用 int 不用 varchar,是为了后续用下拉框映射中文状态时更稳定,避免手输“已满”“己满”这种错别字污染数据。
2.3 字段设计里的实际博弈
建表时最容易掉坑的是学号字段。不要把它设成INT IDENTITY自增,学号是业务主键,应该由教务系统统一分配,你只管写入。如果把学号交给数据库自增,导入真实数据时你会痛不欲生。
性别字段别用 BIT(0/1),别问为什么,等你在 tkinter 界面上把 0 和 1 转成“男/女”来回倒腾几次就明白了。NVARCHAR(2) 虽然看起来不“标准”,但在课设和中小型系统中,可读性大于理论洁癖。
床位数也别写死成 4,用BedCount INT字段存。本科生宿舍 4 人间、研究生 2 人间、博士 1 人间是常见配置,写死后所有房间都只能住 4 人,后面想改就要动表结构。把容量做成字段,统计入住率时 SQL 就能直接算出来,不用在业务层去猜。
3. 把 Python 和 SQL Server 连起来:连接层不该散落在每个按钮里
3.1 用 pyodbc 建立连接:驱动和连接串怎么选
Python 连接 SQL Server 常见方案有 pyodbc 和 pymssql 两种。我推荐 pyodbc,因为微软官方提供了 ODBC Driver 17/18 for SQL Server,配合 Windows 系统最稳。pymssql 在部分 Python 3.9+ 环境会有二进制兼容问题,装不上或连不上时你根本不知道去哪查。
先做环境准备。终端执行pip install pyodbc tkinter(tkinter 一般随 python 安装包自带,如果你的 python 安装时取消勾选了 tcl/tk 组件,需要重新运行安装程序勾上)。再确认驱动存在,在 python 交互环境里执行pyodbc.drivers(),如果返回列表里有SQL Server或ODBC Driver 17 for SQL Server就说明环境 OK。
import pyodbc # 连接串:Driver 用你的机器实际安装的版本 conn_str = ( r"DRIVER={ODBC Driver 17 for SQL Server};" r"SERVER=localhost,1433;" r"DATABASE=DormDB;" r"UID=sa;" r"PWD=your_password;" r"Encrypt=no;TrustServerCertificate=yes;" ) try: conn = pyodbc.connect(conn_str, timeout=5) print("数据库连接成功") except pyodbc.Error as e: print("连接失败:", e)连接串里的参数逐一说明。SERVER=localhost,1433是 SQL Server 的默认监听地址和端口,如果你在 SQLsever 客户端查询操作记录时发现端口被改过(常见 14330、14331),就同步修改这里。Encrypt=no表示关闭加密,本地开发环境必须关,否则 ODBC Driver 18 默认强制加密,会报证书相关的连接错误,这是新手最常遇到的玄学问题之一。
3.2 封装 DBHelper:按钮事件里只写业务 SQL
最忌讳的写法是每个按钮点击函数里都写一遍pyodbc.connect()。连接池不是 Python 的强项,频繁创建连接在 SQL Server 上会堆积会话,导致服务器内存涨上去降不下来。正确做法是封装一个数据库帮助类,全局共享一个连接,用完后提交或回滚事务。
import pyodbc class DBHelper: def __init__(self, conn_str): self.conn = pyodbc.connect(conn_str, timeout=5) self.cursor = self.conn.cursor() def query_all(self, sql, params=()): """查询多条记录,返回 list[dict]""" self.cursor.execute(sql, params) columns = [col[0] for col in self.cursor.description] rows = self.cursor.fetchall() return [dict(zip(columns, row)) for row in rows] def query_one(self, sql, params=()): """查询单条记录,返回 dict 或 None""" rows = self.query_all(sql, params) return rows[0] if rows else None def execute(self, sql, params=()): """插入、更新、删除操作,自动提交""" try: self.cursor.execute(sql, params) self.conn.commit() return self.cursor.rowcount except Exception as e: self.conn.rollback() raise e def close(self): self.cursor.close() self.conn.close()query_all里把游标返回的元组转成了字典列表,tkinter 界面上用表格控件展示时直接按列名取值,比下标row[0]可读性好太多。execute方法里显式 commit,任何一步出错就 rollback,保证宿舍分配这种多表更新操作不会出现“床位减了但入住记录没插入”的中间状态。
3.3 参数化查询与事务边界
SQL 注入在课设里没人攻击你,但参数化仍然值得坚持,因为它同时解决了一个更难缠的问题:中文和特殊字符。新手最容易写的代码是cursor.execute(f"SELECT * FROM Student WHERE Name = '{name}'"),名字里带个单引号就会炸。用参数化占位符?就不存在这个问题。
# 参数化查询:占位符用 ? 而不是 %s db = DBHelper(conn_str) student = db.query_one( "SELECT * FROM Student WHERE StudentID = ? AND Name = ?", ("20240001", "张三") )注意 pyodbc 的占位符是?,不是 pymysql 的%s。我见过有人从 MySQL 转过来,把所有问号都写成%s,报错TypeError: not all arguments converted,折腾半天。
事务边界要这样把握:一次点击事件内涉及两条及以上 SQL 写操作,就应该在一个事务里完成。比如“办理入住”要同时更新 DormRoom 的 Status 和插入 StayRecord,必须保证要么都成功要么都失败。DBHelper 里 execute 方法每次独立提交,如果两条 SQL 全部走 execute,第二条失败第一条已经提交了。遇到这种场景,我建议单独写一个方法,不调用 execute,而是手动控制 commit 和 rollback。
4. tkinter 界面这样搭:从登录框到主控台的完整布局
4.1 三层界面结构:登录、导航与内容区
tkinter 做管理系统界面,网上那些“一个窗体塞满所有控件”的写法别学。信息一多,控件挤在一起,字体一大就错位,维护起来想删掉重写。我一般按三层结构组织:登录窗口、主窗口(左侧导航 + 右侧内容区)、子操作对话框。
登录窗口用Toplevel或直接替换根窗口都行。注意 tkinter 的生命周期由mainloop()驱动,登录成功切换窗口时不要销毁根窗口再新建,否则会出现两个 mainloop 互相打架,窗口闪一下消失。正确做法是登录窗口直接就是主窗口,登录成功后清空原有子控件,再渲染主界面。
import tkinter as tk from tkinter import messagebox class LoginApp: def __init__(self, db): self.db = db self.root = tk.Tk() self.root.title("学生宿舍管理系统 - 登录") self.root.geometry("320x200") tk.Label(self.root, text="用户名:").pack(pady=10) self.user_entry = tk.Entry(self.root, width=25) self.user_entry.pack() tk.Label(self.root, text="密码:").pack(pady=5) self.pwd_entry = tk.Entry(self.root, width=25, show="*") self.pwd_entry.pack() tk.Button(self.root, text="登 录", command=self.do_login).pack(pady=15) self.root.mainloop() def do_login(self): username = self.user_entry.get().strip() password = self.pwd_entry.get().strip() # 演示环境:查数据库管理员表,不要写死账号密码在代码里 user = self.db.query_one( "SELECT * FROM SysUser WHERE UserName = ? AND PassWord = ?", (username, password) ) if user: self.root.destroy() # 关闭登录窗口 MainApp(self.db) # 打开主窗口 else: messagebox.showerror("登录失败", "用户名或密码错误,请重试")登录按钮的回调里self.root.destroy()之后调用MainApp(self.db),这里 db 是同一个连接对象,不重新建连接。登录查询本身走的是参数化查询,密码字段没做加密,课设可以接受,但如果要交到真实环境,至少用 hashlib 做一次 SHA-256 加盐再比对。
4.2 主窗口导航:ttk.Treeview 做数据表格展示
主界面左侧放功能导航按钮,右侧放 Notebook 选项卡或者 Treeview 表格。Treeview 是 tkinter 里最常用的表格控件,支持多列、排序、滚动条。把数据库查询到的 list[dict] 直接填充进去,代码量很小。
import tkinter as tk from tkinter import ttk class MainApp: def __init__(self, db): self.db = db self.root = tk.Tk() self.root.title("学生宿舍管理系统") self.root.geometry("900x600") # 左侧导航栏 nav_frame = tk.Frame(self.root, width=160, bg="#2c3e50") nav_frame.pack(side="left", fill="y") tk.Button(nav_frame, text="入住管理", command=self.show_check_in, width=18, pady=8).pack(pady=10) tk.Button(nav_frame, text="学生查询", command=self.show_student_query, width=18, pady=8).pack() tk.Button(nav_frame, text="宿舍统计", command=self.show_room_stats, width=18, pady=8).pack(pady=10) # 右侧内容区 self.content_frame = tk.Frame(self.root) self.content_frame.pack(side="right", expand=True, fill="both") self.root.mainloop() def show_student_query(self): # 清空内容区再重建,防止控件叠加 for widget in self.content_frame.winfo_children(): widget.destroy() # 查询当前所有在住学生 rows = self.db.query_all( "SELECT s.StudentID, s.Name, s.Gender, s.ClassName, " "d.BuildingName, r.RoomNo " "FROM Student s " "JOIN StayRecord st ON s.StudentID = st.StudentID AND st.CheckOutDate IS NULL " "JOIN DormRoom r ON st.RoomID = r.RoomID " "JOIN DormBuilding d ON r.BuildingID = d.BuildingID" ) columns = ("学号", "姓名", "性别", "班级", "楼栋", "房间号") tree = ttk.Treeview(self.content_frame, columns=columns, show="headings") for col in columns: tree.heading(col, text=col) tree.column(col, width=120, anchor="center") for row in rows: tree.insert("", "end", values=( row["StudentID"], row["Name"], row["Gender"], row["ClassName"], row["BuildingName"], row["RoomNo"] )) tree.pack(fill="both", expand=True, padx=10, pady=10)这段代码里有两个细节值得注意。第一,内容区的每次渲染都先destroy()所有子控件再重建,这是 tkinter 里避免控件残留的标准套路。不清理的话,切换几个功能后界面上会出现一堆重叠的表格和按钮。第二,SQL 里用st.CheckOutDate IS NULL做过滤条件,配合之前的表设计,查询当前在住学生就是一条 JOIN 搞定。
4.3 下拉框联动布局:选楼栋再选房间的级联逻辑
tkinter 实现级联下拉框是个高频需求,但很多新手不知道如何实时刷新第二个下拉框。我见过有人把所有房间一次性列在一个下拉框里,几百条记录根本没法选。正确做法是用一个函数根据第一个下拉框的值重新加载第二个下拉框的选项。
# 假设 room_combo 是房间下拉框的控件实例 def load_rooms_by_building(building_id): rooms = self.db.query_all( "SELECT RoomID, RoomNo FROM DormRoom WHERE BuildingID = ? AND Status != 3", (building_id,) ) self.room_combo["values"] = [r["RoomNo"] for r in rooms] if rooms: self.room_combo.current(0) else: self.room_combo.set("") # 楼栋下拉框选中事件里调用加载房间 def on_building_selected(event): building = self.building_combo.get() # 先根据楼栋名查ID,再加载房间 b = self.db.query_one( "SELECT BuildingID FROM DormBuilding WHERE BuildingName = ?", (building,) ) if b: load_rooms_by_building(b["BuildingID"])下拉框的values直接赋一个字符串列表,tkinter 会渲染成展开选项。current(0)表示默认选中第一项。级联的关键是第一个控件的事件要绑定<ComboboxSelected>,这样用户一旦切换楼栋,房间列表就立刻刷新。绑定事件用self.building_combo.bind("<<ComboboxSelected>>", on_building_selected),注意事件参数event必须接住,否则会报缺参错误。
5. 宿舍管理系统避坑指南:六个高频问题一次说透
5.1 中文乱码:界面显示正常但数据库里是问号
现象:tkinter 界面输入中文,插入 SQL Server 后查询出来显示???。
原因:创建数据库时排序规则选错了。默认可能选了SQL_Latin1_General_CP1_CI_AS,这个排序规则不认中文。字符集只是表象,根本问题是数据库的 collation 不支持 Unicode 中文字符。
解决:建库时执行CREATE DATABASE DormDB COLLATE Chinese_PRC_CI_AS。如果已经建好,可以右键库名 → 属性 → 选项 → 排序规则改为 Chinese_PRC_CI_AS,然后重启 SQL Server 服务。注意改排序规则前先备份数据,否则可能出现索引失效。
5.2 连接超时:程序启动卡住十几秒才报错
现象:双击运行程序,窗口迟迟不出现,过一会弹TimeoutError: [WinError 10060]。
原因:连接串里没有设置 timeout,SQL Server 服务没启动或者防火墙拦了 1433 端口,pyodbc 默认等待很久才放弃。一半的“程序卡死”其实是等数据库响应。
解决:连接串加上timeout=5(单位秒),快速失败比慢速成功更容易排查。另外检查 SQL Server 服务是否启动,Win+R 输入services.msc,找到SQL Server (MSSQLSERVER)确认状态是“正在运行”。
5.3 操作记录查不到:SQLsever 客户端能查到但程序查不到
现象:用 SSMS(SQLsever 客户端查询操作记录)能看到表和数据,但 Python 程序查询返回空列表。
原因:最常见的是conn_str里的DATABASE写错,连到了另一个库。还有一种情况是程序使用了事务但没提交,数据还在未提交事务里,SSMS 默认读已提交数据所以看不到。
解决:在 DBHelper 的 execute 方法里确保每次操作后commit()。排查时先用一个最简单的 SELECT(不依赖业务表)验证连接目标库是不是对的,比如SELECT DB_NAME()。
5.4 tkinter 窗口无响应:mainloop 被阻塞
现象:点击某个按钮后,整个界面卡住,无法拖动窗口。在热词里搜 tkinter 相关问题时,“能否在没有 mainloop 主线中打开非阻塞窗口”是高频提问。
原因:按钮事件里执行了长时间运行的 SQL 或time.sleep(),阻塞了 tkinter 的事件循环。SQL 查询几万条数据并渲染到 Treeview 时,界面必然假死。
解决:耗时操作放入threading.Thread里执行,查询完成后用root.after(0, callback, result)回到主线程刷新界面。不要在子线程里直接操作 tkinter 控件,会报线程错误。简单场景可以先用root.update()在耗时循环里手动刷新事件队列。
5.5 相同密码字段存明文:安全意识不足
现象:SysUser 表里的密码一查就是原文。
原因:课设通常没人攻击,但“用户管理”功能一旦加上,明文密码就是靶子。SQL Server 本身不校验业务数据的安全性,问题出在代码设计上。
解决:用hashlib.sha256((password + salt).encode()).hexdigest()存哈希值,salt 可以固定一个项目常量。登录时先哈希再比对,用户表和前端代码都看不到明文。这个成本很低,建议一开始就做好。
5.6 打包 exe 后连不上数据库
现象:在 PyCharm 里运行正常,pyinstaller打包后双击 exe,报数据库连接失败。
原因:ODBC 驱动没有随 exe 打包,或者目标电脑没装 SQL Server 驱动。pyinstaller 不会自动收集系统级 DLL 和驱动。
解决:打包时用--hidden-import pyodbc,有条件的话在目标机器安装ODBC Driver 17 for SQL Server(微软官网提供静默安装参数)。如果目标机器不允许装驱动,换一种思路:用pymssql重新实现连接层,pymssql 不依赖 ODBC 管理器,打包后兼容性更好。
6. 进阶:把课设状态拉到“能交差”的水平
基础功能跑通只是起点,想让答辩老师觉得你想清楚了,再加三个能力:统计报表、操作日志、优雅退出。
统计报表不用做花哨图表,一个 Treeview 展示“每栋楼入住率”就够了。SQL 一句搞定:
SELECT d.BuildingName, COUNT(r.RoomID) AS 总房间数, SUM(CASE WHEN r.Status = 2 THEN 1 ELSE 0 END) AS 已满房间数, CAST( SUM(CASE WHEN r.Status = 2 THEN 1 ELSE 0 END) * 100.0 / COUNT(r.RoomID) AS DECIMAL(5,1) ) AS 满员率 FROM DormRoom r JOIN DormBuilding d ON r.BuildingID = d.BuildingID GROUP BY d.BuildingName;操作日志是体现工程意识的加分项。在 DBHelper 的execute方法里顺手插一条操作日志到 Log 表:操作人、操作时间、SQL 摘要。实现方式是在 execute 的 try 块里加一行self.cursor.execute("INSERT INTO OperLog (OperTime, OperDesc) VALUES (GETDATE(), ?)", (sql[:50],))。这样宿管能追溯谁在什么时候改了什么数据,答辩时这就是一个亮点。
系统退出要干净。tkinter 主窗口关闭时,DBHelper的连接和游标都要显式释放。重写主类的on_closing方法,调用self.db.close()再self.root.destroy()。不关闭连接的话,SQL Server 的会话会挂着,时间久了服务器连接数爆掉。
最后说一个我自己的习惯:每次改动数据库表结构后,拿 SQL Server Management Studio 的“生成脚本”功能存一份建表脚本在项目目录下,标记好日期。改坏了能往回退,答辩时老师问“你的表怎么设计的”,你也拿得出文档。这个习惯救过我很多次,现在测试任何 tkinter 项目都会先跑一遍数据层再开始画界面。按这套思路做完,你会发现宿舍管理系统的架构完全能复用给学生选课、图书馆借阅这类同构业务,一次投入不算亏。希望帮到你。
本文还有配套的精品资源,点击获取