简介:基于Python与MySQL实现的学校宿舍管理网站系统,针对高校及培训机构宿舍管理中宿舍分配、床位登记、学生信息维护等环节,提供了一套功能完整、可直接运行的管理方案。属于典型的Web开发实战项目,覆盖用户管理、宿舍分配、床位管理、信息查询与统计报告等核心模块,后端采用Python主流Web框架搭建,前端结合模板渲染动态页面,MySQL负责持久化学生、宿舍、床位等关键数据。压缩包为zip格式,大小8.09MB,内含程序源代码、数据库建表与初始化脚本、环境配置说明,源码结构包含模型、视图、模板、路由及全局配置等层次,目录清晰。同时附有详细部署步骤,指导用户安装依赖库、配置数据库连接并启动服务器。目前已有85人学习下载,尤其适合作为计算机相关专业的毕业设计或课程设计参考,也可帮助开发者理解Python与MySQL在实际项目中的协作方式。
1. 这个 Python + MySQL 宿舍管理系统:不是拿来就能跑,半小时内让它跑起来
每年毕设季都会有一批「宿舍管理系统」类项目在各处流传,但这个基于 Python + MySQL 的学校宿舍管理网站系统,跟那些只有登录和 CRUD 的壳子不一样——它把宿舍管理里最容易翻车的三件事都做了进去:权限分离(管理员/宿管)、床位状态跟踪(入住/退宿/换寝)、水电费月度记录。我拆过几十个课设包,大多数源码要么连不上数据库,要么跑起来全是 500,这个包结构算规整,不是那种随手打包的残缺品。它适合两类人:一类是毕设/课设选了同类题目的学生,想找一个能讲清楚原理、能答上答辩追问的底子;另一类是刚学完 Python 基础、想用 Flask + 原生 SQL 把 Web 串起来练手的开发者。接下来我按「数据库怎么设计 → 核心代码怎么落地 → 怎么部署跑通 → 哪些坑必踩 → 怎么改出亮点」这个顺序把它拆透,跟着操作,半小时内能看见登录页。
2. 系统拆解与数据库设计:六张表怎么撑起宿舍管理的全部业务
拿到项目先别急着运行,第一步是看清它到底用的是什么技术栈。这个系统的后端是 Flask + PyMySQL,前端是 Jinja2 模板 + Bootstrap,数据库是 MySQL。选这套组合有个很实际的原因:答辩的时候老师一定会问「为什么不用 ORM?」,答案是原生 SQL 更容易展示你对数据关系的理解,而且课设规模下 ORM 的便利性体现不出来,SQL 的可控性和可解释性反而更重要。
2.1 业务模块与表结构的关系
整个系统的业务边界非常清晰,用户只有两类角色:管理员(超级管理员、宿管),以及被管理的对象——学生和宿舍资源。所有页面都是围绕「人—房—账」这三条线展开的:
- 人:学生信息的新增、编辑、查询,包括班级、学院、联系方式;
- 房:楼栋、楼层、房间号、床位总数、当前已住人数;
- 账:入住登记记录、退宿记录、每个月的水电费账单。
这三条线落到数据库里就是六张表:admin(管理员)、student(学生)、building(楼栋)、dormitory(宿舍房间)、check_in(入住记录)、utility_record(水电费记录)。其中 check_in 是核心关联表,它把学生和房间绑定起来,同时也承担了历史追溯的功能——一个学生多次入住,就会有多个 check_in 记录,查「这个床位谁住过」时直接按 dorm_id 和时间段筛选。
2.2 六张核心表的字段设计
表结构是整个项目的地基,我整理成下面这张表,字段都是实际跑通过的,不是凑数的:
| 表名 | 关键字段 | 作用与约束 |
|---|---|---|
| admin | id, username, password_hash, salt, role, created_at | role 用 TINYINT,1=超级管理员,2=宿管;密码不存明文 |
| student | id, student_no, name, gender, college, class_name, phone, status | student_no 唯一索引;status 标记在校/离校 |
| building | id, name, address | 楼栋基础信息,name 唯一 |
| dormitory | id, building_id, room_no, bed_count, occupied, is_available | room_no 在楼栋内唯一;occupied <= bed_count 靠代码与事务保证 |
| check_in | id, student_id, dorm_id, bed_no, check_in_date, check_out_date, status | 学生与房间多对多关系表;status 0=在住 1=已退宿 |
| utility_record | id, dorm_id, record_month, electricity_fee, water_fee, status | record_month 用 VARCHAR(7) 存 '2025-06' 这类格式,方便按月统计 |
这个设计最值得借鉴的地方是把「宿舍房间」和「床位状态」分开维护。一个宿舍的床位数和已住人数都在 dormitory 表里,check_in 只负责记录「谁在哪段时间住了哪个床」,这样设计的好处是:查空床不用 JOIN 两张表再数一遍,直接看 occupied 字段就行,查询性能高,代码也简单。
2.3 建库建表 SQL:一张 schema.sql 搞定初始化
项目包里的 database 目录下有一个 schema.sql,这就是全部建表语句。如果你打算自己从零建库,可以直接用下面这份精简后的版本:
CREATE DATABASE IF NOT EXISTS dorm_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE dorm_system; CREATE TABLE admin ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash CHAR(64) NOT NULL COMMENT 'sha256(salt+password)', salt CHAR(8) NOT NULL COMMENT '8位随机盐值', role TINYINT NOT NULL DEFAULT 1 COMMENT '1=超级管理员 2=宿管', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE student ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, gender TINYINT NOT NULL DEFAULT 1 COMMENT '1=男 0=女', college VARCHAR(100), class_name VARCHAR(50), phone VARCHAR(20), status TINYINT NOT NULL DEFAULT 1 COMMENT '1=在校 0=离校' ) ENGINE=InnoDB; CREATE TABLE building ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL UNIQUE, address VARCHAR(100) ) ENGINE=InnoDB; CREATE TABLE dormitory ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, building_id INT UNSIGNED NOT NULL, room_no VARCHAR(20) NOT NULL, bed_count TINYINT NOT NULL DEFAULT 4, occupied TINYINT NOT NULL DEFAULT 0, is_available TINYINT NOT NULL DEFAULT 1 COMMENT '1=可用 0=停用', UNIQUE KEY uk_room (building_id, room_no), CONSTRAINT fk_dorm_building FOREIGN KEY (building_id) REFERENCES building(id) ) ENGINE=InnoDB; CREATE TABLE check_in ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, student_id INT UNSIGNED NOT NULL, dorm_id INT UNSIGNED NOT NULL, bed_no TINYINT NOT NULL COMMENT '床号 1-4', check_in_date DATE NOT NULL, check_out_date DATE, status TINYINT NOT NULL DEFAULT 0 COMMENT '0=在住 1=已退宿', CONSTRAINT fk_check_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_check_dorm FOREIGN KEY (dorm_id) REFERENCES dormitory(id) ) ENGINE=InnoDB; CREATE TABLE utility_record ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, dorm_id INT UNSIGNED NOT NULL, record_month VARCHAR(7) NOT NULL COMMENT '格式 2025-06', electricity_fee DECIMAL(10,2) NOT NULL DEFAULT 0.00, water_fee DECIMAL(10,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 0 COMMENT '0=未结算 1=已结算', CONSTRAINT fk_util_dorm FOREIGN KEY (dorm_id) REFERENCES dormitory(id) ) ENGINE=InnoDB;几个容易忽略的参数说明:
- 字符集必须用 utf8mb4,不是 utf8。utf8 在 MySQL 里最多存 3 字节,遇到生僻字或特殊符号会直接报错或变成乱码,这就是网上很多人说「数据库中文显示 ??” 的根源。
- password_hash 用 CHAR(64) 是因为 SHA-256 输出固定 64 位十六进制;salt 用 CHAR(8),登录时把用户输入的密码和库里存的盐拼接再哈希,防止简单的彩虹表攻击。
- DECIMAL(10,2) 是水电费的字段类型,千万别用 FLOAT。浮点数在涉及金额时会有精度误差,答辩时老师很可能会揪这个点。
- 外键约束都写了 ON DELETE 默认行为,生产环境可能为了性能去掉外键,但课设里保留外键反而是加分项,因为它能直接证明你有数据库完整性概念。
2.4 初始化数据:管理员账号不是明文密码
项目包里通常还会带一段初始化 SQL,用来插入默认管理员和几栋楼的演示数据。这里有一个关键细节:管理员表的密码是加盐哈希之后的字符串,不是明文。如果你是自己新建的项目,初始化的方式可以写在 app.py 里用脚本生成,也可以直接在 SQL 里预置一条已经算好的哈希值。我一般习惯写个独立的 init_db.py 来干这件事:
import hashlib, os, pymysql from config import DB_CONFIG def gen_salt(length=8): return os.urandom(length).hex() def hash_password(password, salt): return hashlib.sha256((salt + password).encode("utf-8")).hexdigest() def init_admin(): conn = pymysql.connect(**DB_CONFIG) try: with conn.cursor() as cur: salt = gen_salt() hashed = hash_password("admin123", salt) cur.execute( "INSERT INTO admin (username, password_hash, salt, role) VALUES (%s, %s, %s, 1)", ("admin", hashed, salt) ) conn.commit() print("admin 账号初始化完成,默认密码 admin123") finally: conn.close() if __name__ == "__main__": init_admin()这段代码里的 gen_salt 用的是 os.urandom,每次生成不同的盐,所以即使两个账号密码相同,哈希值也不同。初始化完成后把数据库里的 admin 行删掉再重新插入,哈希值会变,但都能用 admin123 登录,原因就是登录时用的是「盐 + 明文密码」重新算哈希再比对。这种做法比直接在 SQL 里写死一条记录要稳妥,也能在答辩时讲出「密码安全存储」的设计思路。
3. 核心代码落地:连接池、登录鉴权与床位分配的事务实现
表结构看明白了,接下来看代码。整个项目最关键的不是页面多好看,而是三个底层能力:数据库连接怎么管、登录状态怎么维持、床位分配怎么保证不超卖。这三个点也是答辩时最容易被追问的地方。
3.1 数据库连接层:为什么不用全局连接
很多初学者会写一个全局的 connection,整个程序共用一个 MySQL 连接,跑起来偶尔报错,或者数据不刷新。原因很简单:MySQL 服务端的 wait_timeout 默认是 8 小时,连接超过这个时间没活动会被服务端断开,但 Python 侧并不知道,下次查询就会抛 "MySQL server has gone away"。
这个项目用的是 Flask 的应用上下文(g 对象)来管理连接:每个请求进来时创建连接,请求结束时关闭。这样做的好处是连接的生命周期和请求绑定,不会出现跨请求的脏连接。核心代码在 db.py 里:
import pymysql from flask import g DB_CONFIG = { "host": "127.0.0.1", "port": 3306, "user": "root", "password": "your_password", "database": "dorm_system", "charset": "utf8mb4", "cursorclass": pymysql.cursors.DictCursor, "autocommit": False, # 关闭自动提交,事务由代码控制 } def get_db(): """获取当前请求的数据库连接,存在 g 对象里复用""" if "db" not in g: g.db = pymysql.connect(**DB_CONFIG) return g.db def close_db(exception=None): """请求结束时自动调用,关闭连接""" db = g.pop("db", None) if db is not None: db.close() def init_app(app): app.teardown_appcontext(close_db)参数说明在代码注释里已经标了一条关键的:autocommit 必须设为 False。如果不关掉自动提交,后面做入住分配时,两个 SQL 语句之间一旦出现异常,就会发生「床位已经插入、已住人数没更新」这种数据不一致。关闭自动提交之后,所有写操作都走 begin() 和 commit(),异常时 rollback,数据才能保持一致。
如果你希望并发能力更强,可以把它改成 DBUtils 的 PooledDB 连接池,但课设场景下 g 对象模式完全够用,而且更好讲。连接池的引入会在第 6 章提到怎么改。
3.2 登录鉴权与 session 维持
登录模块是每个页面的入口,这部分的实现策略是:密码加盐哈希存储 + Flask session 保存登录态 + 装饰器校验权限。注意它不是用 JWT,而是用 Flask 自带的 session——因为服务端渲染的项目里,session 更简单,而且浏览器关闭 cookie 就失效,安全边界清晰。
import hashlib from flask import session, request, redirect, flash from db import get_db def do_login(username, password): """校验用户名密码,成功返回管理员行,失败返回 None""" db = get_db() with db.cursor() as cur: cur.execute("SELECT * FROM admin WHERE username=%s", (username,)) row = cur.fetchone() if not row: return None # 用库里的盐重新算一遍哈希再比较,不比对明文 hashed = hashlib.sha256((row["salt"] + password).encode("utf-8")).hexdigest() if hashed == row["password_hash"]: return row return None def login_required(view_func): """装饰器:未登录跳转登录页,已登录放行""" from functools import wraps @wraps(view_func) def wrapper(*args, **kwargs): if "admin_id" not in session: return redirect("/login") return view_func(*args, **kwargs) return wrapper这段代码里值得注意的点是 login_required 装饰器。它解决了「每个视图函数都要重复写一遍 if session 判断」的问题,权限控制集中在装饰器里,后续如果要加「只有超级管理员才能访问的页面」,只要再写一个 admin_required 装饰器,在函数上叠加就行。校验登录态用的是 session 里的 admin_id,登录成功后要显式设置,这个放在视图函数里。
3.3 床位分配:事务 + 行锁,防止超卖
这是全项目里含金量最高的一段代码。宿舍床位分配的场景是:多个宿管同时操作时,如果只做「先 SELECT 判断有没有空床,再 INSERT」,并发情况下可能两个人都查到 occupied=3、bed_count=4,然后都执行插入,最后住进去 5 个人,床位就超了。解决思路就两件事:事务 + SELECT FOR UPDATE 行锁。
def assign_bed(student_id, dorm_id, bed_no=None): """ 分配床位:事务保证 check_in 插入和 dormitory.occupied 更新原子性 FOR UPDATE 锁住 dormitory 行,防止并发超售 """ db = get_db() try: db.begin() with db.cursor() as cur: # 锁定这间宿舍的行,直到事务提交才释放 cur.execute( "SELECT bed_count, occupied FROM dormitory WHERE id=%s FOR UPDATE", (dorm_id,) ) dorm = cur.fetchone() if not dorm: raise RuntimeError("宿舍不存在") if dorm["occupied"] >= dorm["bed_count"]: raise RuntimeError("该宿舍已无空床位") # 床位号不指定时自动补一个空号 if bed_no is None: cur.execute( "SELECT bed_no FROM check_in WHERE dorm_id=%s AND status=0", (dorm_id,) ) used_beds = {row["bed_no"] for row in cur.fetchall()} available = [i for i in range(1, dorm["bed_count"] + 1) if i not in used_beds] if not available: raise RuntimeError("床位号计算异常,请刷新后重试") bed_no = available[0] # 写入住记录 + 更新已住人数 cur.execute( "INSERT INTO check_in (student_id, dorm_id, bed_no, check_in_date, status) " "VALUES (%s, %s, %s, CURDATE(), 0)", (student_id, dorm_id, bed_no) ) cur.execute( "UPDATE dormitory SET occupied = occupied + 1 WHERE id=%s", (dorm_id,) ) db.commit() return True except Exception as e: db.rollback() raise e逐个说这里的设计决策:
- SELECT ... FOR UPDATE 是行级锁,事务提交或回滚时释放。只要事务还没提交,另一个事务的 SELECT FOR UPDATE 会阻塞等待,这样就不会出现两个事务同时读到 occupied=3 的情况。
- 先 SELECT 占用床位号再补空号,这一步是为了把床位号掰到 1-4 的正常范围,而不是随手填一个「第 5 床」,后续查卫生、查用电对不上号会很难受。
- 为什么不用 INSERT 带条件的方式(比如 INSERT INTO check_in SELECT ... WHERE occupied < bed_count)?因为课设代码里宿主逻辑更直观,且 MySQL 在 REPEATABLE READ 隔离级别下,纯靠 INSERT SELECT 也可能因为间隙锁之外的原因误判,事务 + 行锁是最稳的教学方案。
- 任何异常都会触发 rollback,包括「宿舍被锁了但锁的行不存在」这种情况,防止出现孤儿记录。
3.4 视图层如何处理错误信息
视图层我不展开全部路由,只说错误回显的处理。这个项目的视图函数普遍遵循一个模式:操作成功后 redirect 到列表页,操作失败则 flash 一条消息并 render_template 回原页面。flash 消息在模板里统一渲染,而不是在每个页面里手写:
@app.route("/dorm/assign", methods=["POST"]) @login_required def assign(): student_id = request.form.get("student_id", type=int) dorm_id = request.form.get("dorm_id", type=int) try: assign_bed(student_id, dorm_id) flash("分配成功", "success") except RuntimeError as e: flash(str(e), "danger") return redirect("/dorm/list")这种「业务逻辑放 service 层,视图只做参数解析和跳转」的分层,是答辩时很加分的点。如果代码全堆在视图函数里,老师看到几百行的函数,第一印象就是工程素养不够。
4. 部署三步走:环境配置、初始化数据与联调排错
代码层面看完了,现在把整个项目跑起来。这一步卡住的人最多,我按「装 MySQL → 装 Python 依赖 → 初始化库和启动」这个顺序来,每一步都给了明确的验证方法。
4.1 环境准备:Python 3.8+ 与 MySQL 8.0
项目依赖清单在 requirements.txt 里,核心就三个包:flask、pymysql、cryptography(PyMySQL 连 MySQL 8 时做认证用)。如果你用的是 PyCharm,直接在项目根目录打开终端执行:
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txtMySQL 的安装分平台差异较大,这里说两个共同要点:一是 MySQL 8.x 默认的认证插件是 caching_sha2_password,老版本的 PyMySQL(1.0 以下)不支持,会报认证失败,直接把 pymysql 升到最新版就好;二是安装完成后先确认服务启动,Linux 下是systemctl status mysqld,Windows 下是「服务管理器里看 MySQL80 是否正在运行」。如果启动都过不去,后面所有步骤都没意义。
4.2 配置数据库连接与初始化
项目根目录的 config.py 里有一个 DB_CONFIG 字典,上一章已经看过它的结构,这里只需要改三样:密码、ip、端口。注意 host 不要写成 localhost 而写 127.0.0.1,原因在避坑章里细说。
然后执行建库和初始化:
mysql -uroot -p < database/schema.sql python init_db.pyschema.sql 会创建数据库和全部表结构,init_db.py 会插入默认管理员和演示数据。执行完 init_db.py 看到 "admin 账号初始化完成" 这行输出,说明数据库侧已经就绪。演示数据一般包含 3 栋楼、每栋 2-3 层、每层几个房间、十来个学生,覆盖了「有人的房间、空房间、满员的房间」三种状态,方便你登录后马上看到不同页面效果。
初始化完成后用一个可视化工具验证一下:Navicat 或 MySQL Workbench 都行,连接信息填 config.py 里的同一套账号密码。连接成功后在 dorm_system 库里随便执行一条SELECT * FROM student;,能看到数据就说明库没问题。这里有一个常见场景:用 Navicat 连本机 MySQL 8 报 2059 错误,这是因为认证插件不兼容,Navicat 12 以下版本支持不了 caching_sha2_password,解决方法是把账号的认证方式改回 mysql_native_password,命令是:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';4.3 启动 Flask 并登录
数据库就绪后,回到项目根目录启动:
python app.py看到Running on http://127.0.0.1:5000就说明服务已经起来了。浏览器打开 http://127.0.0.1:5000/login,用 init_db.py 里生成的 admin / admin123 登录。登录后应该看到仪表盘,上面显示楼栋数、宿舍数、在住学生数、剩余床位这几个统计卡片,数据来自聚合查询。
联调阶段我建议按这个顺序过一遍功能:
- 新增一个学生(学生列表 → 新增)→ 列表里能看到刚加的人;
- 给这个学生分配床位(宿舍列表 → 分配)→ 宿舍的已住人数 +1;
- 点退宿 → 已住人数 -1,check_in 记录变成已退宿;
- 录一笔本月水电费 → 账单列表显示对应宿舍的金额。
任何一步报错,先看终端里的 Flask 日志和 MySQL 的错误码,大部分问题都集中在连接参数和字符集上,下一章专门列排查清单。
4.4 一个容易被忽略的细节:模板里静态文件的引用
如果你改了页面样式,发现图片和 CSS 加载不出来,先看模板的头部是不是用了url_for('static', filename='...')。这个函数会根据实际部署路径自动拼接,如果代码里写死成了/static/css/style.css,在根目录运行时没问题,但以后部署到子路径下就会全部 404。这个项目里的模板统一用的是 url_for,这点做得比较规范,你自己改前端时要保持同样的写法。
5. 常见问题与排查:五个让新手卡住的真实场景
这部分每一行都是实操中真实出现过的报错。按「现象 → 原因 → 解决」来写,你遇到同类问题时直接对照着处理。
5.1 ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock'
- 现象:命令行执行
mysql -uroot -p报这个错,或者 Python 连接时报 Can't connect to local MySQL server through socket。 - 原因:socket 文件不存在,说明 MySQL 服务根本没启动。用 localhost 连接时,MySQL 客户端默认走 unix socket,而不是 TCP 端口。
- 解决:先启动服务。Linux 上执行
systemctl start mysqld(或 mysql 服务名),Windows 上在服务管理器中启动 MySQL80。然后把 config.py 里的 host 从 localhost 改成 127.0.0.1,这样 PyMySQL 会走 TCP 协议而不是 socket,问题绕开了。
5.2 页面数据全是乱码或中文变成 ??
- 现象:从数据库查出来的专业名、学生姓名显示成问号,或写进数据库就直接变成 utf8 乱码。
- 原因:三层字符集不统一。MySQL 服务端字符集是 latin1,或者连接串里没指定 charset,或者建表时用了默认字符集。
- 解决:建库时显式加上 DEFAULT CHARACTER SET utf8mb4(schema.sql 里已经带了);DB_CONFIG 里必须有 "charset": "utf8mb4";同时打开 MySQL 配置文件,在 [mysqld] 段加 character-set-server=utf8mb4。三层全部统一之后,删库重建再导入数据才会干净。这个顺序不能反,先改配置再导数据。
5.3 Packet sequence number wrong - expecting 0, got 1
- 现象:项目运行几分钟后,第一次请求正常,第二次请求开始报 Packet sequence number wrong。
- 原因:MySQL 服务端断开了空闲连接,但客户端还在用这个连接发请求。服务端主动断开时序列号重置,客户端没感知。这正好是第 3 章说的「全局连接」问题的最典型症状。
- 解决:不要用模块级全局 connection,改成 g 对象按请求创建关闭。已经改完还出现,查一下 MySQL 的 wait_timeout 是不是被改小了,默认 28800 秒基本不会触发这个问题。
5.4 UNIQUE 约束冲突 / Duplicate entry '1-203' for key 'uk_room'
- 现象:添加宿舍时提示宿舍号重复,明明这栋楼里没有这个房间号。
- 原因:uk_room 的唯一键是 (building_id, room_no) 的组合,楼栋不同但房间号相同,不会冲突;如果报错,大概率是 building_id 选错了楼栋,或者页面上下拉框的值和前端的 label 对不上。
- 解决:先用 SQL 查
SELECT id, name FROM building;确认 id 映射,再看页面提交的表单里 building_id 的值是不是选中的那栋楼。常见做法是在模板里用value="{{ b.id }}"而不是用循环下标,下标一旦经过筛选就会错位。
5.5 明明有床位却提示「该宿舍已无空床位」
- 现象:dormitory 表的 occupied 明显小于 bed_count,但 assign 时被拦了。
- 原因:最常见的是 occupied 和真实入住记录不一致——比如手动在 check_in 表里插了数据但没更新 occupied,或者退宿时只改了 check_in 状态没把 occupied 减回去。这个项目的事务处理本来已经把两处更新绑定了,但如果你在 Navicat 里手动改过数据,就会捅出这个篓子。
- 解决:不要手动往这两张表里插数据,所有入住退宿都走页面操作。已经被改乱了的话,执行一条修复 SQL 重建 occupied:
UPDATE dormitory d SET d.occupied = ( SELECT COUNT(*) FROM check_in c WHERE c.dorm_id = d.id AND c.status = 0 );跑完之后再刷新宿舍列表,应该就和实际入住记录对上了。这条修复语句本身也是答辩时一个不错的「你遇到数据不一致怎么办」的答案。
6. 进阶改造:加一个统计看板,把答辩评委最关心的功能亮出来
基础流程跑通只是及格线,想让这个课设从「能用」变成「有亮点」,我建议加一个数据看板。改动量不大,但视觉效果和讲故事的深度会明显不一样。
在仪表盘页面上增加一块宿舍入住率排行,按楼栋分组统计入住率,并显示空床位最多和最少的楼栋。SQL 用聚合查询一次搞定:
SELECT b.name AS building_name, COUNT(d.id) AS room_count, SUM(d.bed_count) AS total_beds, SUM(d.occupied) AS occupied_beds, CONCAT(ROUND(SUM(d.occupied) * 100.0 / SUM(d.bed_count), 1), '%') AS occupancy_rate FROM building b LEFT JOIN dormitory d ON d.building_id = b.id GROUP BY b.id, b.name ORDER BY occupancy_rate DESC;这段 SQL 用 GROUP BY 按楼栋聚合,LEFT JOIN 保证没有宿舍的楼栋也能显示(虽然实际上不会出现这种情况,但防御性写法能让老师看到你的严谨)。返回结果后,在模板里用循环渲染成表格,入住率可以配一个简单的进度条样式,用的还是现成的 Bootstrap 组件。
第二个加分项是把水电费模块改成「月度账单导出」。这个系统的账单表结构按月存,天然适合出报表。加一个导出按钮,后端用 openpyxl 把当月账单生成一个 Excel 文件返回给浏览器。新增一个依赖和一小段代码:
from openpyxl import Workbook from flask import send_file import io @app.route("/billing/export/<month>") @login_required def export_billing(month): db = get_db() with db.cursor() as cur: cur.execute(""" SELECT b.name as building, d.room_no, r.water_fee, r.electricity_fee, (r.water_fee + r.electricity_fee) as total FROM utility_record r JOIN dormitory d ON r.dorm_id = d.id JOIN building b ON d.building_id = b.id WHERE r.record_month=%s AND r.status=0 """, (month,)) rows = cur.fetchall() wb = Workbook() ws = wb.active ws.append(["楼栋", "房间号", "水费", "电费", "合计"]) for row in rows: ws.append(list(row.values())) buf = io.BytesIO() wb.save(buf) buf.seek(0) return send_file(buf, mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", as_attachment=True, download_name=f"billing_{month}.xlsx")这段代码的两个注意点:一是 JOIN 把楼栋名也带出来了,导出文件不是给开发者看的,是给宿管看的,字段要人能直接读懂;二是 send_file 用的是 BytesIO 内存对象,不落到磁盘,避免多用户并发导出时文件名互相覆盖。回答「为什么用 openpyxl 不用 csv」时可以说:Excel 原生支持分列筛选和格式调整,宿管拿到就能直接打印,不需要额外处理。
这些改造做完之后,答辩时你能讲的东西就很立体了:业务上讲宿舍分配的事务一致性,部署上讲 MySQL 8 的认证和字符集问题,亮点上讲统计看板和报表导出的落地场景。从那以后我拆任何课设包都强制走一遍同样流程:先看表结构有没有坑,再跑通主流程,最后才改代码加功能,而不是拿到手就急着点运行。顺序反了,你的时间会全部耗在环境问题上,真正的业务逻辑反而没时间看。希望帮到你。
本文还有配套的精品资源,点击获取