
简介这是一套基于C#与Winform开发的停车场收费系统桌面应用源码适用于C#初学者、高校课程设计以及希望掌握数据库桌面项目开发的读者。系统覆盖车辆入场登记、出库计费、按时/按日/按次灵活计费、收费记录查询、车辆信息管理与权限设置等完整业务流程清晰演示了SQL Server数据库在真实业务中的支撑作用也展现了Winform控件事件驱动的界面交互设计。压缩包共95个文件大小约1.32MB核心内容包括37个.cs源码文件和13个.resx界面资源文件同时附带数据库SQL脚本、.mdf/.ldf数据库文件、可直接运行的.exe程序及完成效果图能够帮助读者快速还原项目环境并对照阅读代码。目前已有1323人浏览学习源码目录结构清晰适合循序渐进拆解整个项目。通过该项目可以掌握停车收费算法、参数化SQL查询、界面与数据层交互等关键技能还能学习从数据库建表到界面调用的完整设计思路为独立开发类似管理系统积累实用经验。1. C#停车场收费系统一份把数据库、Winform界面和计费逻辑串起来的完整案例商场地下停车场的值班室里保安点一下“入场登记”车牌号落库顾客出场时系统按停留时长算钱免不了一场“我明明只停了59分钟”的扯皮。C#停车场收费系统这个Winform项目做的就是这套从入场到出场、从计费到对账的桌面闭环。它不炫技但五脏俱全SQL Server负责存数据Winform负责点按钮收费算法负责算钱。适合两类人——刚学完C#语法想完整走一遍真实业务的新手以及需要给学员或内部团队当练手模板的带教。12小时前我刚把这份源码从头到尾拆了一遍这篇文章就把过程、参数和坑一次说清。2. 数据库先行先看懂表结构再谈业务逻辑原项目压缩包里同时带着Parking.mdf、Parking_log.ldf和附加脚本.sql这三样的关系第一次接触的人容易弄混。mdf是SQL Server的主数据文件表结构和数据都放这里面ldf是事务日志文件记录每一次数据变更附加脚本.sql则是建库建表的可执行文本没库的时候用它一键建出来。很多人上来就双击.sln编译结果界面弹出来了登录却报“无法连接到数据库”就是因为根本没把数据文件和脚本用起来。文件作用什么时候用Parking.mdf主数据库文件含表结构数据用SSMS“附加”或连接串里AttachDbFilename加载Parking_log.ldf事务日志文件附加时自动与mdf配对缺失会报错附加脚本.sql建库、建表、插入基础收费标准的SQL文本没有mdf文件时整库重建Parking.sln解决方案入口双击打开后用VS编译运行这个系统里最核心的数据表是车辆信息表、停车记录表、收费记录表三张。我一般看源码会先看建表脚本这样后面读代码时不会对着列名猜含义。典型建表脚本如下CREATE TABLE dbo.VehicleInfo ( VehicleID INT IDENTITY(1,1) PRIMARY KEY, -- 车辆内部编号 PlateNumber NVARCHAR(20) NOT NULL UNIQUE, -- 车牌号中文必须用NVARCHAR VehicleType NVARCHAR(10) DEFAULT N临时车, -- 临时车/月租车 OwnerName NVARCHAR(50), -- 车主姓名月租车位用 Phone NVARCHAR(20), -- 联系电话 CreateTime DATETIME DEFAULT GETDATE() ); CREATE TABLE dbo.ParkingRecord ( RecordID INT IDENTITY(1,1) PRIMARY KEY, PlateNumber NVARCHAR(20) NOT NULL, -- 车牌号业务查询主键字段 VehicleType NVARCHAR(10) DEFAULT N临时车, EntryTime DATETIME NOT NULL, -- 入场时间计费起点 ExitTime DATETIME NULL, -- 出场时间NULL表示仍在场 Fee DECIMAL(10,2) DEFAULT 0, -- 本次应收金额出场时回填 Status TINYINT DEFAULT 1, -- 1在场 0已出场防重复出场关键 CarID INT FOREIGN KEY REFERENCES dbo.VehicleInfo(VehicleID) ); CREATE TABLE dbo.ChargeRecord ( ChargeID INT IDENTITY(1,1) PRIMARY KEY, RecordID INT NOT NULL FOREIGN KEY REFERENCES dbo.ParkingRecord(RecordID), PlateNumber NVARCHAR(20) NOT NULL, Fee DECIMAL(10,2) NOT NULL, PayType NVARCHAR(10) DEFAULT N现金, -- 现金/微信/支付宝 ChargeTime DATETIME DEFAULT GETDATE() );这是我按这个项目业务场景补的标准写法下载包里附加脚本.sql的内容方向也一致。三张表各管一段VehicleInfo只存车的档案不存每一次进出ParkingRecord记每一次进出的时间和收费出场时通过PlateNumber加Status1去锁定在场记录ChargeRecord记每一次实际收钱的流水出库一次插入一条方便和前台现金流水对账。几个关键字段设计的取舍值得说透。车牌用NVARCHAR而不是VARCHAR因为国内车牌有汉字VARCHAR配错代码页就是一片问号这是停车场系统最常见的乱码来源。Status TINYINT只有0和1两个值但意义重大——所有查询在场车辆的SQL都带WHERE Status1出场更新也是UPDATE ParkingRecord SET ExitTimeGETDATE(), Status0 WHERE PlateNumberplate AND Status1这条WHERE Status1就是并发场景下防止重复出场的保险。Fee用DECIMAL(10,2)金额字段永远别用FLOAT二进制浮点算钱会出9.999999这种邪门数字SQL Server和C#里都一样。INSERT INTO dbo.VehicleInfo (PlateNumber, VehicleType) VALUES (N京A12345, N临时车); INSERT INTO dbo.ParkingRecord (PlateNumber, VehicleType, EntryTime) VALUES (N京A12345, N临时车, GETDATE());上面两条是添加停车时后端会执行的核心SQL。第一条先把车写进车辆档案如果UNIQUE约束在重复入场会直接报错第二条插入一条在场记录EntryTime用GETDATE()取数据库服务器时间这样计费以数据库时间为准不依赖客户机时钟——客户机时间被人改了计费全乱这是收费系统必须用服务器时间的原因。再看收费标准落地。大多数停车场是首小时X元、之后每小时Y元、24小时封顶Z元这些参数按摘要描述“应能灵活调整”源码里常见的做法要么建一张FeeRule表要么在App.config里配三个键。我更推荐放数据库表不用重新编译就能改价CREATE TABLE dbo.FeeRule ( RuleID INT PRIMARY KEY, FirstHourFee DECIMAL(10,2) DEFAULT 10, -- 首小时收费 ExtendHourFee DECIMAL(10,2) DEFAULT 5, -- 超过首小时每小时收费 DayMaxFee DECIMAL(10,2) DEFAULT 50, -- 24小时内封顶价 FreeMinutes INT DEFAULT 15 -- 免费时长很多商场15分钟内不收费 ); INSERT INTO dbo.FeeRule (RuleID) VALUES (1);这套设计的核心思路是把“价格”从代码里搬出来搬到库里。以后调价只跑UPDATE前台程序重启就生效不用改一行C#。后面改成阶梯费率的时候只需要在这个表上扩展字段或在代码里做分段计算不用动数据库表结构。所以这份资料值得先看库——所有业务规则最后都沉淀在表和脚本里代码只是一层壳。3. 跑起来再说附加数据库、改连接串、F5调试的完整步骤光看表结构不跑起来等于纸上谈兵。这个项目从零到能点“入场登记”按钮通常要过五关解压路径、附加数据库、确认SQL Server实例、改连接串、编译运行。每一步的报错都有固定套路先按下面的流程走一遍能省掉大半翻车时间。3.1 解压与准备先把它解压到一个权限干净的位置。我习惯解到D:\ParkingSource\避免放C:\Program Files这种需要管理员权限的目录也避免路径带中文和空格——这两个因素都会让SQL Server“附加数据库”时报“无法打开物理文件”。解压后确认Parking.mdf和Parking_log.ldf在同一个目录里这两个文件如果被人为拆散附加就会失败。目录里有Parking.sln就说明VS工程文件齐全可以继续往下走。3.2 附加数据库两种方式方式A用SQL Server Management Studio图形界面。打开SSMS连上本机实例.或.\SQLEXPRESS或.\MSSQLLocalDB取决于你装的是Developer版还是LocalDB右键“数据库”→“附加”点“添加”选到D:\ParkingSource\Parking.mdf确认右侧列表同时出现了Parking.mdf和Parking_log.ldf两条点确定。附加成功后左侧能看到Parking这个库展开“表”能看到三张以上的业务表这步成了后面才能谈连接串。方式B没装SSMS只有命令行时用sqlcmd执行附加sqlcmd -S .\SQLEXPRESS -E -Q CREATE DATABASE Parking ON (FILENAMED:\ParkingSource\Parking.mdf), (FILENAMED:\ParkingSource\Parking_log.ldf) FOR ATTACH;这条命令的-S后面是SQL Server实例名-E表示用Windows身份登录。如果用的是LocalDB实例名要写成(localdb)\MSSQLLocalDB并且sqlcmd要用对应版本。执行成功后同样可以用sqlcmd验证sqlcmd -S .\SQLEXPRESS -E -Q SELECT name FROM sys.databases看到Parking就是成功了。如果附加时提示ldf损坏又不需要历史日志可以先删掉Parking_log.ldf再执行FOR ATTACH_REBUILD_LOG重建日志。这个操作比较深新手别轻易在原数据上试先拿副本练手。3.3 找到并修改连接串源码里负责连数据库的地方通常两类App.config里配connectionStrings或者代码里直接写SqlConnection字符串。Winform项目最常见的是写在App.config里编译后对应生成Parking.exe.config。先全局搜索“Data Source”或“AttachDbFilename”定位到连接串典型长这样connectionStrings add nameParkingDB connectionStringData Source.\SQLEXPRESS;AttachDbFilenameD:\ParkingSource\Parking.mdf;Integrated SecurityTrue;User InstanceFalse;Connection Timeout5; providerNameSystem.Data.SqlClient / /connectionStrings参数说明Data Source是本机SQL Server实例名.\SQLEXPRESS是默认Express实例(localdb)\MSSQLLocalDB是LocalDBAttachDbFilename指定.mdf文件路径这句可写可不写——如果已经在SSMS里附加过建议删掉这句改成Initial CatalogParking让程序直接连已存在的库避免每次启动时要再挂一次文件Integrated SecurityTrue表示用Windows账号登录如果SQL Server端开了混合登录模式也可以改成User IDsa;PasswordxxxConnection Timeout5让连接失败时5秒内报错不用干等15秒默认超时。最常见的坑是Data Source对不上机器上实际装的实例。不知道机器上装了什么实例的打开SQL Server配置管理器看“SQL Server服务”那一栏服务名后面括号里的文字就是实例名。看到的是MSSQLSERVER连接串写Data Source.;看到SQLEXPRESS写.\SQLEXPRESS装LocalDB写(localdb)\MSSQLLocalDB。这步是最费时间的大头通了之后后面基本顺畅。3.4 编译运行双击Parking.sln用VS打开等待NuGet还原这个项目一般没有复杂依赖包几秒的事。确认“启动项目”是主窗体或登录窗体直接按F5。如果报错“未能加载文件或程序集”或System.Data.SqlClient相关多半是目标框架版本问题——在项目属性里看目标框架是不是和本机装的一致一般.NET Framework 4.x装对应版本的Developer Pack即可。能弹窗、点“入场登记”后数据库表里能查到记录就算完全跑通了。还有个常见误用有人把连接串直接硬编码在某个.cs文件的按钮事件里换机器就要打开源码找一圈。如果下载包里的代码是这样写的建议顺手把它挪到App.config这不影响功能但后面改库路径时能少翻一次车。3.5 没有mdf时的补救方案万一解压后发现mdf和ldf损坏或文件缺失只剩下附加脚本.sql也能整库重建在SSMS里连上实例右键“数据库”→“新建数据库”命名Parking选中Parking执行脚本文件脚本里如果没有建库语句就按第2章给出的CREATE TABLE逐个执行把INSERT收费标准那几条也一起跑掉。最后把连接串改成Initial CatalogParking不加AttachDbFilename效果和附加mdf几乎一样。这套补救做完系统照样能用只是少了原库里的演示数据。4. 核心模块拆解入口登记、出场计费、收费查询的代码怎么协同进度走到能跑只是开始看源码的价值在于弄明白“点一下按钮背后SQL和C#是怎么配合的”。这一章按添加停车、车辆出库、收费查询、管理模块四个方向逐段拆不贴整文件只贴每个动作最关键的方法片段。4.1 添加停车先查在场状态再插入记录入场登记UI上通常是一个车牌输入框加一个“入场”按钮。按下按钮后业务层最常见的写法是先在数据库查一下“这辆车是不是还在场里”防止同一辆车入场没出库又录一条public int AddParking(string plateNumber, string vehicleType) { string checkSql SELECT COUNT(*) FROM ParkingRecord WHERE PlateNumberplate AND Status1; using (var cmd new SqlCommand(checkSql, conn)) { cmd.Parameters.AddWithValue(plate, plateNumber); if ((int)cmd.ExecuteScalar() 0) { MessageBox.Show(该车辆已在场请核对车牌后出库); return -1; } } string insertSql INSERT INTO dbo.ParkingRecord(PlateNumber, VehicleType, EntryTime) VALUES(plate, type, GETDATE());; using (var cmd new SqlCommand(insertSql, conn)) { cmd.Parameters.AddWithValue(plate, plateNumber); cmd.Parameters.AddWithValue(type, vehicleType); return cmd.ExecuteNonQuery(); } }逻辑说明整个方法分两步第一次查询的作用是状态守卫——很多新手写入场只写INSERT结果同一辆车还没出库又录了一次入场产生两条在场记录出场时查哪条都不对第二次执行真正的插入。注意EntryTime不传DateTime.Now而是GETDATE()让数据库服务器时间做为准。参数说明plate和type都用SqlParameter传值不要拼SQL字符串——车牌是中文含特殊字符一旦包含单引号直接语法错误或者变成注入入口返回-1给UI层弹提示返回1表示插入成功。4.2 车辆出库事务里先取记录、算费、再更新状态出库比入场多一个环节算钱。常见做法是开一个事务先查出在场记录和入场时间按时长算费再更新ParkingRecord并插入ChargeRecord保证两步要么都成功要么都失败public decimal CheckOut(string plateNumber, string payType) { decimal fee 0m; using (var tx conn.BeginTransaction()) { string query SELECT TOP 1 RecordID, EntryTime FROM ParkingRecord WHERE PlateNumberplate AND Status1 ORDER BY EntryTime DESC; SqlCommand queryCmd new SqlCommand(query, conn, tx); queryCmd.Parameters.AddWithValue(plate, plateNumber); decimal Id 0; DateTime entryTime DateTime.Now; using (var reader queryCmd.ExecuteReader()) { if (!reader.Read()) { throw new Exception(未找到该车辆的在场记录); } Id (int)reader[RecordID]; entryTime (DateTime)reader[EntryTime]; } fee CalculateFee(DateTime.Now - entryTime); // 传入停留时长算费 SqlCommand updateCmd new SqlCommand( UPDATE ParkingRecord SET ExitTimeGETDATE(), Feefee, Status0 WHERE RecordIDid AND Status1, conn, tx); updateCmd.Parameters.AddWithValue(fee, fee); updateCmd.Parameters.AddWithValue(id, Id); int affected updateCmd.ExecuteNonQuery(); SqlCommand insertCmd new SqlCommand( INSERT INTO ChargeRecord(RecordID, PlateNumber, Fee, PayType) VALUES(id, plate, fee, payType), conn, tx); insertCmd.Parameters.AddWithValue(id, Id); insertCmd.Parameters.AddWithValue(plate, plateNumber); insertCmd.Parameters.AddWithValue(fee, fee); insertCmd.Parameters.AddWithValue(payType, payType); insertCmd.ExecuteNonQuery(); tx.Commit(); } return fee; }逻辑说明事务把“查场记录→算费→改状态→记流水”包在一起任何一步异常就回滚不会出现状态改了但费用没记录的情况。UPDATE那条WHERE RecordIDid AND Status1是防重出库的关键——如果手快点了两次出库按钮第二次执行时Status已经是0UPDATE影响行数是0业务层可以根据affected0给“该车已出场”提示不会扣两次钱。参数说明BeginTransaction后所有SqlCommand必须显式传入同一个tx对象否则SQL会在事务外执行直接报“当前命令无法在事务中执行”PayType按界面下拉框传现金/微信/支付宝。这里还要强调一个细节出场时间同样用GETDATE()而不用C#端时间。我做过的收费系统出过这种事故——客户机时间被员工调慢了半小时出场时间取C# DateTime.Now收费记录就凭空少了半小时的金额对账查很久才发现。统一取数据库时间这个洞就堵上了。4.3 计费算法按小时取整是最容易出错的段落计费函数是停车场系统里最不该写错的地方。按摘要描述的“按时、按日或按次”最常见的实现是超过首小时后按整小时向上取整同时考虑免费时长public decimal CalculateFee(TimeSpan stay) { decimal fee 0m; var rule GetFeeRule(); // 从FeeRule表读当前收费标准 if (stay.TotalMinutes rule.FreeMinutes) return 0m; // 免费时长内不收费 if (stay.TotalHours 1m) fee rule.FirstHourFee; // 一小时以内按首小时收 else if (stay.TotalDays 1m) fee rule.DayMaxFee; // 停满一天按封顶价收 else fee rule.FirstHourFee Math.Ceiling(stay.TotalHours - 1m) * rule.ExtendHourFee; return fee; }看着简单坑全在边界停车59分钟stay.TotalHours是0.98小于1应收首小时费停车1小时1分钟首小时费加1小时续费但很多人直接(int)TotalHours去截断59分钟被算成0小时变成免费了跨天比如停了25小时TotalDays大于1直接用封顶价会少收钱正确产品里应该按“24小时内封顶50超过一天重新累计”处理。Math.Ceiling向上取整是这个函数的核心行为任何一秒都按一个小时算收费规则上占得住实现上也简单。4.4 收费查询有条件筛选和汇总对账查询模块一般是DataGridView绑DataTable支持按车牌、按日期段过滤底部一个Label汇总总金额。常见写法private DataTable QueryCharge(string plate, DateTime start, DateTime end) { string sql SELECT c.ChargeTime, c.PlateNumber, c.Fee, c.PayType FROM ChargeRecord c WHERE c.ChargeTime start AND c.ChargeTime end; if (!string.IsNullOrWhiteSpace(plate)) sql AND c.PlateNumber plate; sql ORDER BY c.ChargeTime DESC; // 执行SqlCommand并填充DataTable后返回 }逻辑说明日期条件用start和end而不是BETWEEN因为BETWEEN会把end当天23:59:59.997都算进去只有小于第二天零点才能精确卡出左闭右开区间车牌条件动态拼接时用IsNullOrWhiteSpace判断空值就不加过滤条件。汇总金额的做法是在DataTable上用计算列Sum不要循环累加后还依赖DataGridView——刷新DataSource后上一次的累加结果没清空金额会翻倍这是Winform开发高频出现的老问题。4.5 管理模块系统参数和用户权限的落地方式摘要里说管理模块包括车辆信息管理、用户权限、系统参数配置。在Winform项目里这些通常集中在专门的“设置”或“管理员”窗体权限一般用登录表用户名、密码、角色配合窗体级判断操作员只能出入库管理员才能进管理界面系统参数就是第2章那张FeeRule表的增删改查界面车辆信息管理则是VehicleInfo的CRUD加上月租车、固定车位的标记位。这部分代码量大但模式重复核心一句话任何管理功能都要做权限校验再落库密码不能明文存至少哈希一下——哈希的做法第6章展开。5. 踩坑实录本地上线最容易翻车的五处这一章是这次拆完源码最想先写出来的。上面三章按“能跑通”的顺序在走但真正耽误时间的从来不是功能逻辑而是一堆看着不起眼的运行环境坑。以下五条都来自做Winform加SQL Server项目的实际经验现象、原因、解决一条条捋。5.1 附加数据库报“无法打开物理文件”现象SSMS附加Parking.mdf时弹“无法打开物理文件…操作系统错误5(拒绝访问)”或“路径不存在”sqlcmd附加也同样报错。原因mdf被解压到Program Files等UAC保护目录SQL Server服务账号没有读权限或者zip解压后ldf与mdf不在同一目录附加时找不到配套日志文件。两者都是初学者最常踩的物理路径问题不是数据库本身坏了。解决把Parking.mdf和Parking_log.ldf一起挪到D:\ParkingSource这类普通目录右键文件属性确认Users或Authenticated Users有读取权限附加时日志列表里同时出现两个文件再点确定。如果ldf确实损坏备份mdf后用FOR ATTACH_REBUILD_LOG重建日志数据无损但历史日志会丢。预防性检查很简单每换一台机器跑这个项目第一步就确认文件在同一目录、不在系统保护目录再动数据库。5.2 程序报“无法连接到数据库”但SSMS连得上现象登录界面点按钮就报“在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误”可同一个实例用SSMS能正常打开。原因连接串里的Data Source实例名和本机实际实例名不一致或者SQL Server登录模式只开了Windows身份认证而连接串用了SQL账号登录或防火墙拦了1433端口——localhost一般不受影响但Data Source写了机器名带端口就会触发。解决先用配置管理器确认实例名再改连接串登录模式改为“Windows和SQL Server混合模式”并重启SQL服务不确定端口时连接串里不要写逗号加端口让客户端走实例解析。最快验证连接串的办法是把它抄到SSMS的“其他连接参数”里测试能连通就是程序代码配置错了不能连通就是数据库端问题sqlcmd -S .\SQLEXPRESS -E -Q SELECT VERSION这一条命令返回SQL Server版本号就说明实例名和身份验证都没问题返回登录失败就要回头检查混合登录和sa密码策略。5.3 车牌中文乱码成问号现象插入的车牌在表里变成“京?????”按车牌查询也查不出来。原因建表时列类型用了VARCHAR插入时又没加N前缀或者代码里AddWithValue传值时没走Unicode。中文在所有SQL Server场景里都要求NVARCHAR加N…VARCHAR走本机代码页碰到汉字就丢了。“本机测试用英文车牌没问题一换中文就现形”是这条坑的典型信号。解决表结构改成NVARCHAR(20)插入参数统一用SqlDbType.NVarChar并显式指定长度已经乱码的旧数据只能手工清理后重新入库。顺手再检查一下SQL脚本里的INSERT语句字符串字面量都要带N前缀比如N京A12345少一个N都可能踩中。5.4 点两次出库按钮扣了两次费用现象出库按钮连续快速点了两下收费记录里出现两条相同的ChargeRecord金额翻倍。原因CheckOut方法里没有状态校验第二次点击时查到同一条Status1记录又算了一次钱或者虽然更新了状态但按钮事件没有做互斥两次点击几乎同时进入方法都读到了旧状态。解决三个层级叠加才能根治。UI层点击后先Disable按钮方法执行完再Enable业务层按第4.2节写法UPDATE带WHERE Status1用受影响行数判断是否已出场数据库层给ChargeRecord的RecordID加唯一约束让数据库直接拒绝第二条流水btnCheckOut.Enabled false; try { decimal f service.CheckOut(txtPlate.Text, cmbPayType.Text); MessageBox.Show($应收 {f} 元); } finally { btnCheckOut.Enabled true; }这个片段把按钮互斥写在最外层即使业务逻辑漏了状态判断UI也已经挡住了大部分双击。5.5 收费查询的金额汇总对不上账现象查询界面底下显示的总金额和从SQL Server直接跑SUM(ChargeRecord.Fee)的结果对不上差出重复金额或漏掉几条。原因DataGridView重新绑定DataSource之前上一次的汇总结果没清零或者查询时过滤条件没套同一套——界面日期选了7月1日到7月3日SQL里却写死7月1日当天。两个错误叠加起来最难排查尤其在反复切换查询条件的场景下。解决绑定前先清空汇总Label并重新计算日期过滤条件全局统一用一个DateTime变量界面赋值给变量SQL参数也取自同一个变量不要一边取dateTimePicker1.Value一边取dateTimePicker2.Value还写错下标。这类对不上账的问题全流程先跑一遍纯手工登记的例子入场A、出库A、再次入场A再对比界面金额和SQL SUM是最有效的排错路径。从那以后我每次接手带查询汇总的Winform项目第一件事就是查刷新绑定处有没有清空累计值。6. 进阶改造把阶梯计费和对账导出一次做利索基础跑通后有价值的改造是把固定费率改成阶梯计费再把查询结果导出成Excel方便财务对账。前者练业务逻辑后者练文件IO和编码两个都是单人项目里最能出效果的升级点。阶梯计费的核心是一次函数替换。把第4.3节CalculateFee的线性算法换成多段阶梯0到15分钟免费15分钟到2小时收10元2小时到5小时每小时累加5元5到12小时封顶40元超过12小时但不满24小时封顶60元满24小时重新按此周期循环private decimal CalcLatestFee(TimeSpan stay) { decimal fullDays Math.Floor((decimal)stay.TotalDays); // 满24小时的天数 var remain stay - TimeSpan.FromDays((double)fullDays); // 剩余不足一天的时长 decimal remainFee CalcByTier(remain); return fullDays * 60m remainFee; // 每天封顶60加当天阶梯 }这个写法相比原版把“跨天”和“当天内阶梯”拆开算满天的部分直接乘封顶价剩下的零头再走当天阶梯避免停了25小时仍按60封顶的漏收。CalcByTier内部按一个按阈值排序的List 遍历每个Tier有上限小时和费率落在哪个区间就按哪个区间计价。改价只动配置不动逻辑。我一般会把边界用例注释在方法头上0分钟返回059分钟返回首小时价刚好2小时返回首小时加一档刚好24小时返回6025小时返回60加首小时。改价前先跑一遍这些用例能挡住九成计费翻车。对账导出用CSV比直接用Excel COM组件省事得多。DataGridView里查出来的结果循环写入StringBuilder再File.WriteAllText文件用带BOM的UTF-8编码Excel双击打开就不会中文乱码var sb new StringBuilder(); sb.AppendLine(车牌号,入场时间,出场时间,金额,支付方式); for (int i 0; i dataGridView1.Rows.Count; i) { if (dataGridView1.Rows[i].IsNewRow) continue; var r dataGridView1.Rows[i]; sb.AppendLine(${r.Cells[PlateNumber].Value},{r.Cells[EntryTime].Value},{r.Cells[ExitTime].Value},{r.Cells[Fee].Value},{r.Cells[PayType].Value}); } File.WriteAllText(saveFileDialog1.FileName, sb.ToString(), new UTF8Encoding(true));逻辑说明IsNewRow跳过DataGridView末尾的空行避免导出多一行空白车牌和金额字段不会出现逗号所以不用转义new UTF8Encoding(true)是带BOM的UTF-8BOM是Excel识别UTF-8的关键用Encoding.UTF8写出的无BOM文件在Excel里会乱码。参数说明saveFileDialog1要在按钮点击前ShowDialog并把FileName赋值过来导出路径里别带中文——有些老版本Excel对中文路径兼容性不好直接报“文件格式和扩展名不匹配”。做完这两个改造这套停车场系统就从“能跑”变成了“能当收银工具用”。我自己的习惯是所有计费相关改动都先把边界用例清单一字不差跑一遍再提交给同事用。特别是阶梯计费这种分段逻辑肉眼很难看出25小时的错只能靠枚举输入输出。从那以后我每次改完计费都强制先跑一遍0分钟、59分、2小时整、24小时整这四组用例再放心交付。希望这份拆解对你有用踩过的地方别再踩改起来顺手。本文还有配套的精品资源点击获取