简介:一份基于C#与SQL Server的图书管理系统课程设计完整项目,适合正在学习C#编程、数据库开发或需要完成课程设计的高校学生。项目基于Visual Studio 2005与SQL Server 2000,从用户界面设计、控件布局、事件响应到业务逻辑分层实现,完整覆盖图书借阅、归还、续借、读者管理等功能,可直接运行,也可作为毕业设计框架参考。资源压缩包共155个文件,大小7.05MB,以C#源文件、动态链接库、资源文件、窗体布局定义及可执行程序为主,同时附有SQL Server数据库文件(mdf/ldf)与Word格式的设计说明文档,便于附加数据库后运行并对照源码理解系统架构。目前已有1771人学习下载。通过实践可掌握ADO.NET数据访问(Connection、Command、DataAdapter、DataSet)、SQL查询语句与事务处理、ER模型向表结构的转换,以及需求分析、系统设计文档的撰写方法,是提升C#编程与数据库管理能力的完整实战素材。
1. 图书管理系统:一门 C# 课程设计背后的完整闭环
图书管理系统,配上 C# 和 SQL Server,再加一份设计文档,这个组合在大学机房里的出现频率高得惊人。很多同学拿到题目第一时间去搜源码,下下来发现跑不起来,或者跑起来了但答辩时被老师问住——因为数据库里的表是你从网上抄的,你根本说不清为什么要有三张表而不是两张。这门课设的真正难点从来不是写代码,而是从选题到数据库设计再到文档成稿的一整条链路。这篇文章就按最靠谱的路线走一遍:从建库开始,到 C# 连上 SQL Server 跑通增删改查,再到把设计文档写成一个能帮自己兜底的东西。项目本身不大,但该踩的坑一个不少,照着做能让你少走一半弯路。
2. 技术选型先想明白:为什么是 C# + SQL Server,为什么不是别的
2.1 课程设计选型的潜规则:稳定比炫技重要
图书管理系统最常见的实现方式有三种:WinForms 桌面端、ASP.NET Web 端、WPF 端。课程设计里选 WinForms + SQL Server 的人最多,不是因为技术最先进,而是因为这套组合的容错率最高。WinForms 是 C# 最传统的 UI 框架,拖控件就能搭界面,事件模型直观,网上资料存量巨大,遇到问题搜得到答案。SQL Server 和 C# 都是微软系产品,SqlClient 命名空间是官方提供的数据库驱动,连接字符串的坑少,类型映射也顺——C# 里的 string 对应 SQL Server 的 nvarchar,DateTime 对应 datetime,基本不需要手动转换。
如果你在纠结要不要用 WPF,我的建议是别折腾。WPF 的 MVVM 绑定、DataTemplate、异步线程模型,每一个都会消耗你大量调 bug 的时间。课程设计的评分重点从来不是界面好看,而是功能完整、代码结构清楚、文档规范。用 WinForms 两三天能做完的东西,WPF 可能要一周,而且你自己根本说不清绑定失效的问题出在哪。用 Web 端也一样,ASP.NET 的请求生命周期、状态管理、IIS 部署,任何一个环节出问题你都得额外学一门课。
数据库选 SQL Server 而不是 MySQL,原因也很实际。你的开发机装一个 SQL Server Express 就够了,它和 Visual Studio 的集成做得最好——服务器资源管理器里可以直接建表、看数据、生成脚本。MySQL 在 C# 里要用第三方驱动 MySql.Data,NuGet 包版本一乱,编译都过不去。SQL Server 还有一个杀手锏:SSMS(SQL Server Management Studio)的图形化设计器,建表、改字段、看外键关系都是点鼠标的事,对不熟悉 SQL 语法的同学友好得多。
2.2 图书管理系统的数据模型:三张核心表和一个关系陷阱
图书管理系统的功能一般就四块:图书信息管理、读者信息管理、借书还书操作、系统登录。对应的数据模型至少要有四张表:用户表(User)、图书表(Book)、读者表(Reader)、借阅表(Borrow)。有些同学会把读者和用户合并成一张表,这会带来一个隐蔽的问题——读者登录系统用的账号和借书证号的概念混在一起,老师和学生都能登录,但只有管理员能进后台,权限判断会变得别扭。我一般建议拆开:User 表只管登录账号,Reader 表管借书人的信息,两个表通过一个字段关联或者干脆不关联,借书的时候只认 Reader 的主键。
图书表和读者表之间是多对多关系——一本书可以被多个读者借过(历史记录),一个读者可以借多本书。所以需要借阅表作为中间表,这是整个数据库设计的核心。借阅表里至少要有借阅 ID 主键、图书 ID 外键、读者 ID 外键、借书日期、还书日期、状态字段。状态字段很关键,用 0 表示借出未还,1 表示已归还,查询“当前谁借了什么书没还”就靠这个字段过滤。
一个常见的错误是只做两张表——Book 和 Borrow——读者信息直接写在借阅记录里。这样看起来简单,但每借一次书就要重复录入读者姓名和联系方式,数据冗余带来的是更新异常。读者改手机号了,你得把所有历史借阅记录的手机号全改一遍。课程设计的答辩老师特别喜欢问这个问题,答不上来会被扣分。正确的做法就是拆表,用外键关联。对外键的约束策略也必须想清楚:删除图书时,如果这本书有未还的借阅记录,是拒绝删除(NO ACTION)还是把借阅记录一起删掉(CASCADE)?我建议用 NO ACTION,程序里先判断有没有未还记录再执行删除,这样数据安全。
3. 动手第一步:用 SQL 脚本把数据库立起来
3.1 建库建表脚本:字段类型、长度和约束一次到位
打开 SSMS,新建一个数据库,名字建议简单一点,比如 LibraryDB。不要用中文名,也不要带空格,因为连接字符串里写数据库名写错了排查起来很费劲。接着新建查询,把下面的建表脚本跑一遍。注意先建不依赖其他表的表——用户表、图书表、读者表——最后建借阅表,因为它有外键依赖。
-- 创建用户表:存放登录账号 CREATE TABLE [User] ( UserId INT IDENTITY(1,1) PRIMARY KEY, -- 自增主键 UserName NVARCHAR(50) NOT NULL UNIQUE, -- 登录名,不能重复 Password NVARCHAR(50) NOT NULL, -- 密码 IsAdmin BIT NOT NULL DEFAULT 0 -- 是否为管理员,0否1是 ); -- 创建图书表 CREATE TABLE Book ( BookId INT IDENTITY(1,1) PRIMARY KEY, BookName NVARCHAR(100) NOT NULL, -- 书名 Author NVARCHAR(50), -- 作者 Publisher NVARCHAR(100), -- 出版社 PublishDate DATETIME, -- 出版日期 ISBN NVARCHAR(20), -- ISBN编号 TotalCount INT NOT NULL DEFAULT 1, -- 馆藏总量 BorrowedCount INT NOT NULL DEFAULT 0, -- 已借出数量 Status BIT NOT NULL DEFAULT 1 -- 1在库 0下架 ); -- 创建读者表 CREATE TABLE Reader ( ReaderId INT IDENTITY(1,1) PRIMARY KEY, ReaderName NVARCHAR(50) NOT NULL, Gender NVARCHAR(10), Phone NVARCHAR(20), RegDate DATETIME NOT NULL DEFAULT GETDATE() -- 注册日期 ); -- 创建借阅表 CREATE TABLE Borrow ( BorrowId INT IDENTITY(1,1) PRIMARY KEY, BookId INT NOT NULL FOREIGN KEY REFERENCES Book(BookId), ReaderId INT NOT NULL FOREIGN KEY REFERENCES Reader(ReaderId), BorrowDate DATETIME NOT NULL DEFAULT GETDATE(), DueDate DATETIME NOT NULL, -- 应还日期 ReturnDate DATETIME, -- 实际归还日期,NULL表示未还 Status BIT NOT NULL DEFAULT 0 -- 0借出 1已还 );所有非空字段都加了 NOT NULL,避免程序里出现空引用异常。UserName 字段加了 UNIQUE 约束,防止注册重复账号。BIT 类型存布尔值,C# 里读取出来可以直接转 bool,不用判断 0 和 1——这是 SQL Server 比 MySQL 方便的地方。
3.2 初始化数据:没有数据的系统,程序写不下去
表建好了,马上插入几条测试数据。不然后面写 C# 代码时,界面上什么都没有,你无法判断查询逻辑对不对。插入顺序也是先插用户、图书、读者,再插借阅记录,不是随便插。
-- 插入管理员账号 INSERT INTO [User] (UserName, Password, IsAdmin) VALUES ('admin', '123456', 1); -- 插入图书记录 INSERT INTO Book (BookName, Author, Publisher, PublishDate, ISBN, TotalCount, BorrowedCount) VALUES ('C#程序设计', '某作者', '某出版社', '2020-01-01', '978-7-111-00000-0', 5, 0), ('数据库系统概论', '某作者', '某出版社', '2019-06-01', '978-7-111-00000-1', 3, 0); -- 插入读者 INSERT INTO Reader (ReaderName, Gender, Phone) VALUES ('A同学', '男', '13800000000'); -- 插入一条借阅记录,状态为0表示未归还 INSERT INTO Borrow (BookId, ReaderId, DueDate) VALUES (1, 1, DATEADD(DAY, 30, GETDATE()));这里有个细节:Book 表里的 BorrowedCount 字段,手动插入时填了 0。但在真实系统里,这个字段应该由程序在借书成功后自动加 1,还书后自动减 1,不能靠人手工维护。有些同学会把“借了几本”临时算出来,也不维护这个字段——那也可以,只要有地方能查到就行。但如果你选择了维护这个字段,就必须保证更新借阅表时同一事务里同步更新它,否则数据对不上。
同样要注意的是 IDENTITY 自增列,插入数据时千万别给 BookId 显式赋值,否则后续自增计数会乱掉。插完数据后用 SELECT * FROM Book 看一眼,确认 ID 从 1 开始。如果看到 ID 跳到了 1000 这种数字,说明之前有人手动插入过带 ID 的数据,脚本得重跑或者用 DBCC CHECKIDENT 重置。这种问题虽然不影响运行,但答辩时被老师翻出来会很尴尬。
提示:SQL 脚本文件建议单独保存成 .sql 文件放在项目根目录。一方面方便你重跑数据,另一方面设计文档里要贴数据库脚本,到时候直接从文件复制,比从 SSMS 里重新导出要干净。
4. 从空窗体到跑通登录:C# 连接 SQL Server 的最小可运行代码
4.1 连接字符串与 SqlConnection:为什么服务器名写死是个坏习惯
打开 Visual Studio,新建一个 Windows 窗体应用(.NET Framework),注意别选成 .NET Core 的 WinForms 模板,课程设计用的基本都是 .NET Framework,后面引 SqlClient 更省事。在解决方案里添加一个类文件 DbHelper.cs,用来集中管理数据库连接。这是整个项目的地基,几乎所有窗体都要调用它。
using System; using System.Data; using System.Data.SqlClient; public class DbHelper { // 连接字符串:服务器为本地,数据库为 LibraryDB private static string connStr = "Data Source=.;Initial Catalog=LibraryDB;Integrated Security=True"; // 执行查询,返回 DataTable public static DataTable Query(string sql, SqlParameter[] paras = null) { using (SqlConnection conn = new SqlConnection(connStr)) { using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (paras != null) cmd.Parameters.AddRange(paras); SqlDataAdapter da = new SqlDataAdapter(cmd); DataTable dt = new DataTable(); da.Fill(dt); return dt; } } } // 执行增删改,返回受影响行数 public static int Execute(string sql, SqlParameter[] paras = null) { using (SqlConnection conn = new SqlConnection(connStr)) { using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (paras != null) cmd.Parameters.AddRange(paras); conn.Open(); return cmd.ExecuteNonQuery(); } } } }Data Source=. 表示连接本机默认实例。如果你的 SQL Server 不是默认实例而是命名实例,比如 SQLEXPRESS,就要写成 Data Source=.\SQLEXPRESS。这是新手遇到最多的连接失败原因——装数据库时装了 Express 版本,实例名不是默认的,连接字符串却写成默认的。
用 SqlParameter 数组而不是直接拼接 SQL 字符串,这不仅是防 SQL 注入,还解决了日期类型、字符串里带单引号导致的拼串报错问题。比如书名里有“C#程序设计”,直接拼 SQL 没问题,但要是书名是“Let's Go”,单引号就会让 SQL 语法崩溃。参数化查询彻底避开这个坑。
4.2 登录窗体的完整实现:参数化查询与权限跳转
登录功能是每个课程设计都有的第一步。新建一个 LoginForm 窗体,放两个 TextBox、一个 Button,按钮的点击事件里写验证逻辑。验证分两层:第一层判断输入是否为空,第二层查数据库匹配账号密码。
private void btnLogin_Click(object sender, EventArgs e) { // 前端必填检查:账号密码不能为空 if (string.IsNullOrWhiteSpace(txtUserName.Text) || string.IsNullOrWhiteSpace(txtPassword.Text)) { MessageBox.Show("账号和密码不能为空!"); return; } string sql = "SELECT UserId, UserName, IsAdmin FROM [User] WHERE UserName=@name AND Password=@pwd"; SqlParameter[] paras = { new SqlParameter("@name", txtUserName.Text.Trim()), new SqlParameter("@pwd", txtPassword.Text.Trim()) }; DataTable dt = DbHelper.Query(sql, paras); if (dt.Rows.Count > 0) { // 记录登录用户信息,供其他窗体使用 Session.UserId = Convert.ToInt32(dt.Rows[0]["UserId"]); Session.IsAdmin = Convert.ToBoolean(dt.Rows[0]["IsAdmin"]); MainForm main = new MainForm(); main.Show(); this.Hide(); } else { MessageBox.Show("账号或密码错误!"); } }Query 方法内部用 SqlDataAdapter 的 Fill 方法执行查询,它不需要显式 Open 连接,DataAdapter 会自动管理连接的打开和关闭。表里的 IsAdmin 是 BIT 类型,用 Convert.ToBoolean 可以直接转成 C# 的 bool,不用先转 int 再判断。
Session 这个类需要你自己定义,它就是几个静态字段,用来模拟全局会话。课程设计的系统规模很小,不值得引入真实的 Session 框架。两个静态属性就能解决问题——UserId 存当前登录用户 ID,IsAdmin 存是否为管理员。主窗体加载时判断 IsAdmin 的值,控制“用户管理”按钮的可见性。
5. 核心功能落地:图书管理模块的增删改查与界面联动
5.1 DataGridView 绑定数据:显示、刷新与行号陷阱
图书管理模块是课程设计的重头戏。窗体上放一个 DataGridView、四个 TextBox(书名、作者、出版社、ISBN)、三个按钮(添加、修改、删除)。窗体加载时执行 RefreshData() 方法,把图书数据加载到 DataGridView 上。
private void RefreshData() { string sql = @"SELECT BookId, BookName, Author, Publisher, ISBN, TotalCount, BorrowedCount FROM Book WHERE Status = 1"; DataTable dt = DbHelper.Query(sql); dgvBooks.DataSource = dt; // 设置列标题为中文 dgvBooks.Columns["BookId"].HeaderText = "编号"; dgvBooks.Columns["BookName"].HeaderText = "书名"; dgvBooks.Columns["Author"].HeaderText = "作者"; dgvBooks.Columns["Publisher"].HeaderText = "出版社"; dgvBooks.Columns["ISBN"].HeaderText = "ISBN"; dgvBooks.Columns["TotalCount"].HeaderText = "馆藏量"; dgvBooks.Columns["BorrowedCount"].HeaderText = "已借出"; }把 DataTable 直接赋值给 DataSource 是 WinForms 里最省事的绑定方式。DataGridView 会自动生成所有列,注意 SELECT 里查了哪些字段,表格里就会显示哪些列,所以别把不打算显示的字段查出来。列名是英文,需要手动改成中文显示,不然界面很丑,答辩老师第一眼印象就扣分了。
DataGridView 默认是多行选中,记得在属性窗口里把 MultiSelect 设为 False。不然用户按住 Ctrl 能选中多行,你的程序里取当前行的逻辑就可能拿到一个空引用。另外,用鼠标点击行时,整个行会高亮,但如果你没写 CellClick 事件,TextBox 里的内容不会自动跟着变——用户体验很差。通常的做法是在 CellClick 事件里把选中行的数据回填到 TextBox 里,方便修改。
5.2 添加与修改图书:参数化 SQL 的两种写法对比
添加图书就是往 Book 表 INSERT 一条记录。修改图书是 UPDATE,通过主键定位。这几段代码看起来简单,但有几个细节会影响你答辩时的发挥。
private void btnAdd_Click(object sender, EventArgs e) { // 基本校验:书名和ISBN不能为空 if (string.IsNullOrWhiteSpace(txtName.Text)) { MessageBox.Show("请输入书名!"); return; } string sql = @"INSERT INTO Book (BookName, Author, Publisher, PublishDate, ISBN, TotalCount) VALUES (@name, @author, @publisher, GETDATE(), @isbn, @count)"; SqlParameter[] paras = { new SqlParameter("@name", txtName.Text.Trim()), new SqlParameter("@author", txtAuthor.Text.Trim()), new SqlParameter("@publisher", txtPublisher.Text.Trim()), new SqlParameter("@isbn", txtISBN.Text.Trim()), new SqlParameter("@count", Convert.ToInt32(nudCount.Value)) }; if (DbHelper.Execute(sql, paras) > 0) { MessageBox.Show("添加成功!"); RefreshData(); // 操作成功后刷新表格 } }执行 INSERT 的 SQL 语句,用 Execute 方法返回受影响行数来判断是否成功。受影响的行数大于 0,说明插入成功了。GETDATE() 是 SQL Server 的内置函数,取当前时间,数据库端的写法比自己传一个 DateTime 参数更省事,也避免了 C# 和 SQL Server 日期格式不一致的问题。
修改图书的 SQL 类似,但 WHERE 子句必须用主键 BookId 定位,别用书名——万一有两本同名书呢。在 DataGridView 的 CellClick 事件里,把选中的 BookId 存在窗体的一个私有变量里,修改时带上这个变量:
private int currentBookId = 0; // 当前选中图书的主键 private void dgvBooks_CellClick(object sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0) return; // 点击列头时 RowIndex 为 -1,直接退出 DataGridViewRow row = dgvBooks.Rows[e.RowIndex]; currentBookId = Convert.ToInt32(row.Cells["BookId"].Value); // 回填数据到文本框,方便修改 txtName.Text = row.Cells["BookName"].Value.ToString(); txtAuthor.Text = row.Cells["Author"].Value.ToString(); txtPublisher.Text = row.Cells["Publisher"].Value.ToString(); txtISBN.Text = row.Cells["ISBN"].Value.ToString(); }这段 CellClick 有个容易翻车的点:用户点空白区域或者点行头,e.RowIndex 可能是 -1 或越界值,不加判断直接访问 Rows[e.RowIndex] 会抛 ArgumentOutOfRangeException。这种异常在调试时很烦人,但又极其常见——只要你在 DataGridView 上点过列头就复现。
5.3 删除图书的约束处理:先查后删,避免外键报错
删除图书不是简单地执行 DELETE FROM Book WHERE BookId=@id。如果这本书已经存在借阅记录,直接删会被外键约束拦住,SQL Server 会抛异常。正确做法是删除前先查一下 BookId 有没有未归还的借阅记录:
private void btnDelete_Click(object sender, EventArgs e) { if (currentBookId == 0) { MessageBox.Show("请先选择要删除的图书!"); return; } // 第一步:查询这本书是否有未归还的借阅记录 string checkSql = "SELECT COUNT(*) FROM Borrow WHERE BookId=@bookId AND Status=0"; SqlParameter[] checkParas = { new SqlParameter("@bookId", currentBookId) }; DataTable dt = DbHelper.Query(checkSql, checkParas); int unreturnedCount = Convert.ToInt32(dt.Rows[0][0]); if (unreturnedCount > 0) { MessageBox.Show("该书还有未归还的借阅记录,不能删除!"); return; } string deleteSql = "DELETE FROM Book WHERE BookId=@bookId"; SqlParameter[] deleteParas = { new SqlParameter("@bookId", currentBookId) }; if (DbHelper.Execute(deleteSql, deleteParas) > 0) { MessageBox.Show("删除成功!"); RefreshData(); } }这个“先查后删”的模式值得你记住,它不只是应付这个课设,以后的任何管理系统项目——订单系统、库存系统、人员系统——都会遇到父子表的删除约束问题。逻辑上分两步处理,比在 SQL 里用 CATCH 捕获异常再回滚要直观,也容易在文档里写清楚。
顺手把借书和还书的操作也做了:借书时先检查 TotalCount 是否大于 BorrowedCount 也就是还有没有库存,然后插入一条 Status 为 0 的借阅记录,同时让 Book 表的 BorrowedCount 加 1。这两个操作必须放在同一个逻辑里,否则可能出现借阅记录插入成功、库存却没减的情况。推荐的做法是在存储过程里写事务,或者用 SqlTransaction 手动控制。课程设计要求不高的话,可以在 C# 里用事务包装多条 SQL:
using (SqlConnection conn = new SqlConnection(DbHelper.ConnStr)) { conn.Open(); SqlTransaction tx = conn.BeginTransaction(); try { SqlCommand cmd1 = new SqlCommand("INSERT INTO Borrow (BookId, ReaderId, DueDate) VALUES (@b,@r,@d)", conn, tx); cmd1.Parameters.AddRange(new SqlParameter[] { new SqlParameter("@b", bookId), new SqlParameter("@r", readerId), new SqlParameter("@d", DateTime.Now.AddDays(30)) }); cmd1.ExecuteNonQuery(); SqlCommand cmd2 = new SqlCommand("UPDATE Book SET BorrowedCount = BorrowedCount + 1 WHERE BookId=@b", conn, tx); cmd2.Parameters.AddWithValue("@b", bookId); cmd2.ExecuteNonQuery(); tx.Commit(); } catch { tx.Rollback(); MessageBox.Show("借书失败,请检查库存"); } }注意这里用了 BeginTransaction 并把 SqlCommand 的 Transaction 属性指定成 tx,两条命令才会纳入同一个事务。任何一步出错,回滚都能让数据库回到操作前的状态。这是事务最基础、最必要的用法,在课程设计里写到这个层次,已经是加分项了。
6. 避坑手册:图书管理系统最常见的 6 个翻车现场
现象一:程序启动后一执行查询就报错“无法打开登录所请求的数据库 LibraryDB。登录失败。”原因有几个可能:连接字符串里 Initial Catalog 写错、SQL Server 没有启用混合登录模式、数据库服务没启动。解决方法是先打开 SSMS 确认数据库真实名称,再检查服务名——在命令行输入 services.msc 找到 SQL Server 服务,看它是否正在运行。如果装了多个 SQL Server 实例,连接字符串的 Data Source 要写清楚实例名。
现象二:连接字符串写 “Data Source=localhost” 在别人电脑上跑不通。原因是 localhost 是当前机器的意思,你把自己的项目发给别人,别人电脑上没有你的数据库,当然连不上。这不是玄学,是环境问题。解决办法是不要在代码里把服务器名写死,把连接字符串放到 App.config 里,改起来方便。给老师演示前先确认你用的那台机器上数据库已经附加进去了。如果只是本机演示,Data Source=. 就是最安全的写法。
现象三:DataGridView 显示的中文正常,但插入数据库后变成了问号或者乱码。多数情况下是因为字段类型用了 varchar,它是非 Unicode 类型,中文存进去会丢字符。SQL Server 里中文应该用 nvarchar。你建表时如果偷懒用了 VARCHAR(50),现在要么改表结构(ALTER TABLE Book ALTER COLUMN BookName NVARCHAR(100)),要么在建表脚本里直接改成 NVARCHAR。顺带说一句,C# 字符串默认是 Unicode,和 nvarchar 是天然匹配的,这也是当初推荐 nvarchar 的核心原因。
现象四:删除图书时程序直接崩了,异常信息提示“DELETE 语句与 REFERENCE 约束冲突”。这正是第 5.3 节里处理的那个问题。你有两种解法:一种是程序里先判断再删除,另一种是建表时把外键的更新规则设为 CASCADE。我强烈建议选第一种,因为 CASCADE 会把借阅记录也删掉,历史数据就没了——系统里应该保留借阅历史,哪怕书已经下架。书面语叫“保留审计轨迹”,人话叫“以后查账靠它”。
现象五:往 TextBox 里输入的数字带空格或者符号,Convert.ToInt32 报 FormatException。原因是没有做类型转换前的校验。解决方法是在转换外面套 int.TryParse 方法,返回 false 就提示用户输入有误,而不是让异常直接扔到用户脸上。顺带把书名的 Trim() 做掉,去掉首尾空格可以避免很多“我明明输入了一样为什么查不到”的困惑。
现象六:窗体切换后,登录窗体的按钮还能再点,导致一次程序开了好几个主窗体。这是因为你用 this.Hide() 隐藏而不是关闭登录窗体。正确的做法是登录成功后 this.Hide() 并在这个方法里设置一个标志位,或者干脆用 Application.Run 的流程控制。最简单的方案是把主窗体设为唯一的入口,登录只是主窗体里的一个模态框。当然,这是设计层面的问题,答辩老师不一定会问,但如果你主窗体里的用户信息是静态的,多次打开主窗体就是用同一份数据,逻辑上已经没有意义了。
7. 设计文档的写作技巧:把 ER 图、用例图和核心代码说明写透
文档在课程设计评分里的占比通常不低于 40%,甚至有的学校规定文档分占总分一半。很多同学代码写完才开始着急文档,最后一晚上从网上找模板改数据,结果前后逻辑对不上——类图里有十个类,代码里只有五个;用例图画了四种角色,系统里只有管理员和普通用户。这种文档一眼假,老师翻两页就扔回去了。
写文档的正确时间点是开工之前,至少要在建表之前把 ER 图画出来。图书、读者、借阅三张表之间的关系,在建表脚本里是用外键体现的,在文档里就是用 ER 图体现的。ER 图画好,建表和写实体类的过程就变成了照图施工。你如果已经做完了,就倒推——把建表脚本里的外键关系整理出来,画一个标准的 ER 图。图里要标清楚主键、外键、1 对多还是多对多关系。一张 ER 图,加上表结构描述表(字段名、类型、是否主键、说明),这部分内容足够撑起文档的数据设计章节。
用例图也是必画的,但不用太复杂。系统里有两种角色:管理员和普通读者。管理员能做的动作包括图书管理、读者管理、借书、还书、查看统计;读者能做的动作是查询图书、查看个人借阅记录。画到一个用例图里,用角色框区分权限。这张图的价值在于它能帮你理清功能模块——如果某个操作你画了用例但代码里没实现,后期补代码;如果代码里有了但用例图没画,就补图。文档和代码要对齐,这是硬要求。
用例图、ER 图之外,核心代码说明部分不要整个类地粘贴代码。挑两三个有代表性的方法写清楚逻辑,比如登录验证、借书事务、删除约束判断。每个方法下面用一段话说明:这个方法是干什么的、用了哪些参数、异常处理是怎样的。别把 DbHelper 的二十行代码贴上去然后什么也不说,老师想看的不是代码本身,而是你有没有理解这段代码。
测试用例是这个文档里最容易被忽略但最加分的一块。列几个典型的测试用例:正常登录、密码错误登录、添加图书成功、删除有借阅记录的图书失败、借书的库存扣减。每个用例按“输入、操作、预期结果、实际结果”四栏写。如果你时间紧,至少写八到十个用例,把核心操作的正常流程和异常流程都覆盖到。这会让老师觉得你做了系统性验证,而不是只把界面点了一遍。
最后,如果想让系统在答辩时多几分亮点,可以在架构上做一个小的加法:把常用的借书、还书操作写成存储过程,代码里用 SqlCommand 的 CommandType.StoredProcedure 调用。它不改变你现有的代码结构,只是把 SQL 逻辑挪到了数据库端。缺点是存储过程的语法对新手有隔阂,写错了调试起来比 C# 还难受——也是个血泪经验。如果存储过程没把握写好,最稳妥的做法是保持现在的参数化 SQL,然后在文档里写清楚“为什么没有用存储过程,而是用参数化查询保证 SQL 注入安全”——这本身就是很好的答辩话术。
这个系统做完,你的收获不只是会写几行 C# 代码。从数据建模到事务处理,从连接管理到参数化查询,每一步都是以后做任何管理系统都能复用的底子。等到将来真的去企业做项目,你会发现课设里学的事务、外键、参数化这些概念,每一个都在工作里的某个深坑处等你。AB同学当年就是课设的时候把连接字符串写死成别人的服务器名,答辩现场翻车,被老师追问了三分钟环境配置——这个教训至今记得。希望这篇文章帮到你,让你少走这一步弯路。
本文还有配套的精品资源,点击获取