尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C#图书管理系统实战:从数据库设计到罚金计算全解析

C#图书管理系统实战:从数据库设计到罚金计算全解析 简介这是一份基于C#与Visual Studio开发、搭配SQL Server 2008数据库的图书管理系统完整工程适合正在做课程设计或练习桌面应用开发的读者参考。系统覆盖读者信息、图书信息、借阅信息与用户信息四类核心数据表实现了管理员对读者、图书、借阅记录的增删改查以及按读者查询未还图书读者端则可查看馆藏、个人借阅与未还书籍。借书还书时逾期罚金会自动计算并支持罚款缴纳与账户信息修改业务流程比较完整。压缩包共105个文件以42个.cs源码文件为主配合20个.resx资源与20个.resources资源另含可直接运行的.exe程序以及SQL Server的.mdf/.ldf数据库文件包体仅623KB便于快速部署研究。截至当前已有2737人学习下载适合需要对照完整项目结构、理解三层事务处理与数据库关联操作的C#学习者。1. 一套能跑的 C# 图书管理系统长什么样一套基于 C# 的图书管理系统配合 Visual Studio 和 SQL Server2008是很多课程设计、毕业设计和中小型资料室最常采用的组合。它的核心价值不在界面多好看而是把“读者—图书—借阅—罚款”这条主链路拆干净四张表承载业务WinForms 窗体按角色分开管理员和读者各自只看到自己能操作的功能。这套系统能自动处理逾期罚款这是它比 Excel 记录强的地方。借书时写入应还日期还书时按实际日期计算罚金不用人工翻记录。适合馆藏几百到几千册的资料室也适合想从零看一遍 C# 桌面程序如何连接 SQL Server2008 的开发者。下面直接从数据库设计开始拆然后是 Visual Studio 里的 WinForms 分层实现再落到借还书和罚金计算的边界问题最后给出我实际调试老项目时踩过的坑。这样即使你拿到源码包后不急着跑起来也能知道每段代码停在哪、为什么停在哪。2. 数据库设计四张表把借阅业务卡住2.1 四张表的字段规划这个系统的业务边界很明确管理员登录用用户信息表读者信息、图书信息、借阅信息各占一张表。罚款没有单独成表而是放在借阅表里。这样每一次借还记录自带处罚结果查历史罚款时直接按借阅记录统计不需要再做一次关联。表结构如下表名核心字段作用user_infouser_id, user_name, password, role管理员账号role 区分权限reader_inforeader_id, reader_name, gender, department, phone, reg_date, password读者档案book_infobook_id, book_name, author, publisher, isbn, total_count, available_count图书库存borrow_infoborrow_id, book_id, reader_id, borrow_date, due_date, return_date, fine_amount借阅流水和罚款字段命名尽量直白。一般主键用定长CHAR图书馆的借书证号和图书条码通常是固定长度定长字段在 SQL Server2008 里的索引查找更快书名、人名这种可能变长的内容用NVARCHAR避免中文乱码。建库脚本先创建几张表日期字段用DATETIME并让borrow_date默认取当前时间CREATE TABLE book_info ( book_id CHAR(10) PRIMARY KEY, book_name NVARCHAR(50) NOT NULL, author NVARCHAR(20) NULL, publisher NVARCHAR(50) NULL, isbn NVARCHAR(20) NULL, total_count INT NOT NULL DEFAULT(1), available_count INT NOT NULL DEFAULT(1), location_desc NVARCHAR(30) NULL ); CREATE TABLE borrow_info ( borrow_id INT IDENTITY(1,1) PRIMARY KEY, book_id CHAR(10) NOT NULL, reader_id CHAR(10) NOT NULL, borrow_date DATETIME NOT NULL DEFAULT(GETDATE()), due_date DATETIME NOT NULL, return_date DATETIME NULL, fine_amount DECIMAL(10,2) NOT NULL DEFAULT(0) );borrow_id用IDENTITY(1,1)自增避免多个人同时手写主键冲突。available_count表示当前可借数量它与total_count配合后面的借书事务会用到这个字段。注意fine_amount用DECIMAL(10,2)如果只取FLOAT涉及小数金额时可能出现类似 0.30000000000000004 的精度问题。2.2 外键约束与删除策略借阅表是中间表必须让book_id和reader_id只能引用真实存在的图书和读者。建外键最常见的坑是删除数据时被约束卡住比如读者还有未还图书时删不掉。我的建议是外键保持默认的NO ACTION不级联删除把“是否允许删除”的判断放到程序里。ALTER TABLE borrow_info ADD CONSTRAINT FK_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader_info(reader_id); ALTER TABLE borrow_info ADD CONSTRAINT FK_borrow_book FOREIGN KEY (book_id) REFERENCES book_info(book_id);这里故意不写ON DELETE CASCADE否则误删一个读者会把他的所有借阅记录一起删掉连罚款历史都没了。业务上删除读者前应该先查borrow_info中是否有return_date IS NULL的记录有未还图书就先提示“请先完成还书”。给常用查询字段加普通索引也是这套系统容易被忽略的部分CREATE INDEX IX_borrow_reader ON borrow_info(reader_id); CREATE INDEX IX_borrow_returndate ON borrow_info(return_date);reader_id加索引后读者端查询自己的未还图书更快return_date加索引后“查所有未还记录”这个高频动作就不会每次都整表扫描。SQL Server2008 对这类普通索引的维护成本很低可以放心加。2.3 初始化数据与 SQL Server2008 注意事项直接用INSERT给基础表填充测试数据。管理员密码一般不要明文存但 SQL Server2008 对新式哈希函数的支持很有限早期项目里更多是把密码在 C# 端做一次 MD5 或 SHA1 再入库。INSERT INTO user_info (user_id, user_name, password, role) VALUES (admin, 系统管理员, admin123, A); INSERT INTO reader_info (reader_id, reader_name, department, phone, reg_date, password) VALUES (R000000001, N张明, N技术部, 13800000000, GETDATE(), 123456);在 SQL Server2008 里要注意批处理分隔符GO。如果脚本里有多段CREATE TABLE或ALTER TABLE最好在每段之间加上GO因为有些语句的语法检查依赖批处理边界。另外 2008 不支持TRIM等新函数写查询时用LTRIM和RTRIM代替避免整段脚本在低版本数据库上报“未定义函数”。3. Visual Studio 里的 WinForms 分层从窗体到数据访问3.1 源码包的窗体结构从项目文件里能直接看出这套 WinForms 程序的模块划分Form1是登录后的主界面FrmAdd负责新增读者或图书Frmdeletebook负责删除图书Frmnewborrow处理借书Frmnewpunish处理罚款缴纳。每个Designer.cs只放控件布局业务代码在对应的.cs文件里。窗体主要职责相关数据表Form1登录、主界面、按角色加载不同按钮user_infoFrmAdd新增读者、新增图书reader_info, book_infoFrmdeletebook删除图书、下架图书book_infoFrmnewborrow借书、还书操作borrow_info, book_infoFrmnewpunish查看未还记录、缴纳罚款borrow_info一个窗体对应一个业务动作比把所有功能堆在一个 Form 里好维护。登录后的角色控制是关键管理员能进 FrmAdd、Frmdeletebook读者只能进查询和修改密码的窗口。可以在Form1_Load时按role字段隐藏对应按钮。3.2 连接 SQL Server2008 的统一入口程序在 Visual Studio 里调试最常放错的地方是连接字符串。如果本机装的是 SQL Server2008 Express实例名默认是.\SQLEXPRESS标准版通常直接写.。这里把连接字符串独立成一个静态类避免每个窗体各写一遍。using System.Data.SqlClient; public static class DbHelper { public static readonly string ConnStr Data Source.\SQLEXPRESS;Initial CatalogBookManage;Integrated SecurityTrue;; public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(ConnStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } } public static SqlDataReader ExecuteReader(string sql, params SqlParameter[] parameters) { 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); } }ExecuteNonQuery用using管理连接和命令资源适合增删改和返回行数的操作。ExecuteReader不能直接套用同一个写法因为 DataReader 是延迟流式读取用完需要关闭这里用CommandBehavior.CloseConnection让 reader 执行Close()时自动关连接减少遗漏。参数化查询是底线用户输入一旦拼接进 SQL 字符串就有注入风险。3.3 增删改查的写法与边界新增读者时用参数对象给 SQL 填值string sql INSERT INTO reader_info (reader_id, reader_name, gender, department, phone, reg_date, password) VALUES (id, name, gender, department, phone, GETDATE(), pwd); SqlParameter[] paras { new SqlParameter(id, txtId.Text.Trim()), new SqlParameter(name, txtName.Text.Trim()), new SqlParameter(gender, cmbGender.SelectedItem.ToString()), new SqlParameter(department, txtDept.Text.Trim()), new SqlParameter(phone, txtPhone.Text.Trim()), new SqlParameter(pwd, txtPwd.Text) }; int rows DbHelper.ExecuteNonQuery(sql, paras); if (rows 0) MessageBox.Show(读者添加成功);reg_date直接交给数据库用GETDATE()生成少一次往返。要特别检查主键冲突如果reader_id已经存在ExecuteNonQuery会抛SqlException错误码 2627 对应主键重复。程序里应该try-catch这个异常后提示“编号已存在”否则用户会看到一个长英文报错体验很差。删除图书的边界放在Frmdeletebook里先查borrow_info是否有该书记录有就不允许物理删除string checkSql SELECT COUNT(*) FROM borrow_info WHERE book_id bookId; using (SqlDataReader reader DbHelper.ExecuteReader(checkSql, new SqlParameter(bookId, txtBookId.Text.Trim()))) { if (reader.Read() reader.GetInt32(0) 0) { MessageBox.Show(该书已有借阅记录不能直接删除可下架处理); return; } } string delSql DELETE FROM book_info WHERE book_id bookId; DbHelper.ExecuteNonQuery(delSql, new SqlParameter(bookId, txtBookId.Text.Trim()));这段代码先读后删在单机或小并发场景下够用。更严格的方式是把“查询是否存在”和“执行删除”放进同一个事务后面借书流程会展示事务写法。DataGridView 和数据源的绑定也很直接查询出DataTable后设置dataGridView1.DataSource dt;列名会自动对应字段名不需要手动逐个控件填充。4. 借书还书的核心流程让罚金自动算出来4.1 借书时的并发保护借书不是写一条INSERT那么简单。同一本书可能有多个副本available_count代表当前可借数量。如果两个窗口同时看到可借 1 本然后都执行插入最后库存会变成负数。C# 端用事务加锁保证数据一致借阅状态判断条件程序动作在借未逾期return_date IS NULL 且 GETDATE() due_date显示“未到期”在借逾期return_date IS NULL 且 GETDATE() due_date还书时自动计算罚金已还return_date IS NOT NULL记录中保留罚款金额供 Frmnewpunish 缴纳核心借书方法public void BorrowBook(string bookId, string readerId, int days) { string checkSql SELECT available_count FROM book_info WITH(UPDLOCK, ROWLOCK) WHERE book_id bookId; string updateSql UPDATE book_info SET available_count available_count - 1 WHERE book_id bookId AND available_count 0; string insertSql INSERT INTO borrow_info (book_id, reader_id, borrow_date, due_date) VALUES (bookId, readerId, GETDATE(), DATEADD(DAY, days, GETDATE())); using (SqlConnection conn new SqlConnection(DbHelper.ConnStr)) { conn.Open(); using (SqlTransaction tx conn.BeginTransaction()) using (SqlCommand cmd conn.CreateCommand()) { cmd.Transaction tx; try { cmd.CommandText checkSql; cmd.Parameters.AddWithValue(bookId, bookId); int available (int)cmd.ExecuteScalar(); if (available 0) throw new Exception(库存不足); cmd.CommandText updateSql; cmd.Parameters.AddWithValue(bookId, bookId); int update cmd.ExecuteNonQuery(); if (update 0) throw new Exception(更新失败可能已经无库存); cmd.CommandText insertSql; cmd.Parameters.AddWithValue(readerId, readerId); cmd.Parameters.AddWithValue(days, days); cmd.ExecuteNonQuery(); tx.Commit(); } catch { tx.Rollback(); throw; } } } }WITH(UPDLOCK, ROWLOCK)是 SQL Server2008 支持的锁提示。UPDLOCK防止两个事务同时读到相同available_countROWLOCK缩小锁粒度避免整表被锁住。事务的意义在于插入借阅记录失败时扣减库存的操作也能回滚两个动作要么都成功要么都不发生。4.2 还书时计算逾期罚金逾期罚金不需要定时任务还书那一刻计算一次即可。常见规则是每本每天 0.1 元不足一天按一天算。我一般直接在 SQL 里算不取回 C# 再循环private void btnReturn_Click(object sender, EventArgs e) { string sql UPDATE borrow_info SET return_date GETDATE(), fine_amount CASE WHEN GETDATE() due_date THEN DATEDIFF(DAY, due_date, GETDATE()) * 0.1 ELSE 0 END WHERE borrow_id borrowId AND return_date IS NULL; int rows DbHelper.ExecuteNonQuery(sql, new SqlParameter(borrowId, txtBorrowId.Text.Trim())); if (rows 0) { MessageBox.Show(未找到未还记录可能已经还过); return; } string updateBook UPDATE book_info SET available_count available_count 1 WHERE book_id (SELECT book_id FROM borrow_info WHERE borrow_id borrowId); DbHelper.ExecuteNonQuery(updateBook, new SqlParameter(borrowId, txtBorrowId.Text.Trim())); }DATEDIFF(DAY, due_date, GETDATE())返回两个日期相差的天数。这里把“还书时间”和“计算罚金”放进同一个 UPDATE保证只有一次状态变更。问题在于第二个UPDATE book_info和第一个 UPDATE 不是同一个事务如果加库存失败会出现“还书记录存在但库存没恢复”的情况。正式代码里应该把两个 UPDATE 放进一个SqlTransaction。罚金系数 0.1 硬编码在 SQL 里以后要改规则就得改代码。更好的做法是把罚金单价放进一张配置表或者由Frmnewpunish作为参数传入这样管理员可以在界面上调。4.3 读者端的未还图书查询与罚款缴纳读者登录后只查自己的未还记录条件是reader_id id AND return_date IS NULL同时显示是否逾期string sql SELECT b.book_name, br.borrow_date, br.due_date, CASE WHEN GETDATE() br.due_date THEN N已逾期 ELSE N未到期 END AS state FROM borrow_info br JOIN book_info b ON br.book_id b.book_id WHERE br.reader_id readerId AND br.return_date IS NULL ORDER BY br.due_date;CASE WHEN生成计算列直接在 DataGridView 绑定就能看到状态。最容易犯的错是漏掉JOIN book_info只查borrow_info界面上只能看到书号看不到书名。罚款缴纳在Frmnewpunish里操作逻辑是把某条已经还书但fine_amount 0的记录标记为已缴。如果有pay_status字段就更新这个字段没有的话可以在fine_amount旁边加一个fine_paid字段。保持简单的方式string sql UPDATE borrow_info SET fine_status P WHERE borrow_id borrowId AND fine_amount 0; int rows DbHelper.ExecuteNonQuery(sql, new SqlParameter(borrowId, txtBorrowId.Text.Trim())); if (rows 0) MessageBox.Show(该书没有待缴罚款);这里我给原表补了fine_status字段。如果不改表结构就要设计一个“待缴罚款视图”通过return_date IS NOT NULL AND fine_amount 0 AND pay_status IS NULL过滤。缴纳动作不要用 DELETE 清掉fine_amount否则之后对账看不到这笔历史。5. 编译、部署与版本兼容的排错技巧5.1 SQL Server2008 连接失败的常见原因SQL Server2008 连接不上绝大多数是实例名和身份认证问题。先用 SQL Server Management Studio 登录确认服务启动了再检查连接串里的Data Source。如果要用sa登录数据库必须设置成“SQL Server 和 Windows 身份验证模式”并在连接串里加上User IDsa;Password...。SQL Server2008 在 Windows 10/11 上安装时可能卡在“要求重新启动计算机”。这个提示是因为安装程序检测到注册表里有挂起的文件重命名操作。常规处理是删除HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager下的PendingFileRenameOperations2再重新安装。修改注册表前先备份该项。5.2 Visual Studio 新版打开老项目的问题用 Visual Studio 2022 打开 2010 年前后的 C# 项目解决方案管理器会提示“需要更新”。选择升级即可WinForms 项目里的Form1.cs和.Designer.cs结构基本兼容。如果遇到“由于出现错误无法启动 Visual Studio。Microsoft.ServiceHub.Client.Controller”这个报错多半是 ServiceHub 进程残留或缓存损坏。关掉 VS在任务管理器结束所有ServiceHub.*进程再删除%TEMP%\ServiceHub目录重新启动。现象排查顺序无法连接 SQL Server服务启停 → 连接串 → 防火墙端口VS 启动报 ServiceHub 错误结束 ServiceHub 进程 → 清缓存 → 修复 VS老项目编译报缺少引用目标框架改为 .NET Framework 4.x → 重新添加 System.Data5.3 用一条 SQL 验证整套规则验证罚金是否计算正确的快捷方法是在查询窗口直接模拟超期状态SELECT borrow_id, book_id, reader_id, DATEDIFF(DAY, due_date, GETDATE()) AS overdue_days, CASE WHEN return_date IS NULL AND GETDATE() due_date THEN DATEDIFF(DAY, due_date, GETDATE()) * 0.1 ELSE fine_amount END AS fine FROM borrow_info WHERE return_date IS NULL这段查询能同时看到超期天数和应罚金额。日常开发时可以把它保存为视图v_overdue_fineFrmnewpunish查询时直接写SELECT * FROM v_overdue_fine WHERE reader_id rid这样既减少 C# 端的循环也方便排查同一本书在不同时间还书时罚金是否一致。本文还有配套的精品资源点击获取
返回列表