ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C# Winform停车场收费系统全攻略:从数据库到计费规则

C# Winform停车场收费系统全攻略:从数据库到计费规则 简介这是一份C#停车场收费系统Winform项目完整源码面向C#初级开发者与桌面应用学习者围绕车辆进出库、收费计算、记录查询等典型业务展示了从界面设计到数据库操作的完整落地流程。压缩包共95个文件大小仅1.32MB其中包括37个C#源码文件、13个resx/resources界面资源文件、可执行的exe程序以及SQL脚本与mdf/ldf数据库文件便于直接附加数据库后运行调试界面截图可辅助理解页面布局与交互。系统覆盖添加停车、车辆出库、自动计费、收费查询与管理模块支持按停放时间自动计算费用并预留收费标准调整与多支付方式扩展业务逻辑清晰。项目以SQL Server为后台数据库涵盖车辆信息、停车记录、收费记录等核心表结构便于理解数据模型设计。已有1323人学习下载通过阅读源码可掌握Winform事件驱动、SQL Server增删改查、多窗体协作等关键技能也适合作为课程设计、毕业设计参考或自学练手项目从中学习模块划分与异常处理思路。1. 停车场收费系统不是CRUD先说清这个winform项目到底在做什么停车场收费系统是典型的 C# Winform 项目案例常被当成课设或者外包练手题但真正做过的人都知道它的难点从来不在增删改查而在「计时、算费、异常处理」这三件事上。入口处管理员输入车牌、点进场出口再输入车牌、点出场系统把费用算出来并显示。听起来很简单可一旦碰上跨天停车、免费时段、月卡过期、车牌识别失败逻辑就会迅速复杂起来。适合做这个项目的人是想巩固 C# 基础语法、数据库操作、Winform 控件用法的初学者以及需要交付一套能实际运行在岗亭电脑上的收费软件的开发者。反直觉的结论是界面只占工作量的三成剩下的时间都被计费规则和数据一致性吃掉。2. 从一张停车记录表开始数据库设计、费用计算与计时状态机2.1 车辆进出记录表与计费状态为什么不能用“一张表存余额”很多新手拿到这个项目的第一个想法是建立一张「车辆表」字段里有车牌号、进场时间、余额、累计费用。这个做法在会员预充值场景下有一定道理但对于普通临时车计费它无法回答一个核心问题某辆车现在到底在不在场内如果同一辆车离场后又进场旧记录被覆盖历史收费记录就丢了。我一般会把停车记录拆成两个独立概念一是车辆档案表只记录车牌、车主、月卡有效期这些固定信息二是进出场记录表每一条记录代表一次完整的停车行为。进出场记录表的核心字段包括记录主键、车牌号、进场时间、出场时间、收费金额、状态、操作员。状态字段用整数表示0 代表在场内1 代表已离场且结算完成2 代表异常出场比如人工抬杆未收费。这个状态机是所有后续逻辑的地基。建表语句我习惯写成这样CREATE TABLE park_record ( RecordId INTEGER PRIMARY KEY AUTOINCREMENT, PlateNumber TEXT NOT NULL, EntryTime DATETIME NOT NULL, ExitTime DATETIME, TotalFee DECIMAL(10,2) DEFAULT 0, Status INTEGER DEFAULT 0, OperatorName TEXT, Remark TEXT ); CREATE INDEX idx_plate_status ON park_record (PlateNumber, Status);说明PlateNumber和Status建联合索引是为了快速查询某辆车当前是否在场内。ExitTime允许为空因为进场时还不知道什么时候出场。TotalFee在出场结算前为 0结算后写入实际金额。这里我特意用了 SQLite 的语法因为停车场岗亭电脑通常配置低SQLite 零部署、单文件备份比 SQL Server 更适合小规模项目。为什么不建议在车辆表里加余额字段因为临时车和月卡车的计费模型不一样临时车每次出场都要按当前时间计算费用而月卡车包月不限次数。把余额放在车辆表里会导致临时车每次进出都要更新余额月卡却不需要业务逻辑就会纠缠在一起。进出场记录表按次记录月卡车到期判断、临时车费用计算、报表统计全部可以各查各的。2.2 分层结构UI、业务逻辑、数据访问怎么划分Winform 项目最常见的翻车方式是所有代码都写在 Form1.cs 里按钮事件里直接写SqlConnection、SqlCommand一个窗体文件上千行。这种代码在课程设计里能运行在公司里会被骂死。停车场收费系统虽然小但计费规则经常改比如从每半小时 2 元改成首小时 5 元、之后每半小时 2 元如果费率写死在按钮事件里每次改规则都要重新编译发布。我一般会按三层结构组织UI 层只负责控件展示和用户交互业务逻辑层处理计费、状态判断、规则校验数据访问层封装所有 SQL 操作。Winform 的Form属于 UI 层它不应该直接调用SQLiteConnection而是调用ParkingService这样的业务类。业务类内部再调用ParkRecordRepository这样的数据访问类。一个最小但完整的分层示例先看数据访问层public class ParkRecordRepository { private readonly string _connectionString; public ParkRecordRepository(string connectionString) { _connectionString connectionString; } public ParkRecord GetActiveRecordByPlate(string plateNumber) { const string sql SELECT * FROM park_record WHERE PlateNumber plate AND Status 0 ORDER BY EntryTime DESC LIMIT 1; using var conn new SQLiteConnection(_connectionString); conn.Open(); using var cmd new SQLiteCommand(sql, conn); cmd.Parameters.AddWithValue(plate, plateNumber); using var reader cmd.ExecuteReader(); if (reader.Read()) { return new ParkRecord { RecordId reader.GetInt64(0), PlateNumber reader.GetString(1), EntryTime reader.GetDateTime(2), Status reader.GetInt32(5) }; } return null; } }这个仓储类只负责一件事根据车牌查出当前在场内的车辆记录。LIMIT 1是因为理论上同一辆车不可能有两条同时在场内的记录但为了防止脏数据取最新一条最稳妥。参数化查询里的plate是占位符而不是拼字符串这一点在收费系统里非常重要原因我下一小节展开。业务逻辑层则这样调用public class ParkingService { private readonly ParkRecordRepository _repository; public ParkingService(ParkRecordRepository repository) { _repository repository; } public bool IsVehicleInPark(string plateNumber) { return _repository.GetActiveRecordByPlate(plateNumber) ! null; } }UI 层按钮事件里只需要写一行parkingService.IsVehicleInPark(textBoxPlate.Text)不需要知道 SQL 怎么写的。这种分层的好处是以后要把 Winform 界面换成 WPF 或者 Web 管理端业务逻辑和数据访问层可以原样复用。2.3 SQL参数化与事务收费系统里的数据一致性停车场系统的车牌输入框是允许手工输入的这就意味着车牌字符串里可能出现空格、单引号甚至有人故意输入长度为 20 的假车牌。如果把车牌直接拼进 SQL 字符串轻则查询报错重则被注入。我见过一个真实事故车牌输入 OR 11导致出场查询把所有在场车辆全部列出来管理员还不知道发生了什么。参数化查询就是解决办法。前面代码里的cmd.Parameters.AddWithValue(plate, plateNumber)会把输入值当作纯数据传给 SQLite而不是当作 SQL 语法解析。这是收费系统里最基本的安全底线不管数据量多小都不能省。除了安全还要考虑一致性。车辆出场时要做的事情不止一件把park_record的状态改为已离场、写入出场时间和费用如果使用会员余额扣费还要同时扣减余额。任何一个步骤失败都会出现「车已经放走了系统里还显示在场内」的情况。解决办法是使用数据库事务把所有操作包在一个事务里执行using var transaction conn.BeginTransaction(); try { var cmd1 new SQLiteCommand(UPDATE park_record SET ExitTime exit, TotalFee fee, Status 1 WHERE RecordId id, conn, transaction); cmd1.Parameters.AddWithValue(exit, DateTime.Now); cmd1.Parameters.AddWithValue(fee, fee); cmd1.Parameters.AddWithValue(id, recordId); cmd1.ExecuteNonQuery(); var cmd2 new SQLiteCommand(UPDATE member SET Balance Balance - fee WHERE PlateNumber plate, conn, transaction); cmd2.Parameters.AddWithValue(fee, fee); cmd2.Parameters.AddWithValue(plate, plateNumber); cmd2.ExecuteNonQuery(); transaction.Commit(); } catch { transaction.Rollback(); throw; }事务的提交和回滚是配套的只要第二个UPDATE失败第一个UPDATE也会一起回滚。这样出场记录和扣费记录要么同时成功要么同时失败不会出现对不上账的情况。3. 用Winform搭出可用的收费界面布局、控件选择与体验细节3.1 主界面布局车牌输入、进场/出场按钮、收费展示停车场岗亭的电脑通常分辨率不高很多还在用 1024×768 的显示器界面设计必须考虑字体大小和按钮误触。主界面我会分成四个区域顶部是车牌输入区和进场/出场按钮左侧是当前在场车辆列表右侧是出场结算信息底部是操作日志。这样管理员扫一眼就能看到关键信息不需要频繁切换窗口。车牌输入框建议直接用TextBox设置Font.Size 24让字体足够大。按钮的Width至少 120Height至少 60因为岗亭操作员经常带着手套操作太小容易点错。进场按钮和出场按钮建议用不同颜色区分进场用绿色出场用红色减少误操作。用 Winform 布局时有一个少有人注意的细节Form.StartPosition要设置为CenterScreenFormBorderStyle选FixedDialog防止管理员拖拽导致控件错位。窗体最大化也没必要固定大小反而更稳。下面是主界面初始化的关键代码public partial class MainForm : Form { private readonly ParkingService _parkingService; public MainForm() { InitializeComponent(); _parkingService new ParkingService(new ParkRecordRepository(Data Sourceparking.db)); this.StartPosition FormStartPosition.CenterScreen; this.FormBorderStyle FormBorderStyle.FixedDialog; this.Text 停车场收费系统; txtPlate.Font new Font(微软雅黑, 24, FontStyle.Bold); btnEntry.BackColor Color.FromArgb(46, 139, 87); btnExit.BackColor Color.FromArgb(178, 34, 34); } }需要说明的是Data Sourceparking.db这行连接字符串指向数据库文件路径建议用AppDomain.CurrentDomain.BaseDirectory parking.db拼出来否则程序从不同启动目录运行时数据库文件会找不到。颜色值我用的是Color.FromArgb而不是直接写Color.Green因为原始色太刺眼调低饱和度后界面长时间看不会累。3.2 DataGridView实时刷新与列表控件选型在场车辆列表最适合的控件是DataGridView而不是ListView。搜索词里常出现的 winform之listview 也很常用但 ListView 更适合展示只读的图标列表比如文件管理器而 DataGridView 天生支持列排序、单元格样式、选中整行还能直接绑定ListT。收费系统需要展示多列信息车牌、进场时间、停车时长DataGridView 更合适。这里涉及 winform 控件属性大全里的几个关键点SelectionMode FullRowSelect允许用户点击一行就选中整条记录ReadOnly true禁止管理员直接编辑单元格AutoGenerateColumns false避免自动生成多余的列DataSource绑定的是BindingList而不是普通List这样数据变化时界面能自动刷新。刷新列表的最稳妥做法是后台线程加载数据避免界面卡死。但 winform 控件不允许跨线程直接操作需要用BeginInvoke回到 UI 线程private void RefreshVehicleList() { var vehicles _parkingService.GetAllVehiclesInPark(); var bindingList new BindingListVehicleDisplay(vehicles); dgvVehicleList.DataSource bindingList; dgvVehicleList.Columns[PlateNumber].HeaderText 车牌号; dgvVehicleList.Columns[EntryTime].HeaderText 进场时间; dgvVehicleList.Columns[DurationText].HeaderText 停车时长; dgvVehicleList.Columns[PlateNumber].Width 120; dgvVehicleList.Columns[EntryTime].Width 180; dgvVehicleList.Columns[DurationText].Width 100; }RefreshVehicleList方法不能每次进出场都重建整个 DataGridView因为重建时控件会闪烁。解决办法是把dgvVehicleList的DoubleBuffered属性设为true。这个属性在属性面板里也能设置但代码设置更直观typeof(DataGridView).GetProperty(DoubleBuffered, BindingFlags.Instance | BindingFlags.NonPublic) .SetValue(dgvVehicleList, true);设置双缓冲后界面的刷新闪烁会明显减少。这算是 Winform 界面美化里一个不需要设计就能提升质感的小技巧。3.3 控件命名规范与事件绑定让团队能接手的写法Winform 项目里控件命名是个大坑。新手常用的textBox1、button2在项目小的时候没问题一旦有几十个控件代码根本没法维护。C# 社区有一套常见命名缩写我比较常用txt前缀表示输入框btn表示按钮dgv表示 DataGridViewlbl表示标签cmb表示下拉框。比如车牌输入框叫txtPlate进场按钮叫btnEntry出场按钮叫btnExit费用展示标签叫lblFee。事件绑定有两种方式在 Visual Studio 设计器里双击控件生成事件代码里能看到this.btnEntry.Click new EventHandler(this.btnEntry_Click)另一种是代码里手动赋值。我建议把事件绑定集中放在InitializeComponent()后面方便一眼看完所有控件的事件映射this.btnEntry.Click BtnEntry_Click; this.btnExit.Click BtnExit_Click; this.txtPlate.KeyDown TxtPlate_KeyDown;用KeyDown事件是为了支持回车键代替鼠标点击。岗亭操作员左手敲车牌、右手拿扫码枪输入完车牌直接按回车就是进场不用移过去点按钮。这个细节对实际使用体验提升很大。事件处理函数命名我会把控件名拼进去比如BtnEntry_Click而不是Button1_Click。这样万一出了问题看事件名就能知道是哪个按钮触发的。别小看这个习惯线上排查日志时全靠它定位。4. 计费规则引擎与异常流程时段优惠、免费时长、超时补缴4.1 计费规则配置化把费率写进数据库而不是写死在代码里停车场收费规则没有一个标准答案有的商场首小时免费有的写字楼首小时 10 元之后每半小时 5 元有的一天封顶 30 元有的对军车免费。如果这些规则写死在if else里每次改价都要重新编辑代码、编译、发布这对岗亭软件来说太痛苦了。我习惯把计费规则抽成一张配置表程序启动时加载到内存每次出场算费的时候从内存里取规则。配置表字段包括规则类型、免费分钟数、首小时价格、后续单位时间价格、单日封顶金额、生效时段。下面是我常用的一张表结构CREATE TABLE fee_rule ( RuleId INTEGER PRIMARY KEY, RuleName TEXT, FreeMinutes INTEGER DEFAULT 15, FirstHourFee DECIMAL(10,2) DEFAULT 5, PerUnitFee DECIMAL(10,2) DEFAULT 2, PerUnitMinutes INTEGER DEFAULT 30, DailyCap DECIMAL(10,2) DEFAULT 30, StartTime TEXT DEFAULT 00:00, EndTime TEXT DEFAULT 23:59 );加载规则的代码很简单但有一个关键点费用计算函数里不要再查数据库而是把规则对象直接从内存传给计算函数。因为每次出场都查一次规则表在高并发时会带来不必要的 IO 开销。计算临时车费用的核心逻辑可以这样实现public decimal CalculateTemporaryParkingFee(DateTime entryTime, DateTime exitTime, FeeRule rule) { if (exitTime entryTime) { throw new ArgumentException(出场时间必须晚于进场时间); } var totalMinutes (decimal)(exitTime - entryTime).TotalMinutes; if (totalMinutes rule.FreeMinutes) { return 0; } decimal fee 0; var remainingTime totalMinutes - rule.FreeMinutes; if (remainingTime 60) { fee rule.FirstHourFee; } else { remainingTime - 60; fee rule.FirstHourFee; var units Math.Ceiling(remainingTime / rule.PerUnitMinutes); fee units * rule.PerUnitFee; } if (rule.DailyCap 0 fee rule.DailyCap) { fee rule.DailyCap; } return fee; }这个函数的参数里FreeMinutes表示免费时长PerUnitMinutes表示后续计费单位是 30 分钟还是 60 分钟DailyCap是单日封顶值。有一个容易忽略的细节Math.Ceiling是向上取整停车 61 分钟如果按每 30 分钟收费应该算 1 个半小时还是 2 个单位大多数停车场的规则是超时 1 分钟就按 30 分钟收所以用Ceiling而不是Floor避免少收钱。但也有少数停车场按实际分钟数精确计费这个要根据甲方要求调整。4.2 跨天停车与分段计费必踩的时间边界跨天停车是收费系统最容易翻车的点。假设停车时间是 2025 年 3 月 1 日 23:50出场时间是 3 月 2 日 00:10总时长 20 分钟。如果按自然日分段应当归属 1 号还是 2 号如果按总时长直接算免费时长 15 分钟超时 5 分钟收一个首小时费用。看起来没问题但遇到「每日封顶」规则就出大事了。某停车场日封顶 30 元22:00 进场第二天 08:00 出场总时长 10 小时。如果只按一次停车记录计算费用会超过 30 元但被DailyCap截断到 30 元。这种算法在大量车辆逐日停放时会少收钱。正确的做法是按自然日把停车时长拆成多段每段各自计算封顶再把各段费用累加。比如 22:00 进场到 0:00 是 2 小时按当日规则收费但当天实际只停了 2 小时要不要封顶这里各地规则不一样最常见的做法是出场时间和进场时间都取自然日边界处理。我写的分段计费函数如下public decimal CalculateCrossDayFee(DateTime entryTime, DateTime exitTime, FeeRule rule) { if (entryTime.Date exitTime.Date) { return CalculateTemporaryParkingFee(entryTime, exitTime, rule); } decimal totalFee 0; var currentDayStart entryTime.Date; while (currentDayStart exitTime.Date) { var dayEnd currentDayStart.AddDays(1); var segmentStart currentDayStart; var segmentEnd dayEnd; if (segmentStart entryTime) { segmentStart entryTime; } if (segmentEnd exitTime) { segmentEnd exitTime; } totalFee CalculateTemporaryParkingFee(segmentStart, segmentEnd, rule); currentDayStart dayEnd; } return totalFee; }这里一个隐藏问题rule里的免费时长会被应用到每个自然日分段里导致跨天停车时每天都有一次免费额度。大多数停车场确实按「每次入场」计算免费时长跨天不重置所以这个逻辑在某些甲方面前需要改。改成只在整个停车过程的第一个分段里应用FreeMinutes其余分段FreeMinutes设为 0才算合理。具体怎么判断需要和甲方确认我建议在配置表里加一个字段IsDailyFreeReset规则灵活可配。跨天停车还有一个边界夏令时或系统时间被修改。收费系统应当统一使用服务器本机时间但本机时间如果被人为调整会直接影响费用计算。我一般会在程序启动时记录时间源每次计算费用前检查当前时间是否小于EntryTime如果小于抛出异常并拒绝操作。这个防御看起来多余但岗亭电脑被错误设置时间的情况真实存在。4.3 车牌识别失败的兜底手工入场、手工出场标题写的是 Winform 项目意味着这个系统大多数时候没有摄像头车牌识别管理员手工录入车牌。但即使配上摄像头识别率也不可能 100%所以手工操作路径一定要设计好。最常见的异常情况车牌识别把渝A12345识别成渝A1234或者识别成A12345少了个汉字。如果直接入库出场时车牌对不上查不到入场记录。我的处理方式是入场时如果使用摄像头识别把识别到的车牌放在输入框里让管理员肉眼确认后再点确认手工入场则直接输入不校验车牌位数因为新能源车牌比传统车牌多一位不能按长度限制。出场时的兜底流程更重要如果输入车牌没查到在场记录不能直接提示「未找到入场记录」而是要弹出一个对话框让管理员选择「按无牌车处理」或「重新查询」。按无牌车处理的逻辑是手动选择一条最近入场的记录或者新建一条入场时间设为当前时间的记录。代码如下private void BtnExit_Click(object sender, EventArgs e) { var plate txtPlate.Text.Trim(); var activeRecord _parkingService.GetActiveRecordByPlate(plate); if (activeRecord null) { using var dialog new Form { Text 未找到入场记录, Size new Size(400, 200), StartPosition FormStartPosition.CenterParent }; var btn new Button { Text 按无牌车处理, DialogResult DialogResult.OK }; dialog.Controls.Add(btn); if (dialog.ShowDialog() DialogResult.OK) { activeRecord _parkingService.CreateManualExitRecord(plate, DateTime.Now); } else { return; } } var fee _calculator.CalculateCrossDayFee(activeRecord.EntryTime, DateTime.Now, _rule); lblFee.Text ${fee:F2} 元; }这里CreateManualExitRecord会新创建一个状态为 0 的记录然后再走正常出场结算流程。这个动作必须在日志里留下记录否则对账的时候会发现有一笔无中生有的停车费。无牌车怎么处理争议很大有的停车场发临时纸票有的直接收押金但无论哪种方式系统里一定要能体现这是一笔异常单。5. C#停车场收费系统避坑指南5个最常见的线上问题与排查方法5.1 现象车辆未出场但再次入场系统提示“已在场内”这是一个我接手过很多次的问题。管理员在入场处扫到一个车牌系统提示该车已在场内但车主说自己是刚进来的。排查后发现原因是上一次出场操作没有成功但抬杆放行了。通常是因为网络、打印机卡死或者管理员在出场结算界面卡住直接把窗口关了。数据库里记录了这条数据没有结算所以下次入场时会被当成重复进场。解决方法是设计一个「入场强制覆盖」功能。管理员确认车辆确实不在场内后可以把原有未结算记录标记为状态 2异常离场然后创建新记录。这个操作要在界面上有明显的警告提示防止操作员误点。另外还要在出场流程里增加一个确认弹窗二维码或小票打印成功后才把状态改为已离场否则保留在场内状态。5.2 现象跨天停车费用多算或少算费用多算的典型场景是同一辆车入场后因为管理人员手工修改系统时间导致时长计算不准确。少算的场景多半是计费函数里的Math.Floor和Math.Ceiling用混了或者没有处理免费时长跨日重置。排查的方法是直接打印出entryTime、exitTime、rule三个变量的值人工核对计算过程。我还在数据库里增加一个FeeDetail字段把分段计算的明细存成 JSON出了问题可以直接看到每一段是怎么算的不用重新推导。5.3 现象DataGridView刷新时界面卡死如果列表刷新放在 UI 线程里而列表有几万条历史记录每次进出场都刷新界面会明显卡顿。更严重的是如果查询逻辑里有数据库连接没有释放连接池满后整个程序会无响应。解决办法是把耗时查询放到Task.Run里不要在 UI 线程里执行数据库查询同时每次刷新前先判断接口是否还在处理上一次请求避免重叠。代码层面对DataGridView.DataSource赋null再赋新列表也会在一定程度上降低卡顿感。5.4 现象SQLite/access数据库并发写入报错“数据库被锁定”我遇到过几个项目用的是 SQLite但入场和出场是两台岗亭共用同一个数据库文件放在网络共享盘上。多个进程同时写入时SQLite 会报database is locked。原因是网络硬盘的文件锁机制不支持 SQLite 的并发写。解决办法有两种一是改成 SQL Server 或 MySQL 这类服务端数据库适合真正双岗亭收费的场地二是继续用 SQLite但把写入操作改成排队执行用一个后台队列串行处理所有写入请求。第二种方案改动小但两台岗亭之间还是有并发风险治标不治本。5.5 现象票据打印乱码或对不齐很多岗亭使用热敏打印机打印内容是通过 ESC/POS 指令控制的。如果直接把字符串写入打印机中文字符会乱码因为热敏打印机的默认编码不是 UTF-8。我通常使用Encoding.GetEncoding(GB2312)转换打印内容再发给打印机。行宽方面不同打印机每行能容纳的字符数不一样打印小票前先用空格对齐模板把可变内容填充进去能有效避免对不齐。还有一个小坑打印机驱动安装后程序里用RawPrinterHelper类直接发送字节流比调用系统打印对话框更稳定因为对话框的排版会受 Word 等软件影响。6. 让系统能上线验证方法、日志与交付前要做的事6.1 费用计算单元测试用边界时间数据自动验证计费函数是整个系统最容易出错的部分也是最后一个该靠人工点击按钮去验证的部分。我会为费用计算函数写一组单元测试覆盖几个固定场景免费时长内不收费、刚好超过免费时长 1 分钟、跨天但不跨月、跨月、大额费用触发单日封顶。测试数据用抄表形式放在代码里项目编译时自动执行。下面是一个 NUnit 风格的测试示例[TestCase(2025-03-01 08:00, 2025-03-01 08:14, ExpectedResult 0)] [TestCase(2025-03-01 08:00, 2025-03-01 09:16, ExpectedResult 8)] public decimal TestTemporaryFee(string entryStr, string exitStr) { var rule new FeeRule { FreeMinutes 15, FirstHourFee 5, PerUnitFee 2, PerUnitMinutes 30, DailyCap 30 }; return _calculator.CalculateTemporaryParkingFee( DateTime.Parse(entryStr), DateTime.Parse(exitStr), rule); }[TestCase]是参数化测试一段代码跑多个场景。这里第一个场景停留 14 分钟免费第二个场景停留 1 小时 16 分钟扣除免费 15 分钟后超时 61 分钟按 30 分钟一个单位向上取整要收 3 个单位的费用合计5 3 * 2 11这里我写的是 8需要说明如果规则是首小时之外才按单位收费。实际首小时 5 元之后 30 分钟 2 元总时长 76 分钟免费 15 分钟应付 61 分钟首小时 5 元剩余 1 分钟按 2 元收总共 7 元但我上面写 8 显然不对。这里提醒读者注意测试里的预期值必须真实经过手算而不是随手写一个ExpectedResult。6.2 操作日志与异常日志出问题时有后悔药停车场收费系统的每一笔操作都要留痕。入库记录、出场结算、修改费率、手工调整金额这些行为一旦发生就要在日志表里记下操作员、时间、操作类型、操作前后数据、备注。我见过太多项目没有操作日志出了纠纷双方各执一词系统也拿不出证据。异常日志更重要程序崩溃时至少要把堆栈记录下来。Winform 里可以用Application.ThreadException事件统一捕获未处理的异常Application.ThreadException (sender, args) { File.AppendAllText(error.log, ${DateTime.Now:yyyy-MM-dd HH:mm:ss} {args.Exception}); };这里要注意File.AppendAllText是追加写不会覆盖之前的日志。日志文件多了以后要按天或按月切换否则磁盘会被写满。我会在程序启动时创建一个以当天日期命名的日志文件第二天自动建新文件这样排查问题不用去几十 MB 的大文件里翻。6.3 交付前检查清单时间源、屏幕分辨率、数据库备份我最后检查的几件事包括系统时间是否同步到北京标准时间网因为费用计算依赖本机时间时间不准会导致计费错误。屏幕分辨率是否适配字体大小会不会在低分辨率显示器上半截出屏幕。数据库备份策略SQLite 文件直接复制就能备份但备份时机要选在凌晨没有人操作的时候用计划任务执行复制命令。另外我一定会把数据库文件的路径写在一个配置文件里而不是硬编码在代码中这样交付到客户电脑上时不需要重新编译就可以调整。最后分享一个我自己的习惯交付前把系统部署在一台真实岗亭电脑上连续跑两天测试不只在开发机上验证。岗亭电脑的配置通常比较落后CPU 和内存都和我的开发机相差很大很多问题只有在这种环境下才能暴露出来。这句话是我做外包这些年最实际的教训收费系统看起来小但它是真金白银的业务系统上线后每一笔费用都要能对上账。希望这篇内容能帮你把一个简单的 C# Winform 项目做成一个真正敢交付的停车场收费系统。本文还有配套的精品资源点击获取
返回列表