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

C#开发火车信息管理系统:数据库设计与事务处理实战

发布时间:2026/9/30 1:39:11

资讯中心
01
ARTICLE

C#开发火车信息管理系统:数据库设计与事务处理实战

C#开发火车信息管理系统:数据库设计与事务处理实战
简介这是一份基于C#与SQL Server的火车信息管理系统完整项目源码主要面向计算机相关专业学生、课程设计者以及初学桌面应用开发的程序员帮助理解窗体界面、ADO.NET数据库访问与业务逻辑分层。系统内置火车信息维护、时刻表管理、模拟购票、乘客信息登记、多条件查询及报表统计等功能并区分普通用户与管理员权限演示了从数据库设计到界面呈现的完整流程。压缩包共含186个文件约3.4MB核心包括50个C#源文件、22个resx界面布局文件、11个resources资源文件、1个SQL数据库脚本、2个可运行exe以及皮肤资源文件等随包附带SQL脚本与可执行程序便于快速部署验证。项目涵盖用户管理、查询等多个功能窗体并综合了SqlConnection连接管理、DataGridView数据显示与存储过程等典型技术便于对照学习。目前已有188人学习浏览该资源适合用于毕业设计参考、C#SQL Server综合实训也可作为扩展二次开发的起点。1. 火车信息管理系统C# 开发里最经典的业务闭环练习火车信息管理系统是 C# 课程设计和毕业设计里出现频率极高的题目它覆盖了用户登录、车次查询、余票计算、订票退票这一整条业务链。很多人拿到源码包的第一反应是打开 Visual Studio 按 F5结果要么数据库连不上要么登录后 DataGridView 一片空白——问题多半不在 C# 代码而在你对这套系统的数据结构没概念。这篇笔记直接用这个题目的通用实现方式带你走一遍数据库设计、核心功能代码、源码改造思路和踩坑记录。做完你会发现这套系统的价值不只是交作业而是让你把 C# 连接数据库、参数化查询、事务处理这些日常开发基本功真正打通。2. 数据库设计先行车次、订单、用户的关系搞不清楚就动手后面全是坑2.1 四张核心表和它们之间的关系火车信息管理系统无论界面怎么换核心业务都是围绕四张表转的用户表、车次表、订单表以及站点表。很多源码包会多加一张车次经停表用来表达一趟车经过多个站点的时刻表但简单版本通常只存起点站和终点站。用户表用来存登录账号和角色角色字段直接决定主界面能看到哪些菜单——管理员能改车次、查全部订单普通用户只能查询和订票。车次表存车次号、始发站、终点站、出发时间、到达时间、座位总数。这里有个关键设计座位总数减去已订票数才是余票千万不要在车次表里直接存一个余票数字段否则每次订票都要 UPDATE 两次表并发一高数据就错乱。订单表是业务核心存用户 ID、车次 ID、乘车日期、订票数量、下单时间、订单状态。站点表可以单独建也可以直接存字符串。我的建议是单独建表因为后续要做某站到某站的查询时用外键关联比字符串比较可靠得多而且改站名不用动车次表。2.2 建表 SQL 与种子数据这样写才不容易翻车拿到源码包先看有没有 SQL 脚本有的话直接执行建库。没有的话按下面这个结构建命名清晰后面写 C# 代码时不会一头雾水。CREATE DATABASE TrainDB; GO USE TrainDB; GO CREATE TABLE Users ( UserId INT IDENTITY(1,1) PRIMARY KEY, Username NVARCHAR(50) NOT NULL UNIQUE, PasswordHash NVARCHAR(64) NOT NULL, RealName NVARCHAR(20) NOT NULL, Role TINYINT NOT NULL DEFAULT 0 -- 0普通用户 1管理员 ); GO CREATE TABLE Stations ( StationId INT IDENTITY(1,1) PRIMARY KEY, StationName NVARCHAR(50) NOT NULL UNIQUE, CityName NVARCHAR(50) NOT NULL ); GO CREATE TABLE Trains ( TrainId INT IDENTITY(1,1) PRIMARY KEY, TrainNo NVARCHAR(10) NOT NULL UNIQUE, DepartStation INT NOT NULL FOREIGN KEY REFERENCES Stations(StationId), ArriveStation INT NOT NULL FOREIGN KEY REFERENCES Stations(StationId), DepartTime TIME NOT NULL, ArriveTime TIME NOT NULL, SeatCount INT NOT NULL CHECK (SeatCount 0) ); GO CREATE TABLE Orders ( OrderId INT IDENTITY(1,1) PRIMARY KEY, UserId INT NOT NULL FOREIGN KEY REFERENCES Users(UserId), TrainId INT NOT NULL FOREIGN KEY REFERENCES Trains(TrainId), TravelDate DATE NOT NULL, TicketCount INT NOT NULL CHECK (TicketCount 0), OrderTime DATETIME NOT NULL DEFAULT GETDATE(), Status TINYINT NOT NULL DEFAULT 0 -- 0已预订 1已退票 ); GO这段 SQL 里做了三件重要的事。第一PasswordHash 用了 NVARCHAR(64)对应 MD5 的 32 位十六进制字符串后面 C# 里加密后正好塞进去。第二DepartTime 和 ArriveTime 用了 TIME 类型而不是字符串这样排序和计算间隔都方便。第三Orders 表加了 Status 字段而不是退票就删记录保留订单轨迹这也是为什么订票和退票要做成更新状态而不是删除行。2.3 连接字符串与数据库附加的两种方式源码包里如果带 .mdf 数据库文件最常见的是通过 Visual Studio 的服务器资源管理器附加。但更推荐直接在 SQL Server Management Studio 里附加因为 VS 附加经常会遇到权限问题——启动 VS 的账号没有 SQL Server 数据目录的写权限。连接字符串建议统一放到 App.config 里别在代码里写死connectionStrings add nameTrainDB connectionStringServerlocalhost\SQLEXPRESS;DatabaseTrainDB;User Idsa;Password你的密码; providerNameSystem.Data.SqlClient/ /connectionStringsC# 里读取这个配置的方法是ConfigurationManager.ConnectionStrings[TrainDB].ConnectionString记得在项目里引用 System.Configuration 程序集。数据库相关代码单独放在一个 DBHelper 类里后面所有窗体都通过它拿 SqlConnection改连接的时候只需要动 App.config不用每个窗体翻一遍。3. 核心功能实现登录、查询、订票的 C# 代码怎么搭3.1 登录模块用参数化查询和 MD5 加密挡住最常见的攻击很多旧源码的登录判断是直接拼接 SQL比如string sql SELECT * FROM Users WHERE Username txtUser.Text AND Password txtPwd.Text ;。这是最容易被打脸的地方输入一个 OR 11 --就能绕过登录。正确做法是参数化查询加上密码哈希。using System.Data; using System.Data.SqlClient; using System.Security.Cryptography; using System.Text; public static class SecurityHelper { public static string Md5Hash(string input) { using (MD5 md5 MD5.Create()) { byte[] bytes Encoding.UTF8.GetBytes(input); StringBuilder sb new StringBuilder(); foreach (byte b in md5.ComputeHash(bytes)) sb.Append(b.ToString(x2)); return sb.ToString(); } } } public bool ValidateLogin(string username, string rawPassword, out string role) { role string.Empty; string connStr ConfigurationManager.ConnectionStrings[TrainDB].ConnectionString; string sql SELECT Role FROM Users WHERE Username uname AND PasswordHash pwd; using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(uname, username); cmd.Parameters.AddWithValue(pwd, SecurityHelper.Md5Hash(rawPassword)); conn.Open(); object result cmd.ExecuteScalar(); if (result ! null) { role result.ToString(); return true; } return false; } }参数化查询的逻辑是让 ADO.NET 把uname和pwd当作参数传给 SQL Server而不是拼进 SQL 文本这样注入语句就失去了作用。Md5Hash 方法把密码转成 32 位小写十六进制字符串数据库里存的始终是哈希值即便数据库泄露也不会直接暴露明文密码。用using包裹 SqlConnection 和 SqlCommand 是必须的ExecuteScalar()返回查询结果的第一行第一列正好用来取出单个值——角色号。3.2 车次查询与余票计算LEFT JOIN 比子查询更稳余票计算的本质是车次总座位数 - 某天已订出的票数。有些源码包直接在 Orders 表里存了一个BuyCount然后在车次表里扣减座位数这种设计在高并发订同一趟车的票时会算错。正确思路是用聚合查询实时计算。public DataTable SearchTrains(string departCity, string arriveCity, string travelDate) { string sql SELECT t.TrainNo, ds.StationName AS DepartStation, ar.StationName AS ArriveStation, CONVERT(VARCHAR(5), t.DepartTime, 108) AS DepartTime, CONVERT(VARCHAR(5), t.ArriveTime, 108) AS ArriveTime, t.SeatCount - ISNULL(bo.BookedCount, 0) AS RemainSeats FROM Trains t INNER JOIN Stations ds ON t.DepartStation ds.StationId INNER JOIN Stations ar ON t.ArriveStation ar.StationId LEFT JOIN ( SELECT TrainId, SUM(TicketCount) AS BookedCount FROM Orders WHERE TravelDate date AND Status 0 GROUP BY TrainId ) bo ON t.TrainId bo.TrainId WHERE ds.CityName departCity AND ar.CityName arriveCity AND t.SeatCount ISNULL(bo.BookedCount, 0) ORDER BY t.DepartTime; using (SqlConnection conn new SqlConnection(connStr)) using (SqlDataAdapter da new SqlDataAdapter(sql, conn)) { da.SelectCommand.Parameters.AddWithValue(date, travelDate); da.SelectCommand.Parameters.AddWithValue(departCity, departCity); da.SelectCommand.Parameters.AddWithValue(arriveCity, arriveCity); DataTable dt new DataTable(); da.Fill(dt); return dt; } }这段 SQL 的逻辑说清楚先按出发站和到达站对应站点表的城市名筛出符合条件的车次再通过 LEFT JOIN 把当天已订票数关联进来。LEFT JOIN 而不是 INNER JOIN 的原因是——某趟车如果当天还没有任何订单INNER JOIN 会把这个车次丢掉LEFT JOIN 才能用 ISNULL 把 BookedCount 补成 0。CONVERT(VARCHAR(5), ..., 108)是把 TIME 类型转成HH:mm格式避免 DataGridView 里显示一堆零点零分。最后的t.SeatCount ISNULL(bo.BookedCount, 0)是只显示有余票的车这个条件放在 WHERE 里比放在 C# 里判断更合适减少网络传输的数据量。3.3 订票与退票事务是保证数据不出错的地基订票操作涉及两步向 Orders 表插入一条记录同时车次表的余票要变化。两步操作如果中间崩了数据库就处于不一致状态——票订出去了但座位没扣或者反过来。解决办法是把它包在一个 SqlTransaction 里要么都成功要么都回滚。public bool BookTicket(int userId, int trainId, string travelDate, int count) { string connStr ConfigurationManager.ConnectionStrings[TrainDB].ConnectionString; string insertSql INSERT INTO Orders (UserId, TrainId, TravelDate, TicketCount, Status) VALUES (uid, tid, date, count, 0); using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); SqlTransaction tx conn.BeginTransaction(); try { using (SqlCommand cmd new SqlCommand(insertSql, conn, tx)) { cmd.Parameters.AddWithValue(uid, userId); cmd.Parameters.AddWithValue(tid, trainId); cmd.Parameters.AddWithValue(date, travelDate); cmd.Parameters.AddWithValue(count, count); cmd.ExecuteNonQuery(); } tx.Commit(); return true; } catch { tx.Rollback(); return false; } } }订票时为什么不扣车次表的 SeatCount因为余票是实时聚合算出来的插入订单本身就等于扣掉了余票再去 UPDATE Trains 表反而会造成双重扣减。事务在这里的作用是保证 Insert 和后续其他操作比如写日志能一起成功或一起失败实际项目中还要在 Insert 前用SELECT ... FOR UPDATE或加条件判断防止超卖。简单版系统可以接受十万级用户量但并发超过一百人同时抢同一趟车就需要引入锁或乐观并发控制这是后话。4. 把源码包改造成自己的项目文件清单、命名空间、UI 替换4.1 拿到源码包先看哪几个文件解压一个火车信息管理系统的源码包不要急着开 .sln先看文件夹根目录。通常会有TrainSystem.sln、TrainSystem/源码目录、database/或db/目录放 SQL 脚本或 .mdf 文件、README.txt或使用说明.docx。打开源码目录后按这个顺序看文件App.config或app.config连接字符串、DBHelper.cs或SqlHelper.cs数据库访问封装、LoginForm.cs登录窗体、MainForm.cs主窗体。如果这几类文件都存在说明项目结构是完整的可以进入下一步调试。如果连 App.config 都没有连接字符串大概率写在某个窗体的构造函数里需要全局搜索SqlConnection或connStr关键词找出来替换。所有源码文件注意检查是不是以 UTF-8 with BOM 编码保存的。如果是无 BOM 的 UTF-8Visual Studio 默认按 GB2312 解析中文注释和字符串字面量全部乱码。解决方法是在 VS 里打开文件后选择文件 - 另存为 - 编码保存选 UTF-8 with BOM。4.2 用全局替换改掉命名空间和数据库连接的步骤拿到别人的源码第一件该做的事情不是看逻辑而是把命名空间改成自己的避免以后和别的项目混在一起时分不清类是谁的。在 VS 里按CtrlShiftH打开在文件中替换把默认命名空间比如TrainManager替换成你自己的名字比如MyTrainSystem替换范围选整个解决方案。然后处理连接字符串。如果源码用的是写死在代码里的字符串全局搜索data source或server找到所有出现位置一一替换成你的 SQL Server 实例名。如果源码用的是 App.config直接改add nameTrainDB connectionString.../里的 Server 和密码。需要注意 SQL Server 的认证模式如果只改了连接字符串但还报用户登录失败说明你的 SQL Server 是 Windows 身份认证模式需要在 SSMS 里启用 sa 账号或改用Integrated SecurityTrue。最后一步是检查目标框架。老项目可能是 .NET Framework 4.0你本机装的是 4.8通常能直接编译。但如果是 .NET Core 或 .NET 5 的新模板System.Data.SqlClient 需要换成 Microsoft.Data.SqlClient 包否则会报命名空间不存在。这条坑在从网上下载的老源码包上非常常见。4.3 把 WinForms 迁移到 WPF 的代价评估网上能下载到的火车信息管理系统源码八成是 WinForms 写的因为它制作门槛低、教程多。但如果你打算在简历里把这个项目写进去WinForms 在 2024 年的技术面试里几乎没有加分项——面试官更愿意看到 WPF 或 MAUI 的项目。从 WinForms 迁移到 WPF 的真实代价是什么WinForms 的事件驱动模型和 WPF 的绑定模型完全不同。WinForms 里dataGridView1.DataSource dt;直接生效但 WPF 里必须把 DataTable 转成 ObservableCollection 才能让 DataGrid 自动刷新否则数据改了界面纹丝不动。迁移时唯一的安慰是数据库访问层DBHelper、DAL 类可以原封不动拿过来因为 WPF 和 WinForms 共享相同的 ADO.NET 基础。界面层需要重写但业务层和数据层保留率在 70% 以上。如果只是为了交作业别折腾迁移如果想拿它找 C# 开发工作值得花两周时间把 UI 层用 WPF 重写一遍这也是一个能写在简历上的加分点。5. 火车信息管理系统的 5 个高发坑现象、原因、解决5.1 数据库附加失败报错 9004 或提示文件被占用现象SSMS 里附加 TrainDB.mdf 时报错无法打开物理文件拒绝访问或者错误代码 9004。原因通常是当前 Windows 账号对该文件所在的文件夹没有写权限或者该数据库文件之前是从别的电脑拷贝过来且 NTFS 权限里带着原作者的安全标识符。解决右键 .mdf 文件 - 属性 - 安全 - 编辑 - 添加 Authenticated Users 并勾选完全控制。如果仍然失败检查是否已经有一个同名数据库被附加过先在 SSMS 里分离或删除旧的再重新附加。不要网上去找传说中的修复工具这个错误 90% 是权限问题。5.2 订票后余票没有减少重新登录又正常了现象在界面里订完两张票回到查询界面发现余票数纹丝不动。原因大概率是你在同一个窗体里用了两个 SqlConnection 对象一个是查询用的一个是订票用的订票事务提交后查询窗体的 DataGridView 没有重新执行查询而是拿着上一次的 DataTable 继续显示。解决订票成功的回调里重新调用查询方法不要用dataGridView1.Refresh()——它只会刷新界面显示不会重新查数据库。正确做法是dataGridView1.DataSource SearchTrains(...);把整个 DataSource 重新赋值。同理退票操作也要重新绑定数据源。5.3 跨天车次的到达时间计算错误现象查询 G123 次列车23:50 出发、次日 06:30 到达界面上显示到达时间是 06:30但如果你在 C# 里做arriveTime - departTime计算历时得到的是负数。原因TIME 类型的数据单独拎出来做减法不包含日期信息时间跨度超过午夜就会出现负值。很多源码包在计算运行时长这个功能时踩这个坑。解决如果只需要显示出历时 6 小时 40 分钟用DateTime拼接日期后计算。在 SQL 里可以这样处理先判断ArriveTime DepartTime成立则给到达时间加一天。C# 里读取 TIME 类型后转成TimeSpan负值判断后手动加上TimeSpan.FromDays(1)。5.4 DataGridView 绑定 DataTable 后列名变成英文或乱码现象绑定DataTable后DataGridView 的列标题显示的是TrainNo、DepartStation而不是中文。原因DataGridView 默认直接用 DataTable 的列名做标题。解决办法有两个一是在 SQL 里用AS起中文别名SELECT TrainNo AS 车次号, DepartStation AS 出发站二是在 DataGridView 的 ColumnHeaderText 属性里手动改。注意别用CELLFORMAT事件去改列头那会在每次重绘时触发性能虽无大碍但维护起来太别扭。5.5 中文乱码连接字符串和 SQL 文件编码都要查现象界面按钮正常但数据库里存的汉字全是???或者从数据库读出来在 MessageBox 里显示正常、写入 DataGridView 就乱码。原因第一是连接字符串里少了Character Set或编码设置字段——SQL Server 用Language关键字控制比如LanguageChinese PRC第二是建库时排序规则Collation选错了SQL Server 默认排序规则是SQL_Latin1_General_CP1_CI_AS它对中文是按字节存储的只要客户端连接代码用nvarchar并且参数化查询传入N前缀的字符串就不会出问题。解决表字段统一用NVARCHAR而不是VARCHAR所有字符串参数在 SqlCommand 里声明SqlDbType.NVarChar然后连接字符串里不要画蛇添足加字符集项。NVARCHAR是 Unicode 存储天然不受代码页影响。如果已经存入???只能把字段改成NVARCHAR后重新插入没有别的后悔药。6. 从课设到交付给系统加上日志、备份和验收测试把 demo 变成能写进简历的项目最重要的一个动作是加操作日志。在 DBHelper 里加一个WriteLog(string action, string detail)方法在订票、退票、改车次这些关键操作后调用把用户名和时间写入日志表。面试时这一条细节能说明你有数据安全意识不是只会增删改查。其次是数据库备份SSMS 里维护计划或者直接用BACKUP DATABASE TrainDB TO DISKD:\backup\TrainDB.bak一条命令定时跑每晚会自动生成备份文件这比手动导出 .mdf 可靠得多。验收时可以写一组测试数据注册两个用户一个管理员一个普通用户分别测登录越权普通用户点不了管理菜单、查无余票车次、退票后余票恢复全部通过再交付。我的习惯是每改完一个模块就重新编译并跑一遍全链路很多时候问题出在改完 A 模块忘了 B 模块引用了同一个 DataTable 的列别名。改代码时在文件头部留下修改记录注释两行字说明改了哪里、为什么改一个月后你自己回来看代码都能省下大量时间。这套逻辑对火车信息管理系统适用对任何 C# 数据库项目也适用。希望这篇笔记能让你把这个源码包真正吃透而不是停留在按 F5 跑通的阶段。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

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

03
WHY YAOTU

想打造同款高转化官网?

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

◈

场景化定制

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

◐

营销型架构

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

▲

全周期服务

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

免费获取你的建站方案

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