基于C# WinForms与SQL Server的图书管理系统开发与避坑指南
简介这是一份基于C#与SQL Server的图书管理系统课程设计完整源码包面向正在完成期末大作业或希望掌握数据库应用开发的学生。资源共187个文件以C#源文件cs、窗体资源文件resx、SQL数据库文件mdf/ldf及项目配置文件为主压缩包仅2.62MB便于快速下载与部署。cs文件涵盖Windows Forms界面、事件处理与数据访问逻辑resx与resources保存界面布局及资源mdf/ldf为可直接附加的数据库另含项目工程与运行配置结构完整。已有692人学习使用。项目采用VS开发环境通过ADO.NET实现图书信息增删改查包含Books等数据表设计业务逻辑与数据访问分层清晰并附有db.cs数据库连接示例及运行配置说明。使用时结合自身SQL Server调整连接字符串、附加数据库后即可运行适合作为课程设计模板或C#入门进阶的参考项目。1. 课程设计里的图书管理系统到底要先做什么才能不翻车你从老师或网上拿到一个“课程设计-基于C#的图书管理系统源码数据库.zip”解压后看到几个.cs文件、一个bin文件夹和一两张数据库脚本。双击LibraryManagement.sln按了F5结果不是报“找不到服务器”就是“无法连接数据库”。这门课程设计的本质是用C# WinForms搭一个前台界面配合SQL数据库完成图书、读者和借阅管理覆盖增删改查和借还书流程。它不算大但足够把数据库设计、事务和界面绑定这几块基础功一起考掉。这篇文章按“数据库先行界面随后业务兜底”的顺序把整套实现的每一步都拆给你看顺带把最容易在答辩演示时翻车的坑提前踩平。2. 从数据模型到C#项目结构把数据库脚本先立起来一个图书管理系统如果一上来就画窗体十有八九做到一半要推倒重来。原因很简单界面是水数据模型是渠没有渠水只能漫。这部分先把数据库结构和连接串定下来之后再写代码才会顺。2.1 技术栈选择C# WinForms搭配哪种数据库最省事课程设计通常不允许你用控制台或者WPFWinForms是最常见的选择因为拖控件就能出界面老师看着也像那么回事。数据库方面主要有两个流派SQL Server和MySQL。我一般建议用SQL Server Express LocalDB。理由是Visual Studio一般自带LocalDB连接串用AttachDbFilename指向一个.mdf文件就行不需要额外安装服务如果老师要求用独立数据库再换成SQLEXPRESS实例。MySQL也可以但要装服务、装驱动对换电脑演示来说不稳定课程设计场景下反而是给自己加活。所以下面统一以SQL Server为例子脚本和代码都是System.Data.SqlClient。2.2 建库建表图书、读者、借阅三张表的最小模型图书管理系统的核心不是“管理图书”而是“借还书”。所以三张表不能少Books保存图书和库存Readers保存读者信息BorrowRecords保存借阅流水。库存不要用TotalCopies做加减要单独放一个可借数量字段否则借还书时你得去扫描历史记录算剩余量那是给自己挖坑。CREATE DATABASE LibraryDB; GO USE LibraryDB; GO CREATE TABLE Books ( BookID INT IDENTITY(1,1) PRIMARY KEY, Title NVARCHAR(200) NOT NULL, Author NVARCHAR(100) NOT NULL, Publisher NVARCHAR(100) NULL, PublishYear INT NULL, TotalCopies INT NOT NULL DEFAULT 1, AvailableCopies INT NOT NULL DEFAULT 1, CreatedAt DATETIME DEFAULT GETDATE() ); CREATE TABLE Readers ( ReaderID INT IDENTITY(1,1) PRIMARY KEY, ReaderName NVARCHAR(50) NOT NULL, Phone NVARCHAR(20) NULL, RegistrationDate DATETIME DEFAULT GETDATE() ); CREATE TABLE BorrowRecords ( BorrowID INT IDENTITY(1,1) PRIMARY KEY, ReaderID INT NOT NULL, BookID INT NOT NULL, BorrowDate DATETIME DEFAULT GETDATE(), DueDate DATETIME NOT NULL, ReturnDate DATETIME NULL, FineAmount DECIMAL(10,2) DEFAULT 0, CONSTRAINT FK_Borrow_Reader FOREIGN KEY (ReaderID) REFERENCES Readers(ReaderID), CONSTRAINT FK_Borrow_Book FOREIGN KEY (BookID) REFERENCES Books(BookID) );这里所有文本字段都用NVARCHAR而不是VARCHAR因为VARCHAR存中文在SQL Server里受排序规则影响很容易在显示时变成乱码。Identity自增主键省去手动编号的麻烦。外键约束保证不会出现“某条借阅记录指向不存在的书”这种脏数据。AvailableCopies默认值设成1但实际插入图书时最好显式给值。你可能觉得三张表太简单但课程设计评分看重的是完整演示流程能加图书、加读者、借书、还书、查记录这就是基本盘。等这五条跑通还有余力再考虑加“图书分类表”“管理员表”“罚款明细表”。2.3 连接串与App.config让换电脑不再改代码很多学生的代码里直接写死serverlocalhost;uidsa;pwd123456这就是换电脑演示翻车的头号原因。正确做法是把连接串放到App.config配置文件里这样别人拿到源码时改一行就行不用动C#代码。?xml version1.0 encodingutf-8 ? configuration connectionStrings add nameLibraryDb connectionStringData Source(LocalDB)\MSSQLLocalDB;AttachDbFilename|DataDirectory|\LibraryDB.mdf;Integrated SecurityTrue;MultipleActiveResultSetsTrue; providerNameSystem.Data.SqlClient / /connectionStrings startup useLegacyV2RuntimeActivationPolicytrue supportedRuntime versionv4.0 sku.NETFramework,Versionv4.7.2 / /startup /configuration注意|DataDirectory|会指向Visual Studio设置的项目目录所以.mdf文件放项目根目录就行。上面还加了MultipleActiveResultSetsTrue后面做借书操作时如果在同一个连接上先查数据再更新没有这个参数可能会报“已有打开的连接”。读取连接串时用ConfigurationManager需要在项目引用里手动加System.Configuration.dll新手经常找半天这个坑。后面写的DbHelper类就是从这个配置里取连接串。3. 用C#把增删改查封装成通用数据访问层数据库建好了现在要写C#代码。但千万别把SqlConnection和SqlCommand直接撒在窗体的按钮事件里。你当然能跑通但笛卡尔积式的粘贴复制会害死你。先封装一个数据访问层之后每个界面都调它。3.1 为什么需要DAL层而不是把SqlConnection写在窗体里课程设计代码里最常见的问题是“查询图书”的逻辑写在Form1查询读者的逻辑写在Form2两者各写一套连接对象和命令对象。结果就是后期改连接串要改十个地方出错了你在窗体代码里翻半天。我给的建议是新建一个DbHelper.cs专门管连接和命令。窗体只负责传SQL和参数拿DataTable或int结果去显示。这样做的好处有三点第一连接串只在DbHelper里读一次第二SQL参数化统一在一个地方完成不容易漏掉参数第三换数据库驱动时只改一个类。3.2 DbHelper一个支持增删改查的小工具类这个类用using释放资源避免连接泄漏。方法只有三个ExecuteNonQuery执行增删改ExecuteScalar返回单个值ExecuteReader返回数据流。把它当成一个安卓充电头什么设备来了都能插。using System; using System.Configuration; using System.Data; using System.Data.SqlClient; public static class DbHelper { private static readonly string connStr ConfigurationManager.ConnectionStrings[LibraryDb].ConnectionString; public static int ExecuteNonQuery(string sql, SqlParameter[] parameters null) { using (var conn new SqlConnection(connStr)) using (var cmd new SqlCommand(sql, conn)) { if (parameters ! null) { cmd.Parameters.AddRange(parameters); } conn.Open(); return cmd.ExecuteNonQuery(); } } public static object ExecuteScalar(string sql, SqlParameter[] parameters null) { using (var conn new SqlConnection(connStr)) using (var cmd new SqlCommand(sql, conn)) { if (parameters ! null) { cmd.Parameters.AddRange(parameters); } conn.Open(); return cmd.ExecuteScalar(); } } public static SqlDataReader ExecuteReader(string sql, SqlParameter[] parameters null) { SqlConnection conn new SqlConnection(connStr); SqlCommand cmd new SqlCommand(sql, conn); if (parameters ! null) { cmd.Parameters.AddRange(parameters); } conn.Open(); return cmd.ExecuteReader(CommandBehavior.CloseConnection); } }ExecuteReader返回的SqlDataReader用了CommandBehavior.CloseConnection意味着当调用方执行reader.Close()时对应的连接也会跟着关闭这样不用在窗体里再记一个连接对象。ExecuteScalar常用于查“是否存在”和“库存还剩多少”。使用时注意查询数值列返回的是object要转成对应类型。参数化是这里最重要的一点。永远不要拼字符串比如写WHERE BookID txtBookId.Text。那样只要输入一个1 or 11演示现场就成了SQL注入翻车现场。上面的代码接收SqlParameter[]数组调用时用AddWithValue或显式new SqlParameter都行但下面会告诉你AddWithValue其实也有坑。3.3 图书管理窗体用DataGridView和BindingSource绑定数据封装好了DbHelper下面就到了窗体和数据打交道的一步。图书管理页面要做的是把图书列表显示出来并能对单行做修改和删除。用DataGridView绑定一个DataTable是最省心的方式。private void LoadBooks() { string sql SELECT BookID AS 编号, Title AS 书名, Author AS 作者, Publisher AS 出版社, PublishYear AS 出版年份, TotalCopies AS 总库存, AvailableCopies AS 可借数量 FROM Books ORDER BY BookID; using (SqlDataReader reader DbHelper.ExecuteReader(sql)) { DataTable table new DataTable(); table.Load(reader); BindingSource bindingSource new BindingSource(); bindingSource.DataSource table; dataGridView1.DataSource bindingSource; } }table.Load(reader)会把SqlDataReader的数据一次性填进DataTable之后你就可以关闭reader。BindingSource是个中间层当DataGridView排序或筛选时底层的DataTable不会被弄脏。这里的SQL直接把列起中文别名这样界面上不用额外映射老师看起来也直观。如果你使用DataGridView的自动生成列记得把不需要的列设置为Visiblefalse比如BookID。修改和删除时你从当前行取原始ID值别用显示出来的“编号”因为它是自增列但也可以作为标识。下面这段是删除按钮的写法private void btnDelete_Click(object sender, EventArgs e) { if (dataGridView1.CurrentRow null) return; int bookId Convert.ToInt32(dataGridView1.CurrentRow.Cells[编号].Value); string sql DELETE FROM Books WHERE BookID BookID; SqlParameter[] parameters { new SqlParameter(BookID, bookId) }; int rows DbHelper.ExecuteNonQuery(sql, parameters); if (rows 0) { LoadBooks(); } }这里必须先把BookID隐藏列取出来。你要是用Cells[书名].Value去删那等于是按名字删重名学生一多就误删。整个窗体改动的思路就是改DataGridView单元格、拼接UPDATE语句、调用DbHelper。4. 借书还书流程最容易被老师追问的业务逻辑课程设计答辩时老师一般不看你查图书列表他一定会点“借书”和“还书”然后追问“数据怎么保证一致性”。这一章是整篇的硬骨头。4.1 借书流程库存检查与事务一个都不能少借书这个动作至少涉及三件事检查读者存在、检查这本书还有没有库存、插入借阅记录的同时把可借库存减一。这三步必须在一个事务里否则你先减库存后插记录插记录失败时库存就少了。下面这段代码用DbHelper.ConnectionString重新打开连接再用BeginTransaction包住所有步骤。public void BorrowBook(int readerId, int bookId, int daysToBorrow 30) { string checkReaderSql SELECT COUNT(1) FROM Readers WHERE ReaderIDReaderID; string checkBookSql SELECT AvailableCopies FROM Books WHERE BookIDBookID; string borrowSql INSERT INTO BorrowRecords(ReaderID, BookID, BorrowDate, DueDate) VALUES(ReaderID, BookID, BorrowDate, DueDate); string updateStockSql UPDATE Books SET AvailableCopiesAvailableCopies-1 WHERE BookIDBookID; using (SqlConnection conn new SqlConnection(DbHelper.ConnectionString)) { conn.Open(); SqlTransaction tx conn.BeginTransaction(); try { // 1. 检查读者 SqlCommand cmd new SqlCommand(checkReaderSql, conn, tx); cmd.Parameters.AddWithValue(ReaderID, readerId); int readerCount (int)cmd.ExecuteScalar(); if (readerCount 0) throw new Exception(读者不存在); // 2. 检查库存 cmd new SqlCommand(checkBookSql, conn, tx); cmd.Parameters.AddWithValue(BookID, bookId); int available Convert.ToInt32(cmd.ExecuteScalar()); if (available 0) throw new Exception(库存不足); // 3. 插入借阅记录 cmd new SqlCommand(borrowSql, conn, tx); cmd.Parameters.AddWithValue(ReaderID, readerId); cmd.Parameters.AddWithValue(BookID, bookId); cmd.Parameters.AddWithValue(BorrowDate, DateTime.Now); cmd.Parameters.AddWithValue(DueDate, DateTime.Now.AddDays(daysToBorrow)); cmd.ExecuteNonQuery(); // 4. 减库存 cmd new SqlCommand(updateStockSql, conn, tx); cmd.Parameters.AddWithValue(BookID, bookId); cmd.ExecuteNonQuery(); tx.Commit(); } catch { tx.Rollback(); throw; } } }这段代码里有两个地方要特别说明。第一cmd每次都被重新赋值但都指定了conn和tx这样事务上下文是一致的。第二DateTime.Now.AddDays默认带时间但如果你只想按天算建议用DateTime.Now.Date否则日期显示到时分秒老师看着会觉得你故意的。还有AddWithValue这个老朋友它在传日期和时间时会根据数据库列类型推断但推断经常出错。课程设计里应付排查问题不大但我个人还是建议写cmd.Parameters.Add(DueDate, SqlDbType.DateTime)来显式声明类型。你现在偷的懒会在某个奇怪的时间点变成玄学bug。4.2 还书流程日期取整与罚款金额计算还书要做的事比借书还多一步更新归还日期、计算逾期罚款、把可借库存加回去。罚款逻辑通常是“超过截止日期按天算每天0.5元”。public decimal ReturnBook(int borrowId) { string selectSql SELECT BookID, DueDate, ReturnDate FROM BorrowRecords WHERE BorrowIDBorrowID; string updateSql UPDATE BorrowRecords SET ReturnDateReturnDate, FineAmountFineAmount WHERE BorrowIDBorrowID; string restoreSql UPDATE Books SET AvailableCopiesAvailableCopies1 WHERE BookIDBookID; using (SqlConnection conn new SqlConnection(DbHelper.ConnectionString)) { conn.Open(); SqlTransaction tx conn.BeginTransaction(); try { int bookId; DateTime dueDate; SqlCommand cmd new SqlCommand(selectSql, conn, tx); cmd.Parameters.AddWithValue(BorrowID, borrowId); using (SqlDataReader reader cmd.ExecuteReader()) { if (!reader.Read()) throw new Exception(借阅记录不存在); if (reader[ReturnDate] ! DBNull.Value) throw new Exception(这本书已经归还了); bookId (int)reader[BookID]; dueDate (DateTime)reader[DueDate]; } DateTime returnDate DateTime.Now.Date; int overdueDays (returnDate - dueDate).Days; decimal fine overdueDays 0 ? overdueDays * 0.5m : 0m; cmd new SqlCommand(updateSql, conn, tx); cmd.Parameters.AddWithValue(ReturnDate, returnDate); cmd.Parameters.AddWithValue(FineAmount, fine); cmd.Parameters.AddWithValue(BorrowID, borrowId); cmd.ExecuteNonQuery(); cmd new SqlCommand(restoreSql, conn, tx); cmd.Parameters.AddWithValue(BookID, bookId); cmd.ExecuteNonQuery(); tx.Commit(); return fine; } catch { tx.Rollback(); throw; } } }这里的关键是(returnDate - dueDate).Days两个DateTime相减得到TimeSpanDays是整天数。如果dueDate带着时间returnDate也带了时间你会在边界上差一天。所以还书日期强制取.Date借书时的截止日期也建议在借书方法里写DateTime.Now.Date.AddDays(30)。罚款的计算是业务规则老师如果要求不罚款或按周罚款你自己改系数就行。不要在数据库里存“逾期天数”这种派生字段因为计算逻辑一变历史数据就废了。4.3 用GridView展示借阅记录并处理外键显示借阅记录表里只有ReaderID和BookID界面不能直接显示这两个数字要关联图书表显示书名、关联读者表显示姓名。SQL用JOIN搞定不要用子查询string sql SELECT b.BorrowID AS 借阅编号, r.ReaderName AS 读者姓名, bk.Title AS 书名, b.BorrowDate AS 借书日期, b.DueDate AS 应还日期, b.ReturnDate AS 还书日期, b.FineAmount AS 罚款 FROM BorrowRecords b INNER JOIN Readers r ON b.ReaderID r.ReaderID INNER JOIN Books bk ON b.BookID bk.BookID ORDER BY b.BorrowID;这里我用INNER JOIN只显示有效读者和有效图书的记录。如果你用了“软删除”方案这种查询可能还涉及过滤但课程设计阶段不用过度设计。DataGridView会自动生成这些列但日期列建议你设置DefaultCellStyle.Format yyyy-MM-dd不然显示出来是一长串2025-04-01 00:00:00难看。5. 课程设计避坑指南五个让演示现场翻车的细节这一章不是理论是我和不少同学踩过坑之后的血泪经验。每一条都是先讲现象再给原因最后是解决。5.1 现象本机跑得好换电脑就连不上数据库答辩现场借了老师的电脑运行程序弹出“在建立与服务器的连接时出错”。原因通常是连接串里的Data Source指向了你自己电脑的服务器实例名或(LocalDB)路径而目标电脑上根本没这个实例。还有一个隐藏原因.mdf文件被你放到了bin\Debug之外|DataDirectory|定位不到数据库。解决把数据库文件复制到项目根目录确保App.config中的连接串使用|DataDirectory|同时把.mdf文件在“解决方案资源管理器”里的“复制到输出目录”改成“如果较新则复制”。这样每次生成bin目录里都会带一份数据库。如果还是不行就在生成后手动把数据库附加到目标电脑的SQL Server实例并改连接串为服务器实例名。答辩前我一般会做一次“拆箱演练”把项目复制到另一个目录重新编译运行看能不能起来。5.2 现象图书标题显示成“”或乱码这个坑在SQL Server里很典型。原因分三种一是表字段用了VARCHAR而不是NVARCHAR插入中文时因为排序规则和代码页不一致变成问号二是插入数据时SQL字符串本身编码不对比如你在C#代码里拼INSERT INTO Books Title红楼梦其实已经错了三是在连接串里少了Character Set相关设置这是MySQL的坑SQL Server上不常见。解决建表字段一律用NVARCHAR插入语句最好参数化避免字符串常量在编译时被错误编码SQL脚本文件保存时编码选UTF-8 with BOM用Visual Studio打开保存一下就行。如果已经存了乱码只能删掉重插。所以设计表结构时就要做对不要靠显示层补救。5.3 现象删除读者时提示“与外键约束冲突”图书管理系统的借阅记录关联了读者表。你在读者管理界面选了一个有借阅记录的人点删除数据库自然不让你删因为BorrowRecords里的外键还指着它。原因你没有处理关联数据违反了引用完整性约束。解决常见做法是“软删除”给Readers表加一个IsDeleted BIT DEFAULT 0字段删除时执行UPDATE Readers SET IsDeleted1 WHERE ReaderIDReaderID查询时加上WHERE IsDeleted0。这样做既避免了外键冲突又保留了借阅历史。另一个思路是删除前先检查该读者有没有未归还记录有就提示“该读者有未还书籍不能删除”这比软删除更符合图书馆业务。我一般推荐两个方法结合先提示未还书再执行软删除。5.4 现象还书后计算逾期天数总比预想的少一天或多一天比如今天正好是截止日期还书时系统却算你逾期1天。原因是DueDate是DATETIME存的是2025-04-30 00:00:00而今天的日期是2025-04-30 10:35:00两者相减得到不足一天但Days属性会取整。实际上同一天不应罚款。解决在还书方法里把DateTime.Now.Date与dueDate.Date相减。你还可以在数据库生成脚本中给DueDate设为date类型而不是datetimeSQL Server支持date类型只存日期不存时间。这样C#里读到的是DateTime类型但只要比较前都.Date就稳定了。这也是我要把“日期取整”当作单独一条列出来的原因因为它在借书和还书两处都有坑。5.5 现象DataGridView里改了单元格点保存没反应或数据丢新手最容易犯的错误是把DataGridView当成表格控件以为编辑完就自动写数据库。实际上它只是内存里显示数据要保存必须自己写UPDATE语句。解决绑定CellEndEdit事件在该事件里取当前行的各字段值调用DbHelper.ExecuteNonQuery执行UPDATE。也可以用数据适配器SqlDataAdapter自动生成更新语句但课程设计里不建议因为自动生成的UPDATE语句会包含所有列当你需要只更新某几个列时反而难控制。更简单的做法点击“保存”按钮时遍历DataTable的Rows对状态为Modified的行逐条更新。注意如果你的DataGridView里有些列是关联查询出来的别名比如“读者姓名”这些列不能直接更新你得把它们设为只读。6. 答辩前的验证清单让数据自己说话做完功能只是第一步你还要能证明它“对啊”。我自己的习惯是准备两份东西一份是干净的数据初始化脚本一份是手动的场景测试脚本。先准备一个InitData.sql在里面插入两个读者、三本图书其中一本故意设置AvailableCopies0方便演示“库存不足”的场景。答辩时先运行这个脚本保证每次演示起点一致。INSERT INTO Readers (ReaderName, Phone) VALUES (N张三, N13800138000); INSERT INTO Readers (ReaderName, Phone) VALUES (N李四, N13900139000); INSERT INTO Books (Title, Author, Publisher, PublishYear, TotalCopies, AvailableCopies) VALUES (NC#高级编程, NSimon, N人民邮电出版社, 2020, 3, 3); INSERT INTO Books (Title, Author, Publisher, PublishYear, TotalCopies, AvailableCopies) VALUES (NCLR via C#, NJeffrey Richter, N清华大学出版社, 2023, 1, 0);然后按这个顺序走一遍查图书列表 - 给张三借一本C#高级编程- 再看列表库存变成2 - 还书 - 库存恢复成3 - 尝试借CLR via C#系统提示库存不足。整个过程录下来比任何口头解释都有效。另一个技巧是给DataGridView加一行“当前选中读者/图书”的状态提示比如在借书按钮里先判断dataGridView1.CurrentRow是否为空再取BookID。这不算高级功能但能让老师感觉你考虑到了边界情况。借书成功的弹窗里把应还日期和罚款规则一起显示出来也是一种“业务闭环”的证明。最后我想说一句课程设计评分不看你用了多少酷炫控件而看你数据是不是一致、边界是不是想清楚了。我当年就是把连接串写死在代码里结果演示到一半数据库连不上脸都绿了。所以一定要在交源码前做一次从零开始的解压、还原、编译、运行。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →