☰
WinForm停车系统实战:ADO.NET+SQL Server桌面应用开发指南
2026/10/18 4:58:44 网站建设 项目流程

简介:这是一套面向C#初学者与WinForm桌面开发入门者的实践型源码项目,聚焦停车场收费管理这一典型业务场景,帮助学习者掌握数据库驱动的GUI应用开发全流程。资源包含95个文件,主体为37个C#业务逻辑与界面代码文件(.cs)、13个本地化资源文件(.resx)、13个编译资源(.resources)、1个SQL Server数据库脚本(.sql)及1个完整数据库文件(.mdf),辅以解决方案(.sln)、项目配置(.csproj)和运行截图等,整体压缩包仅1.32MB,轻量易部署。已有1323人下载学习,适合通过真实模块(如车辆入场登记、按停留时间自动计费、出库结算、收费记录查询及管理员权限配置)理解数据绑定、事件驱动、SQL Server增删改查及多窗体协作等核心技能。源码结构清晰,含完整可运行exe程序与附加数据库脚本,开箱即用,是巩固C#语法、WinForm控件使用与三层逻辑分离思想的优质练手项目。

1. 一个能当天跑通、第二天就能改出自己小区收费规则的 WinForm 停车系统

你有没有试过:下载了一个标着“C#停车场系统”的压缩包,解压后满屏.cs文件和.mdf,双击Parking.sln却弹出“无法加载项目”?或者好不容易跑起来了,界面是灰色的,点“车辆入场”没反应,查数据库发现Parking_log.ldf居然没自动附加?这不是你代码不行——是绝大多数 WinForm 入门项目缺了三样东西:可复现的数据库初始化路径、WinForm 控件事件与业务逻辑的明确绑定关系、以及收费计算模块的可插拔设计说明。这个Parking.zip正好补上了。它不是玩具 Demo,而是一个真实走完“车辆入场→计时→出场→生成账单→查询历史”全链路的桌面系统,所有 SQL 脚本(附加脚本.sql)已预置,Parking.mdf和Parking_log.ldf是完整数据文件,连QQ图片202105231852xx.png都是实测运行截图。适合两类人:刚学完 WinForm 控件拖拽、正卡在“怎么把 TextBox 里的车牌号存进数据库”的新手;以及需要快速交付一个轻量级停车管理模块、但不想从零写 EF Core 连接字符串的老手。它不炫技,不堆设计模式,但每一步操作都有对应的数据表字段、事件处理函数和 SQL 语句——这才是实践学习该有的样子。

2. 项目结构与技术栈:WinForm + SQL Server 的经典组合如何落地

这个系统没有用 Entity Framework,也没有上 WPF 或 MAUI,它用的是最朴素也最扎实的组合:C# 语言 + Windows Forms 窗体 + ADO.NET 直连 SQL Server。这种选择不是过时,而是精准匹配场景:部署在单台 Windows 电脑上,管理员本地操作,数据量在万级以内,对实时性要求不高但对稳定性要求极高。整个项目结构干净得像教科书:

Parking/ ├── Parking.sln # 解决方案文件,VS2019+ 可直接打开 ├── Parking/ # 主项目文件夹 │ ├── Properties/ │ ├── obj/ │ ├── bin/ │ ├── Form1.cs # 主窗体,含所有按钮、DataGridView、DateTimePicker │ ├── Form1.Designer.cs # 自动生成功能区,定义控件位置与初始属性 │ ├── Form1.resx # 本地化资源(本项目未启用,留空) │ ├── DBHelper.cs # 核心数据访问类,封装 SqlConnection、SqlCommand、SqlDataAdapter │ ├── CarIn.cs # 入场业务逻辑类,含车牌校验、时间戳生成、插入语句拼接 │ ├── CarOut.cs # 出场业务逻辑类,含停留时长计算、费用生成、状态更新 │ ├── FeeCalculator.cs # 收费算法类,支持按小时/天/次三种模式,参数可配置 │ └── Program.cs # 应用程序入口,仅含 Application.Run(new Form1()) ├── Parking.mdf # 主数据库文件(含 CarInfo、ParkingLog、FeeRule 三张表) ├── Parking_log.ldf # 日志文件,确保事务一致性 └── 附加脚本.sql # 用于手动附加数据库或重置环境的 T-SQL 脚本

提示:.mdf和.ldf是 SQL Server Express 的本地数据库文件,无需单独安装 SQL Server 服务——Visual Studio 安装时勾选“SQL Server Data Tools”即可支持。但注意:VS 默认使用 LocalDB 实例,而本项目连接字符串硬编码为(local)\SQLEXPRESS,这是第一个必须确认的点。

2.1 数据库设计:三张表撑起全部业务

系统只用三张表,却覆盖了从车辆登记到财务对账的全部需求,设计非常克制:

表名关键字段作用说明
CarInfoCarID(PK),PlateNumber,CarType,IsParked(bit)存储车辆基础信息,IsParked=1表示当前在场内,避免重复入场
ParkingLogLogID(PK),CarID,InTime,OutTime,FeeAmount,Status(tinyint)每次出入生成一条记录,Status=0为入场未出场,Status=1为已出场结算
FeeRuleRuleID(PK),RuleType(char(2)),UnitPrice,MaxFee,EffectiveDate收费规则表,RuleType='HR'(小时)、'DY'(日)、'EV'(次),支持多时段规则

附加脚本.sql中的建表语句清晰体现了这种设计思想。例如ParkingLog表的创建脚本:

CREATE TABLE [dbo].[ParkingLog]( [LogID] [int] IDENTITY(1,1) NOT NULL, [CarID] [int] NOT NULL, [InTime] [datetime] NOT NULL, [OutTime] [datetime] NULL, [FeeAmount] [decimal](10, 2) NULL, [Status] [tinyint] NOT NULL DEFAULT ((0)), CONSTRAINT [PK_ParkingLog] PRIMARY KEY CLUSTERED ([LogID] ASC) )

注意OutTime和FeeAmount允许为NULL,因为入场时只填InTime和Status=0;出场时才更新这两个字段。这种“分步写入”是桌面系统应对断电、误操作的常见做法,比一气呵成的事务更健壮。

2.2 WinForm 窗体布局:控件命名即逻辑,拒绝“button1_Click”

主窗体Form1.cs的控件命名严格遵循业务语义,这是阅读源码的第一道门槛,也是二次开发的起点:

控件名类型业务含义绑定事件
txtPlateNumberTextBox车牌输入框TextChanged(实时校验格式)
cmbCarTypeComboBox车型下拉框(小车/大车/货车)SelectedIndexChanged
btnCarInButton“车辆入场”按钮Click→ 调用CarIn.ProcessIn()
dgvParkingListDataGridView停车记录列表CellDoubleClick(双击某行触发出场)
dtPickerInTimeDateTimePicker入场时间选择器ValueChanged(可手动修改,默认取系统时间)

关键不在控件多,而在每个控件的Tag属性或注释里都标明了它关联的数据库字段。比如txtPlateNumber.Tag = "PlateNumber",cmbCarType.Tag = "CarType"。当你想新增一个“车主电话”字段时,只需三步:1)在CarInfo表加Phone字段;2)在窗体拖一个txtPhone并设Tag="Phone";3)在CarIn.cs的InsertCarInfo()方法里,把txtPhone.Text加入INSERT语句参数。这种命名即契约的设计,让新手也能安全地扩功能。

2.3 ADO.NET 数据访问层:DBHelper.cs 是整个系统的“血管”

DBHelper.cs不是简单的连接字符串封装,它解决了 WinForm 桌面应用最痛的三个问题:连接泄漏、SQL 注入、以及跨窗体数据共享。它的核心方法只有四个,但每行都有深意:

public static class DBHelper { // 1. 连接字符串从配置文件读取(本项目写死,但留了扩展口) private static readonly string connStr = @"Data Source=(local)\SQLEXPRESS;AttachDbFilename=|DataDirectory|\Parking.mdf;Integrated Security=True;Connect Timeout=30;User Instance=True"; // 2. ExecuteNonQuery:执行 INSERT/UPDATE/DELETE,返回影响行数 public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (var conn = new SqlConnection(connStr)) { using (var cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); // 注意:这里不 catch 异常,由上层处理 } } } // 3. ExecuteDataTable:执行 SELECT,返回 DataTable(供 DataGridView 绑定) public static DataTable ExecuteDataTable(string sql, params SqlParameter[] parameters) { var dt = new DataTable(); using (var conn = new SqlConnection(connStr)) { using (var da = new SqlDataAdapter(sql, conn)) { da.SelectCommand.Parameters.AddRange(parameters); da.Fill(dt); } } return dt; } // 4. GetScalar:执行返回单值的查询(如 COUNT(*) 或 MAX(FeeAmount)) public static object GetScalar(string sql, params SqlParameter[] parameters) { using (var conn = new SqlConnection(connStr)) { using (var cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteScalar(); } } } }

重点看ExecuteDataTable:它返回DataTable而非List<T>,是因为 WinForm 的DataGridView原生绑定DataTable最稳定,无需额外写BindingSource。而GetScalar方法被CarOut.cs用来查“当前车辆最近一次入场时间”,避免了SELECT TOP 1 InTime FROM ParkingLog WHERE CarID=@id ORDER BY InTime DESC这种易错 SQL 的硬编码。这种“用封装换安全”的思路,正是实践项目和课堂作业的本质区别。

3. 核心模块拆解:一个请求从进入到返回的完整链路

我们以最典型的“车辆出场并结算”为例,追踪从用户双击dgvParkingList到费用显示在界面上的完整链路。这不仅是功能验证,更是理解 WinForm 事件驱动模型的关键切口。

3.1 DataGridView 双击事件:如何精准定位待结算车辆

dgvParkingList的CellDoubleClick事件处理函数是整个出场流程的起点。它不做任何业务计算,只做两件事:提取当前选中行的CarID,并弹出确认对话框:

private void dgvParkingList_CellDoubleClick(object sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0) return; // 点击列头不处理 // 1. 从 DataGridView 当前行获取 CarID(注意:不是显示的车牌号,而是隐藏的主键) int carId = Convert.ToInt32(dgvParkingList.Rows[e.RowIndex].Cells["CarID"].Value); // 2. 查询该车辆是否处于“在场”状态(Status=0) string checkSql = "SELECT COUNT(*) FROM ParkingLog WHERE CarID=@carId AND Status=0"; int count = (int)DBHelper.GetScalar(checkSql, new SqlParameter("@carId", carId)); if (count == 0) { MessageBox.Show("该车辆当前未在场,无法出场!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 3. 弹出确认框,防止误操作 DialogResult result = MessageBox.Show($"确认结算车牌号为 {dgvParkingList.Rows[e.RowIndex].Cells["PlateNumber"].Value} 的车辆?", "确认出场", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { // 4. 调用出场业务类 CarOut.ProcessOut(carId); // 5. 刷新列表 LoadParkingList(); } }

这里有个血泪经验:永远不要用dgvParkingList.SelectedRows[0].Cells[0].Value来取值。因为SelectedRows在多选模式下可能为空,且索引 0 不一定对应CarID列。正确做法是用列名"CarID"显式获取,哪怕多写几个字符,也比后期调试半小时强。

3.2 CarOut.ProcessOut():出场逻辑的四步原子操作

CarOut.cs是收费计算的核心,它把“出场”拆解为四个不可分割的步骤,每一步失败都回滚,保证数据一致性:

public static void ProcessOut(int carId) { try { // Step 1: 查询该车辆最近一次入场记录(必须是 Status=0 的) string inSql = @" SELECT TOP 1 LogID, InTime FROM ParkingLog WHERE CarID=@carId AND Status=0 ORDER BY InTime DESC"; var inRow = DBHelper.ExecuteDataTable(inSql, new SqlParameter("@carId", carId)).Rows[0]; int logId = (int)inRow["LogID"]; DateTime inTime = (DateTime)inRow["InTime"]; // Step 2: 计算停留时长(分钟)和费用 TimeSpan duration = DateTime.Now.Subtract(inTime); int minutes = (int)duration.TotalMinutes; decimal fee = FeeCalculator.CalculateFee(minutes); // 调用独立算法类 // Step 3: 更新 ParkingLog 表,设置 OutTime、FeeAmount、Status=1 string updateSql = @" UPDATE ParkingLog SET OutTime=@outTime, FeeAmount=@fee, Status=1 WHERE LogID=@logId"; DBHelper.ExecuteNonQuery(updateSql, new SqlParameter("@outTime", DateTime.Now), new SqlParameter("@fee", fee), new SqlParameter("@logId", logId)); // Step 4: 更新 CarInfo 表,标记车辆已离场 string updateCarSql = "UPDATE CarInfo SET IsParked=0 WHERE CarID=@carId"; DBHelper.ExecuteNonQuery(updateCarSql, new SqlParameter("@carId", carId)); MessageBox.Show($"结算成功!费用:{fee:C},停留时长:{minutes} 分钟", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show($"结算失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); // 注意:这里没有 throw,因为 WinForm 界面层已捕获 } }

关键点在于Step 2 的FeeCalculator.CalculateFee()是完全解耦的。它不依赖数据库,只接收分钟数,返回费用。这意味着你可以轻松替换算法:比如把CalculateFee()改成按“前30分钟免费,之后每15分钟2元”来算,而不用动CarOut.cs里任何一行 SQL。

3.3 FeeCalculator.cs:收费算法的可配置化实现

FeeCalculator.cs是本项目最具二次开发价值的模块。它用一个静态字典缓存当前生效的收费规则,避免每次计算都查数据库:

public static class FeeCalculator { private static Dictionary<string, FeeRule> _currentRules = new Dictionary<string, FeeRule>(); // 初始化:应用启动时调用一次,从 FeeRule 表加载最新规则 public static void LoadRules() { _currentRules.Clear(); string sql = "SELECT RuleType, UnitPrice, MaxFee FROM FeeRule WHERE EffectiveDate <= @now ORDER BY EffectiveDate DESC"; var dt = DBHelper.ExecuteDataTable(sql, new SqlParameter("@now", DateTime.Now)); foreach (DataRow row in dt.Rows) { string ruleType = row["RuleType"].ToString(); _currentRules[ruleType] = new FeeRule { UnitPrice = (decimal)row["UnitPrice"], MaxFee = (decimal)row["MaxFee"] }; } } // 核心计算方法:输入分钟数,返回费用 public static decimal CalculateFee(int minutes) { // 默认按小时计费(RuleType='HR') if (!_currentRules.ContainsKey("HR")) return 0; var rule = _currentRules["HR"]; decimal hours = Math.Ceiling((double)minutes / 60); // 向上取整到小时 decimal fee = hours * rule.UnitPrice; // 不超过封顶价 return Math.Min(fee, rule.MaxFee); } } // 规则实体类,仅作数据载体 public class FeeRule { public decimal UnitPrice { get; set; } public decimal MaxFee { get; set; } }

注意:LoadRules()必须在Program.cs的Main方法中,在Application.Run(new Form1())之前调用,否则首次计算会因_currentRules为空而报错。这是新手最容易翻车的地方——界面能打开,但一点击“出场”就崩。

4. 运行与调试:从解压到看到“结算成功”的全流程

现在,我们把前面所有技术点串起来,走一遍真正落地的操作。这不是“复制粘贴命令”,而是告诉你每一步背后在解决什么问题。

4.1 环境准备:VS 版本、SQL Server 实例、连接字符串三者必须对齐

第一步永远不是写代码,而是确认你的开发环境是否匹配。本项目基于 .NET Framework 4.7.2 构建,因此:

  • Visual Studio 版本:必须是 VS2017 或更高版本(VS2019 推荐)。VS2022 默认创建 .NET 6+ 项目,需手动修改项目属性中的目标框架。
  • SQL Server 实例:项目连接字符串为(local)\SQLEXPRESS,意味着你电脑上必须有 SQL Server Express 实例。如果没有:
    • 下载 SQL Server Express 免费版(带 Management Studio);
    • 安装时勾选“SQL Server 数据库引擎”和“SQL Server Management Studio”;
    • 安装完成后,打开 SSMS,用 Windows 身份验证连接(local)\SQLEXPRESS,确认实例正常。
  • 连接字符串校验:打开Parking.csproj文件(用记事本),搜索<ConnectionStrings>或直接看DBHelper.cs。如果发现Data Source=.或Data Source=localhost,必须手动改为(local)\SQLEXPRESS。这是 80% 的“无法连接数据库”报错的根源。

4.2 数据库附加:不是双击 .mdf,而是用 SQL 脚本“唤醒”它

很多新手以为把Parking.mdf和Parking_log.ldf放进项目文件夹,VS 就会自动识别。错。.mdf是 SQL Server 的数据文件,必须由 SQL Server 实例“附加”(Attach)后才能被访问。正确做法是:

  1. 打开 SQL Server Management Studio (SSMS),连接(local)\SQLEXPRESS;
  2. 在“对象资源管理器”中,右键“数据库” → “附加”;
  3. 点击“添加”,浏览到你解压后的Parking.mdf文件(注意:选.mdf,不是.ldf);
  4. SSMS 会自动检测并填入对应的.ldf文件路径。确认无误后点“确定”。

如果提示“数据库已存在”,说明之前附加过。此时右键该数据库 → “删除”,勾选“删除数据库文件”,再重新附加。这是清理环境的后悔药。

附加成功后,在 SSMS 中展开“数据库”,你会看到名为Parking的数据库。此时再回到 VS,打开DBHelper.cs,确认连接字符串中的AttachDbFilename路径是否指向你当前解压的位置。如果是相对路径|DataDirectory|\Parking.mdf,则需在项目属性 → “发布” → “应用程序文件”中,确保Parking.mdf的“发布状态”为“包括”且“安装模式”为“始终包括”。

4.3 首次运行:绕过编译错误的三个关键修复

解压、附加、打开Parking.sln,按下 F5——大概率会报错。别慌,以下是三个最高频的编译/运行时错误及修复:

  1. 错误 CS0234:“类型或命名空间名称 'SqlClient' 不存在”
    原因:项目未引用System.Data.SqlClient。
    修复:右键项目 → “添加引用” → 勾选System.Data(.NET Framework 4.7.2 已内置,无需 NuGet)。

  2. 运行时报错:“无法打开登录所请求的数据库”
    原因:连接字符串中的|DataDirectory|未被正确解析,导致路径错误。
    修复:在Form1.cs的Form1_Load事件开头,强制设置数据目录:

    private void Form1_Load(object sender, EventArgs e) { // 强制将 DataDirectory 设为当前可执行文件所在目录 AppDomain.CurrentDomain.SetData("DataDirectory", Path.GetDirectoryName(Application.ExecutablePath)); // ... 其他初始化代码 }
  3. DataGridView 显示空白,或报“列名不存在”异常
    原因:dgvParkingList的AutoGenerateColumns属性为True,但数据源列名与数据库字段不一致。
    修复:在设计器中选中dgvParkingList→ 属性窗口 → 找到AutoGenerateColumns→ 设为False;然后手动添加列,DataPropertyName必须与DataTable的列名(即 SQL 查询的 AS 别名)完全一致。

完成这三项修复,F5 运行,主窗体应该能正常弹出,dgvParkingList里显示几条测试数据——恭喜,你已经跨过了最大的门槛。

5. 避坑指南:新手必踩的 4 个坑与血泪解决方案

这个项目看似简单,但每一处“理所当然”的设计背后,都藏着初学者要花半天才能绕出来的坑。我把最痛的四个整理出来,按“现象→原因→解决”给你写透。

5.1 现象:点击“车辆入场”按钮,提示“对象引用未设置到对象的实例”

原因:CarIn.cs中的InsertCarInfo()方法里,有一行string plate = txtPlateNumber.Text.Trim();,但txtPlateNumber是窗体Form1上的控件,而CarIn是一个静态类,它根本拿不到Form1的实例。源码里错误地假设CarIn能直接访问窗体控件,这是典型的 WinForm 作用域误解。

解决:把窗体控件的值作为参数传入业务类。修改CarIn.cs:

// 错误写法(删掉) // string plate = txtPlateNumber.Text.Trim(); // 正确写法:所有输入都由调用方传入 public static bool ProcessIn(string plateNumber, string carType, DateTime inTime) { if (string.IsNullOrWhiteSpace(plateNumber)) return false; // 后续逻辑不变,用传入的 plateNumber // ... }

然后在Form1.cs的btnCarIn_Click里调用:

private void btnCarIn_Click(object sender, EventArgs e) { bool success = CarIn.ProcessIn( txtPlateNumber.Text.Trim(), cmbCarType.SelectedItem?.ToString() ?? "小车", dtPickerInTime.Value); if (success) MessageBox.Show("入场成功!"); }

5.2 现象:数据库里ParkingLog表的OutTime字段全是1900-01-01,费用为 0

原因:CarOut.cs中DateTime.Now.Subtract(inTime)计算时,inTime是从数据库读出的datetime,但 SQL Server 的datetime类型精度为 3.33 毫秒,而 C# 的DateTime是 100 纳秒。当inTime值为2021-05-23 18:52:20.000时,DateTime.Now.Subtract()可能因时区或精度转换产生微小偏差,导致minutes计算为负数,Math.Ceiling()后变成 0。

解决:在计算前做防御性检查,并统一用TimeSpan的TotalMinutes属性:

TimeSpan duration = DateTime.Now.Subtract(inTime); int minutes = (int)Math.Max(0, duration.TotalMinutes); // 强制 >=0

同时,在ParkingLog表的InTime字段上,添加默认值约束GETDATE(),确保入库时间就是精确的当前时间。

5.3 现象:修改了FeeRule表里的UnitPrice,但结算费用没变

原因:FeeCalculator.LoadRules()只在程序启动时调用一次,后续规则变更不会自动刷新内存中的_currentRules字典。

解决:在FeeRule管理界面(如果有)或“系统设置”按钮里,添加一个刷新规则的方法:

public static void RefreshRules() { LoadRules(); // 重新加载 MessageBox.Show("收费规则已刷新"); }

并在CarOut.ProcessOut()开头加上FeeCalculator.RefreshRules();(仅用于演示,生产环境应加锁或定时刷新)。

5.4 现象:程序关闭后重启,dgvParkingList里数据消失

原因:Parking.mdf文件被设置为“复制到输出目录”,但每次编译,VS 都会把原始.mdf覆盖掉输出目录里的文件,导致所有新增数据丢失。

解决:在Parking.mdf文件的属性中,将“复制到输出目录”改为“如果较新则复制”。这样只有你手动修改了.mdf(比如用 SSMS 插入测试数据),才会覆盖。日常开发中,数据变更应通过 SQL 脚本或程序写入,而非直接编辑.mdf文件。

6. 二次开发实战:如何在 30 分钟内增加“月租车位”功能

现在,你已经跑通了系统,也避开了主要的坑。下一步,是把它变成你自己的工具。我以“增加月租车位管理”为例,展示一个真实、可落地的二次开发流程——不讲虚的,只说你要改哪几个文件、加哪几行代码、测试时看什么。

6.1 需求分析:月租车位要解决什么问题?

标准版只支持临时停车,按分钟计费。但小区物业需要:1)车主交一年租金,获得固定车位;2)月租车辆入场不计费,只记录进出;3)到期自动提醒。这需要新增一张表、两个窗体、和一处收费逻辑分支。

6.2 数据库改造:新增MonthlyRent表并建立关联

在 SSMS 中执行以下 SQL(或写入附加脚本.sql):

-- 新增月租表 CREATE TABLE [dbo].[MonthlyRent]( [RentID] [int] IDENTITY(1,1) NOT NULL, [CarID] [int] NOT NULL, [StartDate] [date] NOT NULL, [EndDate] [date] NOT NULL, [Status] [tinyint] NOT NULL DEFAULT ((1)), -- 1=有效, 0=过期 CONSTRAINT [PK_MonthlyRent] PRIMARY KEY CLUSTERED ([RentID] ASC) ) -- 添加外键约束 ALTER TABLE [dbo].[MonthlyRent] WITH CHECK ADD CONSTRAINT [FK_MonthlyRent_CarInfo] FOREIGN KEY([CarID]) REFERENCES [dbo].[CarInfo] ([CarID]) -- 为 CarInfo 表添加月租标识字段 ALTER TABLE [dbo].[CarInfo] ADD [IsMonthly] [bit] NOT NULL DEFAULT ((0))

提示:执行前先备份Parking.mdf。IsMonthly字段是关键,它让CarInfo表能区分普通车和月租车。

6.3 业务逻辑注入:修改CarOut.ProcessOut(),跳过月租车计费

这是最核心的改动。打开CarOut.cs,找到ProcessOut()方法,在计算费用前插入判断:

// 在 Step 2 计算费用前,插入以下代码 string isMonthlySql = "SELECT IsMonthly FROM CarInfo WHERE CarID=@carId"; bool isMonthly = (bool)DBHelper.GetScalar(isMonthlySql, new SqlParameter("@carId", carId)); if (isMonthly) { // 月租车:只更新 OutTime 和 Status,费用为 0 string updateSql = @" UPDATE ParkingLog SET OutTime=@outTime, FeeAmount=0, Status=1 WHERE LogID=@logId"; DBHelper.ExecuteNonQuery(updateSql, new SqlParameter("@outTime", DateTime.Now), new SqlParameter("@logId", logId)); MessageBox.Show("月租车位结算完成(费用:0元)", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information); return; // 提前退出,不执行后续计费逻辑 } // 否则走原有计费流程...

6.4 界面增强:在主窗体添加“月租管理”按钮和状态标识

  1. 在Form1.cs设计器中,拖一个Button,命名为btnMonthlyManage,Text 设为“月租管理”;
  2. 双击它,生成 Click 事件,跳转到新窗体(需新建FrmMonthly.cs);
  3. 在dgvParkingList中,为“车牌号”列添加一个CellFormatting事件,根据IsMonthly字段动态改变背景色:
private void dgvParkingList_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (e.ColumnIndex == dgvParkingList.Columns["PlateNumber"].Index && e.RowIndex >= 0) { // 获取当前行的 CarID int carId = (int)dgvParkingList.Rows[e.RowIndex].Cells["CarID"].Value; string isMonthlySql = "SELECT IsMonthly FROM CarInfo WHERE CarID=@carId"; bool isMonthly = (bool)DBHelper.GetScalar(isMonthlySql, new SqlParameter("@carId", carId)); if (isMonthly) { e.CellStyle.BackColor = Color.LightGreen; e.CellStyle.ForeColor = Color.DarkGreen; } } }

6.5 验证与收尾:三步确认功能可用

  1. 数据验证:用 SSMS 手动给CarInfo表中某辆车的IsMonthly设为1,再给MonthlyRent表插入一条记录;
  2. 行为验证:在程序中双击该车记录,确认弹出的费用为0.00,且ParkingLog表中FeeAmount=0;
  3. 边界验证:修改MonthlyRent.EndDate为昨天,再次出场,确认仍按月租处理(因为IsMonthly字段未变,业务逻辑未检查有效期——这是留给你的下一个优化点)。

从下载解压到完成月租功能,全程不需要重装 VS、不需要重启 SQL Server、不需要查 MSDN。你只是在已有骨架上,精准地缝合了三块新肉:一张表、一段判断、一个颜色。这就是 WinForm 实践项目的魅力——它不追求高大上,但每一步都踏在真实的地面。从那以后我每次接到类似需求,都强制走一遍“画ER图→写建表SQL→改业务类→加界面反馈”的闭环,哪怕只用十分钟。因为我知道,跳过任何一环,三天后就会回来找我修 bug。希望帮到你。

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

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

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

立即咨询