简介C#停车场收费系统是一个基于Windows Forms框架的桌面应用程序主要面向C#初中级开发者和需要完成课程设计、动手实践的读者解决停车场景中车辆登记、计时计费、数据查询等一体化管理问题。系统后端使用SQL Server存储车辆信息、停车记录和收费记录界面层包含添加停车、车辆出库、停车收费、收费查询以及管理模块源码中的事件驱动交互方式、DataGridView数据展示、数据库连接与参数化查询写法都适合初学者逐段研读。压缩包共95个文件其中37个cs源码文件、13个resx界面资源文件和13个resources编译资源文件构成主体另有3个exe可执行文件可直接运行附带sql数据库脚本、mdf及ldf数据库文件整体只有1.32MB下载后便于完整对照学习。已有1323人学习。通过该项目的源码和数据库脚本读者能获得一个可扩展的停车场管理系统框架理解收费算法如何根据停放时间动态计算费用并参考其模块划分与代码组织方式为后续开发更完整的商业管理软件打下基础。1. C#停车场收费系统一个能跑起来的Winform项目小区门口那个收费岗亭还在用纸笔记账换班时对不上账这是很多人想写一个停车场收费系统的起点。这份C#停车场收费系统项目是一个典型的Winform桌面应用集成SQL Server数据库下载后打开Parking.sln附加数据库就能看到完整的进出场登记、计费、查询和管理功能。它不是那种只有登录页的演示项目车辆信息表、停车记录表、收费记录表都是真实建好表结构并带数据的。适合刚学完C#语法、想找一个能跑通的Winform项目案例练手的初学者也适合需要课程设计底稿、或者想搞明白桌面应用怎么做数据库读写的从业者。2. 先从项目文件和数据库下手看懂结构再谈改代码2.1 Parking.sln与文件清单VS解决方案里都放了什么拿到压缩包先别急着双击运行把文件认一遍再动手。很多人第一次打开会困惑为什么有个.mdf还有个.ldf为什么又有一个.sql脚本.vs目录能不能删。这些我拆开说。Project文件清单里核心是这样几类。Parking.sln是解决方案文件VS的项目入口没有它VS不知道要加载什么项目。用VS2015及以上版本打开基本不会报兼容性错误不用纠结创建工具版本。Parking目录是实际的项目目录里面放着.cs源文件、.Designer.cs窗体设计器文件、.resx资源文件、App.config配置文件。.vs目录是VS的本地缓存目录存窗口布局、调试断点之类的用户态数据删掉不影响编译提交代码时通常忽略它。Parking.mdf和Parking_log.ldf是SQL Server的数据库主文件和事务日志文件这两个要成对拷贝缺一个都附加不上。附加脚本.sql是备选的初始化脚本建库建表语句都在里面数据库文件损坏时靠它重建结构。完成图那几张QQ图片就是运行界面截图先看它们能快速知道系统长什么样不用等编译完再猜界面。提示先看完成图再打开解决方案最后附加数据库这个顺序能省不少折腾时间。为什么用SQL Server而不是Access或SQLite停车场收费的数据有明确的“一次进出对应一条记录”的关系查询、统计需要按车牌聚合SQL Server在关系数据完整性和事务支持上比Access扎实比SQLite更适合做桌面应用的服务端存储。而且Winform项目里连接SQL Server是标准组合代码里看到的SqlConnection、SqlCommand这些类换到任何B/S项目里依然成立学习迁移成本低。2.2 数据库表设计车辆、停车记录、收费记录三张表的分工从摘要的业务描述能看出系统至少包含车辆信息表、停车记录表、收费记录表三张核心表。我按这类系统最常见的设计列一下字段你对照手里的脚本看是否对得上。车辆信息表VehicleInfo字段类型说明IDint主键自增PlateNumbernvarchar(20)车牌号唯一约束VehicleTypenvarchar(20)车型小型车或大型车OwnerNamenvarchar(50)车主姓名Phonenvarchar(20)联系电话Remarknvarchar(200)备注停车记录表ParkingRecord字段类型说明RecordIDint主键自增PlateNumbernvarchar(20)车牌号冗余存储InTimedatetime进场时间OutTimedatetime出场时间可空DurationMinutesint停车时长单位分钟Feedecimal(10,2)应收费用Statustinyint0在停1已出收费记录表ChargeRecord字段类型说明ChargeIDint主键自增RecordIDint关联停车记录PlateNumbernvarchar(20)冗余车牌ChargeTimedatetime收费时间Amountdecimal(10,2)实收金额为什么PlateNumber在停车记录和收费记录里都冗余一份因为车辆信息表里的车牌可能被修改而历史流水必须保留入场那一刻的真实值。这是正规流水表该有的样子不是为了省一次联查才这么设计。Status字段用tinyint而不是直接存字符串是为了查询时用CASE WHEN转成中文显示排序也更快。如果你看到的字段名和这个不完全一样不用慌看明白三张表的关系思路就通了车辆信息表管静态资料停车记录表管进出状态收费记录表管金额流水。2.3 附加MDF与执行SQL脚本两条路都要会让系统跑起来的第一步不是按F5编译而是把数据库挂上。这里有两条路我建议两条都会。第一条路是直接附加MDF。打开SQL Server Management Studio右键“数据库”节点选“附加”把Parking.mdf加进去系统会自动找到同级的Parking_log.ldf。附加之前把两个文件放到SQL Server能访问的目录比如默认的DATA目录否则会遇到权限问题这个坑后面单独说。第二条路是执行附加脚本.sql。在SSMS里新建查询把整个脚本跑一遍数据库和表就自动建好了。脚本通常包含CREATE DATABASE、CREATE TABLE和少量初始数据适合数据库文件丢失或者需要重建库的场景。提示无论走哪条路跑起来之后先执行一条查询验证数据能读再打开项目编译。验证数据库是否正常可以用这条SELECT name, database_id, state_desc FROM sys.databases WHERE name Parking;这段SQL查的是系统视图sys.databases如果返回记录的state_desc是ONLINE说明数据库已挂载如果出现RECOVERING或OFFLINE说明文件有问题或者还在做恢复。之后再打开App.config看连接字符串connectionStrings add nameParkingDB connectionStringData Source.;Initial CatalogParking;User IDsa;Password你的密码; providerNameSystem.Data.SqlClient/ /connectionStringsData Source是服务器实例名小数点代表本机默认实例Initial Catalog指定数据库名User ID和Password是SQL Server登录账号如果你用的是Windows身份验证这段应该改成Integrated SecurityTrue并把User ID那两段去掉。这步改不对后面所有查询都会报“无法连接数据库”。3. 核心业务落地进场登记、出场计费与查询的C#实现3.1 添加停车车牌校验与进场时间写入添加停车这一段代码逻辑最关键的是两件事车牌号非空校验、进场时间由数据库服务器取而不是用户手输。常见错误是让用户在界面上挑时间一旦电脑时钟不准计费就全偏了。用GETDATE()让SQL Server返回服务器时间和真实时间保持一致。核心方法大概长这样private void btnAddParking_Click(object sender, EventArgs e) { string plate txtPlateNumber.Text.Trim(); if (string.IsNullOrEmpty(plate)) { MessageBox.Show(车牌号不能为空); return; } string connStr ConfigurationManager.ConnectionStrings[ParkingDB].ConnectionString; string sql INSERT INTO ParkingRecord (PlateNumber, InTime, Status) VALUES (plate, GETDATE(), 0); using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(plate, plate); conn.Open(); cmd.ExecuteNonQuery(); } MessageBox.Show(plate 已入场); LoadCurrentParking(); }这段代码做了四件事取车牌并去掉首尾空格空车牌直接return不往下执行拼SQL语句时用plate占位符而不是字符串拼接连接在参数准备好之后再打开执行完刷新在停列表。为什么我用AddWithValue而不是拼字符串因为停车场系统虽然是内部工具但车牌号被人塞一段SQL并不是玩笑参数化查询是最基本的安全基线。初次写代码的人特别喜欢把plate直接拼进SQL字符串里这是第一个要丢掉的习惯。再补一句上面这段没写try-catch实际项目里我会在conn.Open()外面包一层数据库连不上时弹具体错误信息而不是程序直接崩溃报一串英文堆栈。可以先写成这样后面再慢慢加异常处理。3.2 车辆出库与计费算法时长计算和收费标准怎么定出库是整套系统最核心的一段。计费逻辑一般是入场时插入一条记录出场时用当前时间减InTime算分钟数再套收费标准。常见收费模型是首小时X元、之后每小时Y元、单日封顶Z元甚至按时/按日/按次切换。private decimal CalculateFee(DateTime inTime, DateTime outTime) { // 按分钟计算不足一小时按一小时算 int minutes (int)(outTime - inTime).TotalMinutes; if (minutes 0) minutes 1; decimal baseRate 5m; // 首小时5元 decimal perHour 2m; // 之后每小时2元 decimal dayLimit 20m; // 单日封顶20元 decimal total baseRate; int extraHours (int)Math.Ceiling((minutes - 60) / 60.0); if (extraHours 0) { total extraHours * perHour; } // 超出单日封顶按封顶收费 if (total dayLimit) total dayLimit; return total; }这里有几个细节值得展开。TotalMinutes转int会截断小数如果车辆停了不到一分钟减出来是0所以后面补了一个if判断避免出现0分钟却收钱的情况。Math.Ceiling向上取整让超出的不足一小时按一小时收这是停车场常见的规则停1小时01分按满2小时收。perHour、baseRate、dayLimit三组数字真正的做法是抽成一张FeeConfig配置表让管理员在界面上改而不是像这样写死在代码里。摘要里提到系统应能灵活调整收费标准指的就是这条演进方向。出场时在代码里先调CalculateFee算出金额再UPDATE停车记录表写入OutTime、DurationMinutes、Fee同时把Status改成1最后往收费记录表插一条ChargeRecord。这三个动作要放在同一个事务里执行否则会出现车已经开走了但记录还停在“在停”状态的脏数据。3.3 收费查询与管理DataGridView绑定与参数化查询查询模块是“Winform做简单表格”最常见的场景核心是用DataGridView绑定DataTable。你不需要一行行往DataGridViewRow里塞数据直接用SqlDataAdapter把查询结果塞进DataTable再赋给DataSource就行。private void btnSearch_Click(object sender, EventArgs e) { string plate txtSearchPlate.Text.Trim(); string sql SELECT PlateNumber, InTime, OutTime, Fee, CASE Status WHEN 0 THEN 在停 ELSE 已出 END AS StatusText FROM ParkingRecord WHERE PlateNumber LIKE plate % ORDER BY InTime DESC; DataTable dt new DataTable(); using (SqlConnection conn new SqlConnection(connStr)) using (SqlDataAdapter adapter new SqlDataAdapter(sql, conn)) { adapter.SelectCommand.Parameters.AddWithValue(plate, plate); conn.Open(); adapter.Fill(dt); } dgvResult.DataSource dt; }这个查询用了一招LIKE plate %实现车牌前缀模糊匹配输入“京A”就能列出所有京A开头的车辆。%这个通配符要拼在参数值后面而不是写死在SQL语句里否则参数化意义就变了。SqlDataAdapter的Fill方法会自动打开和关闭连接RunTime层面省了一步。DataGridView直接绑DataTable之后列名默认以SELECT里的别名显示这里把Status转成StatusText就是为了界面直接显示中文状态。管理模块的权限控制常见做法是在登录时把用户角色存到内存里进入管理页面前判断角色值。Winform没有天然的权限中间件只能在每个业务方法的入口做判断代码写起来略啰嗦但足够应付这种规模的桌面应用。你可以看到源码里所有按钮的Click事件第一行基本都在做角色校验这不是冗余是桌面应用最朴素的权限控制方式。4. Winform界面交互控件布局、状态栏刷新与界面美化4.1 主窗体布局从完成图反推控件排布先看完成图再对照窗体设计器你会发现这套界面的布局思路是典型的“上搜索、中列表、下按钮”三段式。顶部放车牌输入框和查询按钮中部放DataGridView做在停列表底部放“添加停车”“车辆出库”“刷新”几个主操作按钮。这种布局的好处是操作路径短输入车牌、查询、选中记录、出库都在一屏内完成不需要频繁切换Tab页。拆这种窗体最好的方式是打开Form的Designer.cs文件。Winform的窗体布局不是运行时画的在InitializeComponent()方法里能看到每个控件的Location、Size、Dock、Anchor属性。我一般会先把这些属性抄下来再用TableLayoutPanel重排一遍。用固定Location布局的问题在于分辨率一变就乱改成Dock Anchor之后窗口拉伸时控件能跟着走。这套项目源码里用的是直接拖拽的固定布局功能没问题但分辨率适配是后续改造的重点。4.2 状态栏与进度条耗时操作不要卡住主线程Winform新手最常见的翻车是查询大数据或者导出报表时直接在UI线程里同步执行界面直接变成“未响应”拖都拖不动。原因是UI线程被SQL查询占满了消息循环處理不过来。给这个系统加一个导出全部停车记录的按钮如果直接在按钮Click里循环一万条数据界面大概率卡死。正确做法是用async/await加Task.Run把耗时操作丢到后台线程用Progress 回传进度private async void btnExport_Click(object sender, EventArgs e) { btnExport.Enabled false; var progress new Progressint(p { progressBar1.Value p; statusStrip1.Items[0].Text 正在导出 p %; }); await Task.Run(() DoExport(progress)); btnExport.Enabled true; statusStrip1.Items[0].Text 导出完成; } private void DoExport(IProgressint progress) { for (int i 0; i 100; i) { // 模拟分批导出逻辑 Thread.Sleep(50); if (progress ! null) progress.Report(i 1); } }关键点在于Progress 是在UI线程创建的它的回调自动封送回UI线程所以进度条和状态栏更新不会跨线程报错。Task.Run把DoExport放到线程池执行UI线程腾出来继续响应鼠标键盘。状态栏和进度条是Winform桌面应用体验的核心这个模式的代码值得抄走。4.3 界面美化给默认控件一点现代感Winform默认控件的观感停留在XP时代但美化不需要引入WPF或重写控件从三件事下手就能明显改善统一字体、统一间距、用好Panel分区。第一窗体字体统一设置为微软雅黑 9pt控件默认继承父窗体的字体不用逐个设置。第二按钮加上FlatStyle FlatFlatAppearance.BorderColor选一个和主题色一致的浅灰视觉上立刻轻量很多。第三用Panel把界面区分成功能区、数据区、操作区三段每个Panel设不同的BackColor比如数据区是白色功能区是浅灰。这套做法在Winform项目案例里很通用不动一行业务代码纯改属性就能见效。主题色也可以用代码统一管理定义一个静态类public static class Theme { public static readonly Color Primary Color.FromArgb(52, 73, 94); public static readonly Color Accent Color.FromArgb(46, 204, 113); }然后在Form_Load里给需要强调的按钮设置BackColor Theme.Accent。这种做法让颜色定义集中在一处后期调色不用满项目找。5. 避坑与排查连接失败、计费偏差、并发与打包部署5.1 附加数据库失败或SQL登录名无法连接现象附加MDF时报错“无法检索此数据库的元数据”或者代码运行时报“用户登录失败”。原因MDF和LDF文件放在C盘Program Files等受保护目录时SQL Server服务账户没有读写权限。另一个常见原因是附加时只加了MDF系统找不到同名的LDF文件Windows资源管理器默认隐藏了扩展名文件其实在但用户没看到。解决先把两个文件复制到SQL Server的DATA目录右键文件属性安全选项卡里给SQL Server服务账户加完全控制权限。附加时通过SSMS对话框选MDF不要手动硬勾选LDF。如果还是失败把文件和日志放进同一目录再重试。5.2 计费金额不对DateTime相减与四舍五入的边界现象车辆停了一分钟费用算成0元停了1小时01分收了5元而不是7元跨天停车只按小时累加没有按天封顶。原因第一类问题是没有处理TotalMinutes为0的边界直接用0乘单价结果就是0。第二类问题是没有用Math.Ceiling向上取整1小时01分取整成1小时少算了。第三类是把封顶判断写成“单次超过dayLimit就按dayLimit收”而不是“超过天数乘dayLimit”导致停两天的车被算成一天封顶价。解决所有时间差都用TotalMinutes先算总分钟数小于等于0时强制置为1。超出首小时的分钟数除以60后Math.Ceiling向上取整。封顶判断改成这样int days (int)Math.Ceiling(minutes / 1440.0); decimal total Math.Min(rawFee, dayLimit * days);1440是一天的分钟数days算出停放天数封顶金额等于天数和单日封顶的乘积。这样停两天就是40元封顶而不是20元封顶。5.3 同一车牌重复进出的并发边界现象同一辆车出场时明明是“在停”状态但执行UPDATE时却影响了0行界面提示出库成功实际记录没更新或者同一车牌在还没出场时又入场了一次数据库里出现两条Status0的记录。原因收费查询页面和管理员页面可能同时打开两条操作线程读到的是同一份旧数据。出场时没有加“当前状态必须是0”的条件直接UPDATE OutTime如果记录已经被另一个线程改成已出场本次更新就像打在空气上。解决出场更新的SQL必须带状态条件string sql UPDATE ParkingRecord SET OutTimeGETDATE(), DurationMinutesminutes, Feefee, Status1 WHERE RecordIDid AND Status0;然后再检查ExecuteNonQuery的返回值等于1说明真的更新到了等于0说明记录状态已经变了此时弹窗提示“该车辆已出场”而不是默认出库成功。入场端同理添加停车前先查一下有没有未出场的记录。5.4 Winform打包成安装程序时的连接字符串问题现象开发环境跑得好好的打包成安装程序装到别的机器上打开就报“建立与SQL Server的连接时发生错误”。原因连接字符串里写的是本机实例名“.”换台机器数据库实例不同自然连不上。还有更隐蔽的App.config在打包时没有跟着输出到发布目录程序读不到配置只能用代码里的默认连接字符串。解决打包成安装程序之前把连接字符串改成可配置项并确认发布目录里有exe.config文件。如果目标机器没装SQL Server可以考虑在打包时带上LocalDB连接字符串改成LocalDB实例或者在安装界面加一步让用户填写服务器地址。常见做法是做一个数据库配置窗体首次启动时让用户填“服务器名/账号/密码”再写入App.config这样安装包不管装到哪台机器都能适配。打包工具我常用VS自带的Installer Projects如果要把项目里的DLL合并成一个exe可以配合Costura.Fody这类工具注意合并之后连接字符串配置文件不要丢失。6. 进阶实践先跑通再重构把系统改成你自己的6.1 增加一个按天封顶的收费标准把写死在代码里的收费标准抽成一张FeeConfig表字段就三个ConfigKey、ConfigValue、Remark。基础费率、每超时费、单日封顶都存成记录。然后修改CalculateFee方法启动时从表里读配置代替硬编码。这步做完管理员就能在界面改价格系统才算真正“灵活调整收费标准”。6.2 用一个SQL查询验证改对了没有重构完先不急着点界面用一条SQL验证逻辑SELECT PlateNumber, InTime, OutTime, DurationMinutes, Fee, CASE WHEN Fee DurationMinutes * 0.1 THEN 疑似偏高 WHEN Fee 0 AND Status 1 THEN 疑似漏收 ELSE 正常 END AS FeeCheck FROM ParkingRecord WHERE Status 1;这条查询把已出场记录的时长和费用做粗略交叉验证能快速找出计费异常的记录。验证通过后再跑一次界面操作入场、出场、查询各点一遍数据落库结果和这条SQL的检查结果一致说明重构没有破坏原逻辑。这套系统真正练到手的标志不是把源码跑起来而是把写死的值改成可配置、把同步查询改成异步带进度、把固定布局改成自适应布局。我自己的习惯是拿到任何Winform项目先做这三步重构再谈别的功能。从那以后我每次接手源码都强制走一遍“先跑通、再拆表、再异步化”的流程。希望帮到你。本文还有配套的精品资源点击获取