☰
C#+MySQL房屋租赁管理系统:数据库设计与增删改查完整实践
2026/10/12 19:20:25 网站建设 项目流程

简介:一套面向高校计算机相关专业课程设计与毕业设计的房屋租赁管理系统资源包,基于C# WinForms与MySQL数据库实现,适合计科、软件、大数据、物联网等方向学生借鉴学习,既可作为课程入门项目,也支持二次功能扩展。压缩包内共71个文件,主要包含25个C#窗体与逻辑源码文件、项目解决方案与工程配置、roomrentsystem.sql数据库脚本、MySQL Connector连接组件安装包,以及课程设计报告(doc/docx)和ER图、数据流图等辅助说明材料,整体大小12.83MB,目录结构清晰。资源已通过运行验证,登录、房源、租客、账单、财务等模块均可正常使用,能够帮助理解管理系统的基本数据流程与界面交互设计;设计报告和SQL脚本也为课程答辩、毕业设计文档撰写提供了直接参考。目前已有303人学习下载,适合需要完整课设源码、数据库脚本与配套文档样例的在校生和入门开发者。

1. 课程设计交不出的时候,问题大多不在C#代码,而在数据库设计

很多同学拿到“房屋租赁管理系统”这个课设题,第一反应是打开 Visual Studio 拖几个按钮,把页面画出来,再回头想数据库怎么建。结果往往是:窗体画得像模像样,一跑起来全是 MySQL 连接失败、外键删不掉、中文乱码、统计报表算不出数。这个标题里的压缩包,表面上是“源码+数据库+设计报告”三件套,本质上是一套把 C# 窗体程序和 MySQL 数据库串成一条线的完整课设方案:你看到的业务界面只是壳,真正决定能不能答辩过关的,是表结构设计是否合理、增删改查是否严谨、报告里能不能把设计过程交代清楚。

这套东西适合两类人:一类是正在做数据库课设、想找个完整可复现路径的学生;另一类是刚入门 C# 桌面开发、想看看商业系统里“数据层”该怎么组织的人。下面按我做课设和帮人救火的顺序,把表设计、连接、增删改查、常见翻车点一次说清。

2. 把业务拆成四张表:房屋租赁系统的数据库设计与DDL

2.1 从需求到ER:房屋、租客、合同、缴费的关系怎么理

房屋租赁系统的核心业务并不复杂:房东有一批房子,租客来租房,双方签合同,租客按合同交租。但落到数据库设计时,很多人会把“房子”和“合同”混在一张表里,把“租金”塞进“房子”表,后面统计时只能写出十几行拼起来的 SQL,自己都看不懂。

正确的做法是先把实体拆开。我一般会画四张主表加一张用户表:sys_user存登录账号,house存房源信息,tenant存租客档案,contract存租赁合同,payment存每次的缴费记录。关系上,一栋房子可以经历多份合同,所以 contract 通过 house_id 引用 house;一个租客也可以签多份合同,所以 contract 通过 tenant_id 引用 tenant;一份合同对应多次缴费,所以 payment 通过 contract_id 引用 contract。

这里有一个新手经常忽略的点:房源状态。同一套房在不同时间可能“未出租”“已出租”“维修中”,这个状态如果每次去合同表里查有没有未到期记录,查询会非常绕。所以我一般会在 house 表里直接放一个 state 字段,签约时更新为已出租,退租时更新为未出租。虽然存在一点冗余,但业务阅读理解成本低,课设答辩时也容易讲清楚。

2.2 建表SQL:用MySQL 5.7/8.0都能跑的DDL写清主外键

下面是这套系统常用的建表脚本。字段命名尽量用下划线风格,和 C# 里的属性名区分开,代码里做映射时一眼就能看出哪个是数据库字段、哪个是程序变量。

CREATE DATABASE IF NOT EXISTS rent_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE rent_db; CREATE TABLE sys_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT '密码,建议存MD5/SHA256', role TINYINT NOT NULL DEFAULT 1 COMMENT '1-管理员 2-普通操作员', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE house ( id INT AUTO_INCREMENT PRIMARY KEY, house_no VARCHAR(30) NOT NULL UNIQUE COMMENT '房号,例如 3-2-501', building VARCHAR(20) NOT NULL COMMENT '楼栋', unit VARCHAR(20) DEFAULT NULL COMMENT '单元', room VARCHAR(20) DEFAULT NULL COMMENT '房间号', area DECIMAL(6,2) NOT NULL COMMENT '面积(平方米)', rent DECIMAL(10,2) NOT NULL COMMENT '月租金(元)', state TINYINT NOT NULL DEFAULT 0 COMMENT '0-未出租 1-已出租 2-维修中', remark VARCHAR(255) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_house_state (state), KEY idx_house_building (building) ) ENGINE=InnoDB; CREATE TABLE tenant ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, id_card VARCHAR(18) DEFAULT NULL COMMENT '身份证号,用于登记', emergency_contact VARCHAR(50) DEFAULT NULL COMMENT '紧急联系人', emergency_phone VARCHAR(20) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_tenant_phone (phone), KEY idx_tenant_name (name) ) ENGINE=InnoDB; CREATE TABLE contract ( id INT AUTO_INCREMENT PRIMARY KEY, contract_no VARCHAR(30) NOT NULL UNIQUE COMMENT '合同编号,例如 HT20240501', house_id INT NOT NULL, tenant_id INT NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, deposit DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '押金', rent DECIMAL(10,2) NOT NULL COMMENT '合同约定月租金', status TINYINT NOT NULL DEFAULT 1 COMMENT '1-生效中 0-已退租', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_contract_house FOREIGN KEY (house_id) REFERENCES house(id), CONSTRAINT fk_contract_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id), KEY idx_contract_house (house_id), KEY idx_contract_tenant (tenant_id) ) ENGINE=InnoDB; CREATE TABLE payment ( id INT AUTO_INCREMENT PRIMARY KEY, contract_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL COMMENT '本次缴费金额', pay_date DATE NOT NULL COMMENT '缴费日期', pay_month VARCHAR(10) NOT NULL COMMENT '对应租期,比如 2024-05', remark VARCHAR(255) DEFAULT NULL, CONSTRAINT fk_payment_contract FOREIGN KEY (contract_id) REFERENCES contract(id) ON DELETE CASCADE, KEY idx_payment_contract (contract_id), KEY idx_payment_date (pay_date) ) ENGINE=InnoDB;

DDL 里有几个设计点值得说。金额一律用 DECIMAL(10,2),不要用 FLOAT 或 DOUBLE,否则累计缴费总额会出现 0.1 + 0.2 != 0.3 这种经典浮点误差,答辩时老师随便查一条统计就对不上账。时间用 DATE 还是 DATETIME 要想清楚:合同起止日期只需要精确到天,用 DATE;房租缴费记录需要精确到时分秒时再考虑 DATETIME。身份证号用 VARCHAR(18),不要用 BIGINT,因为身份证号可能是 X 结尾,而且不用参与数值计算。

外键约束里,payment 我加了ON DELETE CASCADE,表示合同删除时缴费记录一起删,因为缴费记录离开了合同没有独立意义。但 contract 对 house 和 tenant 没有加 CASCADE,目的是防止有人直接把还挂在合同里的房子删掉,让系统处于“合同还在、房子没了”的矛盾状态。这个差异在答辩时经常被老师问,能解释清楚就是加分项。

2.3 数据字典与约束:字段类型、默认值、索引设计的取舍

设计报告里需要有一份数据字典,但很多同学的数据字典就是把建表 SQL 翻译成文字,没有任何设计理由。以下是这个系统需要重点解释的字段和取舍逻辑。

表字段类型约束/默认值设计说明
housestateTINYINT0用数字不用字符串,避免“已出租”“未出租”中文拼写不一致
houserentDECIMAL(10,2)无单月租金最高 99999999.99,足够普通住宅
tenantphoneVARCHAR(20)索引手机号是租客查询的第一入口,必须建索引
contractcontract_noVARCHAR(30)UNIQUE合同编号手工生成,唯一约束防止重复
paymentpay_monthVARCHAR(10)无记录缴费对应租期,统计“哪个月欠租”时直接 GROUP BY
sys_userpasswordVARCHAR(64)无存哈希值不存明文,哪怕课设也要养成习惯

索引不是越多越好。我见过有人给每个字段都建了索引,结果插入数据时 InnoDB 要维护七八个 B+ 树,数据量上千后明显变慢。这个规模的项目,只需要给“查询条件里会出现的字段”建索引:house 的 state 和 building、tenant 的 phone 和 name、contract 的 house_id 和 tenant_id、payment 的 contract_id 和 pay_date。剩下字段不要建。

还有一个容易忽视的细节:同一张表里业务状态字段用 TINYINT 比用 CHAR 更省心。如果你用 VARCHAR 存“已出租/未出租”,一旦代码某处写成了“已出粗”,查询就查不到数据,而且这种 bug 肉眼很难发现。用 0/1/2 这种数字,配合 C# 里的枚举或者注释,至少不会出现错别字问题。

3. C#连接MySQL的最小实现:从驱动安装到DBHelper封装

3.1 选择驱动:MySql.Data与MySqlConnector怎么选

C# 操作 MySQL 最常见的驱动是两个:官方出的 MySql.Data 和开源社区维护的 MySqlConnector。对于课设项目,我一般建议直接用 MySql.Data,原因是教程最多、坑的解决方案最好搜,答辩时老师哪怕不看代码,听到“用的是官方驱动”也会觉得稳妥。

安装时在 Visual Studio 里打开“工具 → NuGet 包管理器 → 管理解决方案的 NuGet 程序包”,搜索 MySql.Data,选择稳定版本安装。常见做法是在程序包管理器控制台执行:

Install-Package MySql.Data -Version 8.0.33

如果机器没联网也没关系,可以从别的机器下载 MySql.Data.dll 拷到项目 bin 目录,右键“添加引用”直接引入。但要注意版本,8.0.x 的 DLL 不会自动兼容所有场景,下文 5.1 会讲一个和驱动版本强相关的坑。

MySqlConnector 性能更好、支持异步,但它的 API 命名和 MySql.Data 略有区别,如果你照着网上 MySql.Data 的教程写 MySqlConnector 的代码,有些地方会报方法不存在。课设阶段没必要两头学,选一个用熟即可。

3.2 连接字符串:六个必调参数与SSL连接报错

很多人的第一个坑就是 MySQL 装好了,C# 连不上。这里十有八九是连接字符串参数不对。我常用的连接字符串模板如下:

string connStr = "Server=127.0.0.1;Port=3306;Database=rent_db;Uid=root;Pwd=123456;Charset=utf8mb4;SslMode=None;AllowPublicKeyRetrieval=True;";

逐个参数说:Server写 127.0.0.1 还是 localhost 都行,但如果 MySQL 装在另一台机器,要写那台机器的 IP。Port默认 3306,安装 MySQL 时改过端口就要跟着改。Database是库名,必须在 MySQL 里先建好。Uid/Pwd是登录账号密码,root 密码忘了就麻烦,建议课设前先在 MySQL 命令行里确认能登录。

最容易被忽略的是后面三个参数。Charset=utf8mb4保证 C# 传过去的中文按 UTF-8 编码,否则插入数据后用 MySQL 客户端看是正常的,界面上显示却是乱码。SslMode=None解决的是 MySQL 8.0 默认启用 SSL 加密连接的问题,本地开发时握手协商失败经常会报 “SSL Connection Error” 或 “Authentication method not supported”,先关掉 SSL 是最快的排除手段。AllowPublicKeyRetrieval=True配合 MySQL 8.0 的 caching_sha2_password 认证插件使用,不加的话驱动可能拿不到 RSA 公钥,连接直接超时。

建议把连接字符串写在一个公共类里,别每个窗体复制一份。后面如果要换库、换账号,只改一处即可。

3.3 封装DBHelper:查询、非查询、事务三件套

窗体代码里直接 new MySqlConnection 再拼 SQL,也可以跑,但每个按钮事件里写一遍连接、打开、关闭,代码会非常臃肿。我习惯做一个 DBHelper 静态类,把查询、非查询、事务三件事封装好。以下是一个最小实现:

using MySql.Data.MySqlClient; using System.Data; public static class DBHelper { private static readonly string connStr = "Server=127.0.0.1;Port=3306;Database=rent_db;Uid=root;Pwd=123456;Charset=utf8mb4;SslMode=None;AllowPublicKeyRetrieval=True;"; public static DataTable ExecuteQuery(string sql, params MySqlParameter[] ps) { using (var conn = new MySqlConnection(connStr)) using (var cmd = new MySqlCommand(sql, conn)) { if (ps != null) cmd.Parameters.AddRange(ps); var adapter = new MySqlDataAdapter(cmd); var dt = new DataTable(); conn.Open(); adapter.Fill(dt); return dt; } } public static int ExecuteNonQuery(string sql, params MySqlParameter[] ps) { using (var conn = new MySqlConnection(connStr)) using (var cmd = new MySqlCommand(sql, conn)) { if (ps != null) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteNonQuery(); } } }

这段代码的核心是 using 语句。MySqlConnection 和 MySqlCommand 都实现了 IDisposable,using 块结束时会自动释放连接,避免断开后 MySQL 长时间持有连接句柄导致 “Too many connections”。参数化查询是另一个关键点:所有值都通过new MySqlParameter("@name", value)传进去,不要用"WHERE name = '" + textBox.Text + "'"这种字符串拼接,否则既容易被 SQL 注入,又会在值里带单引号时直接报语法错误。

使用方式很直接:查询走DataTable dt = DBHelper.ExecuteQuery("SELECT * FROM house WHERE state=@state", new MySqlParameter("@state", 0));,增删改走int rows = DBHelper.ExecuteNonQuery("UPDATE house SET rent=@rent WHERE id=@id", ...)。返回的 rows 是受影响行数,可以用来判断操作是否成功。

事务封装则需要单独处理,因为 BeginTransaction 和提交/回滚必须是同一个连接对象。下面这个签合同的场景就是典型的需要事务的例子,放在 4.2 里一起讲。

4. 房屋租赁核心功能的增删改查:从窗体按钮到SQL语句

4.1 房屋管理的完整链路:查询、新增、编辑、删除(含SQL参数化)

房屋管理是主界面,需要支持按楼栋、房号、状态组合查询,以及新增、编辑、删除。这里最容易翻车的是组合查询:用户只想按楼栋查,但 SQL 里还拼了 “AND state = ''”,查出来为空。我用一个技巧解决:状态条件传 -1 表示“不限状态”,房号条件传空字符串表示“不按房号过滤”。

string sql = @"SELECT * FROM house WHERE (@houseNo = '' OR house_no LIKE CONCAT('%', @houseNo, '%')) AND (@state = -1 OR state = @state) ORDER BY id DESC"; DataTable dt = DBHelper.ExecuteQuery(sql, new MySqlParameter("@houseNo", txtHouseNo.Text.Trim()), new MySqlParameter("@state", comboState.SelectedIndex == 0 ? -1 : comboState.SelectedIndex - 1));

这个 SQL 的原理是:参数@houseNo为空时,第一个条件@houseNo = ''成立,OR 短路,后面的 LIKE 条件不生效;同理@state为 -1 时跳过状态过滤。这样做的好处是不需要在 C# 代码里动态拼 SQL 字符串,逻辑更清晰,也不会因为某个条件为空漏掉参数。

新增房屋的 SQL 要注意 house_no 唯一约束的冲突处理。如果用户录入重复房号,执行时 MySQL 会报 “Duplicate entry”,程序会直接崩溃。常见的做法是插入前先查一次:

string checkSql = "SELECT COUNT(*) FROM house WHERE house_no = @houseNo"; long count = Convert.ToInt64(DBHelper.ExecuteScalar(checkSql, new MySqlParameter("@houseNo", txtHouseNo.Text.Trim()))); if (count > 0) { MessageBox.Show("房号已存在,请确认后再录入"); return; }

这里用到了 ExecuteScalar,它返回查询结果第一行第一列,适合 COUNT 这种聚合查询。DBHelper 里可以随手加一个:

public static object ExecuteScalar(string sql, params MySqlParameter[] ps) { using (var conn = new MySqlConnection(connStr)) using (var cmd = new MySqlCommand(sql, conn)) { if (ps != null) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteScalar(); } }

编辑和删除相对简单:编辑是UPDATE house SET building=@building, unit=@unit, room=@room, area=@area, rent=@rent, remark=@remark WHERE id=@id,删除是DELETE FROM house WHERE id=@id。但删除前必须检查合同引用,否则外键约束会直接报错,这个坑在 5.3 里专门说。

4.2 合同与租客联动:外键引用和删除顺序的代码实现

签合同是整个系统最值得写进设计报告的逻辑,因为它涉及事务和多表联动。一次签约本质上要做三件事:插入合同记录、把房屋状态改为已出租、可能还要给租客生成一份押金记录。这三件事要么全成功,要么全不成功,绝不能出现“合同建了但房子还是未出租”的情况。

using (var conn = new MySqlConnection(DBHelper.GetConnStr())) { conn.Open(); using (var tx = conn.BeginTransaction()) using (var cmd = new MySqlCommand()) { try { cmd.Connection = conn; cmd.Transaction = tx; cmd.CommandText = @"INSERT INTO contract(contract_no, house_id, tenant_id, start_date, end_date, deposit, rent) VALUES(@contractNo, @houseId, @tenantId, @startDate, @endDate, @deposit, @rent)"; cmd.Parameters.Clear(); cmd.Parameters.AddWithValue("@contractNo", txtContractNo.Text.Trim()); cmd.Parameters.AddWithValue("@houseId", Convert.ToInt32(cboHouse.SelectedValue)); cmd.Parameters.AddWithValue("@tenantId", Convert.ToInt32(cboTenant.SelectedValue)); cmd.Parameters.AddWithValue("@startDate", dtpStart.Value.Date); cmd.Parameters.AddWithValue("@endDate", dtpEnd.Value.Date); cmd.Parameters.AddWithValue("@deposit", Convert.ToDecimal(txtDeposit.Text.Trim())); cmd.Parameters.AddWithValue("@rent", Convert.ToDecimal(txtRent.Text.Trim())); cmd.ExecuteNonQuery(); cmd.CommandText = "UPDATE house SET state = 1 WHERE id = @houseId"; cmd.Parameters.Clear(); cmd.Parameters.AddWithValue("@houseId", Convert.ToInt32(cboHouse.SelectedValue)); cmd.ExecuteNonQuery(); tx.Commit(); MessageBox.Show("签约成功"); } catch (Exception ex) { tx.Rollback(); MessageBox.Show("签约失败:" + ex.Message); } } }

这里的事务包裹了 INSERT 和 UPDATE 两步:先写合同,再改房屋状态。如果第二步失败,第一步会被回滚掉,不会留下孤立的合同数据。事务的边界要控制好,不要把 MessageBox 弹窗放在 Commit 之前,否则用户迟迟不点“确定”,事务就一直挂着,数据库连接也被占用。这种问题在真实项目里很常见:事务里有 UI 弹窗,程序看起来没动静,数据库锁却越积越多。

退租流程则是反向操作:更新合同状态为已退租、房屋状态改为未出租。删除顺序上,必须先处理 payment 再处理 contract,最后删 house,否则外键约束报错。很多课设直接无视外键,用“先删房屋,报错了再说”的方式,答辩时老师一跑退租功能就翻车。

4.3 让界面不再死板:ComboBox绑定数据源与状态刷新的套路

房屋和租客的窗体里,ComboBox 是高频控件。常见错误是手动循环往 Items 里 Add 字符串,然后 SelectedItem 拿到的是一串房号文本,根本取不到对应的 house_id。正确的做法是把数据源绑上去:

DataTable houseTable = DBHelper.ExecuteQuery("SELECT id, house_no FROM house WHERE state = 0"); cboHouse.DataSource = houseTable; cboHouse.DisplayMember = "house_no"; cboHouse.ValueMember = "id";

绑定之后,cboHouse.SelectedValue能直接拿到选中房源的 id,cboHouse.Text显示的是房号文本。DisplayMember 告诉 ComboBox 显示哪列,ValueMember 告诉它把哪列的值作为“暗中携带的数据”。签合同时就不需要在内存里维护一个“房号文本 → id”的字典了。

另一个容易被忽略的细节是数据刷新。新增完房屋后,如果界面上还有个列表 DataGridView,它不会自动知道数据库变了,必须重新查询并绑定一次。我一般会在每个增删改方法末尾调用LoadHouseList(),里面重新执行查询、重新给 DataGridView 绑定 DataSource。这个习惯可以避免很多“明明删了还在界面上”“明明改了还显示旧值”的灵异事件。

5. 课设排查手册:连不上、乱码、外键拦路、日期异常与查询玄学

5.1 MySQL 8.0密码插件导致C#连不上

现象:MySQL 命令行能登录,C# 程序一执行就报 “Authentication method 'caching_sha2_password' not supported” 或者连接超时。这是 MySQL 8.0 最常见的坑之一,网上搜 mysql 安装教程时很多人会碰到。

原因:MySQL 8.0 默认把 root 用户的认证插件设置为 caching_sha2_password,而旧版 MySql.Data(8.0.11 之前)不认识这种认证方式,握手阶段直接拒绝。

解决:两种办法选一个。一是升级驱动的 NuGet 包到 8.0.33 或更高,并保证连接字符串里带了AllowPublicKeyRetrieval=True;SslMode=None;。二是在 MySQL 命令行里把 root 的认证方式降级:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;

课设阶段我用第二种方法比较多,因为驱动版本一换可能会牵连其他代码。但要注意,如果你用的 MySQL 是 5.7.x(比如一些电脑上装的是 mysql 5.7.44),默认认证插件是 mysql_native_password,一般不会出这个问题。所以排查时先确认版本,再决定改驱动还是改账号认证方式。

5.2 中文乱码:库、表、连接三级字符集

现象:在 C# 界面录入“张三”,点保存后再查出来变成“??”或“寮犱笁”。MySQL 客户端里看正常,程序里看乱码。这个问题折磨过很多做课设的人。

原因:字符集在三层不一致。数据库建库时用了默认 latin1,或者建表时没指定 utf8mb4,或者 C# 连接字符串里没有 Charset 参数。只要三层任一层不是 utf8mb4,中文就容易出问题。

解决:按照“库、表、连接”三级逐一检查。库和表已经错了的话,要改默认字符集并重建表结构转换:

ALTER DATABASE rent_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE house CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE tenant CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE contract CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE payment CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

然后检查连接字符串里必须有Charset=utf8mb4;。这里补充一个细节:MySQL 里的 utf8 实际是 utf8mb3,只支持最多 3 字节的字符,像 emoji 这类 4 字节字符插进去会报错或变问号。所以新项目一律用 utf8mb4,这也是为什么建表脚本里我特意写了DEFAULT CHARACTER SET utf8mb4。

5.3 外键拦路:有合同的房屋删不掉

现象:想删一条房屋记录,报 “Cannot delete or update a parent row: a foreign key constraint fails”,看起来像是程序 BUG。实际上这是外键在保护数据完整性。

原因:contract 表里存在引用该房子的合同,删了房子,合同就变成了悬空引用。房屋租赁系统里这是完全不应该允许发生的。

解决:先查合同表里有没有引用这个房子的记录:

SELECT COUNT(*) FROM contract WHERE house_id = 3 AND status = 1;

如果还有生效中的合同,就禁止删除并提示“该房屋存在未到期合同,请先处理”。如果合同是历史已退租的,要么把合同一起删掉(注意 payment 联动删除),要么保留合同但把房屋标记为停用。我一般倾向保留合同、给房子加一个逻辑删除状态,而不是物理删除。原因是合同是财务凭证,说删就删会让缴费统计失去依据。

这里还牵出一个设计取舍:payment 表的外键我加了 ON DELETE CASCADE,contract 删了 payment 自动删。但如果你的设计报告里写的是“保留全部历史缴费记录”,那就不该用 CASCADE,而是在删除合同前先人工确认是否要清空缴费数据。答辩时老师会问“你为什么这里级联删除那里不级联”,想清楚再回答。

5.4 日期时间参数:传参格式与时区偏移

现象:合同开始日期选的是 2024-05-01,存到 MySQL 后变成了 2024-04-30 22:00:00 或 2024-05-01 但相差 8 小时;按日期查缴费记录,某一天的账查不全。

原因:MySQL 的 DATE 类型不保留时区,但 DATETIME 类型在连接参数里的时区设置和 C# 本地时间不一致时,底层驱动会做转换。另一个问题是很多人的 MySqlParameter 没有显式指定 MySqlDbType,驱动把 DateTime 默认当成了 DATETIME 或者干脆用了隐式转换,格式对不上就出偏差。

解决:显式声明参数类型,并且在赋值时只取日期部分:

new MySqlParameter("@startDate", MySqlDbType.Date) { Value = dtpStart.Value.Date }

dtpStart.Value.Date把 TimePicker 带来的时分秒清零,避免 “2024-05-01 00:00:00” 和 “2024-05-01 12:30:15” 在查询比较时造成边界遗漏。时区问题则可以在连接字符串里加Connection Timezone=Local或者serverTimezone=Asia/Shanghai。本地开发一般默认就是本机时区,不用特意配;但如果你把数据库部署到云服务器,就必须显式指定,否则 C# 写进去的时间和数据库读出来的时间会莫名其妙差 8 小时,这是典型的跨时区翻车现场。

5.5 查询慢与排序乱:索引失效与默认排序的玄学

现象:数据量只有几百条时一切正常,后来租客和缴费记录上了几千条,统计报表卡顿;分页查询时,翻第二页出现重复数据;DataGridView 里“明明后来新增的记录,没排在最上面”。

原因:第一,查询条件里的字段没有索引,比如WHERE phone = '138xxxx',如果 tenant.phone 没建索引,全表扫描必然慢。第二,MySQL 不保证不带 ORDER BY 的查询结果有序,插入顺序、主键顺序、索引顺序都可能影响返回顺序,界面里看到的顺序是一种玄学。第三,分页用了LIMIT 100, 20这种大偏移量,MySQL 要把前 100 条全部扫一遍再丢弃,翻到后面自然慢。

解决:给查询字段补索引;所有列表查询显式加 ORDER BY;深分页改用游标方式。游标分页的核心是把“翻第几页”改成“从哪条开始”:

SELECT * FROM payment WHERE id > @lastId ORDER BY id LIMIT 20;

这个写法适合主键递增的业务表,滚动加载时把上一页最后一条记录的 id 作为@lastId传进来,MySQL 可以直接走主键索引,不需要扫描前 100 条。课设系统一般翻不了几页,但能在设计报告的“优化方案”里写出这个思路,比写一堆没用的缓存配置实在得多。

6. 把课设从60分做到90分:统计报表、权限控制与验收清单

6.1 一个季度租金统计SQL撑起报表页

课设里最常见的加分项是“统计报表”。很多人的报表是从 DataGridView 里循环累加金额,数据一多就慢。正确做法是把统计放到 SQL 里,界面只负责显示。按月统计租金收入的 SQL 可以这么写:

SELECT DATE_FORMAT(pay_date, '%Y-%m') AS month, SUM(amount) AS total_income FROM payment GROUP BY month ORDER BY month;

按季度统计则需要把MONTH(pay_date)映射到季度,MySQL 内置了 QUARTER 函数。这里注意,DATE_FORMAT 不支持直接输出自定义文本拼接,一般用 CONCAT 处理:

SELECT CONCAT(YEAR(pay_date), '-Q', QUARTER(pay_date)) AS quarter, SUM(amount) AS total_income FROM payment GROUP BY quarter ORDER BY quarter;

这条 SQL 跑出来,再绑到一个 DataGridView 或 Chart 控件上,报表页就活了。答辩时如果老师问“怎么知道哪个月欠租”,再补一条按合同分组、统计最近缴费日期的查询,比界面里放三个文本框来回查要专业得多。

6.2 登录与权限:控制按钮比控制窗体更实用

登录功能人人都会写,但权限控制很多人都做得太重。常见需求是:普通操作员能处理房源、租客、合同,只有管理员能看用户管理和统计报表。简单做法是登录成功后在静态类里保存当前用户角色:

public static class LoginInfo { public static string UserName { get; set; } public static int Role { get; set; } }

然后在主窗体的 Load 事件里根据角色控制按钮或菜单项的可见性。控制按钮比控制整个窗体的好处是:主界面不用为不同角色做多个版本,菜单栏视觉上也更完整。需要注意的是,隐藏按钮并不能真正防住操作——如果用户绕过界面直接调用了某个方法,权限还是会被绕过。课设里做到“按角色显示/隐藏入口”已经足够,真要做权限拦截得在业务层写鉴权,那已经是另一个话题。

6.3 验收自测清单:照着点一遍再提交

提交前把下面这些场景完整跑一遍,比多写 10 个窗体都值钱:

  1. 新装 MySQL 的机器上,依次执行建库、建表、插入初始化数据的脚本,确认无报错。
  2. 系统登录页输入错误密码,确认有友好提示而不是直接崩溃。
  3. 新增一套房,重复输入相同房号,确认校验生效。
  4. 录入租客后签约,确认房屋状态自动变成“已出租”。
  5. 对该合同录入三笔缴费,在报表页确认月度、季度汇总金额正确。
  6. 退租后,确认房屋状态恢复“未出租”,合同标记为“已退租”。
  7. 手动停掉 MySQL 服务,再启动程序,确认报错信息清晰,不会出现未处理异常的黑框。

最后一步很关键。很多课设程序在 MySQL 正常时跑得飞起,数据库一停就抛出一堆英文异常,答辩演示时万一环境抽风,整场就砸了。在程序入口或数据访问层包一层 try-catch,给出“数据库未连接,请检查 MySQL 服务”的中文提示,这个细节能挽回很多印象分。

我以前做课设时也犯过只盯着界面功能、不管数据完整性的毛病,答辩被老师追问“合同删了缴费记录怎么办”一句话噎住。后来养成的习惯是:每个增删改操作都先问自己“这个操作会不会让其他表产生孤儿数据”,再决定是加事务、加校验还是改外键策略。这套思路放到真实项目里同样顶用。希望帮到你。

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

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

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

立即咨询