
简介这是一套面向计算机相关专业在校学生与教师的 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 ) ENGINEInnoDB; -- 读者表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 ) ENGINEInnoDB; -- 借阅记录表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) ) ENGINEInnoDB;逻辑说明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 几乎一样。我一般用 MySqlConnectorNuGet 里搜 MySqlConnector 装上即可。连接串写在 App.config 或 appsettings.json 里不要硬编码在代码里否则换机器就要重新编译。!-- App.config 里放连接串方便部署时改 -- configuration connectionStrings add nameLibraryDb connectionStringServer127.0.0.1;Port3306;Databaselibrary;Uidroot;Pwd你的密码;CharSetutf8mb4;SslModeNone; providerNameMySqlConnector / /connectionStrings /configuration参数逐个说Server 是本机就写 127.0.0.1Port 默认 3306Database 必须和建库名一致Uid/Pwd 是 MySQL 账号CharSetutf8mb4 保证中文SslModeNone 是本地开发常用如果 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 Server127.0.0.1;Port3306;Databaselibrary;Uidroot;Pwd你的密码;CharSetutf8mb4;SslModeNone;; 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.UIWinForms 界面、LibrarySystem.BLL业务逻辑、LibrarySystem.DAL数据访问。也有人全塞一个项目里文件多了以后改一处崩三处。我一般会按下面这样分项目名职责关键类LibrarySystem.UI窗体、按钮事件、数据显示MainForm、BookFormLibrarySystem.BLL借书还书规则、库存校验BorrowServiceLibrarySystem.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 idbid 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 idbid; 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 idid AND status0 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 status1, return_dateNOW() WHERE idid; using (var cmd new MySqlCommand(updRec, conn, tran)) { cmd.Parameters.AddWithValue(id, recordId); cmd.ExecuteNonQuery(); } // 库存加一 string updStock UPDATE books SET stock stock 1 WHERE idbid; 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# 传时间更一致。如果这里不加 status0 条件同一记录还两次库存就会多出来。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连接错误」多半是连接串写了 SslModeRequired 但服务端没配证书本地开发改成 None 即可。4.2 中文书名变问号字符集要三处一致现象插入「C#高级编程」后查出来是「C#????」。原因数据库、表、连接串三处字符集不统一。解决建库时用 utf8mb4连接串加 CharSetutf8mb4C# 里如果手动拼 SQL 确保文件保存为 UTF-8。注意 MySQL 的 utf8 其实是 utf8mb3存 emoji 会报错统一用 utf8mb4 最省心。4.3 并发借书超借没有事务和行锁现象库存只有 1 本两个用户同时点借书两条记录都插入了库存变成 -1。原因检查库存和减库存是两条独立 SQL中间没有锁。解决把借书逻辑包进事务检查库存时用 SELECT ... FOR UPDATE。如果不想用行锁也可以用UPDATE books SET stock stock - 1 WHERE idbid 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 默认开启连接池连接串里可以加Poolingtrue;MinimumPoolSize0;MaximumPoolSize100;。关键是每个 MySqlConnection 都要用 using 包住或者手动 Dispose。我见过有人把连接存成静态变量一直不关跑一天后 MySQL 报「Too many connections」。简易系统并发不高但习惯要养好。5.3 用配置文件管理连接串和分页大小把连接串、每页条数、默认库存这些放在 App.config 的 appSettings 里改的时候不用重新编译。比如add keyPageSize value20/代码里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 我自己的教训早年做这类系统我总想一次把功能写全结果借书、还书、查询、导出全塞一个窗体改一个按钮崩三个地方。后来强迫自己先跑通「建库 → 连接 → 单表增删改查 → 事务借还」这条最小链路再往上加界面。另外连接串千万别硬编码我因为硬编码把测试库密码提交到代码仓库被同事笑了很久。希望帮到你。本文还有配套的精品资源点击获取