简介一份面向计算机专业学生的数据库课程设计完整报告以“图书销售管理系统的设计与实现”为题完整覆盖需求分析、SQL Server 2008环境选择、ER模型构建、数据表字段定义、主外键约束、T-SQL建库建表及系统登录/管理模块划分等数据库开发关键环节。适合正在完成大型数据库技术与应用课程设计、需要参考系统分析过程与数据库建模范例的读者也可用于学习数据库原理课程后的综合实践。资源包仅含1个doc文档体积约441KB文本内容便于直接查阅和复制修改。目前已有122人学习浏览可作为课程设计文档写作与数据库操作的参考。文档除展示概念结构设计和逻辑结构设计外还给出物理结构设计思路与T-SQL语句示例能帮助读者理解从ER模型到实际建表语句的完整落地方案同时提供一套规范的课程设计报告章节框架便于快速搭建自己的设计文档结构。1. 图书管理系统课程设计这份文档值不值得照着重做一遍先说结论这份《图书销售系统的设计与实现》课程设计文档虽说是2011年的老项目但它的数据库设计部分——两张核心表、外键关系、T-SQL建库脚本、销售统计逻辑——放到今天仍然是数据库课程设计里最典型的一类题材。它对应的是「大型数据库技术与应用」这门课作者用了 C# Visual Studio 2010 SQL Server 2008完成了从登录、新书上架、删除图书、图书查询、图书销售到销售查询的完整闭环。数据库应用专业的课程设计里图书销售系统的出镜率极高因为它的实体关系简单业务逻辑进库、出库、统计又足够撑起一篇设计文档。适合谁正在做数据库课程设计、需要参考表结构设计和文档写法的人也适合想把这份老项目改造成自己能交差的版本的从业者。文档里能直接抄的作业有两块一是 SQL Server 2008 的建库建表语句和物理结构设计表二是 C# WinForm 界面的数据访问代码骨架。本文按「数据模型 → 核心SQL → 界面落地 → 踩坑排查 → 进阶改造」的顺序把这套资源拆透。2. 数据模型先行两张表的字段边界与外键关系2.1 逻辑结构设计为什么只需要两张表这套系统的数据库叫salebook全库只有两张表图书信息表bookmanage和销售信息表salemanage。这符合小型图书销售店的真实场景——没有会员体系、没有订单明细、没有供应商核心业务就是「库里有什么书」和「卖出了什么」。图书信息表bookmanage的字段设计如下列名数据类型长度是否可空主键说明编号int4否是设置书的编号书名char16否否书的名称单价float8否否书的单价作者char16是否书的作者出版社char32是否图书出版社数量int4否否书的数量添加时间datetime8是否书的添加时间销售信息表salemanage的字段设计如下列名数据类型长度是否可空主键说明编号int4否是所卖书的编号书名char10否否卖书的书名数量int4否否卖书的数量销售额float8否否书的销售额日期时间datetime8否否卖书的时间两张表通过编号建立关联。正常情况下salemanage.编号应该是外键指向bookmanage.编号但文档中的建表 SQL 有坑这点在后面章节专门说。2.2 物理结构设计数据类型选型的三个细节看表结构时留意三个细节。第一数量字段用的是int而不是smallint对图书这种单品价格不低的商品单次进货几百本很正常int的安全范围足够。第二单价和销售额都用float这在课程设计里常见但生产中我会换成decimal(10,2)——float是浮点0.1 0.2 不等于 0.3 的经典问题在金额计算上容易翻车。第三书名字段在bookmanage里是char(16)在salemanage里却是char(10)。同一份数据在主表和明细表里长度不一致一旦书名超过 10 个字符插入销售记录时会被截断或报错。这是文档里典型的「能跑但设计不一致」的坑。2.3 用 T-SQL 实现建库建表原版脚本与改造建议文档给出了建库建表的 T-SQL 脚本但脚本里有几个明显问题。原文脚本如下create database salebook use salebook create table bookmanage( 编号 int not null primary key, 书名 char(10), 单价 int, 作者 char(20), 出版社 char(10), 数量 int, 添加时间 datetime ) create table salemanage( 编号 char(20) not null primary key, foreign key references bookmanage(编号) 书名 char(50), foreign key references bookmanage(书名) 数量 int, 作者 char(20), foreign key references bookmanage(作者) 价格 int, 日期时间 datetime )这段脚本有三处问题。第一salemanage表的外键语法不完整foreign key references bookmanage(编号)这种写法在 SQL Server 2008 里不能直接执行正确的写法需要给外键约束起名或用foreign key (编号) references bookmanage(编号)。第二salemanage的外键列编号类型是char(20)而主表bookmanage.编号是int类型不一致外键建立时会报「列类型不匹配」。第三salemanage里还多了一个不属于销售记录的作者字段而且把销售额写成了价格和后面 C# 代码里insert into salemanage(编号,书名,数量,销售额,日期时间)的列名对不上。我一般会这样改造建表脚本create database salebook go use salebook go create table bookmanage( 编号 int not null primary key, 书名 nvarchar(50) not null, 单价 decimal(10,2) not null, 作者 nvarchar(20) null, 出版社 nvarchar(30) null, 数量 int not null default 0, 添加时间 datetime not null default getdate() ) go create table salemanage( 编号 int not null, 书名 nvarchar(50) not null, 数量 int not null, 销售额 decimal(10,2) not null, 日期时间 datetime not null default getdate(), constraint pk_salemanage primary key (编号, 日期时间), constraint fk_salemanage_book foreign key (编号) references bookmanage(编号) ) go这里做了几处关键调整salemanage.编号改成int与主表对齐主键从单纯的编号改成编号 日期时间联合主键因为同一本书会在不同时间多次销售单列主键会冲突金额类型用decimal(10,2)替代float外键约束用显式命名方便后面drop constraint操作。3. 核心业务 SQL从入库、销售到统计的增删改查3.1 添加图书原始 insert 语句与其注入风险文档的添加新书模块直接把文本框内容拼进 SQL 字符串cmd.CommandText insert into bookmanage(编号,书名,单价,作者,出版社,数量,添加时间) values ( textBox1.Text , textBox2.Text , textBox3.Text , textBox4.Text , textBox5.Text , textBox6.Text , System.DateTime.Now ); cmd.ExecuteNonQuery();这种字符串拼接写法在课程设计里非常普遍但有两个问题。第一是 SQL 注入在textBox2.Text里输入,) ; drop table bookmanage --这类内容语句就会被截断并执行额外命令。第二是类型问题编号是int却用单引号包成字符串传值SQL Server 会做隐式转换虽然能跑但在某些字符集和排序规则下会有性能损耗。建议改成参数化查询using (SqlCommand cmd new SqlCommand()) { cmd.Connection conn; cmd.CommandType CommandType.Text; cmd.CommandText insert into bookmanage(编号,书名,单价,作者,出版社,数量,添加时间) values (id, bookname, price, author, publisher, count, addtime); cmd.Parameters.AddWithValue(id, int.Parse(textBox1.Text.Trim())); cmd.Parameters.AddWithValue(bookname, textBox2.Text.Trim()); cmd.Parameters.AddWithValue(price, decimal.Parse(textBox3.Text.Trim())); cmd.Parameters.AddWithValue(author, textBox4.Text.Trim()); cmd.Parameters.AddWithValue(publisher, textBox5.Text.Trim()); cmd.Parameters.AddWithValue(count, int.Parse(textBox6.Text.Trim())); cmd.Parameters.AddWithValue(addtime, DateTime.Now); cmd.ExecuteNonQuery(); }AddWithValue是 SQL Server 参数化查询最简写法。注意编号和数量都先做了int.Parse再传参这样数据库拿到的是强类型值而不是字符串可以避免前面说的隐式类型转换问题。3.2 图书销售文档缺失的扣库存逻辑文档的销售模块只做了两件事按编号查bookmanage得到单价然后把销售记录插入salemanage。但这里少了一个关键操作——扣减bookmanage.数量。也就是说按文档原样实现卖完书后库存不会减少图书查询模块查出来的剩余数量永远是初始值。这是整个项目最大的逻辑漏洞。补齐后的销售流程应该是「查单价 → 算销售额 → 更新销售表 → 扣库存」四步string sqlBook select 书名, 单价, 数量 from bookmanage where 编号 id; SqlCommand cmdBook new SqlCommand(sqlBook, conn); cmdBook.Parameters.AddWithValue(id, int.Parse(textBox2.Text.Trim())); SqlDataReader reader cmdBook.ExecuteReader(); if (!reader.HasRows) { MessageBox.Show(图书编号不存在); return; } reader.Read(); string bookName reader[书名].ToString(); decimal price Convert.ToDecimal(reader[单价]); int stock Convert.ToInt32(reader[数量]); int sellCount int.Parse(textBox3.Text.Trim()); reader.Close(); if (stock sellCount) { MessageBox.Show(库存不足当前剩余 stock 本); return; } decimal amount price * sellCount; // 插入销售记录 string sqlInsert insert into salemanage(编号,书名,数量,销售额,日期时间) values(id, name, count, amount, now); SqlCommand cmdInsert new SqlCommand(sqlInsert, conn); cmdInsert.Parameters.AddWithValue(id, id); cmdInsert.Parameters.AddWithValue(name, bookName); cmdInsert.Parameters.AddWithValue(count, sellCount); cmdInsert.Parameters.AddWithValue(amount, amount); cmdInsert.Parameters.AddWithValue(now, DateTime.Now); cmdInsert.ExecuteNonQuery(); // 扣减库存 string sqlUpdate update bookmanage set 数量 数量 - count where 编号 id; SqlCommand cmdUpdate new SqlCommand(sqlUpdate, conn); cmdUpdate.Parameters.AddWithValue(count, sellCount); cmdUpdate.Parameters.AddWithValue(id, id); cmdUpdate.ExecuteNonQuery();注意这里先查库存、判断够不够、再插入销售记录、最后扣库存。如果希望更强的数据一致性应该把插入和扣库存包在同一个事务里using (SqlTransaction tx conn.BeginTransaction()) { // 上面两个 SqlCommand 都指定 cmd.Transaction tx // 全部执行成功后 tx.Commit()任一步失败则 tx.Rollback() }课程设计里不要求事务但如果你想把项目做得像样一点事务是加分项。3.3 销售查询按编号和按日期两种统计口径文档的销售查询分了两个按钮一个是按图书编号查一个是按日期时间查。按编号查的 SQL 是select * from salemanage where salemanage.编号 xxx按日期时间查的场景一般是查「某一天的销售额」。如果查当天应该在日期时间字段上做范围过滤而不是用select * from salemanage where 日期时间 2024-01-01 and 日期时间 2024-01-02这里有个常见误区。日期时间字段是datetime类型它包含时分秒。如果用户输入2024-01-01然后用 2024-01-01去匹配只能查到00:00:00那一秒的记录当天其余时间的数据全查不出来。正确做法是用半开区间 当天零点 and 次日零点。汇总某段时间的销售额可以加个group byselect convert(date, 日期时间) as 销售日期, sum(销售额) as 当日销售额 from salemanage where 日期时间 startDate and 日期时间 endDate group by convert(date, 日期时间) order by 销售日期3.4 删除图书外键约束下的两难文档的删除图书模块就是一句delete from bookmanage where 编号 textBox1.Text 但如果你按照第 2 章的改造建议加了外键约束问题就来了如果该编号的图书已经有销售记录SQL Server 会拒绝删除主表记录报外键冲突错误。这是课程设计里最容易翻车的地方。处理方式有三种一是删除前先检查salemanage里有没有记录有则先删除销售记录二是把外键改成on delete cascade级联删除三是不物理删除给bookmanage加一个「是否下架」字段逻辑删除。课程设计的场景下我会选方案一能体现你对数据完整性的理解string checkSql select count(*) from salemanage where 编号 id; SqlCommand cmdCheck new SqlCommand(checkSql, conn); cmdCheck.Parameters.AddWithValue(id, int.Parse(textBox1.Text.Trim())); int cnt (int)cmdCheck.ExecuteScalar(); if (cnt 0) { string delSaleSql delete from salemanage where 编号 id; SqlCommand cmdDelSale new SqlCommand(delSaleSql, conn); cmdDelSale.Parameters.AddWithValue(id, textBox1.Text.Trim()); cmdDelSale.ExecuteNonQuery(); } string delBookSql delete from bookmanage where 编号 id; SqlCommand cmdDelBook new SqlCommand(delBookSql, conn); cmdDelBook.Parameters.AddWithValue(id, textBox1.Text.Trim()); cmdDelBook.ExecuteNonQuery();4. C/S 界面落地登录控制与数据绑定怎么接4.1 登录模块写死的账号密码与改造方案登录模块的验证逻辑是文档里最好懂的一段if ((textBox1.Text 50902013019) (textBox2.Text 123456)) { Mainform f1 new Mainform(); f1.Show(); this.Hide(); } else { MessageBox.Show(您输入有误请重新输入); textBox1.Clear(); textBox2.Clear(); }账号写死成学号50902013019密码写死成123456这在课程设计里很常见答辩老师一般也不会较真。但从「系统设计」的角度这不算真正的登录判断。真正能快速改造的做法是把账号密码挪到一张管理员表里create table manager( 用户名 varchar(20) not null primary key, 密码 varchar(64) not null, 角色 varchar(10) not null default 管理员 )然后用select count(*) from manager where 用户名 user and 密码 pwd判断登录是否成功。密码存储建议至少做一次Hash课程设计做MD5或SHA2_256足够说明你考虑过安全问题using (var sha System.Security.Cryptography.SHA256.Create()) { byte[] bytes sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(textBox2.Text)); StringBuilder sb new StringBuilder(); for (int i 0; i bytes.Length; i) { sb.Append(bytes[i].ToString(x2)); } string pwdHash sb.ToString(); // 拿 pwdHash 去和数据库里的密码字段比对 }4.2 连接串data source 的写法与含义整个系统反复出现的连接串是conn.ConnectionString data source20110305-1426\\SQLEXPRESS;databasesalebook;integrated securitytrue;;这段连接串写死在代码里逐项拆开看data source20110305-1426\SQLEXPRESS表示数据库实例是20110305-1426这台机器上的SQLEXPRESS命名实例\SQLEXPRESS是 SQL Server Express 版安装时的默认实例名databasesalebook指定默认连接的数据库integrated securitytrue表示用 Windows 身份验证不需要在连接串里写uid和pwd。这套连接串在课程设计里没毛病但有三个环境相关问题需要知道。第一如果你本机没有装 SQL Server Express或者装的是默认实例而不是命名实例这个连接串连不上改成data source.或data sourcelocalhost就行。第二integrated securitytrue只适用于 Windows 身份验证模式如果 SQL Server 改成了混合验证模式需要额外加user idsa;passwordxxx。第三连接字符串应该放在配置文件App.config里而不是散落在每个窗体代码中connectionStrings add nameSaleBookDB connectionStringdata sourcelocalhost;databasesalebook;integrated securitytrue; providerNameSystem.Data.SqlClient / /connectionStrings然后在代码里统一读取string connStr System.Configuration.ConfigurationManager.ConnectionStrings[SaleBookDB].ConnectionString;这样换机器时只需要改配置文件不用重新编译。4.3 数据绑定DataGrid 与 DataGridView 的差异文档里多处出现dataGrid1.DataSource ds这个控件是DataGrid是 .NET Framework 1.1/2.0 时代的旧控件。Visual Studio 2010 里新建 WinForm 项目时工具箱里默认放的是DataGridView两者属性名相近但DataGridView功能更强。文档里.NET相关代码明显是混合了旧代码的习惯。绑定DataGridView的正确做法是SqlConnection conn new SqlConnection(connStr); SqlDataAdapter da new SqlDataAdapter(select * from bookmanage where 编号 id, conn); da.SelectCommand.Parameters.AddWithValue(id, textBox1.Text.Trim()); DataTable dt new DataTable(); da.Fill(dt); dataGridView1.DataSource dt;这套写法比文档里的DataSet DefaultView更简洁。注意da.SelectCommand.Parameters.AddWithValue是给SqlDataAdapter内部的查询命令加参数这样带条件的查询也是参数化的不会拼接 SQL。4.4 窗体跳转逻辑Show 与 Hide 的配合主界面跳转用了f1.Show(); this.Hide();。这里有个需要留意的行为Hide只是把当前登录窗体隐藏并没有关闭。程序主入口如果还保留着对登录窗体的引用那么隐藏的登录窗体进程不会退出。当你在主窗体点「退出系统」时如果只写了this.Close()隐藏的登录窗体还在进程不会结束。退出按钮的正确写法应该是先Application.OpenForms遍历关闭所有窗体再Application.Exit()private void 退出系统_Click(object sender, EventArgs e) { for (int i Application.OpenForms.Count - 1; i 0; i--) { Application.OpenForms[i].Close(); } Application.Exit(); }5. 常见问题排查从连接串到中文乱码的五个现场5.1 连接串里的 SQLEXPRESS换机器就连不上现象把文档里的源码原样拷到自己电脑上运行点击任何按钮都报「建立到服务器的连接时发生错误」或者登录成功后主窗体加载不出数据。原因data source20110305-1426\SQLEXPRESS是作者当年开发机器的机器名加 SQL Server Express 实例名不是通用写法。每台电脑的机器名不同SQL Server 实例名也可能是默认实例MSSQLSERVER此时实例名部分为空连接串应写成data sourcelocalhost。解决把连接串里的20110305-1426\SQLEXPRESS全部替换成localhost或者在 SQL Server 配置管理器里查看当前实例名。给一个小技巧——先用「记事本」对项目做全局搜索替换20110305-1426\\SQLEXPRESS为localhost一次性改完避免漏改。5.2 中文字段名与外键建表脚本执行报错现象按照文档第 3.5 节的 T-SQL 脚本执行SQL Server 报「foreign key 附近有语法错误」或「类型 int 与类型 char 不兼容」。原因文档里的外键语法是残缺的。foreign key references bookmanage(编号)缺少外键列定义也没有给约束起名。另外salemanage.编号定义成了char(20)与bookmanage.编号的int类型不匹配即便语法补全外键也建不起来。解决按第 2 章给出的改造版建表脚本执行。核心改动是显式定义外键列并用constraint 约束名 foreign key (列名) references 主表(主键列)的完整语法。5.3 图书销售后库存不变卖完书再查数量还是原值现象执行图书销售后进入图书查询模块查该编号图书剩余数量没有减少。图书销售量大了以后库存成了负数也没人拦。原因文档的销售模块只往salemanage插记录没有执行update bookmanage set 数量 数量 - 卖出数量。解决按第 3.2 节的完整流程补上库存扣减逻辑。如果不想改后端代码也可以通过在salemanage上建一个after insert触发器来扣库存SQL Server 2008 完全支持create trigger trg_salemanage_insert on salemanage after insert as begin declare id int, qty int select id 编号, qty 数量 from inserted update bookmanage set 数量 数量 - qty where 编号 id end5.4 删除图书时外键冲突删不掉报约束错误现象删除一本已经卖出去过的书报「DELETE 语句与 REFERENCE 约束冲突」。原因salemanage表里还有该编号的销售记录外键约束拒绝了删除主表记录。解决先删除salemanage里对应编号的记录再删bookmanage或者干脆不用物理删除在bookmanage加是否在售字段做逻辑下架。课程设计文档里要求「删除图书」所以先删明细再删主表是最符合要求的做法。5.5 char 类型补空格书名查询匹配不上的玄学现象添加图书时书名输入「数据库原理」查询时输入同样五个字却查不出结果或者显示的书名右侧多出很多空白字符。原因bookmanage.书名定义的是char(16)char是定长类型不足 16 个字符时 SQL Server 会在右侧自动补空格。where 书名 数据库原理传进去的字符串不带空格和存储的「数据库原理 10个空格」比较就匹配不上。解决表结构里把char改成varchar或nvarchar这是最干净的方案如果已经建表用REPLACE(书名, , )做条件过滤只是权宜之计治标不治本。6. 进阶用法把课程设计改成能交差也能跑的系统前面五章解决了「照着重做」的问题。这一章说怎么让它从「课程设计文档」变成「能真正跑起来的系统」。我一般会做四个升级按性价比排序。第一个升级是统一数据访问层。文档里每个窗体都新建SqlConnection和SqlDataAdapter代码重复率高。抽一个公共类DbHelperpublic static class DbHelper { private static readonly string connStr ConfigurationManager.ConnectionStrings[SaleBookDB].ConnectionString; public static DataTable Query(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); SqlDataAdapter da new SqlDataAdapter(cmd); DataTable dt new DataTable(); da.Fill(dt); return dt; } } public static int Execute(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(); } } }以后每个窗体不再自己拼连接串新写一个功能只需要一行调用DataTable dt DbHelper.Query(select * from bookmanage where 编号 id, new SqlParameter(id, textBox1.Text.Trim())); dataGridView1.DataSource dt;第二个升级是把销售记录改成明细表加汇总表。真正的图书销售系统至少需要三张表bookmanage只管图书档案和当前库存saleorder记录每一笔销售订单号、时间、操作员saleitem记录每笔销售的图书明细哪本书、几本、多少钱。salemanage这种单表结构只能统计已知编号的图书做不了「某段时间卖得最好的书排名」这种分析需求。第三个升级是加报表查询。文档的销售查询只有select * from salemanage信息量很低。加一条汇总 SQL学术水平和实用价值立刻上一个台阶select top 10 b.书名, b.出版社, sum(s.数量) as 总销量, sum(s.销售额) as 总销售额 from salemanage s join bookmanage b on s.编号 b.编号 where s.日期时间 startDate and s.日期时间 endDate group by b.书名, b.出版社 order by 总销售额 desc第四个升级是把主键和外键策略写明在文档里。课程设计答辩时老师大概率会问「为什么salemanage要单独设置主键」「为什么用decimal不用float」。把第 2 章那些调整理由整理成「数据库设计说明」段落补进文档这份课程设计的质量评级会明显不一样。这套资源里最有价值的不是现成的代码能不能编译而是它给了你一套标准的数据库课程设计骨架需求分析、概念结构、逻辑结构、物理结构、建库建表、模块实现、测试验证。按这个骨架走一遍换成「学生管理系统」「药品进销存系统」都是通用的。从那以后我每次拿到课程设计类资源都会强制把表结构设计、外键关系和事务处理先过一遍确认这三项没问题才开始看界面代码——希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?