☰
Python+MySQL宿舍管理系统:从数据库设计到答辩避坑的毕设实战
2026/10/3 14:48:38 网站建设 项目流程

简介:基于Python与MySQL的学校宿舍管理网站系统,是一份面向高校计算机相关专业毕业设计或课程设计的完整项目包,适用于需要系统学习Web开发全流程的初中级学习者。项目覆盖用户管理、宿舍分配、床位管理、信息查询和报告统计等核心模块,代码按模型、视图、模板、路由、配置分层组织,并附有数据库建表脚本及环境配置说明,便于从零搭建和二次开发。资源包大小约8.09MB,压缩包内包含源代码与文档,结构清晰,能够帮助读者快速定位到对应功能模块。目前已有85人参与学习浏览,内容对理解Python Web框架(如Django/Flask)与MySQL在真实场景中的配合尤其有帮助。通过实际部署与调试,读者能够掌握从数据库设计、业务逻辑实现到动态页面渲染的完整链路,同时学会如何处理学生信息录入、床位状态更新、入住率统计等典型业务逻辑,为课程设计或毕业设计提供可复用的参考方案。

1. 学校宿舍管理网站系统为什么是 Python + MySQL 毕设的常青树:它到底在解决什么

每年毕业设计开题季,都能在实验室看到同一个画面:学弟打开一个标着“优质毕业设计”的压缩包,里面是一套宿舍管理系统。这套系统的生命力不在功能多花哨,而在它恰好覆盖了毕业设计要考核的全部要素——Python 后端、MySQL 数据库、Web 页面、增删改查、权限区分、统计报表。宿舍管理的业务边界足够清晰:学生要入住、要换寝、要报修、要交住宿费;管理员要分配房间、查看空床位、导出入住率。这些需求既不冷门也不超纲,一个普通学生用三个月时间从零写出来,完全可行。如果你正在找课程设计或毕业设计项目,这套基于 Python + MySQL 的系统是一个投入产出比很高的选择,下面从建表到跑通再到答辩演示的完整路径一次讲清楚。

2. 先把地基打牢:宿舍管理系统的表结构设计与 MySQL 连接参数

2.1 业务边界怎么定:宿舍管理不是进销存

很多第一次做毕设的人,拿到“宿舍管理系统”就开始设计表,结果把表设计得像超市收银台——什么字段都往里塞,什么表都想建。这是最大的开局错误。宿舍管理的核心业务只有四个动词:入住、换寝、退宿、报修。围绕这四个动词,数据模型只要覆盖三件事:谁住在哪(学生与宿舍的关联)、谁报修了什么(报修工单)、谁交了多少钱(费用记录)。

我一般会先画一张极简的业务草图,但这里只说结论:学生表和宿舍表是主表,入住关系通过 student 表里的 dorm_id 外键表达(也可以用独立的入住记录表,但毕设用外键字段更直观);报修和费用是一对多的事务表。超过六张表的模型对毕设来说都是过度设计,后期光是维护数据关系就会消耗大量时间。

2.2 五张核心表的建表 SQL:从学生到宿舍的完整链路

实际落地时,我推荐的表结构是五张核心表加一张用户表:

  • users:登录用户(管理员、宿管员、学生共用)
  • students:学生档案
  • dormitories:宿舍信息
  • repairs:报修工单
  • payments:住宿费用

下面是完整的建表 SQL,在 MySQL Workbench 或者 Navicat 里直接执行即可:

CREATE DATABASE IF NOT EXISTS dormitory DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE dormitory; -- 用户表:三种角色共用一张表,用 role 字段区分 CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(30) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(100) NOT NULL COMMENT '密码,毕设可直接存MD5', role ENUM('admin', 'manager', 'student') NOT NULL DEFAULT 'student' COMMENT '角色', student_id INT DEFAULT NULL COMMENT '学生用户关联的学生ID', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 宿舍表:capacity 是床位总数,current_count 是当前已住人数 CREATE TABLE dormitories ( id INT AUTO_INCREMENT PRIMARY KEY, building VARCHAR(20) NOT NULL COMMENT '楼栋名,如 3栋', room_no VARCHAR(20) NOT NULL COMMENT '房间号,如 301', capacity INT NOT NULL DEFAULT 4 COMMENT '床位数量', current_count INT NOT NULL DEFAULT 0 COMMENT '当前入住人数', gender ENUM('男', '女') NOT NULL COMMENT '宿舍性别', UNIQUE KEY uk_building_room (building, room_no) ); -- 学生表:dorm_id 指向宿舍,是入住关系的载体 CREATE TABLE students ( id INT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender ENUM('男', '女') NOT NULL COMMENT '性别', major VARCHAR(100) COMMENT '专业', phone VARCHAR(20), dorm_id INT DEFAULT NULL COMMENT '当前宿舍ID', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_student_dorm FOREIGN KEY (dorm_id) REFERENCES dormitories(id) ON DELETE SET NULL ); -- 报修表:一个学生可能有多条报修记录 CREATE TABLE repairs ( id INT AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL, dorm_id INT NOT NULL, content TEXT NOT NULL COMMENT '报修内容', status ENUM('待处理', '维修中', '已完成') DEFAULT '待处理', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE, FOREIGN KEY (dorm_id) REFERENCES dormitories(id) ON DELETE CASCADE ); -- 费用表:记录住宿费缴纳情况 CREATE TABLE payments ( id INT AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL COMMENT '金额', pay_time DATETIME DEFAULT CURRENT_TIMESTAMP, semester VARCHAR(20) COMMENT '学期,如 2024-2025-1', FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE );

这段 SQL 里有三个容易忽略的地方。第一,所有表和字段都加了 COMMENT,Navicat 里看表结构一目了然,答辩时评委如果打开数据库看设计,这个细节非常加分。第二,students.dorm_id 的外键用了 ON DELETE SET NULL,意思是宿舍被删除时学生的 dorm_id 自动置空,不会出现指向不存在宿舍的孤儿数据;而 repairs 和 payments 用了 ON DELETE CASCADE,因为报修和缴费记录随学生删除一起清掉是合理的业务逻辑。第三,dormitories 表的 uk_building_room 唯一索引保证了不会出现“3栋 301”被插入两条记录的情况。

2.3 pymysql 连接 MySQL 参数:一个都不能错

表建好之后,接下来是让 Python 和 MySQL 建立连接。最常用的是 pymysql 这个纯 Python 库,安装命令很简单:

pip install pymysql cryptography

连接参数里面有几个坑会在后面的避坑章节详细说,先给一份可以直接用的配置:

# db.py import pymysql DB_CONFIG = { 'host': 'localhost', 'port': 3306, 'user': 'root', 'password': '你的密码', 'database': 'dormitory', 'charset': 'utf8mb4', 'cursorclass': pymysql.cursors.DictCursor, 'autocommit': False, } def get_conn(): return pymysql.connect(**DB_CONFIG)

逐个参数说明:host 不要随便写成 127.0.0.1,在 Linux 服务器上 localhost 会走 unix socket 文件而 127.0.0.1 走 TCP,如果 MySQL 没开 TCP 监听,用 127.0.0.1 就会报 Can't connect 错误。port 默认 3306,除非你在 my.cnf 里改过。charset 必须写 utf8mb4,不要用 utf8,因为 MySQL 8.0 里 utf8 已经是 utf8mb3 的别名,存 Emoji 或生僻字会失败。cursorclass 用 DictCursor 返回字典而不是元组,写代码时用 row['name'] 而不是 row[1],可读性好很多。

autocommit 这个参数是毕设翻车的高发区。MySQL 默认 autocommit=1,但 pymysql 建立连接时会把它关掉,也就是说你执行 INSERT/UPDATE 后必须显式 conn.commit() 数据才会真正落盘。如果不 commit,当前连接里查询能看到数据(因为事务未结束),换个连接就看不到了。

2.4 连接池该不该上:一个被答辩老师问烂但很多人答不上来的设计点

毕设系统的并发量可能只有几十人同时在线,用连接池看起来是杀鸡用牛刀。但答辩老师几乎一定会问一个问题:“如果有一百个学生同时提交入住申请,你的数据库受得了吗?”如果回答“每次请求都新建连接”,那等于承认数据库会被连接开销拖垮。反过来,如果你加了连接池,哪怕只是用 Python 的 DBUtils 库做最基础的连接复用,这个问题就能答得漂亮。

用 DBUtils 的 PooledDB 改造上面的 get_conn 函数:

from dbutils.pooled_db import PooledDB import pymysql pool = PooledDB( creator=pymysql, maxconnections=20, # 池子最大连接数 mincached=2, # 空闲时保持的最小连接数 maxcached=5, # 空闲时保持的最大连接数 blocking=True, # 无连接时阻塞等待 setsession=['SET AUTOCOMMIT = 0'], host='localhost', port=3306, user='root', password='你的密码', database='dormitory', charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor ) def get_conn(): return pool.connection()

几个参数按学生项目的规模这样设就够了:maxconnections=20 意味着最多同时 20 条数据库连接,超过的请求会阻塞等待;mincached=2 保证程序启动后至少有 2 条连接准备就绪,第一次查询不用经历 TCP 握手。blocking=True 比直接抛错友好,在并发高峰时表现是“等一下”而不是“拒绝服务”。PooledDB 会监控连接的健康状态,MySQL 的 wait_timeout 超时断开后,池子能自动丢弃坏连接并新建。

连接池里的连接一旦长时间空闲被 MySQL 服务端断开,旧代码拿到这条“死连接”执行查询就会抛错。设置 mincached 别太高(2-3 就够),同时可以在每次取连接后先 ping 一下:

def get_conn(): conn = pool.connection() conn.ping(reconnect=True) return conn

reconnect=True 让 pymysql 在连接断开时自动重新握手,这个小细节能让你的系统在实验室挂机一晚上后第二天还能正常跑,避免“昨天还能用今天打开就报错”的尴尬。

3. 用 Flask 把后端跑通:登录鉴权、宿舍分配与报修工单的实现

3.1 为什么选 Flask 而不是 Django:毕设的时间成本账

毕设系统选 Web 框架时,最常见的纠结是 Flask 和 Django 二选一。Django 自带 admin 后台、ORM、认证系统,功能全,但学习曲线陡,而且它的 admin 后台会自动生成一套管理界面,很多学生做完后自己都说不清数据是怎么走的。Flask 则只保留最核心的路由和模板渲染,认证、admin 全部自己写。对毕设来说,自己写的东西才讲得清楚,答辩老师问“你这个权限怎么实现的”,你从 Session 说起三句话就能讲明白;如果用 Django 的 auth 模块,讲不清楚时容易被觉得是“抄框架”。

我的建议很直接:如果项目周期在三个月以内,选 Flask;如果导师明确要求用 Django 才用 Django。下面所有代码都基于 Flask。

3.2 登录接口与 Session 管理:从 HTML 表单到数据库的完整链路

先搭一个最小可运行的项目结构:

dormitory_system/ ├── app.py # Flask 主入口 ├── db.py # 数据库连接池 ├── templates/ # Jinja2 模板 │ ├── login.html │ ├── index.html │ └── students.html └── static/ # CSS/JS 文件

app.py 最核心的两段逻辑是登录认证和 Session 校验。登录接口接收 POST 表单,用参数化查询去 users 表匹配用户:

from flask import Flask, render_template, request, redirect, session, url_for from db import get_conn app = Flask(__name__) app.secret_key = 'dormitory-secret-key-2024' # 生产环境要用随机值 @app.route('/login', methods=['GET', 'POST']) def login(): error = None if request.method == 'POST': username = request.form.get('username', '').strip() password = request.form.get('password', '').strip() conn = get_conn() cur = conn.cursor() cur.execute( "SELECT * FROM users WHERE username=%s AND password=MD5(%s)", (username, password) ) user = cur.fetchone() cur.close() conn.close() if user: session['uid'] = user['id'] session['role'] = user['role'] return redirect(url_for('index')) error = '用户名或密码错误' return render_template('login.html', error=error) @app.route('/index') def index(): if 'uid' not in session: return redirect(url_for('login')) return render_template('index.html', role=session['role'])

这里有几个关键的参数细节。第一,SQL 里用了 %s 占位符而不是字符串拼接,pymysql 会自动做转义,能挡住 SQL 注入——答辩时如果老师问“你怎么防 SQL 注入”,这句就能答。第二,密码存的是 MD5 摘要,毕设不需要上 bcrypt 这种重量级方案;注意这里在 SQL 里直接调用 MD5() 函数,写入用户的时候也要用同样的方式,保证两端一致。第三,Session 的 secret_key 必须设置,否则 Flask 的 session 无法签名,刷新页面登录态就丢了。

3.3 宿舍分配的核心逻辑:按性别、楼栋、空床位自动分配

宿舍分配是系统的核心业务。常见做法是管理员选择一栋楼的某间宿舍,系统检查该宿舍是否未满、性别是否匹配,然后执行分配。这里最容易翻车的是“并发分配同一间宿舍”——两个学生同时提交,都查到还有 1 个空位,都执行了入住。后面第 4.2 节会讲事务处理,先给单用户版本的分配逻辑:

@app.route('/assign', methods=['POST']) def assign(): if session.get('role') not in ('admin', 'manager'): return '无权限操作', 403 student_id = request.form.get('student_id') dorm_id = request.form.get('dorm_id') conn = get_conn() cur = conn.cursor() try: # 查宿舍当前人数和容量 cur.execute( "SELECT capacity, current_count, gender FROM dormitories WHERE id=%s", (dorm_id,) ) dorm = cur.fetchone() if not dorm: return '宿舍不存在', 404 if dorm['current_count'] >= dorm['capacity']: return '该宿舍已满,请选择其他宿舍', 400 # 查学生性别,防止男女生混住 cur.execute("SELECT gender FROM students WHERE id=%s", (student_id,)) student = cur.fetchone() if not student: return '学生不存在', 404 if student['gender'] != dorm['gender']: return '性别不匹配,不能分配此宿舍', 400 # 更新宿舍人数和学生宿舍ID cur.execute( "UPDATE dormitories SET current_count=current_count+1 WHERE id=%s", (dorm_id,) ) cur.execute( "UPDATE students SET dorm_id=%s WHERE id=%s", (dorm_id, student_id) ) conn.commit() return redirect(url_for('students')) except Exception as e: conn.rollback() return f'分配失败: {str(e)}', 500 finally: cur.close() conn.close()

这个版本在事务边界上已经做对了:先查宿舍状态,确认有床位、性别匹配,再执行更新,最后 commit。所有数据库操作放在同一个连接里,用 try/except/finally 保证出错时滚回。

但有个问题要特别说明:这段代码在并发场景下不等于安全。两个请求同时查到 current_count=3 capacity=4,都会认为有床位,然后都执行 UPDATE,最终 current_count 变成 5,超过容量。解决这个问题要靠数据库层面的行锁,第 4.2 节专门讲。

3.4 Jinja2 模板渲染:Python 变量怎么变成页面上的表格

后端接口返回的数据,最终要靠 Jinja2 模板渲染成 HTML。以学生列表页为例,视图函数把学生列表传给模板:

@app.route('/students') def students(): if 'uid' not in session: return redirect(url_for('login')) conn = get_conn() cur = conn.cursor() cur.execute(""" SELECT s.id, s.student_no, s.name, s.gender, s.major, s.phone, d.building, d.room_no FROM students s LEFT JOIN dormitories d ON s.dorm_id = d.id ORDER BY s.student_no """) data = cur.fetchall() cur.close() conn.close() return render_template('students.html', students=data)

注意这里用 LEFT JOIN 而不是 INNER JOIN,因为存在还没分配宿舍的学生——如果学生没有 dorm_id,INNER JOIN 会让这个学生直接从列表里消失。这是很小的细节,但能避免“新生列表里少了一个人”这种难排查的问题。

对应的 students.html 片段:

<table class="table table-bordered"> <thead> <tr> <th>学号</th><th>姓名</th><th>性别</th><th>专业</th><th>电话</th><th>宿舍</th> </tr> </thead> <tbody> {% for stu in students %} <tr> <td>{{ stu.student_no }}</td> <td>{{ stu.name }}</td> <td>{{ stu.gender }}</td> <td>{{ stu.major }}</td> <td>{{ stu.phone }}</td> <td> {% if stu.building %} {{ stu.building }}栋 {{ stu.room_no }}室 {% else %} <span class="text-muted">未分配</span> {% endif %} </td> </tr> {% endfor %} </tbody> </table>

Jinja2 模板里 {%%} 是控制结构,{{}} 是变量输出。这里的 if stu.building 判断就是处理未分配宿舍学生的展示逻辑。不要在 Python 里拼 HTML 字符串,那既难维护又会被评委追问“为什么不分离”。Flask 内置的 render_template 会自动去 templates 目录找同名文件,目录名不要拼错,否则会报 TemplateNotFound。

4. 从“能跑”到“能答辩”:权限控制、事务与入住率报表

4.1 三种角色权限控制:admin、manager、student 各看各的页面

宿舍管理系统至少要有三种角色:系统管理员(admin)能增删改查所有数据;宿管员(manager)能分配宿舍和处理报修,但不能删宿舍、不能改费用;学生(student)只能看自己的宿舍信息和提交报修申请。

最简实现是装饰器。写一个 role_required 装饰器,在每个路由函数上声明允许的角色:

from functools import wraps def role_required(*roles): def decorator(f): @wraps(f) def wrapper(*args, **kwargs): if 'role' not in session: return redirect(url_for('login')) if session['role'] not in roles: return '无权限访问', 403 return f(*args, **kwargs) return wrapper return decorator @app.route('/dormitories') @role_required('admin', 'manager') def dormitories(): # 宿舍列表管理,管理员和宿管员都能看 ... @app.route('/dormitories/<int:dorm_id>/delete') @role_required('admin') def delete_dorm(dorm_id): # 删除宿舍只有 admin 能干 ...

装饰器的执行顺序要记牢:@app.route 在外层,@role_required 在里层,这样 Flask 先注册路由,然后请求进来时先走权限检查。如果把顺序写反,路由注册会拿到被包装后的函数,容易出现“权限检查不生效”的诡异现象。这种 bug 属于典型的黑匣子问题,表面上报错信息看不出来,调试时只有加日志看请求走了哪个分支才明白。

权限这块还有一个容易忽略的点:前端按钮要让无权限的用户看不到。比如学生登录后不应该看到“删除宿舍”这个按钮。实际操作是在模板里判断角色:

{% if session.role == 'admin' %} <a href="/dormitories/{{ dorm.id }}/delete" class="btn btn-danger">删除</a> {% endif %}

服务端校验和前端隐藏要同时做,前端只是体验,后端才是安全边界。

4.2 用事务和行锁解决“一间宿舍被分给两个人”

第 3.3 节留了一个并发问题:两个请求同时分配同一间宿舍的最后一个床位。数据库层面的标准解法是 SELECT ... FOR UPDATE 行锁:

@app.route('/assign_safe', methods=['POST']) def assign_safe(): student_id = request.form.get('student_id') dorm_id = request.form.get('dorm_id') conn = get_conn() cur = conn.cursor() try: # 锁住宿舍行,其他事务要读取这行会被阻塞 cur.execute( "SELECT capacity, current_count, gender FROM dormitories WHERE id=%s FOR UPDATE", (dorm_id,) ) dorm = cur.fetchone() if not dorm: return '宿舍不存在', 404 if dorm['current_count'] >= dorm['capacity']: conn.rollback() return '该宿舍已满', 400 cur.execute( "UPDATE dormitories SET current_count=current_count+1 WHERE id=%s", (dorm_id,) ) cur.execute( "UPDATE students SET dorm_id=%s WHERE id=%s", (dorm_id, student_id) ) conn.commit() return '分配成功' except Exception as e: conn.rollback() return f'分配失败: {str(e)}', 500 finally: cur.close() conn.close()

FOR UPDATE 会让 MySQL 对被选中的行加排他锁,事务提交前其他事务无法修改或再次加锁这行。如果两个请求同时进来,第二个请求的 SELECT ... FOR UPDATE 会阻塞,等第一个 commit 后它读到的是 current_count=4 的新数据,自然就判断宿舍已满。这样“超卖”问题就解决了。事务隔离级别默认是 REPEATABLE READ,在 InnoDB 下这条 FOR UPDATE 会被当作当前读,读到的是最新已提交的数据,不需要改隔离级别就能生效。

这里要补充一个容易忽略的细节:FOR UPDATE 生效的前提是表引擎是 InnoDB。如果建表时用了 MyISAM,行锁不生效,整个查询期间会对表加共享锁,并发一高就全表阻塞。检查方式很简单,执行 SHOW TABLE STATUS LIKE 'dormitories'; 看 Engine 字段是否为 InnoDB。MySQL 8.0 默认就是 InnoDB,但如果你是从老项目里拷来的表,一定要确认。

4.3 入住率统计报表:给答辩评委一个能讲三分钟的数据亮点

毕设答辩最怕的是“功能都有了,但讲不出亮点”。入住率统计报表是一个低成本高回报的亮点:一个页面、一条 SQL,就能把宿舍资源的利用率数字化地呈现出来。按楼栋统计入住率的 SQL 如下:

SELECT d.building, SUM(d.capacity) AS total_beds, SUM(d.current_count) AS occupied_beds, ROUND(SUM(d.current_count) / SUM(d.capacity) * 100, 2) AS occupancy_rate FROM dormitories d GROUP BY d.building ORDER BY occupancy_rate DESC;

在 Flask 里包一层路由,把结果渲染成柱状图或纯表格都行。如果是纯表格,模板里直接遍历;如果要做柱状图,引入 ECharts——它是纯前端 JS 库,不需要服务器额外支持。把 SQL 查询出的数据转成列表传给模板:

@app.route('/report') @role_required('admin', 'manager') def report(): conn = get_conn() cur = conn.cursor() cur.execute(""" SELECT d.building, SUM(d.capacity) AS total_beds, SUM(d.current_count) AS occupied_beds, ROUND(SUM(d.current_count) / SUM(d.capacity) * 100, 2) AS occupancy_rate FROM dormitories d GROUP BY d.building ORDER BY occupancy_rate DESC """) report_data = cur.fetchall() cur.close() conn.close() return render_template('report.html', report=report_data)

报表的价值在于能发现管理问题。比如某栋楼入住率 95%,另一栋只有 60%,可以提出“是否要做跨楼栋调剂”的业务建议。答辩时你把这个观察讲出来,就不是简单的增删改查展示了,而是体现你理解了数据背后的业务逻辑。如果再给报表加一个时间维度的筛选条件,例如统计“本学期各楼栋报修数量”,就能看出哪栋楼设施老化严重。SQL 加 WHERE create_time BETWEEN %s AND %s,前端放一个日期选择器,代码量不大,但数据库查询、参数传递、模板回显这些点全都能覆盖到,是典型的低成本加分项。

5. 避坑指南:Python + MySQL 宿舍管理系统最容易踩的 5 个坑

5.1 Can't connect to local MySQL server through socket:服务没起还是 host 配错

现象:pymysql 连接时报错 “Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)”。

原因:这个报错在 Linux 和 macOS 上最常见。host 用了 localhost 时,pymysql 尝试通过 unix socket 文件连接,但 MySQL 服务没有启动,或者 socket 文件路径不是默认位置。这个报错跟防火墙、端口都没关系。Windows 上的变体是 “Can't connect to MySQL server on localhost (10061)”,那是 TCP 连接被拒,多数是 MySQL 服务类型设成了 Manual,开机没自动拉起。

解决:先分两层排查。第一层确认 MySQL 服务是否在运行,Linux 下执行 systemctl status mysqld,macOS 下用 brew services list,Windows 下用 net start 查看 MySQL 服务。第二层确认连接参数,如果 MySQL 在远程服务器上,把 host 改成实际 IP,强制走 TCP。如果想继续用 localhost 走 socket,可以指定 unix_socket 参数:

pymysql.connect( host='localhost', unix_socket='/var/run/mysqld/mysqld.sock', user='root', password='密码', database='dormitory' )

对于部署在云服务器上的毕设系统,我的建议是统一用内网 IP 作为 host,避免折腾 socket 路径。

5.2 中文乱码:宿舍号变成“3æ ̂”的成因与修法

现象:页面显示宿舍楼栋名正常,但从 MySQL 查询出来变成一堆乱码,比如“3栋”显示成“3æ ̂”,或者插入数据库后 SELECT 出来是问号。

原因:三个层面的字符集没有统一。第一层是数据库字段字符集,CREATE DATABASE 时没写 CHARACTER SET utf8mb4,或者从别处拷来的库是 latin1。第二层是连接字符集,pymysql 连接时 charset 参数没写或写成了 utf8。第三层是前端页面编码,HTML 没声明 。

解决:从建库开始统一,所有环节全用 utf8mb4。检查现有库的字符集执行 SHOW CREATE DATABASE dormitory; 如果发现不是 utf8mb4,用下面的语句修复:

ALTER DATABASE dormitory CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE students CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

Python 连接参数里 charset='utf8mb4',HTML 模板开头写 。三个环节全部对齐后乱码问题就会消失,这个坑属于典型的环境配置问题,跟业务代码无关。

5.3 插入了数据但前端查不到:pymysql 的 autocommit 陷阱

现象:在 Python 里执行 INSERT 后马上 SELECT,能查到新数据;但关掉程序重新启动,或者用 Navicat 打开数据库,看到的还是旧数据。

原因:pymysql 连接默认 autocommit=False,INSERT/UPDATE 之后没有执行 conn.commit(),事务一直处于未提交状态。当前连接内查询能看到自己事务里的数据,其他连接看不到,造成了一种“数据已写入”的错觉。Navicat 打开看到的还是旧数据,就是因为它开的是另一个独立连接。

解决:在每次写操作后显式调用 conn.commit()。更省事的做法是在建立连接时设置 autocommit=True:

pymysql.connect( host='localhost', user='root', password='密码', database='dormitory', charset='utf8mb4', autocommit=True )

但对有事务需求的操作(比如宿舍分配),autocommit=True 会让每条语句自动提交,FOR UPDATE 行锁在语句结束时就释放了,无法保证分配原子性。所以我的习惯是:连接层保持 autocommit=False,在需要落盘的地方显式 commit,把事务控制权握在自己手里。

5.4 删除宿舍时报外键约束错误:FOREIGN KEY 的连锁反应

现象:执行 DELETE FROM dormitories WHERE id=1 报错 “Cannot delete or update a parent row: a foreign key constraint fails”。

原因:students 表的外键 dorm_id 默认行为是 RESTRICT,只要还有学生引用这间宿舍,宿舍就不能被删除。毕设里常见的场景是管理员退宿了一个学生但没把学生的 dorm_id 置空,或者干脆没走退宿流程直接去删宿舍。

解决:方案一是业务上先清空引用,在删除宿舍前把所有 dorm_id 指向它的学生置为 NULL:

UPDATE students SET dorm_id = NULL WHERE dorm_id = 1; DELETE FROM dormitories WHERE id = 1;

方案二是改外键约束行为,在 students 表创建时用 ON DELETE SET NULL(前面建表 SQL 里已经用了这个写法)。这样删除宿舍时 MySQL 自动把学生的 dorm_id 置空。如果表已经建好,可以用 ALTER TABLE 修改约束:

ALTER TABLE students DROP FOREIGN KEY fk_student_dorm; ALTER TABLE students ADD CONSTRAINT fk_student_dorm FOREIGN KEY (dorm_id) REFERENCES dormitories(id) ON DELETE SET NULL;

修改外键前先确认约束名,在 Navicat 的设计表界面里能看到约束名称,名字对不上就执行不了。

5.5 部署到云服务器后浏览器访问不了:安全组和 bind-address 的双重门

现象:本地用 Flask run 能访问,部署到云服务器后用公网 IP 访问 http://公网IP:5000 超时,或者 MySQL 用 Navicat 连不上。

原因:两层原因叠加。第一,Flask 默认只监听 127.0.0.1,需要指定 host='0.0.0.0' 才能对外访问;第二,云服务器安全组默认不开放 5000/3306 端口,需要在云控制台加一条入站规则。MySQL 的 bind-address 如果绑定了 127.0.0.1,即使安全组开放了 3306,外部也连不上。

解决:Flask 启动时写死监听地址:

app.run(host='0.0.0.0', port=5000, debug=False)

MySQL 的配置文件 /etc/my.cnf 里把 bind-address 改成 0.0.0.0(或注释掉),重启 mysqld。安全组在云控制台加两条规则:TCP 5000 端口放行,TCP 3306 端口放行(只放行你本机 IP 会更安全)。最后不要忘了,MySQL 8.0 默认的 root 用户只允许 localhost 登录,要创建一个允许远程访问的用户:

CREATE USER 'dorm_app'@'%' IDENTIFIED BY '强密码'; GRANT ALL PRIVILEGES ON dormitory.* TO 'dorm_app'@'%'; FLUSH PRIVILEGES;

MySQL 8.0 默认的 caching_sha2_password 认证插件对旧版 Navicat 和某些客户端不兼容,如果报 authentication plugin 错误,改用 mysql_native_password 可以解决:

ALTER USER 'dorm_app'@'%' IDENTIFIED WITH mysql_native_password BY '强密码';

这个坑是部署环节最折磨人的,很多人本地跑通了一切顺利,一上云就卡在这一步。

6. 答辩前夜的三件事:初始化数据、备份数据库和设计演示路线

6.1 用存储过程生成测试数据:别手动敲 200 条学生记录

演示的时候最怕现场数据太少,宿舍列表空空荡荡,分配宿舍时下拉框里没几个学生。提前用一条存储过程批量生成测试数据:

DROP PROCEDURE IF EXISTS generate_test_data; DELIMITER $$ CREATE PROCEDURE generate_test_data(IN stu_count INT) BEGIN DECLARE i INT DEFAULT 1; -- 先造 20 间宿舍:5 栋楼,每栋 4 间 WHILE i <= 20 DO INSERT INTO dormitories (building, room_no, capacity, gender) VALUES (CONCAT(CEIL(i/4), '栋'), CONCAT('30', i), 4, IF(i % 2 = 1, '男', '女')); SET i = i + 1; END WHILE; -- 再造学生 SET i = 1; WHILE i <= stu_count DO INSERT INTO students (student_no, name, gender, major, phone, dorm_id) VALUES ( CONCAT('2024', LPAD(i, 3, '0')), CONCAT('测试', i, '号'), IF(i % 2 = 1, '男', '女'), ELT(MOD(i, 5) + 1, '计算机科学', '软件工程', '网络工程', '数据科学', '人工智能'), CONCAT('138', LPAD(MOD(i, 100000000), 8, '0')), MOD(i, 20) + 1 ); SET i = i + 1; END WHILE; END$$ DELIMITER ; CALL generate_test_data(200);

执行完 CALL generate_test_data(200); 数据库里就有 200 个学生、20 间宿舍,演示时数据量足够支撑任何页面。存储过程里 CONCAT、LPAD、ELT 几个函数都是 MySQL 字符串处理的常用操作,评委问起来也很好讲。

6.2 演示路线图:先登录、再分配、最后报表

答辩演示只有几分钟,路线设计直接影响评委观感。我会按这个顺序走:先打开登录页,用管理员账号登录,展示不同角色登录后看到的菜单不同,这一步已经把权限功能演示完了。然后进入学生列表页,筛选一个“未分配”的学生,演示分配宿舍;分配后马上刷新宿舍列表,床位数量发生变化,这一步把事务和关联更新展示清楚。最后打开入住率报表,指着一栋楼的柱状图说入住率高、另一栋有调剂空间,收尾落在数据价值上。

中间如果评委问“数据库长什么样”,再打开 Navicat 展示几张表的关联关系,不要一上来就展示数据库结构,那样会把演示节奏拖慢。

6.3 用 mysqldump 一键备份:防止答辩现场翻车

演示前最担心的就是数据库被搞坏。答辩前一晚跑一次完整备份,把整库导出成一个 SQL 文件:

mysqldump -u root -p --default-character-set=utf8mb4 dormitory > dormitory_backup.sql

恢复的时候用:

mysql -u root -p dormitory < dormitory_backup.sql

备份文件一定要加 --default-character-set=utf8mb4 参数,否则备份出的 SQL 文件里中文注释和字符串会变成乱码,恢复进去整库中文化就毁了。这是我踩过的坑,当时备份没加这个参数,恢复后所有楼栋名乱码,花了半小时才用第 5.2 节的 ALTER TABLE 转回来。这几件事做完,剩下的就看临场表达了。第一次做这种系统的人,最容易在“数据库连不上”“页面 500”这类环境问题上翻车,而上面这几条恰好把这类问题提前堵住。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询