☰
C#基于MySQL图书管理系统实战:从sln解决方案到事务与避坑
2026/10/12 20:21:08 网站建设 项目流程

简介:这是一套面向计算机相关专业在校学生与教师的 C# 课程设计资源,基于 .Net Framework WinForm 与 MySQL 8.0.21 实现简易图书管理系统,适合作为期末大作业、课程设计、毕设或入门进阶项目。压缩包共 79 个文件,约 8.12MB,包含 47 个 cs 源码、12 个 resx 界面资源、6 个 config 配置、4 个 csproj 工程文件,以及 sln 解决方案、sql 建库脚本和项目说明文档,分层涵盖 DAL、BLL、Model 与 WinForm 界面。功能上已实现用户登录、新书入库、图书维护、权限管理、借书、续借、还书、修改密码等模块,并采用操作缓存机制,所有增删改需提交后才写入数据库,也可整体放弃,便于理解事务式交互设计。目前已有 189 人学习,代码经运行验证稳定可靠,可直接用于答辩演示或二次开发,帮助读者快速掌握 C# 与 MySQL 的桌面端开发流程。

1. 从一份 sln 解决方案说起:C# + MySQL 图书管理系统到底能跑出什么

很多人第一次拿到「C#基于Mysql的简易图书管理系统+sql数据库+项目说明+sln解决方案.zip」这类压缩包,第一反应是双击 .sln 看能不能直接编译,结果往往卡在 MySQL 连接字符串或者缺库缺表上。这个标题背后其实是一套非常典型的桌面端 CRUD 项目:C# 写 WinForms 或控制台界面,MySQL 存图书、读者、借阅记录,sln 解决方案把数据访问层、业务层、界面层串起来。它解决的是「小规模图书借还、查询、统计」这类需求,适合课程设计、内部工具、刚入门 C# 数据库编程的从业者。你不需要先啃完 C#高级编程,只要能把连接串配对、表结构建对、增删改查跑通,这套东西就能落地。下面按「先立住原理,再动手复现,最后避坑」的顺序拆开讲。

2. 环境与数据库先落地:MySQL 建库建表和 C# 连接串怎么配

2.1 为什么选 MySQL 而不是 SQL Server 或 SQLite

标题里写死了 MySQL,这不是随便选的。SQL Server 在 Windows 上确实和 C# 亲,但安装包大、授权复杂,热搜里「先安装sql server2025数据库,后安装visual studio installer sql server manageme」这种顺序问题经常把人劝退。SQLite 够轻,但不支持多用户并发写,图书管理系统如果有多台机器同时借还,容易锁库。MySQL 处在中间:免费、跨平台、支持事务、有成熟的 .NET 驱动,社区里「mysql安装教程8.0」「mysql安装配置教程」一抓一大把,遇到问题好搜。我一般会推荐 MySQL 8.0 系列,字符集用 utf8mb4,排序规则用 utf8mb4_0900_ai_ci,避免中文书名乱码。

2.2 建库建表的最小 SQL 脚本

下面这段 SQL 可以直接在 MySQL 命令行或 Navicat 里执行。它建了一个 library 库,三张核心表:books、readers、borrow_records。注意外键和索引,后面 C# 查询借阅记录时会用到。

-- 创建数据库,字符集用 utf8mb4 防止中文乱码 CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_0900_ai_ci; USE library; -- 图书表:isbn 唯一,stock 表示可借库存 CREATE TABLE books ( id INT AUTO_INCREMENT PRIMARY KEY, isbn VARCHAR(20) NOT NULL UNIQUE, title VARCHAR(200) NOT NULL, author VARCHAR(100) DEFAULT '', stock INT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 读者表:card_no 是借书证号 CREATE TABLE readers ( id INT AUTO_INCREMENT PRIMARY KEY, card_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, phone VARCHAR(20) DEFAULT '' ) ENGINE=InnoDB; -- 借阅记录表:status 0=借出 1=已还 CREATE TABLE borrow_records ( id INT AUTO_INCREMENT PRIMARY KEY, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_date DATETIME DEFAULT CURRENT_TIMESTAMP, return_date DATETIME NULL, status TINYINT NOT NULL DEFAULT 0, CONSTRAINT fk_br_book FOREIGN KEY (book_id) REFERENCES books(id), CONSTRAINT fk_br_reader FOREIGN KEY (reader_id) REFERENCES readers(id), INDEX idx_status (status) ) ENGINE=InnoDB;

逻辑说明:books 表的 stock 字段是库存冗余,借出一本减一,归还加一,比每次去 borrow_records 里 count 要快。borrow_records 的 status 加索引,因为「查当前未还」是最高频查询。参数方面,isbn 长度 20 够用,title 给 200 是留余量,stock 默认 0 表示新书入库后要手动设库存。

2.3 C# 连接 MySQL 的两种方式和连接串写法

C# 连 MySQL 常见驱动是 MySql.Data 或 MySqlConnector。前者是 Oracle 官方,后者是社区高性能版,API 几乎一样。我一般用 MySqlConnector,NuGet 里搜 MySqlConnector 装上即可。连接串写在 App.config 或 appsettings.json 里,不要硬编码在代码里,否则换机器就要重新编译。

<!-- App.config 里放连接串,方便部署时改 --> <configuration> <connectionStrings> <add name="LibraryDb" connectionString="Server=127.0.0.1;Port=3306;Database=library;Uid=root;Pwd=你的密码;CharSet=utf8mb4;SslMode=None;" providerName="MySqlConnector" /> </connectionStrings> </configuration>

参数逐个说:Server 是本机就写 127.0.0.1,Port 默认 3306;Database 必须和建库名一致;Uid/Pwd 是 MySQL 账号;CharSet=utf8mb4 保证中文;SslMode=None 是本地开发常用,如果 MySQL 开了强制 SSL,会报「mysql ssl连接错误」,这时要么配证书,要么在服务端关掉 require_secure_transport。生产环境不建议 None,但课程设计本地跑没问题。

2.4 用 C# 跑通第一条查询:验证环境是否配对

建一个控制台程序,引用 MySqlConnector,写下面这段代码。它的作用是从 books 表读前 10 条,打印书名和库存。能跑通,说明数据库、驱动、连接串三件事都对了。

using System; using MySqlConnector; // NuGet 安装 MySqlConnector class Program { static void Main() { // 从配置文件读连接串,实际项目用 ConfigurationManager string connStr = "Server=127.0.0.1;Port=3306;Database=library;Uid=root;Pwd=你的密码;CharSet=utf8mb4;SslMode=None;"; using (var conn = new MySqlConnection(connStr)) { conn.Open(); string sql = "SELECT id, title, stock FROM books LIMIT 10"; using (var cmd = new MySqlCommand(sql, conn)) using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { // GetInt32/GetString 按列序号取,比列名快一点 Console.WriteLine($"{reader.GetInt32(0)} | {reader.GetString(1)} | 库存 {reader.GetInt32(2)}"); } } } } }

逻辑说明:using 确保连接和命令对象释放,避免连接池耗尽。reader.GetInt32(0) 按序号取列,顺序必须和 SELECT 一致。如果这里抛「Access denied」就是账号密码错;抛「Unknown database」就是库没建;抛「Unable to connect」先看 MySQL 服务是否启动、端口是否被占。

3. 把 sln 解决方案拆开看:分层、增删改查和事务怎么写

3.1 一个能维护的 sln 应该怎么分层

压缩包里的 sln 解决方案,常见结构是一个解决方案下挂 2 到 3 个项目:LibrarySystem.UI(WinForms 界面)、LibrarySystem.BLL(业务逻辑)、LibrarySystem.DAL(数据访问)。也有人全塞一个项目里,文件多了以后改一处崩三处。我一般会按下面这样分:

项目名职责关键类
LibrarySystem.UI窗体、按钮事件、数据显示MainForm、BookForm
LibrarySystem.BLL借书还书规则、库存校验BorrowService
LibrarySystem.DALSQL 执行、连接管理BookRepository

这样分的好处是:换数据库只改 DAL,改界面不动业务。热搜里「c#控件多致winfrom卡」的问题,往往是因为 UI 层直接写 SQL 还同步阻塞,分层后可以把查询放后台线程。

3.2 借书操作的完整代码:事务 + 库存校验

借书不是简单插一条记录,它要同时做三件事:检查库存、插借阅记录、减库存。这三步必须在一个事务里,否则并发时会超借。下面用 MySqlConnector 写一个 BorrowService.Borrow 方法。

public bool Borrow(int bookId, int readerId) { string connStr = System.Configuration.ConfigurationManager .ConnectionStrings["LibraryDb"].ConnectionString; using (var conn = new MySqlConnection(connStr)) { conn.Open(); using (var tran = conn.BeginTransaction()) // 开启事务 { try { // 1. 锁定该行并检查库存,FOR UPDATE 防止并发超借 string checkSql = "SELECT stock FROM books WHERE id=@bid FOR UPDATE"; using (var checkCmd = new MySqlCommand(checkSql, conn, tran)) { checkCmd.Parameters.AddWithValue("@bid", bookId); int stock = Convert.ToInt32(checkCmd.ExecuteScalar()); if (stock <= 0) { tran.Rollback(); return false; } } // 2. 插入借阅记录 string insSql = "INSERT INTO borrow_records(book_id, reader_id, status) VALUES(@bid, @rid, 0)"; using (var insCmd = new MySqlCommand(insSql, conn, tran)) { insCmd.Parameters.AddWithValue("@bid", bookId); insCmd.Parameters.AddWithValue("@rid", readerId); insCmd.ExecuteNonQuery(); } // 3. 减库存 string updSql = "UPDATE books SET stock = stock - 1 WHERE id=@bid"; using (var updCmd = new MySqlCommand(updSql, conn, tran)) { updCmd.Parameters.AddWithValue("@bid", bookId); updCmd.ExecuteNonQuery(); } tran.Commit(); // 三步都成功才提交 return true; } catch { tran.Rollback(); // 任何一步失败全部回滚 throw; } } } }

逻辑说明:FOR UPDATE 是行级锁,保证两个用户同时借同一本书时,第二个会等第一个提交后再读库存。参数用 AddWithValue 而不是拼字符串,防 SQL 注入。catch 里先 Rollback 再 throw,让上层知道失败原因。如果这里不写事务,插了记录但减库存失败,就会出现「记录显示借了,库存没减」的脏数据。

3.3 查询借阅记录:联表 + 分页 + 排序

图书管理系统里「查某人当前借了哪些书」是高频操作。下面这条 SQL 联了三张表,按借出时间倒序,带分页。热搜里「mysql排序」在这里就是 ORDER BY borrow_date DESC。

-- 查询某个读者当前未还的书,每页 20 条 SELECT br.id, b.title, b.isbn, br.borrow_date FROM borrow_records br JOIN books b ON br.book_id = b.id JOIN readers r ON br.reader_id = r.id WHERE r.card_no = @cardNo AND br.status = 0 ORDER BY br.borrow_date DESC LIMIT @offset, 20;

参数说明:@cardNo 是借书证号,@offset 是 (页码-1)*20。LIMIT 在 MySQL 里是 offset, count 顺序,别写反。如果数据量大,status 和 borrow_date 上建联合索引会更快,但简易系统几千条数据不用太纠结。

3.4 还书操作和库存回加

还书是借书的逆操作:更新 borrow_records 的 status 和 return_date,同时 books.stock 加一。同样要事务。

public bool Return(int recordId) { string connStr = System.Configuration.ConfigurationManager .ConnectionStrings["LibraryDb"].ConnectionString; using (var conn = new MySqlConnection(connStr)) { conn.Open(); using (var tran = conn.BeginTransaction()) { try { // 先查出 book_id,并确认这条记录是未还状态 string getSql = "SELECT book_id FROM borrow_records WHERE id=@id AND status=0 FOR UPDATE"; int bookId; using (var getCmd = new MySqlCommand(getSql, conn, tran)) { getCmd.Parameters.AddWithValue("@id", recordId); object result = getCmd.ExecuteScalar(); if (result == null) { tran.Rollback(); return false; } // 已还或不存在 bookId = Convert.ToInt32(result); } // 更新记录为已还 string updRec = "UPDATE borrow_records SET status=1, return_date=NOW() WHERE id=@id"; using (var cmd = new MySqlCommand(updRec, conn, tran)) { cmd.Parameters.AddWithValue("@id", recordId); cmd.ExecuteNonQuery(); } // 库存加一 string updStock = "UPDATE books SET stock = stock + 1 WHERE id=@bid"; using (var cmd = new MySqlCommand(updStock, conn, tran)) { cmd.Parameters.AddWithValue("@bid", bookId); cmd.ExecuteNonQuery(); } tran.Commit(); return true; } catch { tran.Rollback(); throw; } } } }

逻辑说明:先 SELECT ... FOR UPDATE 确认记录存在且未还,避免重复还书导致库存多加。return_date 用 NOW() 由数据库生成,比 C# 传时间更一致。如果这里不加 status=0 条件,同一记录还两次,库存就会多出来。

4. 避坑与排查:连接、编码、并发和部署里最容易翻车的地方

4.1 连接串对了但连不上:先看服务、端口和 SSL

现象:C# 程序抛 MySqlException「Unable to connect to any of the specified MySQL hosts」。原因通常有三种:MySQL 服务没启动、端口不是 3306、或者 MySQL 8.0 默认开了 caching_sha2_password 而旧驱动不认。解决:先在命令行跑mysql -u root -p确认服务活着;再看netstat -ano | findstr 3306确认端口;如果驱动版本老,把 MySQL 用户认证插件改成 mysql_native_password,或者升级 MySqlConnector 到最新。热搜里「mysql ssl连接错误」多半是连接串写了 SslMode=Required 但服务端没配证书,本地开发改成 None 即可。

4.2 中文书名变问号:字符集要三处一致

现象:插入「C#高级编程」后查出来是「C#????」。原因:数据库、表、连接串三处字符集不统一。解决:建库时用 utf8mb4,连接串加 CharSet=utf8mb4,C# 里如果手动拼 SQL 确保文件保存为 UTF-8。注意 MySQL 的 utf8 其实是 utf8mb3,存 emoji 会报错,统一用 utf8mb4 最省心。

4.3 并发借书超借:没有事务和行锁

现象:库存只有 1 本,两个用户同时点借书,两条记录都插入了,库存变成 -1。原因:检查库存和减库存是两条独立 SQL,中间没有锁。解决:把借书逻辑包进事务,检查库存时用 SELECT ... FOR UPDATE。如果不想用行锁,也可以用UPDATE books SET stock = stock - 1 WHERE id=@bid AND stock > 0,然后判断受影响行数是否为 1,为 0 就回滚。两种都行,前者更直观。

4.4 窗体卡死:UI 线程里跑了同步查询

现象:点「查询」按钮后界面白屏几秒,热搜里「c#控件多致winfrom卡」就是这个。原因:数据查询在 UI 线程同步执行,数据量大时阻塞消息循环。解决:用 async/await 把查询放后台。MySqlConnector 支持 OpenAsync 和 ExecuteReaderAsync,把按钮事件改成 async void,查询前禁用按钮,查完再启用。注意跨线程更新控件要用 Invoke 或 async 自动回到 UI 上下文。

4.5 部署到别人机器上跑不起来:缺驱动或缺库

现象:自己机器能跑,拷到同学电脑上报「未能加载文件或程序集 MySqlConnector」。原因:NuGet 包没有随输出目录一起拷过去,或者目标机器没装对应 .NET 运行时。解决:发布时用「发布」功能生成完整目录,或者把 MySqlConnector.dll 一起复制。数据库那边,把建库建表 SQL 导出成 .sql 文件,让对方先执行再运行程序。项目说明里最好写清楚 MySQL 版本和 .NET 版本,省得来回问。

5. 进阶技巧:把简易系统做得更稳的几个习惯

5.1 用参数化查询替代字符串拼接,顺手防注入

很多简易项目为了省事写"SELECT * FROM books WHERE title='" + txtTitle.Text + "'",书名里带个单引号就崩,还容易被注入。改成参数化只是多两行代码,收益很大。上面借书还书的例子已经全是参数化,你照抄就行。如果非要用拼接,至少把单引号替换成两个单引号,但这不是长久之计。

5.2 连接池和 using:别让连接泄漏拖垮程序

MySqlConnector 默认开启连接池,连接串里可以加Pooling=true;MinimumPoolSize=0;MaximumPoolSize=100;。关键是每个 MySqlConnection 都要用 using 包住,或者手动 Dispose。我见过有人把连接存成静态变量一直不关,跑一天后 MySQL 报「Too many connections」。简易系统并发不高,但习惯要养好。

5.3 用配置文件管理连接串和分页大小

把连接串、每页条数、默认库存这些放在 App.config 的 appSettings 里,改的时候不用重新编译。比如<add key="PageSize" value="20"/>,代码里int pageSize = int.Parse(ConfigurationManager.AppSettings["PageSize"]);。这样部署到不同环境只改配置文件,符合「项目说明」里应该交代的部署步骤。

5.4 验证方法:用三条 SQL 快速确认数据一致性

系统跑一段时间后,怎么知道有没有脏数据?我一般会跑这三条检查:

-- 1. 库存为负的书(说明超借了) SELECT id, title, stock FROM books WHERE stock < 0; -- 2. 已还记录但 return_date 为空 SELECT id FROM borrow_records WHERE status = 1 AND return_date IS NULL; -- 3. 借阅记录里的 book_id 在 books 表里不存在(外键没生效时会出现) SELECT br.id FROM borrow_records br LEFT JOIN books b ON br.book_id = b.id WHERE b.id IS NULL;

这三条都返回空,说明基本数据是干净的。第一条非空就去查借书逻辑的事务;第二条非空看还书代码是不是漏了 return_date;第三条非空说明外键没建或者被禁用了。养成定期跑一遍的习惯,比出了问题再翻日志快得多。

5.5 我自己的教训

早年做这类系统,我总想一次把功能写全,结果借书、还书、查询、导出全塞一个窗体,改一个按钮崩三个地方。后来强迫自己先跑通「建库 → 连接 → 单表增删改查 → 事务借还」这条最小链路,再往上加界面。另外,连接串千万别硬编码,我因为硬编码把测试库密码提交到代码仓库,被同事笑了很久。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询