拓冰建站拓冰建站
首页 / 资讯中心 / 正文

C#与MySQL图书管理系统实战:sln解决方案、CRUD与事务避坑指南

简介这份资源是面向计算机相关专业在校学生与教师的 C# 图书管理系统课程设计完整方案基于 .Net Framework WinForm 与 MySQL 8.0.21 开发适合作为期末大作业、课程设计或入门进阶练习。功能覆盖用户登录、图书入库与维护、权限管理、借书续借还书、修改密码等模块并采用操作缓存机制——所有增删改需手动提交才写入数据库可随时放弃便于理解事务与数据一致性思路。压缩包共 79 个文件约 8.12MB以 47 个 cs 源码、12 个 resx 窗体资源、6 个 config 配置及 csproj 工程文件为主另含 sql 建库脚本、sln 解决方案与项目说明文档分层结构清晰涵盖 DAL、BLL、Model 与 UI 各层。已有 189 人学习下载可直接运行验证也便于在此基础上进行二次开发与功能拓展。1. 从一份 sln 解决方案说起C# MySQL 图书管理系统到底能跑出什么很多人第一次拿到「C#基于Mysql的简易图书管理系统sql数据库项目说明sln解决方案.zip」这类资源时第一反应是双击 .sln 然后 F5结果要么是 MySQL 连接字符串报错要么是 DataGridView 一片空白。这个标题背后其实是一套非常典型的桌面端 CRUD 工程C# WinForms 做界面MySQL 存数据sln 解决方案把 UI、业务逻辑、数据访问拆成若干项目。它解决的核心问题是——让一个刚学完 C# 基础、知道c#数组和集合区别、但没做过完整项目的人能在一个下午内跑通「登录 → 图书增删改查 → 借阅归还」这条主链路。适合谁适合课程设计、毕业设计起步、或者想用c#上位机思路练手数据库交互的开发者。别把它当玩具它麻雀虽小但连接池、事务、参数化查询这些坑一个不少。2. 拆开 sln 解决方案三层结构怎么分、MySQL 表怎么建2.1 先看清 sln 里到底有几个项目一份规范的简易图书管理系统 sln通常包含三个项目BookManagement.UIWinForms 窗体、BookManagement.BLL业务逻辑、BookManagement.DAL数据访问。有些版本会偷懒只放一个项目所有代码堆在 Form1.cs 里这种跑起来快但改起来痛。我一般会先看解决方案资源管理器里的引用关系UI 引用 BLLBLL 引用 DALDAL 引用MySql.Data或MySqlConnector。如果引用是反的或者 UI 直接引用 DAL说明分层没做干净后面加功能会翻车。判断依据很简单打开.csproj文件看PackageReference或Reference节点。如果是老式 .NET Framework 项目会在packages.config里看到MySql.Data的版本号。这里有个血泪经验——MySql.Data8.0 以上和 .NET Framework 4.5 混用容易出Authentication method caching_sha2_password not supported的错因为 MySQL 8 默认用 caching_sha2_password 插件老驱动不认。解决办法要么升级驱动要么在 MySQL 里把用户改成mysql_native_password。2.2 数据库表设计四张表撑起主链路图书管理系统的核心表不多但字段类型和约束决定了后面代码好不好写。下面是我从常见项目里整理出的最小可用表结构直接可以在 MySQL Workbench 里执行-- 图书表存书的基本信息和库存 CREATE TABLE books ( book_id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE, title VARCHAR(100) NOT NULL, author VARCHAR(50) DEFAULT 未知, publisher VARCHAR(50) DEFAULT 未知, total_copies INT NOT NULL DEFAULT 1, available_copies INT NOT NULL DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 读者表借书的人 CREATE TABLE readers ( reader_id INT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(30) NOT NULL, phone VARCHAR(15), status TINYINT DEFAULT 1 -- 1正常 0冻结 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 借阅记录表谁借了哪本书、什么时候还 CREATE TABLE borrow_records ( record_id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE DEFAULT NULL, FOREIGN KEY (book_id) REFERENCES books(book_id), FOREIGN KEY (reader_id) REFERENCES readers(reader_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户表系统登录用 CREATE TABLE users ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password_hash VARCHAR(64) NOT NULL, role VARCHAR(10) DEFAULT admin ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明books表用total_copies和available_copies两个字段分开记总库存和可借数比只记一个库存字段更稳因为借出时只减available_copies归还时加回来不会把总库存改乱。borrow_records用外键约束保证不会出现借一本不存在的书。users表存password_hash而不是明文哪怕简易系统也别留这个隐患。参数说明utf8mb4是为了支持中文书名和特殊符号InnoDB引擎支持事务借书时「减库存 插记录」要放在一个事务里否则可能减了库存但记录没插进去。AUTO_INCREMENT主键让 C# 端插入后能用LAST_INSERT_ID()拿到新 ID。2.3 连接字符串怎么配才不报错C# 连 MySQL 的连接字符串常见写法// App.config 或代码里直接拼 string connStr Serverlocalhost;Port3306;Databaselibrary_db; Uidroot;Pwd你的密码;CharSetutf8mb4;SslModeNone;;SslModeNone在本地开发时能避免证书报错但生产环境别这么干。CharSetutf8mb4要和建表时一致否则中文书名会变问号。如果 MySQL 装在另一台机器把localhost换成 IP同时确认防火墙放行 3306 端口。常见翻车点密码里带;或#直接拼字符串会截断要用MySqlConnectionStringBuilder来构造。3. 用 C# 把增删改查写进 DAL参数化查询与事务的落地写法3.1 为什么必须用参数化查询而不是拼字符串新手最容易犯的错是SELECT * FROM books WHERE title txtTitle.Text 这不仅有 SQL 注入风险还会因为书名里带单引号直接报语法错误。正确做法是用MySqlParameterpublic ListBook SearchBooks(string keyword) { var list new ListBook(); string sql SELECT book_id, isbn, title, author, available_copies FROM books WHERE title LIKE kw OR author LIKE kw; using (var conn new MySqlConnection(connStr)) using (var cmd new MySqlCommand(sql, conn)) { // 参数化% 在值里加不在 SQL 里拼 cmd.Parameters.AddWithValue(kw, % keyword %); conn.Open(); using (var reader cmd.ExecuteReader()) { while (reader.Read()) { list.Add(new Book { BookId reader.GetInt32(book_id), Isbn reader.GetString(isbn), Title reader.GetString(title), Author reader.GetString(author), AvailableCopies reader.GetInt32(available_copies) }); } } } return list; }逻辑说明using保证连接和命令对象用完就释放避免连接池被占满。AddWithValue把%keyword%作为参数值传入MySQL 会把它当纯文本处理单引号、百分号都不会破坏 SQL 结构。reader.GetInt32(book_id)按列名取值比按索引取值更抗字段顺序变化。参数说明LIKE kw里的kw是占位符实际值由AddWithValue提供。如果搜索关键词为空%%会匹配所有记录所以业务层要先判断空字符串直接返回空列表或全量列表别让数据库白跑。3.2 借书操作一个事务里完成减库存和插记录借书不是单条 SQL 能搞定的它要同时做两件事把available_copies减 1往borrow_records插一条记录。如果减完库存程序崩了记录没插进去库存就白白少了一本。所以必须用事务public bool BorrowBook(int bookId, int readerId, DateTime dueDate) { using (var conn new MySqlConnection(connStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { try { // 第一步检查库存并锁定行防止并发借同一本书 string checkSql SELECT available_copies FROM books WHERE book_id bid FOR UPDATE; int available; using (var checkCmd new MySqlCommand(checkSql, conn, tran)) { checkCmd.Parameters.AddWithValue(bid, bookId); available Convert.ToInt32(checkCmd.ExecuteScalar()); } if (available 0) { tran.Rollback(); return false; // 库存不足 } // 第二步减库存 string updateSql UPDATE books SET available_copies available_copies - 1 WHERE book_id bid; using (var updateCmd new MySqlCommand(updateSql, conn, tran)) { updateCmd.Parameters.AddWithValue(bid, bookId); updateCmd.ExecuteNonQuery(); } // 第三步插借阅记录 string insertSql INSERT INTO borrow_records (book_id, reader_id, borrow_date, due_date) VALUES (bid, rid, CURDATE(), due); using (var insertCmd new MySqlCommand(insertSql, conn, tran)) { insertCmd.Parameters.AddWithValue(bid, bookId); insertCmd.Parameters.AddWithValue(rid, readerId); insertCmd.Parameters.AddWithValue(due, dueDate); insertCmd.ExecuteNonQuery(); } tran.Commit(); return true; } catch (Exception ex) { tran.Rollback(); // 实际项目里记日志别吞异常 throw new Exception(借书失败 ex.Message); } } } }逻辑说明FOR UPDATE在事务里锁住那一行防止两个管理员同时借最后一本书导致库存变负数。三步操作全在同一个MySqlTransaction里任何一步抛异常都Rollback保证数据一致。CURDATE()用数据库时间而不是 C# 本地时间避免客户端时区不一致。参数说明dueDate由 UI 层传入一般是借书日期加 30 天。ExecuteScalar()返回第一行第一列适合查单个值。ExecuteNonQuery()返回受影响行数可以用来判断 UPDATE 是否真的改到了记录。3.3 归还操作与逾期判断归还比借书简单但要注意把return_date填上同时库存加回来public string ReturnBook(int recordId) { using (var conn new MySqlConnection(connStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { // 查出这条记录对应的 book_id 和 due_date string querySql SELECT book_id, due_date FROM borrow_records WHERE record_id rid AND return_date IS NULL; int bookId; DateTime dueDate; using (var queryCmd new MySqlCommand(querySql, conn, tran)) { queryCmd.Parameters.AddWithValue(rid, recordId); using (var reader queryCmd.ExecuteReader()) { if (!reader.Read()) return 记录不存在或已归还; bookId reader.GetInt32(book_id); dueDate reader.GetDateTime(due_date); } } // 更新归还日期 string updateRecord UPDATE borrow_records SET return_date CURDATE() WHERE record_id rid; using (var cmd1 new MySqlCommand(updateRecord, conn, tran)) { cmd1.Parameters.AddWithValue(rid, recordId); cmd1.ExecuteNonQuery(); } // 库存加一 string updateStock UPDATE books SET available_copies available_copies 1 WHERE book_id bid; using (var cmd2 new MySqlCommand(updateStock, conn, tran)) { cmd2.Parameters.AddWithValue(bid, bookId); cmd2.ExecuteNonQuery(); } tran.Commit(); // 逾期判断还书日期超过应还日期就算逾期 int overdueDays (DateTime.Today - dueDate).Days; return overdueDays 0 ? $已归还逾期 {overdueDays} 天 : 已归还; } } }逻辑说明先查记录确认没还过再更新归还日期和库存。return_date IS NULL条件防止重复归还同一本书导致库存虚增。逾期天数在 C# 端算不依赖数据库函数方便以后换数据库。参数说明recordId是借阅记录的主键不是图书 ID。UI 层传参时别搞混否则会还错书。4. 避坑与排查连接、编码、并发这三类问题最耗时间4.1 现象程序启动就报Unable to connect to any of the specified MySQL hosts原因MySQL 服务没启动或者连接字符串里的端口、主机不对。Windows 上 MySQL 默认端口 3306但有些安装包会改成 3307。另外localhost在某些驱动版本里会走命名管道而不是 TCP导致连不上。解决先在命令行跑mysql -u root -p -h 127.0.0.1 -P 3306确认能连。如果命令行能连但 C# 连不上把连接字符串里的localhost换成127.0.0.1强制走 TCP。再检查 MySQL 服务是否在运行services.msc里找 MySQL80 服务状态要是「正在运行」。4.2 现象中文书名存进去变成???或乱码原因建表时用了latin1字符集或者连接字符串没指定CharSetutf8mb4。还有一种情况是 C# 源文件保存时用了 GBK 编码字符串常量本身就已经乱了。解决三步走——建表和字段都用utf8mb4连接字符串加CharSetutf8mb4C# 文件用 UTF-8 with BOM 保存。如果已经建了表用ALTER TABLE books CONVERT TO CHARACTER SET utf8mb4;改过来。注意utf8和utf8mb4不一样MySQL 里的utf8只支持 3 字节存不了 emoji 和某些生僻字统一用utf8mb4。4.3 现象两个管理员同时借最后一本书库存变成 -1原因借书代码先查库存再更新中间没有锁。两个线程同时查到available_copies 1都认为可以借然后都执行减一结果库存变 -1。解决用SELECT ... FOR UPDATE在事务里锁行如 3.2 节代码所示。或者用一条原子 SQLUPDATE books SET available_copies available_copies - 1 WHERE book_id bid AND available_copies 0然后判断ExecuteNonQuery()返回的影响行数如果是 0 说明库存不足。后者更简洁但要把插借阅记录也放在同一事务里。4.4 现象DataGridView 绑定数据后修改单元格数据库没更新原因DataGridView 直接绑定了ListBook而List不实现IBindingList单元格编辑不会触发任何事件。或者用了BindingSource但没调EndEdit()。解决用BindingListBook代替ListBook它实现了IBindingList能通知 UI 更新。保存时遍历BindingList里状态为Modified或Added的项逐条执行 UPDATE 或 INSERT。更省事的做法是用DataTableMySqlDataAdapteradapter.Update(dataTable)自动生成增删改语句但要注意它默认不会处理并发冲突。4.5 现象程序运行一段时间后报Too many connections原因每次操作都new MySqlConnection但没Dispose连接池被占满。或者MySqlDataReader没关闭导致连接一直处于InUse状态。解决所有MySqlConnection、MySqlCommand、MySqlDataReader都用using包起来。连接池默认最大 100正常用using不会超。如果确实需要调大在连接字符串加Max Pool Size200。排查时可以在 MySQL 里跑SHOW PROCESSLIST;看有多少连接处于Sleep状态如果很多说明连接没释放。5. 进阶技巧用存储过程封装借阅统计顺带聊聊验证方法简易系统跑通之后下一步通常是加统计功能比如「本月借阅排行榜」「逾期未还名单」。这些用 C# 拼 SQL 也能做但 SQL 会越拼越长不如把逻辑收进 MySQL 存储过程。下面这个存储过程返回借阅次数最多的前 N 本书DELIMITER // CREATE PROCEDURE GetTopBorrowedBooks(IN topN INT) BEGIN SELECT b.title, b.author, COUNT(br.record_id) AS borrow_count FROM books b JOIN borrow_records br ON b.book_id br.book_id GROUP BY b.book_id, b.title, b.author ORDER BY borrow_count DESC LIMIT topN; END // DELIMITER ;C# 端调用public DataTable GetTopBooks(int topN) { var dt new DataTable(); using (var conn new MySqlConnection(connStr)) using (var cmd new MySqlCommand(GetTopBorrowedBooks, conn)) { cmd.CommandType CommandType.StoredProcedure; cmd.Parameters.AddWithValue(topN, topN); using (var adapter new MySqlDataAdapter(cmd)) { adapter.Fill(dt); } } return dt; }逻辑说明CommandType.StoredProcedure告诉驱动这是存储过程而不是普通 SQL。MySqlDataAdapter自动打开和关闭连接Fill把结果灌进DataTable直接绑 DataGridView 就能显示。存储过程的好处是统计逻辑改的时候不用重新编译 C# 程序改完 SQL 直接生效。参数说明topN是输入参数调用时传 10 就是前 10 名。LIMIT在存储过程里可以用变量但某些旧版本 MySQL 不支持LIMIT topN需要用PREPARE语句动态拼这里为了简洁假设用的是 MySQL 5.7 以上版本。验证方法上我习惯用「双端对账」在 C# 里执行一次借书然后立刻去 MySQL Workbench 里跑SELECT * FROM borrow_records ORDER BY record_id DESC LIMIT 1;和SELECT available_copies FROM books WHERE book_id X;看记录和库存是否同步变化。如果对不上就在 DAL 层加日志把执行的 SQL 和参数打出来对比数据库实际执行结果。这个习惯帮我省了很多「玄学」调试时间——很多时候不是代码逻辑错而是参数传错或者事务没提交。最后说个我自己的教训别在 UI 线程里直接跑数据库查询。WinForms 里按钮点击事件是 UI 线程如果查询慢界面会卡成白屏。我一般用async/await配MySqlConnector的异步方法或者至少把查询丢到Task.Run里查完再用Invoke回 UI 线程更新控件。这个改动不大但体验差别巨大。希望帮到你。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门