尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

C#房屋租赁系统:三层架构+事务+业务状态机实战

发布时间:2026/9/23 23:34:33

资讯中心
01
ARTICLE

C#房屋租赁系统:三层架构+事务+业务状态机实战

C#房屋租赁系统:三层架构+事务+业务状态机实战
简介本资源是一套完整的C#房屋租赁管理系统课程设计实践包面向高校数据库与C#编程初学者解决课程设计选题难、系统开发流程不清晰、报告撰写无参考等实际问题。压缩包共71个文件含25个核心C#源码文件.cs、11个资源文件.resx/.resources支撑多语言与界面资源管理、4个缓存与配置文件保障运行稳定性以及SQL建库脚本、Visual Studio解决方案.sln、可执行程序.exe和两份Word格式课程设计报告含封面与正文整体大小为12.86MB。目前已有146人学习下载体现了其在教学实践中的实用认可度。读者可直接导入VS工程编译运行结合ER图、数据流图、README说明及完整SQL脚本快速掌握数据库设计、三层架构实现、WinForm界面交互与课程报告撰写全流程是入门级项目实战的优质范例。1. C#房屋租赁管理系统不是写个增删改查就叫课程设计它得能跑通真实业务流你交的“数据库课程设计”是不是刚建好三张表用户、房源、合同点开窗体就弹出 DataGridView双击一行就跳转编辑——然后答辩时老师一问“租客退租怎么处理押金多个中介同时修改同一套房源怎么办合同到期前七天怎么自动提醒”当场卡壳这不是代码没写完是业务闭环没建起来。这套 C# 房屋租赁管理系统核心不在“用没用 MySQL”而在用 C# 把租赁场景里的状态流转、数据一致性、边界校验全兜住比如房东下架房源时系统必须拦住已有未结束合同的房源租客续租时新合同起始日不能早于旧合同结束日管理员导出报表得按“本月签约数/退租数/空置率”分维度聚合不是简单 SELECT * FROM contract。它适合两类人一是大三刚学完 ADO.NET 和 WinForms想拿个能讲清业务逻辑、答辩不被追问倒地的课程设计二是自学 C# 的转行者需要一个含完整三层架构、事务控制、日期计算、权限隔离的中小型项目练手。别被标题里“小白必看”骗了——它真上手你会在 DateTime.Now.AddDays(-30) 和 SqlTransaction.Rollback() 之间反复横跳但跳完你就懂什么叫“数据库不是存数据的地方是保业务规则的地方”。2. 用 C# MySQL 搭建三层架构为什么不用 Entity Framework 而选原生 ADO.NET2.1 课程设计选型真相EF 太“聪明”反而掩盖业务细节很多同学一上来就搜“C# 房屋租赁 EF Core”结果生成的 DbContext 里全是 auto-generated 的 DbSet 、DbSet 连外键约束都靠 Fluent API 配置。答辩时老师问“如果租客违约提前退租押金怎么扣扣多少这个逻辑写在 Model 还是 Service”——你翻源码发现所有业务判断都在 Controller 里硬编码Model 就是个属性集合。课程设计要暴露思考过程不是隐藏它。我们选原生 ADO.NET因为每个 SQL 命令都得手写INSERT INTO rent_contract (house_id, tenant_id, start_date, end_date, deposit) VALUES (houseId, tenantId, startDate, endDate, deposit)你自然会意识到start_date和end_date必须校验大小关系手动开SqlTransaction时你被迫思考“创建合同”和“更新房源状态为‘已出租’”这两步必须原子性执行否则出现“合同建了但房源还显示可租”的脏数据SqlParameter的显式传参逼你处理DBNull.Value场景比如租客身份证号可为空但数据库字段设为 NULL。2.2 数据库表结构三张核心表 两个关键约束MySQL 8.0 环境下建表语句必须带ENGINEInnoDB支持事务和CHARSETutf8mb4防 emoji 存储乱码。重点不是字段多而是约束定义是否覆盖业务规则-- 房源表status 字段用 TINYINT(1) 代替 VARCHAR值域明确0待租,1已租,2下架 CREATE TABLE house ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, address VARCHAR(200) NOT NULL, rent_price DECIMAL(10,2) NOT NULL CHECK (rent_price 0), status TINYINT(1) NOT NULL DEFAULT 0 CHECK (status IN (0,1,2)), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB CHARSETutf8mb4; -- 租赁合同表联合唯一索引防止同一租客对同一房源重复签约 CREATE TABLE rent_contract ( id INT PRIMARY KEY AUTO_INCREMENT, 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, status ENUM(valid,expired,terminated) NOT NULL DEFAULT valid, FOREIGN KEY (house_id) REFERENCES house(id) ON DELETE CASCADE, FOREIGN KEY (tenant_id) REFERENCES tenant(id) ON DELETE RESTRICT, UNIQUE KEY uk_house_tenant (house_id, tenant_id) ) ENGINEInnoDB CHARSETutf8mb4; -- 租客表手机号加唯一索引避免同一人用不同姓名注册 CREATE TABLE tenant ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(11) NOT NULL, id_card VARCHAR(18) NULL, UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB CHARSETutf8mb4;提示ON DELETE RESTRICT是关键——租客删了已有合同不能自动消失必须人工处理而房源删了关联合同必须级联删除ON DELETE CASCADE否则出现“合同指向不存在的房源”的脏引用。2.3 C# 三层分层DAL 层的 ConnectionString 怎么管才不翻车不要把连接字符串硬编码在 DAL 的每个方法里建一个ConfigHelper.cs统一管理// ConfigHelper.cs public static class ConfigHelper { private static readonly string _connectionString Serverlocalhost;Port3306;Databaserent_system;Uidroot;Pwd123456;Charsetutf8mb4;; public static string GetConnectionString() _connectionString; }DAL 层方法示例带事务的合同创建// HouseDAL.cs public bool CreateContract(RentContract contract) { string sql INSERT INTO rent_contract (house_id, tenant_id, start_date, end_date, deposit, status) VALUES (houseId, tenantId, startDate, endDate, deposit, status);; using (var conn new MySqlConnection(ConfigHelper.GetConnectionString())) { conn.Open(); using (var trans conn.BeginTransaction()) { try { using (var cmd new MySqlCommand(sql, conn, trans)) { cmd.Parameters.AddWithValue(houseId, contract.HouseId); cmd.Parameters.AddWithValue(tenantId, contract.TenantId); cmd.Parameters.AddWithValue(startDate, contract.StartDate); cmd.Parameters.AddWithValue(endDate, contract.EndDate); cmd.Parameters.AddWithValue(deposit, contract.Deposit); cmd.Parameters.AddWithValue(status, contract.Status); cmd.ExecuteNonQuery(); } // 同事务内更新房源状态 UpdateHouseStatus(contract.HouseId, 1, conn, trans); // 1已租 trans.Commit(); return true; } catch (Exception ex) { trans.Rollback(); throw new Exception($创建合同失败{ex.Message}); } } } }参数说明startDate和endDate传入DateTime类型MySQL 自动转为 DATEcmd.Parameters.AddWithValue在课程设计阶段够用但生产环境必须用Add指定类型如Add(startDate, MySqlDbType.Date).Value contract.StartDate避免隐式转换错误UpdateHouseStatus方法需在同一conn和trans下调用否则事务失效。3. 关键业务逻辑落地合同状态机与日期校验不是“if else”堆出来的3.1 合同状态流转用枚举 状态检查表守住业务底线别用string status valid这种自由文本定义强类型枚举并在 BLL 层做状态迁移校验// ContractStatus.cs public enum ContractStatus { Valid 0, // 正常履约中 Expired 1, // 自然到期 Terminated 2 // 违约终止 } // ContractBLL.cs public bool TerminateContract(int contractId, decimal refundAmount) { var contract _dal.GetContractById(contractId); if (contract.Status ! ContractStatus.Valid) throw new InvalidOperationException(仅允许终止状态为Valid的合同); // 校验退款金额不能超过押金 if (refundAmount contract.Deposit) throw new ArgumentException(退款金额不能超过押金总额); // 执行终止操作更新合同状态 记录退款 return _dal.TerminateContract(contractId, refundAmount); }为什么必须这样写答辩时老师问“合同到期后还能续签吗”——你答“到期状态是ExpiredBLL 层RenewContract()方法会检查contract.Status ContractStatus.Expired才允许续签否则抛异常”如果用字符串expired代码里可能写成if (status Expired)或if (status expired)大小写不一致导致逻辑漏洞。3.2 日期校验三个必须拦截的坑租赁业务里日期不是单纯存个DateTime.Now而是业务规则的载体// ContractValidator.cs public static class ContractValidator { /// summary /// 检查合同起止日期是否合法起始日 ≤ 结束日且起始日不早于今天不允许签过去日期的合同 /// /summary public static bool IsValidDateRange(DateTime startDate, DateTime endDate) { if (startDate endDate) return false; // 关键起始日不能早于系统当前日期防止补签历史合同 if (startDate.Date DateTime.Today) return false; // 额外校验合同期限不能超过 5 年业务规则 if (endDate.AddYears(-5) startDate) return false; return true; } /// summary /// 检查续租合同新起始日必须等于旧结束日无缝续租或晚于旧结束日空档期 /// /summary public static bool CanRenewTo(DateTime newStartDate, DateTime oldEndDate) { return newStartDate oldEndDate; } }血泪经验第一次部署测试时发现DateTime.Now在服务器和本地时区不一致导致“今天”判断出错——所有日期校验必须用DateTime.Today只取日期部分且数据库字段用DATE类型不是DATETIMEoldEndDate是DateTime类型newStartDate oldEndDate会比较到秒级但业务上只关心日期。所以实际代码中应写newStartDate.Date oldEndDate.Date。3.3 押金计算不是简单减法要区分“正常退租”和“违约退租”押金逻辑藏在 BLL 层不是 DAL// ContractBLL.cs public decimal CalculateRefundAmount(int contractId, bool isBreach) { var contract _dal.GetContractById(contractId); if (contract.Status ! ContractStatus.Terminated) throw new InvalidOperationException(仅终止合同可计算退款); if (isBreach) { // 违约扣除一个月租金 滞纳金按日千分之五 var monthlyRent _dal.GetHouseRentPrice(contract.HouseId); var daysOverdue (DateTime.Today - contract.EndDate).Days; var penalty Math.Round(monthlyRent / 30 * daysOverdue * 0.005m, 2); return Math.Max(0, contract.Deposit - monthlyRent - penalty); } else { // 正常退租全额退还但需扣除水电费从另一张 fee_record 表查 var totalFee _dal.GetUnpaidFees(contractId); return Math.Max(0, contract.Deposit - totalFee); } }注意Math.Max(0, ...)防止算出负数——这是业务兜底不是数学严谨性问题。4. 避坑指南课程设计里最常让小白当场懵掉的 4 个问题4.1 现象DataGridView 双击编辑后点保存按钮报错 “Object reference not set to an instance of an object”原因WinForms 中 DataGridView 默认绑定ListT但ListT不支持INotifyPropertyChanged当用户在单元格里直接修改值时底层对象属性没同步更新SaveButton_Click里取dataGridView.CurrentRow.DataBoundItem得到的是 null。解决不要用ListT直接绑定改用BindingListT支持添加/删除通知更稳妥做法双击时弹出独立编辑窗体用TextBox.Text显式读取用户输入再构造新对象传给 BLL 层。4.2 现象MySQL 插入中文显示为 “???”但 Navicat 里看是好的原因C# 连接字符串没指定Charsetutf8mb4或 MySQL 服务端my.cnf里collation-server未设为utf8mb4_unicode_ci。解决连接字符串必须包含Charsetutf8mb4;检查 MySQL 全局变量SHOW VARIABLES LIKE character_set%;确保character_set_server和collation_server都是utf8mb4表和字段创建时显式声明CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci。4.3 现象管理员删除租客系统报错 “Cannot delete or update a parent row: a foreign key constraint fails”原因tenant表被rent_contract.tenant_id外键引用但FOREIGN KEY定义用了ON DELETE RESTRICT默认行为而代码里没先删合同。解决在 BLL 层DeleteTenant()方法中先调用_contractDal.GetContractsByTenantId(tenantId)获取所有关联合同若存在Valid或Expired状态的合同抛出InvalidOperationException(该租客存在未处理合同请先终止合同)仅当合同全部为Terminated状态时才执行删除。4.4 现象导出 Excel 报表时日期列显示为数字如 44562原因使用Microsoft.Office.Interop.Excel时直接range.Value DateTime.TodayExcel 将其识别为序列号1900-01-01 起的天数。解决设置单元格格式range.NumberFormat yyyy-mm-dd;更推荐用ClosedXML库NuGet 安装ClosedXML代码简洁且无 Office 依赖using (var wb new XLWorkbook()) { var ws wb.Worksheets.Add(合同统计); ws.Cell(1, 1).Value 合同编号; ws.Cell(1, 2).Value 开始日期; ws.Cell(2, 2).Value contract.StartDate; // ClosedXML 自动识别 DateTime 类型 ws.Columns().AdjustToContents(); wb.SaveAs(report.xlsx); }5. 报告撰写与答辩话术让老师觉得你“真干过”而不是“抄过”5.1 报告里必须出现的 3 张图不是截图是设计证据别交一份全是窗体截图的报告老师要看你的设计决策痕迹ER 图用 draw.io 画重点标出rent_contract.house_id → house.id的1:N关系以及ON DELETE CASCADE箭头旁注明“级联删除”状态转换图画合同生命周期Valid → Expired / Terminated每条箭头标注触发条件如“到期日到达 → 自动转 Expired”三层调用时序图用 PlantUML 写展示Form.ButtonClick → BLL.CreateContract() → DAL.InsertContract() → MySQL并在BLL层标注“事务开启”、“日期校验”、“状态检查”三个关键动作。5.2 答辩高频问题应答模板背下来Q为什么不用 Web 做而用 WinFormsA“课程设计要求体现‘数据库应用系统开发全流程’WinForms 能更直接暴露 ADO.NET 连接管理、事务控制、UI 与数据层解耦等核心能力Web 方案如 ASP.NET Core会引入 HTTP 生命周期、Session 管理等无关概念冲淡数据库设计主线。”Q怎么保证多用户同时操作不冲突比如两个管理员同时给同一房源签合同A“在 DAL 层CreateContract()方法中我用SELECT ... FOR UPDATE加行锁先SELECT * FROM house WHERE idhouseId FOR UPDATE确认房源status0后再插入合同。这样第二个请求会阻塞等待直到第一个事务提交或回滚避免超租。”注MySQL InnoDB 支持需在事务内执行Q报告里写的‘系统可扩展’具体指什么A“三层架构下DAL 层所有数据库操作都通过接口IHouseDAL定义未来若换 SQL Server只需新增SqlServerHouseDAL : IHouseDAL实现类BLL 层代码完全不动另外合同提醒功能目前用 Windows 服务轮询但已预留IContractNotifier接口后续可替换为 RabbitMQ 消息队列触发。”5.3 源码组织技巧让老师一眼看出你懂架构别把所有.cs文件扔进一个文件夹按标准三层建文件夹RentSystem/ ├── DAL/ // 所有以 DAL 结尾的类如 HouseDAL.cs、ContractDAL.cs ├── BLL/ // 所有以 BLL 结尾的类如 HouseBLL.cs、ContractBLL.cs ├── Model/ // 实体类如 House.cs、Tenant.cs、RentContract.cs ├── View/ // WinForms 窗体如 MainForm.cs、ContractEditForm.cs └── Utils/ // 工具类如 ConfigHelper.cs、ContractValidator.cs关键细节每个 DAL 类的构造函数接受MySqlConnection参数方便单元测试 Mock而不是在方法里new MySqlConnection()——这叫“依赖注入雏形”老师看到会点头。6. 进阶验证用 3 个真实场景测试证明你不是“假跑通”6.1 场景一模拟房东紧急下架房源检验事务回滚是否生效步骤手动在 MySQL 中插入一条house记录status0在系统里创建一份该房源的合同statusValid修改 DAL 层CreateContract()方法在trans.Commit()前故意抛异常如throw new Exception(模拟网络中断);点击保存观察合同表无新增记录房源表status仍为0未被错误更新为1弹出异常提示框内容含“创建合同失败模拟网络中断”。价值证明你理解事务的 ACID不是把try-catch包一层就叫事务。6.2 场景二用 Fiddler 拦截并篡改请求验证 BLL 层校验不可绕过步骤启动系统打开 Fiddler在合同编辑窗体填入start_date2020-01-01过去日期点击保存Fiddler 拦截请求将startDate参数改为2020-01-01故意绕过前端 JS 校验放行请求观察系统弹出“合同起始日期不能早于今天”保存失败数据库无新增记录。价值证明你明白“前端校验只是体验优化后端校验才是安全底线”。6.3 场景三导出报表时故意断网验证 Excel 生成是否脱离数据库步骤在ExportReport()方法里先执行var data _bll.GetContractSummary();获取内存数据断开本机 WiFi点击导出按钮观察Excel 文件正常生成内容为断网前最后一次查询结果无数据库连接异常弹窗。价值证明你理解“导出是读操作应尽量减少数据库连接时间用内存数据生成文件”。我的习惯每次改完 DAL 层必做三件事——在 MySQL Workbench 手动执行一遍对应 SQL确认语法和逻辑用SELECT autocommit;确认当前会话是手动事务模式0在catch块里Console.WriteLine(ex.ToString())把异常栈复制到记事本逐行看哪一行抛的——别信“无法将数据写入传输连接”这种模糊提示它底下一定藏着MySqlException的真正原因。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。