简介:这是一份面向计算机专业学生的C#房屋租赁管理系统数据库课程设计资料包,提供从需求设计、数据库实现到编码交付的完整参考,适合课程设计答辩、项目实训或毕业设计场景下的小白开发者借鉴。压缩包共71个文件,大小约12.86MB,既包含25个C#源码,也有resx/resources界面资源、SQL数据库脚本、项目配置文件,以及ER图、数据流图、doc/docx格式的课程设计报告和封皮,能够同时满足改代码和写文档两大需求。其中cs文件主要实现业务逻辑与窗体交互,resx/resources文件保存界面显示资源,配合ER图和数据流图可以快速理解数据库结构与系统流程。RoomRentSystem工程包含完整解决方案文件,SQL脚本可直接还原数据库,配合MySQL连接器与数据库组件,可在较短时间内把项目跑通并理解整体实现流程。目前已有146人学习/下载,对希望快速获得可靠课设模板的初学者而言,这份从源码、数据库脚本、设计图表到报告模板一应俱全的资料包具有较高参考价值。
1. 房屋租赁管理系统到底难在哪:一个课程设计里藏着完整的 C# 项目基本功
第一次看到“房屋租赁管理系统”这个课程设计题目时,多数人的反应是:不过又是一个增删改查。真动手才发现,它比图书馆管理系统、学生信息管理系统多了一道坎——租赁业务涉及两套核心数据(房屋和租客)的交叉状态流转。房子从“空置”到“已租”再到“退租”,合同到期提醒、押金扣缴、逾期费计算,这些逻辑要在 C# 界面和数据库之间来回倒腾。做得好了,这就是一份能进简历的完整项目;做得潦草,答辩时老师一句“你的房子退租后状态为什么没变回空置”就能让你当场卡壳。
这套系统适合两类人:一类是正在做数据库课程设计、需要源码和报告撑起学分的在校生;另一类是刚学完 C# 语法、想找个完整项目把 ADO.NET、三层架构、DataGridView 绑定串起来自学的初学者。你不需要懂高深的设计模式,但得能老老实实把一张表的关系理清楚、把一条 SQL 写对。这篇笔记就按我自己做这类管理系统时的顺序来讲——先选技术栈和项目骨架,再把数据库设计砸实,接着写 C# 访问层和界面逻辑,最后把最容易翻车的地方一个个排掉。
2. 先定技术栈和项目骨架:C# 版本、数据库选型和三层结构
2.1 为什么推荐 WinForms + SQL Server,而不是其他组合
做课程设计最常见的选择是 C# WinForms 配 SQL Server,其次是 C# WinForms 配 MySQL 或 SQLite。如果你用的是 Visual Studio 2022,WinForms 窗体设计器最顺手,拖控件绑定数据源都方便;SQL Server Express 直接随 Visual Studio 安装,本地跑起来零成本,老师验收时也不用额外装环境。
这里要泼一盆冷水:别为了显得“高级”去选 WPF + MVVM 或 ASP.NET Core。课程设计的时间本来就紧,WPF 的数据绑定概念够你折腾一周,ASP.NET Core 还得考虑部署问题。WinForms 虽然“老气”,但它的 DataGridView、ComboBox、DateTimePicker 等控件天生适合管理类系统,两三下就能把界面搭出来。技术选型的核心不是追新,而是保证你能在答辩前把功能做完整。
版本方面,.NET Framework 4.7.2 或 .NET 6/8 都可以。前者在 VS 里创建项目时直接可见,兼容性最好;后者跨平台且语法更现代。我建议用 .NET Framework 4.7.2,因为网上能找到的参考代码和控件兼容问题最少,对于新手,别人的经验就是你的后悔药。
2.2 项目文件夹怎么划分:三层架构其实可以很轻
很多小白把全部代码塞进 Form1.cs,最后代码文件三五千行,改一个功能要全局搜索。虽然也能跑,但报告不好写,答辩也容易被问住。常见的做法是做一个轻量三层:UI 层(窗体)、业务逻辑层(BLL)、数据访问层(DAL)。对课程设计来说,BLL 不一定要写很多,但至少要有这个分层意识。
我的习惯是先在解决方案里建三个类库项目,再加一个 WinForms 启动项目:
RentalSystem.sln ├── RentalSystem.UI (WinForms 启动项目,放窗体) ├── RentalSystem.BLL (业务逻辑:合同到期计算、状态变更规则) ├── RentalSystem.DAL (数据访问:SqlHelper + 各表的 CRUD) └── RentalSystem.Model (实体类:House, Tenant, Lease, Payment, User)提示:如果嫌类库项目多,把 Model 和 DAL 合并到一个类库也行。但 UI 和 DAL 一定要分开,这是报告里“三层架构”的体现。
稍微解释一下为什么这么拆:Model 里定义实体类,对应数据库表的每一行,比如 House 类里有 HouseId、Address、Rent、Status 等属性;DAL 里只写 SQL 语句和执行方法,比如AddHouse(House house)、GetAllHouses();BLL 里处理业务规则,比如退租时要判断是否有未缴费用;UI 只管调用 BLL 拿数据、绑定到 DataGridView。这样如果你后期想把 SQL Server 换成 MySQL,只需要改 DAL,界面和业务逻辑不用动。
2.3 连接字符串和配置文件:App.config 的正确用法
连接字符串不要硬编码在代码里。写死在代码里的连接串,一旦数据库密码改了就得重新编译,答辩换机器时也容易翻车。WinForms 项目里双击 App.config,在里面配置连接字符串:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" /> </startup> <connectionStrings> <add name="RentalDb" connectionString="Data Source=.;Initial Catalog=RentalDb;Integrated Security=True;TrustServerCertificate=True;" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration>然后写一个静态类取连接串:
using System.Configuration; public static class DbConfig { public static string GetConnectionString() { return ConfigurationManager.ConnectionStrings["RentalDb"].ConnectionString; } }这里几个参数要说明:Data Source=.表示本机 SQL Server,如果想连局域网内的数据库服务器,改成服务器 IP 或主机名;Initial Catalog是数据库名;Integrated Security=True表示用 Windows 身份认证登录,适合本地开发和答辩演示;TrustServerCertificate=True是针对新版 SQL Server 连接时证书校验问题的,不加可能报“证书链”错误。这套配置在你写好数据库脚本、建好库之后马上能用,我会在下一章把建库脚本完整给出来。
3. 数据库设计:表结构、关系和数据字典才是课程设计的灵魂
3.1 五张核心表:用户、房屋、租客、合同、缴费
课程设计评分的重头戏往往不在界面多华丽,而在数据库设计是否合理。房屋租赁系统最少要有五张表:用户表(登录用)、房屋信息表、租客信息表、租赁合同表、缴费记录表。房屋和租客之间不直接关联,而是通过合同表建立多对多关系——一个房屋不同时期可以租给不同租客,一个租客也可以先后租过不同房屋。
表结构按我实际做过的方案来定:
| 表名 | 主要字段 | 说明 |
|---|---|---|
| Users | UserId, UserName, Password, Role | Role 区分管理员/操作员 |
| House | HouseId, Address, Area, Rent, Status | Status: 空置/已租/维修中 |
| Tenant | TenantId, Name, Phone, IdCard | IdCard 可选,用于报告体现“完整性约束” |
| Lease | LeaseId, HouseId, TenantId, StartDate, EndDate, Deposit | 外键关联房屋和租客 |
| Payment | PaymentId, LeaseId, Amount, PayDate, PayType | PayType: 租金/押金/违约金 |
房屋表里建议加一个Remark字段,用来记录房屋设施状况,虽然不参与核心逻辑,但报告里写“该字段满足业务扩展需求”会显得你考虑周全。租客表里IdCard可以用身份证号模拟,建表时加唯一约束,表示一个身份证号只能登记一次。
3.2 建库脚本书写:外键、默认值和索引一个都不能少
很多小白建表不写外键,所有表都是孤立的。老师一眼就能看出来这是“表堆砌”而不是“数据库设计”。外键能保证数据完整性——比如你删掉一个正在合同期内的房屋,外键会阻止删除,避免产生孤儿数据。下面是核心建库脚本:
CREATE DATABASE RentalDb; GO USE RentalDb; GO CREATE TABLE Users ( UserId INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(50) NOT NULL UNIQUE, Password NVARCHAR(50) NOT NULL, Role NVARCHAR(20) NOT NULL DEFAULT 'Operator' ); CREATE TABLE House ( HouseId INT IDENTITY(1,1) PRIMARY KEY, Address NVARCHAR(100) NOT NULL, Area DECIMAL(8,2) NOT NULL, Rent DECIMAL(10,2) NOT NULL, Status NVARCHAR(20) NOT NULL DEFAULT '空置', Remark NVARCHAR(200) NULL ); CREATE TABLE Tenant ( TenantId INT IDENTITY(1,1) PRIMARY KEY, Name NVARCHAR(50) NOT NULL, Phone NVARCHAR(20) NOT NULL, IdCard NVARCHAR(18) NOT NULL UNIQUE ); CREATE TABLE Lease ( LeaseId INT IDENTITY(1,1) PRIMARY KEY, HouseId INT NOT NULL, TenantId INT NOT NULL, StartDate DATE NOT NULL, EndDate DATE NOT NULL, Deposit DECIMAL(10,2) NOT NULL DEFAULT 0, CONSTRAINT FK_Lease_House FOREIGN KEY (HouseId) REFERENCES House(HouseId), CONSTRAINT FK_Lease_Tenant FOREIGN KEY (TenantId) REFERENCES Tenant(TenantId), CONSTRAINT CK_Lease_Date CHECK (EndDate > StartDate) ); CREATE TABLE Payment ( PaymentId INT IDENTITY(1,1) PRIMARY KEY, LeaseId INT NOT NULL, Amount DECIMAL(10,2) NOT NULL, PayDate DATETIME NOT NULL DEFAULT GETDATE(), PayType NVARCHAR(20) NOT NULL, CONSTRAINT FK_Payment_Lease FOREIGN KEY (LeaseId) REFERENCES Lease(LeaseId) ); GO CREATE INDEX IX_Lease_HouseId ON Lease(HouseId); CREATE INDEX IX_Lease_TenantId ON Lease(TenantId); CREATE INDEX IX_Payment_LeaseId ON Payment(LeaseId);这个脚本里有几个细节值得在报告里写一笔:IDENTITY(1,1)是自增主键,保证并发插入时主键不冲突;NVARCHAR存中文必须用,VARCHAR在某些排序规则下会乱码;CHECK (EndDate > StartDate)是约束层的最后防线,防止界面层漏判导致结束日期早于开始日期。给外键列建索引也是好习惯,课程设计报告里写明“外键列建立索引可加速连接查询”,答辩时就是加分项。
3.3 插入测试数据:别用中文当主键,别把敏感信息写明文
测试数据不能只有三五条,否则查询效果演示不出来。我一般会往 House 表插入 15~20 条不同租金、不同状态的数据,Tenant 表插入 8~10 条,Lease 表覆盖“已到期”和“未到期”两种情况——这样到期提醒功能才有数据可查。注意插入顺序:先插 House 和 Tenant,再插 Lease,否则外键找不到父表记录会报错。
INSERT INTO House (Address, Area, Rent, Status) VALUES (N'阳光花园3栋502', 89.50, 2200.00, N'空置'), (N'阳光花园3栋602', 89.50, 2300.00, N'已租'), (N'滨江公馆2单元1101', 120.00, 3500.00, N'维修中'); GO INSERT INTO Tenant (Name, Phone, IdCard) VALUES (N'张三', N'13800001111', N'110101199001011234'), (N'李四', N'13900002222', N'110101199202022345'); GO INSERT INTO Lease (HouseId, TenantId, StartDate, EndDate, Deposit) VALUES (2, 1, '2024-01-01', '2025-01-01', 2000);插完数据后用几条联查验证外键是否生效,比如查“所有已租房屋及当前租客姓名”:
SELECT h.Address, h.Rent, t.Name AS TenantName, l.EndDate FROM Lease l JOIN House h ON l.HouseId = h.HouseId JOIN Tenant t ON l.TenantId = t.TenantId WHERE h.Status = N'已租';这里注意 SQL 中字符串跟 N 前缀的配合使用——以前见过不少人在代码里拼 SQL 时忘加N,中文条件死活查不到数据。数据库设计这块做完,C# 代码写起来就会顺很多,因为你心里清楚每张表能拿出什么字段。
4. 用 C# 把功能串起来:连接库、增删改查和租赁状态流转
4.1 SqlHelper:一个通用的数据库访问类,减少重复代码
在 DAL 层写一个通用的 SqlHelper,把SqlConnection、SqlCommand、SqlDataReader都封装好,后面所有数据访问代码都调用它。这是课程设计最值得写的一个类,因为它在报告里体现“代码复用思想”,在实际编码时也省去大量重复粘贴。
using System.Data; using System.Data.SqlClient; public class SqlHelper { private static readonly string connStr = DbConfig.GetConnectionString(); /// <summary> /// 执行 SELECT,返回 DataTable,适合绑定 DataGridView /// </summary> public static DataTable ExecuteDataTable(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (parameters != null) cmd.Parameters.AddRange(parameters); conn.Open(); SqlDataAdapter adapter = new SqlDataAdapter(cmd); DataTable dt = new DataTable(); adapter.Fill(dt); return dt; } } /// <summary> /// 执行 INSERT / UPDATE / DELETE,返回受影响行数 /// </summary> public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (parameters != null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } } /// <summary> /// 取单个值,比如 SELECT COUNT(*) 或 MAX(ID) /// </summary> public static object ExecuteScalar(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (parameters != null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteScalar(); } } }这段代码有四个要点。第一,using块确保连接和命令对象用完后自动释放,这比手动conn.Close()可靠得多,能避免连接池被占满的坑。第二,所有参数都走SqlParameter,不要用字符串拼接拼出 SQL,否则单引号输入直接报语法错误,这是用户输入导致 SQL 注入的老问题。第三,查询用SqlDataAdapter直接填DataTable,UI 层绑定数据源很简单;非查询操作返回受影响行数,可以用来判断插入是否成功。第四,params关键字允许调用时传任意多个参数,这样不同业务的 SQL 都能复用同一个方法。
4.2 房屋管理页面:DataGridView 绑定和状态过滤
房屋管理是系统最核心的页面,界面布局一般是顶部一排查询条件(地址关键字、状态下拉框),下面一个DataGridView显示结果,右侧或上方一排按钮(新增、修改、删除、租出、退租)。窗体加载时调用 DAL 层方法查出所有房屋:
private void FrmHouse_Load(object sender, EventArgs e) { LoadHouses(); } private void LoadHouses() { string sql = @"SELECT h.HouseId, h.Address, h.Area, h.Rent, h.Status, CASE WHEN l.LeaseId IS NULL THEN N'无合同' ELSE N'合同进行中' END AS HasContract FROM House h LEFT JOIN Lease l ON h.HouseId = l.HouseId AND l.EndDate >= GETDATE() ORDER BY h.HouseId DESC"; DataTable dt = SqlHelper.ExecuteDataTable(sql); dt.Columns["HouseId"].ColumnName = "房屋编号"; dt.Columns["Address"].ColumnName = "地址"; dt.Columns["Area"].ColumnName = "面积(m²)"; dt.Columns["Rent"].ColumnName = "月租(元)"; dt.Columns["Status"].ColumnName = "状态"; dt.Columns["HasContract"].ColumnName = "合同情况"; dataGridView1.DataSource = dt; }说明一下这段的逻辑:LEFT JOIN连到 Lease 表,条件是EndDate >= GETDATE(),意思是只看未到期的合同。CASE WHEN判断这个房子当前有没有在履行中的合同,避免你看到一个“已租”房子却没有对应合同的矛盾数据。列名改成中文后,DataGridView 显示更直观,答辩演示时老师不用对着英文表头猜。这里有个小陷阱:修改列名后,如果后续代码用dataGridView1.Rows[i].Cells["HouseId"].Value取值,会取不到,因为列名已经改了。所以我一般把数据源里原始的英文字段保留一份,或者手动设置表格列。
状态过滤的 SQL 写法要注意参数化,下拉框选择“已租”时:
private void btnFilter_Click(object sender, EventArgs e) { string sql = @"SELECT * FROM House WHERE 1=1"; List<SqlParameter> paramList = new List<SqlParameter>(); if (!string.IsNullOrWhiteSpace(txtKeyword.Text)) { sql += " AND Address LIKE @kw"; paramList.Add(new SqlParameter("@kw", "%" + txtKeyword.Text + "%")); } if (comboStatus.SelectedIndex > 0) { sql += " AND Status = @status"; paramList.Add(new SqlParameter("@status", comboStatus.Text)); } DataTable dt = SqlHelper.ExecuteDataTable(sql, paramList.ToArray()); dataGridView1.DataSource = dt; }1=1是拼接条件时的常用写法,它的作用是让后面的每个条件都自然以AND开头,不用单独判断是不是第一个条件。LIKE @kw的值在参数里手动拼上%,这样用户在文本框输入半角或全角符号,都不会破坏 SQL 结构。这套写法覆盖了“多条件自由组合查询”的通用场景,课程设计报告里写“实现了灵活的模糊查询”,就是靠它撑腰。
4.3 租赁登记:一次操作改动两张表的状态
租出房屋时,要同时做三件事:在 Lease 表插入一条合同,把 House 的 Status 更新为“已租”,在 Payment 表插入押金记录。这三步必须放在同一个数据库事务里,否则中间任何一步失败都会造成数据不一致。C# 代码里用SqlTransaction控制:
public bool RentHouse(int houseId, int tenantId, DateTime start, DateTime end, decimal rent, decimal deposit) { using (SqlConnection conn = new SqlConnection(DbConfig.GetConnectionString())) { conn.Open(); SqlTransaction tran = conn.BeginTransaction(); try { // 1. 插入合同 string sqlLease = @"INSERT INTO Lease (HouseId, TenantId, StartDate, EndDate, Deposit) VALUES (@houseId, @tenantId, @start, @end, @deposit); SELECT SCOPE_IDENTITY();"; SqlCommand cmdLease = new SqlCommand(sqlLease, conn, tran); cmdLease.Parameters.AddWithValue("@houseId", houseId); cmdLease.Parameters.AddWithValue("@tenantId", tenantId); cmdLease.Parameters.AddWithValue("@start", start); cmdLease.Parameters.AddWithValue("@end", end); cmdLease.Parameters.AddWithValue("@deposit", deposit); int leaseId = Convert.ToInt32(cmdLease.ExecuteScalar()); // 2. 更新房屋状态 string sqlHouse = @"UPDATE House SET Status = N'已租' WHERE HouseId = @houseId;"; SqlCommand cmdHouse = new SqlCommand(sqlHouse, conn, tran); cmdHouse.Parameters.AddWithValue("@houseId", houseId); cmdHouse.ExecuteNonQuery(); // 3. 插入收费记录 string sqlPay = @"INSERT INTO Payment (LeaseId, Amount, PayType) VALUES (@leaseId, @deposit, N'押金');"; SqlCommand cmdPay = new SqlCommand(sqlPay, conn, tran); cmdPay.Parameters.AddWithValue("@leaseId", leaseId); cmdPay.ExecuteNonQuery(); tran.Commit(); return true; } catch { tran.Rollback(); throw; } } }三个动作的先后顺序不能乱:先插合同并拿到自增的LeaseId,再用这个 ID 插缴费记录。SCOPE_IDENTITY()返回当前会话最近一条 INSERT 的自增值,用它在事务里取回生成的合同号。事务提交前任何一行代码抛异常,都会回滚,数据库回到初始状态。答辩时如果老师问“为什么不用三个独立方法”,你可以直接甩出这个事务版本,再解释“事务保证原子性,要么全部成功,要么全部失败”。
4.4 退租和到期提醒:把状态变干净才算完成
退租时要做的事正好相反:先检查这个房子名下是否有未缴费记录,有的话弹窗提醒;然后更新房屋状态为“空置”;如果退租时有押金需要退还,在 Payment 表里插入一条负金额的记录(或单独标记退款类型)。这样缴费统计报表才会有完整数据。
private void btnCheckOut_Click(object sender, EventArgs e) { DataGridViewRow row = dataGridView1.CurrentRow; if (row == null) return; int houseId = Convert.ToInt32(row.Cells["房屋编号"].Value); // 检查是否存在未结清费用 string sqlCheck = @"SELECT COUNT(*) FROM Payment p JOIN Lease l ON p.LeaseId = l.LeaseId WHERE l.HouseId = @houseId AND p.Amount < 0"; // 示例:金额为负的是欠费/违约金 int unpaidCount = Convert.ToInt32(SqlHelper.ExecuteScalar(sqlCheck, new SqlParameter("@houseId", houseId))); if (unpaidCount > 0) { MessageBox.Show("该房屋存在未结清费用,请先处理后再退租!"); return; } // 更新房屋状态为空置 string sqlUpdate = @"UPDATE House SET Status = N'空置' WHERE HouseId = @houseId"; SqlHelper.ExecuteNonQuery(sqlUpdate, new SqlParameter("@houseId", houseId)); MessageBox.Show("退租成功,房屋已恢复为空置状态!"); }到期提醒用DateTimePicker或直接查“合同结束日期在 7 天内”的租约。把 SQL 里的日期偏移写在参数里,便于测试时随意改阈值:
string sql = @"SELECT h.Address, t.Name, t.Phone, l.EndDate FROM Lease l JOIN House h ON l.HouseId = h.HouseId JOIN Tenant t ON l.TenantId = t.TenantId WHERE l.EndDate BETWEEN @today AND DATEADD(DAY, 7, @today) AND h.Status = N'已租' ORDER BY l.EndDate"; DataTable dt = SqlHelper.ExecuteDataTable(sql, new SqlParameter("@today", DateTime.Today));DATEADD(DAY, 7, @today)是 SQL Server 的日期计算函数,表示从今天往后再推 7 天。这个查询跑出来的结果就是“未来一周内合同要到期、还没退租”的清单,界面用一个单独的 TabPage 展示,老师看完会留下“系统有业务意识”的印象。
5. 避坑与排查:课程设计里最常见的 8 个翻车现场
5.1 中文乱码:插入正常,查询条件死活查不到
现象:往数据库插入的中文显示正常,但用中文做 WHERE 条件时查不到数据;或者从数据库读出来显示为“?”。
原因:两种常见情况。一是建表时用了VARCHAR而不是NVARCHAR,插入的中文经过代码页转换后丢失;二是 SQL 字符串里的中文前面没加N前缀,导致中文被当作 ANSI 字符处理。
解决:建表统一用NVARCHAR,SQL 里所有中文常量写成N'空置'这种形式,C# 端拼接 SQL 时中文条件同样通过SqlParameter传值,并且参数类型显式声明为NVarChar。已经存在的表,用ALTER TABLE House ALTER COLUMN Status NVARCHAR(20)改列类型,数据不会丢。
5.2 登录界面连不上数据库:神秘错误“证书链”
现象:本机能跑,换一台电脑打开登录窗体,点登录直接抛SqlException,提示证书链或加密相关错误。
原因:新版 SqlClient 在连接 SQL Server 时默认加密连接,而未配置证书的 SQL Server Express 会拒绝这种连接,或需要额外校验服务器证书。
解决:连接字符串加TrustServerCertificate=True;。如果还报错,检查Data Source是否写成了对方机器的错误主机名,或者 SQL Server 是否开启了 TCP/IP 协议——默认安装下 TCP/IP 可能处于禁用状态,需要在“SQL Server 配置管理器”里启用。
5.3 DataGridView 列名对不上:改了列名后代码取值抛异常
现象:绑定数据源后手动改了列名显示为中文,但在按钮事件里写Cells["HouseId"].Value,运行时报ArgumentException,提示找不到该列。
原因:修改DataTable的列名会直接覆盖原始列名,DataGridView 绑定后也是用新列名访问。
解决:要么在绑定前把原始字段复制一份(比如dt.Columns.Add("_HouseId", typeof(int), "HouseId")),要么取值时用Cells[0].Value按下标取,但这种方式对列顺序敏感,增删列后容易越界。最省心的方法是:代码里取值统一取原始字段名,只把显示列名留在 SQL 的AS别名层做。
5.4 外键阻止删除:明明是空数据也删不掉
现象:想删除一个没有合同关联的房屋,数据库报外键冲突。
原因:房屋表被 Lease 表引用后,即使 Lease 里没有记录,SQL Server 也会检查外键约束。但如果屋下有历史合同记录(即使是结束的合同),删除就会被禁止,因为外键没有可级联删除的规则。
解决:最稳妥的做法不是删,而是给房屋表加一个IsDelete BIT DEFAULT 0字段,删除操作变成UPDATE House SET IsDelete = 1。查询时默认加WHERE IsDelete = 0。这样既保留业务数据,又避免外键冲突。如果一定要物理删除,需要先删关联的 Payment 和 Lease 记录,再用事务包住三步操作。
5.5 自增 ID 断档:为什么删除几条后 ID 不从 1 开始
现象:删掉几条测试数据后,新插入的记录 ID 从 5 开始,而不是 1。有同学觉得这是个 bug 想手动重置。
原因:IDENTITY自增列不会因为删除而回退,它保证的是唯一性而不是连续性。课程设计里拿它当主键完全正常,不需要也没必要手动重置。
解决:写报告时明确说明“自增主键不保证连续,其作用是代理主键,避免业务字段做联合主键的复杂性”。如果答辩老师现场要求重置 ID,用DBCC CHECKIDENT('House', RESEED, 0)可以重置,但你得先想好这样做的副作用——ID 可能被其他表引用,重置后会造成关联错乱,不建议。
5.6 事务没提交,数据看着“丢了”
现象:点了“保存合同”,界面提示成功,但重新打开房屋列表,房屋状态还是“空置”,合同表也没有记录。
原因:代码里tran.Commit()没执行到,或者异常处理中只写了throw没有Rollback。更常见的错误是连接对象在using块外被垃圾回收,事务隐式回滚。
解决:严格采用“try-catch-commit-rollback”的写法,事务、连接都在同一个using或 try 块内。写完后用 SQL Server Management Studio 查询验证,不要只靠界面判断。
5.7 界面卡顿:加载大数据量时窗体假死
现象:房屋数据只有几千条,但每次打开窗体都要等好几秒,期间窗口无法拖动。
原因:查询是同步执行的,UI 线程被数据库操作阻塞。另一个原因是没有分页,一次性把所有数据塞给 DataGridView。
解决:课程设计量级的数据用同步查询没问题,但至少要加一个分页查询:SELECT * FROM House ORDER BY HouseId OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY。如果想追求更顺滑,用async/await配合ExecuteDataTableAsync也不是不行,但异步会给课程设计增加调试复杂度,我一般建议先分页。
5.8 密码明文存储:虽然说不上错,但报告会掉档次
现象:Users 表的 Password 字段直接存了明文,比如“abc123”。
原因:图省事。
解决:哪怕课程设计也应该做最基本的光处理——存哈希值。做法是用SHA256或MD5哈希后入库,登录时把输入的密码哈希后再比对。代码量不大,但报告里写“密码经过哈希存储,保障数据安全”能撑住一段内容。我一般做系统时,对密码的处理至少到加盐哈希,课程设计做到 SHA256 就够讲了。
6. 验收级打磨:三个让系统从“能跑”到“好答辩”的细节
6.1 增加一个简单的数据看板窗体
不用做报表控件,就用 Label 加 Panel,加载时执行几条SELECT COUNT或SUM汇总。比如“空置房屋数量”“在租合同数量”“本月应收租金合计”。这三条数据在首页一摆,老师打开系统第一眼的感受就不一样了。统计语句里本月应收用MONTH(PayDate) = MONTH(GETDATE())做过滤,记得同时考虑跨年问题,稳妥写法是PayDate >= DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1)。
6.2 把操作日志留下痕迹
新建一张Logs表,字段:LogId, UserId, Action, Detail, CreateTime。在新增房屋、租赁登记、退租这些方法里插入一行日志记录。这个设计直接命中课程设计评分点里的“系统完整性与可追溯性”。实现上不需要引入 AOP,就在 BLL 层每个核心方法末尾调一个LogHelper.WriteLog静态方法。这一层的意义不只是展示,等你做真实项目时也会发现操作日志对排查业务纠纷有多重要。
6.3 给数据库脚本和报告做“版本说明”
很多同学交上去的数据库脚本是从 SSMS 里“生成脚本”直接导出的,里面带着IF NOT EXISTS和一大堆系统注释,答辩时自己都讲不清哪段脚本是干嘛的。我建议手写一份简洁的create.sql,带编号注释:
-- 1. 创建数据库 -- 2. 创建用户表 -- 3. 创建房屋表 -- 4. 创建租客表 -- 5. 创建租赁合同表 -- 6. 创建缴费记录表 -- 7. 插入基础数据每段注释对应报告里的一个章节。这样老师追问“你这数据库怎么设计的”,你可以按编号顺序讲下来,思路清晰比什么都重要。我自己做项目到后期,文件夹里一定有一个docs目录,放数据库脚本、部署说明和已知问题清单,这个习惯是从做课程设计时被问住之后养成的——把文档当代码一样维护,后面交接能省掉很多口头沟通成本。希望这些经验和踩坑记录能帮你把这一份课程设计做得扎实,少走几步弯路。
本文还有配套的精品资源,点击获取