☰
教材订购系统课设实战:SQL Server数据库设计与ASP.NET部署全解析
2026/10/13 14:05:37 网站建设 项目流程

简介:基于ASP.NET与SQL Server的高校课程教材在线征订系统课程设计项目,面向计算机专业学生,可用于数据库、Web开发等课程的课设参考与毕业设计借鉴。系统围绕教材征订场景,实现管理员维护课程班级信息、教师提交教材订购申请、管理员审核并填写拒绝原因、采购入库与状态更新、教材发放通知、教师端进度查询等完整流程,业务清晰,逻辑完善。压缩包共121个文件,约874KB,包含aspx页面、vb代码、resx资源、xsd数据集、mdf/ldf数据库文件以及doc设计报告等,工程目录完整,便于直接编译运行或研读。目前已有1704人学习浏览,是较实用的数据库课程设计案例。除源码和数据库外,报告文档有助于对照理解表结构、页面流转与业务实现,适合在此基础二次开发或撰写课程设计说明书。

1. 教材订购系统课设包:真正的分值不在代码,在数据库设计与报告答辩

拿到《数据库课程设计》学校教材订购系统asp.net+SQL Server这套源码和报告时,你大概率是在赶课程设计或者补交某门课的验收材料。很多同学的第一反应是去读那些aspx页面写业务逻辑,但我做过十几套这类课设的辅导后可以明确告诉你:这类管理系统的代码即便全部重写,也就一两个通宵的功夫,真正卡住分数的是数据库设计是否规范、报告里的ER图和流程描述是否经得起答辩追问。教材订购系统的核心业务并不复杂——学生登录后按教材名称或出版社查询书目、加入订购单,管理员维护教材信息和审核订单,本质上就是一套带用户权限的增删改查页面加几张有外键关联的表。这套东西适合正在做数据库课程设计的在校生、想快速套一个B/S管理系统的初级开发者,也适合需要接手维护ASP.NET WebForms老项目的同事。它最典型的技术构成,是SQL Server中的五张表和ASP.NET里以GridView加Session实现的订购主流程,把这两块吃透,你交上去的报告和演示就不会翻车。

2. 先还原数据库再读代码:五张表才是这套课设的骨架

2.1 为什么第一步必须是还原数据库而不是打开源码

课程设计类项目的验收逻辑和真实项目不一样,老师第一眼看的是数据库脚本能不能在你的机器上跑起来、表结构是否完整、外键关系是否合理,然后才看页面功能。所以拿到压缩包后,我一般先做两件事:解压后找.bak备份文件或.sql建库脚本,先把它还原到本地SQL Server实例里;再用SSMS或Navicat for SQL Server这类客户端连接上去,看一眼左栏的表清单。这个顺序能帮你用最短时间判断这套源码的可信度——如果还原后表里没有种子数据,或者外键关系显示为虚线(意味着没有实际建立约束),那后面调页面时大概率会遇到空引用或主键冲突,越早发现越好。

还原时有个最常见的坑:直接双击.bak文件是没用的,必须打开SSMS,右键“数据库”节点选择“还原数据库”,在源设备里定位那个备份文件。如果宿主SQL Server版本比备份文件版本低,还原会直接报错,这个问题我在第4章会专门展开。这里额外提醒一点,还原成功后立刻检查数据库名称,很多课设包的脚本里写死了数据库名(比如BookManage),页面里的连接字符串也指向这个名字,你要是为了图省事改成别的名字,后面部署时就会遇到“无法打开登录请求的数据库”这类排查起来很绕的问题。

2.2 五张核心表的DDL脚本与字段设计理由

教材订购系统的经典数据模型就五张表:用户表、教材表、订购单主表、订购单明细表、购物车表。为什么不是四张也不是六张?因为学生选教材是一个“先加购物车、后统一提交”的过程,购物车表承担了临时数据的持久化,而订单主表和明细表的设计意图是一对多结构——一次订购对应多本教材。这个一对多关系是课设答辩时老师几乎必问的点,你如果能把“为什么拆主表和明细表”讲清楚,比你在页面上多做两个按钮拿分多得多。

下面是我常用的建表SQL脚本,直接在一台装有SQL Server的机器上执行即可:

-- 1. 用户表:学生与管理员共用一个表,用Role区分 CREATE TABLE Users ( UserId INT IDENTITY(1,1) PRIMARY KEY, -- 自增主键 UserName NVARCHAR(50) NOT NULL UNIQUE, -- 登录名,唯一约束避免重名 Password NVARCHAR(64) NOT NULL, -- 密码(课设用明文可接受,生产必须哈希) RealName NVARCHAR(20) NOT NULL, -- 真实姓名,用于订单收货人展示 Role TINYINT NOT NULL DEFAULT 0, -- 0=学生 1=管理员 CreateTime DATETIME NOT NULL DEFAULT GETDATE() ); -- 2. 教材表:课程设里教材信息不需要过多规范化,冗余出版社和作者是合理的 CREATE TABLE Books ( BookId INT IDENTITY(1,1) PRIMARY KEY, BookName NVARCHAR(100) NOT NULL, Author NVARCHAR(50) NOT NULL, Publisher NVARCHAR(80) NOT NULL, -- 出版社,查询频率高,建议建索引 Price DECIMAL(6,2) NOT NULL CHECK (Price >= 0), -- 价格,CHECK约束是加分项 Stock INT NOT NULL DEFAULT 0 -- 库存量,下单时要用 ); -- 3. 购物车表:一个学生加购多条教材,UserId+BookId做联合唯一 CREATE TABLE CartItems ( CartId INT IDENTITY(1,1) PRIMARY KEY, UserId INT NOT NULL REFERENCES Users(UserId), -- 外键指向用户表 BookId INT NOT NULL REFERENCES Books(BookId), -- 外键指向教材表 Quantity INT NOT NULL DEFAULT 1 CHECK (Quantity > 0), AddTime DATETIME NOT NULL DEFAULT GETDATE(), CONSTRAINT UQ_UserBook UNIQUE (UserId, BookId) -- 同一本书不能重复加入 ); -- 4. 订购单主表:一单一条记录,记录总金额和状态 CREATE TABLE Orders ( OrderId INT IDENTITY(1,1) PRIMARY KEY, UserId INT NOT NULL REFERENCES Users(UserId), OrderNo NVARCHAR(32) NOT NULL UNIQUE, -- 订单号,用编码规则生成 TotalAmount DECIMAL(8,2) NOT NULL DEFAULT 0, Status TINYINT NOT NULL DEFAULT 0, -- 0=待审核 1=已通过 2=已拒绝 CreateTime DATETIME NOT NULL DEFAULT GETDATE() ); -- 5. 订购单明细表:与主表构成一对多,体现每本书的下单数量与单价快照 CREATE TABLE OrderDetails ( DetailId INT IDENTITY(1,1) PRIMARY KEY, OrderId INT NOT NULL REFERENCES Orders(OrderId) ON DELETE CASCADE, -- 删主表带明细 BookId INT NOT NULL REFERENCES Books(BookId), Quantity INT NOT NULL CHECK (Quantity > 0), UnitPrice DECIMAL(6,2) NOT NULL -- 冗余单价,防止教材改价影响历史订单 );

这套建表逻辑里有两个细节建议保留:一个是订单明细里的UnitPrice冗余字段,另一个是购物车表上的联合唯一约束UQ_UserBook。前者是为了保证“教材涨价后旧订单价格不变”,这是真实业务里会遇到的场景,写在报告的数据设计说明里能体现你考虑过一致性问题;后者是为了防止页面重复点击提交导致同一本书出现在购物车两行。字段类型上,凡是存中文的都走NVARCHAR,如果你是2008 R2这种老版本,默认排序规则选Chinese_PRC_CI_AS,能避免大部分中文乱码的坑。

2.3 外键约束与种子数据:还原后先跑通这条链路

表建完只是第一步,课设验收时老师会在你演示过程中现场往表里插数据测试,所以外键约束和种子数据必须完整。我在给学生检查课设时发现最普遍的问题是:建表语句里有外键,但脚本从头到尾执行完,外键却因为插入顺序不对而没有生效——先把订单数据插进去,用户表还是空的,自然违反约束报错。正确做法是先插用户和教材,再插购物车或订单,顺序满足引用关系的拓扑。

下面这组种子数据可以直接在SSMS里执行,用来验证链路:

-- 先插用户:两个学生加一个管理员 INSERT INTO Users (UserName, Password, RealName, Role) VALUES ('stu001', '123456', '张三', 0), ('stu002', '123456', '李四', 0), ('admin', 'admin888', '王老师', 1); -- 再插教材:库存给足数值,方便后面演示扣减 INSERT INTO Books (BookName, Author, Publisher, Price, Stock) VALUES ('数据库系统概论', '王珊', '高等教育出版社', 49.50, 100), ('ASP.NET程序设计', '陈志泊', '清华大学出版社', 45.00, 80), ('软件工程导论', '张海藩', '清华大学出版社', 39.80, 60); -- 最后插购物车与订单(依赖前面表的数据) INSERT INTO CartItems (UserId, BookId, Quantity) VALUES (1, 1, 2); INSERT INTO Orders (UserId, OrderNo, TotalAmount, Status) VALUES (1, '202401001', 99.00, 0);

执行完后你可以立刻做两个验证动作:查一下Orders表的UserId=1对应的购物车行数,再尝试往OrderDetails里插入一个不存在的OrderId,如果外键没生效,这条脏数据会插入成功。把这个“故意插错”的测试过程写进报告的测试用例部分,算是一个免费加分项。种子数据的数量不用多,每个表两三行就够了,但一定要覆盖到每个外键关系,否则演示时下拉框是空的,会显得系统像没做完。

3. 用ASP.NET实现订购主流程:登录校验、购物车与订单生成的代码骨架

3.1 登录页的Session管理:为什么不能用ViewState存登录态

ASP.NET WebForms老项目的登录逻辑看起来大同小异,但有个典型的初学者错误——有人会把登录状态存进ViewState或者Application里,导致页面一刷新登录态就丢,或者所有登录用户共用一份状态。正确做法是把用户的UserId和Role写进Session,在需要鉴权的页面用Session["UserId"]判空来决定是否跳回登录页。Session是服务端会话状态,浏览器关闭或超过IIS默认的20分钟无操作会自动失效,这才是符合预期的课设行为。

登录按钮的后置代码我一般会这样写:

protected void btnLogin_Click(object sender, EventArgs e) { string connStr = System.Configuration.ConfigurationManager .ConnectionStrings["BookDb"].ConnectionString; string sql = "SELECT UserId, RealName, Role FROM Users WHERE UserName=@name AND Password=@pwd"; using (SqlConnection conn = new SqlConnection(connStr)) { using (SqlCommand cmd = new SqlCommand(sql, conn)) { // 参数化查询,防SQL注入,课设里写拼接字符串是大忌 cmd.Parameters.AddWithValue("@name", txtUserName.Text.Trim()); cmd.Parameters.AddWithValue("@pwd", txtPassword.Text.Trim()); conn.Open(); SqlDataReader reader = cmd.ExecuteReader(); if (reader.Read()) { // 登录成功:只把必要凭证放Session,不要把整行DataTable塞进去 Session["UserId"] = reader.GetInt32(0); Session["RealName"] = reader.GetString(1); Session["Role"] = reader.GetInt32(2); Response.Redirect("Default.aspx"); } else { lblMsg.Text = "用户名或密码错误"; } } } }

这段代码里有几个参数值得注意:AddWithValue是课程设计中最常用的参数绑定写法,它能防止经典的'1' OR '1'='1'注入;但它在处理NVARCHAR字段时偶尔会因隐式类型转换导致索引失效,真实项目里我一般会用SqlParameter显式指定SqlDbType.NVarChar,课设里用AddWithValue足够。密码用明文是课设常态,但在报告的“不足与改进”里主动写一句“密码应存储为哈希值”,又是个免费的成熟度展示点。还有一点,Response.Redirect默认会抛ThreadAbortException,不需要去捕获它,在部分老框架版本里不加false参数也没事,但如果页面上有未完成的输出,建议用Response.Redirect("Default.aspx", false)再Context.ApplicationInstance.CompleteRequest()收尾。

3.2 教材查询与加入购物车:GridView与DataTable的最小实现

教材查询页是这套系统里最体现ASP.NET WebForms特色的部分,核心控件就是GridView加SqlDataSource,或者后台代码里手动绑定DataTable。两种做法里我更推荐后者,因为SqlDataSource虽然拖拽简单,但它的查询语句写死在aspx文件里,一旦字段名改了你得去界面里翻配置,维护起来比查代码还费劲。手动绑定的话,你可以在RowCommand事件里通过CommandArgument拿到当前行的BookId,执行加购操作,逻辑链路是代码可读的。

加购物车最简洁且不容易出错的写法,是把已选数据存在Session中的DataTable里,然后重绑定GridView:

private DataTable GetCartFromSession() { // 每个用户一个购物车DataTable,以Session["Cart_" + UserId]隔离 if (Session["Cart_" + Session["UserId"]] == null) { DataTable dt = new DataTable(); dt.Columns.Add("BookId", typeof(int)); dt.Columns.Add("BookName", typeof(string)); dt.Columns.Add("Price", typeof(decimal)); dt.Columns.Add("Quantity", typeof(int)); Session["Cart_" + Session["UserId"]] = dt; } return (DataTable)Session["Cart_" + Session["UserId"]]; } protected void grdBooks_RowCommand(object sender, GridViewCommandEventArgs e) { if (e.CommandName == "AddToCart") { // CommandArgument传入的是BookId,注意这里取出来是string,要转int int bookId = Convert.ToInt32(e.CommandArgument); DataTable cart = GetCartFromSession(); // 判断购物车里是否已有这本书,已有则数量加1 DataRow[] rows = cart.Select("BookId = " + bookId); if (rows.Length > 0) { rows[0]["Quantity"] = (int)rows[0]["Quantity"] + 1; } else { // 从Books表查出名称和价格快照,存入购物车 DataRow newRow = cart.NewRow(); newRow["BookId"] = bookId; newRow["BookName"] = BookHelper.GetNameById(bookId); newRow["Price"] = BookHelper.GetPriceById(bookId); newRow["Quantity"] = 1; cart.Rows.Add(newRow); } grdCart.DataSource = cart; grdCart.DataBind(); } }

这段的逻辑要点在于“用量加1而不是重复插行”,以及“以Session["Cart_" + UserId]做键隔离不同学生的购物车”。这里有一个隐蔽的坑是:Session里的DataTable随便怎么改都行,但你一旦改了它的结构(比如添加列),再绑定GridView会报DataTable doesn't contain a column named之类的错,所以列结构最好从第一次创建起就固定住。另外,DataTable.Select里拼接BookId = 123的字符串在课设里完全够用,虽然它在生成条件表达式时有一次解析开销,但几百行数据感觉不到延迟。如果你不想踩字符串拼接的坑,用Linq的AsEnumerable().Where(...)会更安全,不过那需要引入System.Data.DataSetExtensions,老框架里偶尔会有引用缺失问题,看你自己取舍。

3.3 生成订单的事务写法:多表写入时别让库存对不上

从购物车提交生成订单,是这套系统里唯一必须用事务的地方。为什么这么说?因为一次提交涉及三次写入:往Orders插主表记录、往OrderDetails插明细、更新Books里的库存。这三步任何一步失败,另外两步都应该回滚,否则会出现“订单建了但明细是空的”或者“库存减了但订单没生成”这种数据不一致,在答辩现场非常难看。事务的写法在ADO.NET里很标准,用SqlTransaction对象把同一连接上的多个命令包起来,只要有一个执行失败就Rollback。

protected void btnSubmitOrder_Click(object sender, EventArgs e) { string connStr = ConfigurationManager.ConnectionStrings["BookDb"].ConnectionString; DataTable cart = GetCartFromSession(); if (cart.Rows.Count == 0) { lblMsg.Text = "购物车为空,无法提交"; return; } using (SqlConnection conn = new SqlConnection(connStr)) { conn.Open(); SqlTransaction tran = conn.BeginTransaction(); // 开启事务,后续命令都挂在它上 try { // 1. 生成订单号并插入主表,拿到新的自增OrderId string orderNo = "ORD" + DateTime.Now.ToString("yyyyMMddHHmmss"); string sqlOrder = "INSERT INTO Orders (UserId, OrderNo, TotalAmount, Status) " + "VALUES (@uid, @no, @total, 0); " + "SELECT SCOPE_IDENTITY();"; using (SqlCommand cmd = new SqlCommand(sqlOrder, conn, tran)) { cmd.Parameters.AddWithValue("@uid", (int)Session["UserId"]); cmd.Parameters.AddWithValue("@no", orderNo); cmd.Parameters.AddWithValue("@total", 0); // 先填0,明细插入后累加 int orderId = Convert.ToInt32(cmd.ExecuteScalar()); } // 2. 逐行插入明细,同时累加总金额 decimal total = 0; foreach (DataRow row in cart.Rows) { int bookId = (int)row["BookId"]; int qty = (int)row["Quantity"]; decimal price = (decimal)row["Price"]; total += price * qty; string sqlDetail = "INSERT INTO OrderDetails (OrderId, BookId, Quantity, UnitPrice) " + "VALUES (@oid, @bid, @qty, @price)"; using (SqlCommand cmd = new SqlCommand(sqlDetail, conn, tran)) { cmd.Parameters.AddWithValue("@oid", orderId); cmd.Parameters.AddWithValue("@bid", bookId); cmd.Parameters.AddWithValue("@qty", qty); cmd.Parameters.AddWithValue("@price", price); cmd.ExecuteNonQuery(); } } // 3. 更新订单总金额 string sqlUpdate = "UPDATE Orders SET TotalAmount = @total WHERE OrderId = @oid"; using (SqlCommand cmd = new SqlCommand(sqlUpdate, conn, tran)) { cmd.Parameters.AddWithValue("@total", total); cmd.Parameters.AddWithValue("@oid", orderId); cmd.ExecuteNonQuery(); } tran.Commit(); // 全部成功,提交 Session.Remove("Cart_" + Session["UserId"]); // 清空购物车 lblMsg.Text = "订单提交成功"; } catch (Exception ex) { tran.Rollback(); // 任何一步失败,数据全部回滚到提交前状态 lblMsg.Text = "提交失败:" + ex.Message; } } }

这里大家容易忽略的实用细节是:SCOPE_IDENTITY()是在同一个作用域里取自增ID的靠谱方式,它返回的是当前连接上最后一次插入的标识值,比@@IDENTITY安全得多。如果用@@IDENTITY,一旦Books表上存在触发器插入其他自增表,你取到的ID就会串号,课设里虽然很少踩到,答辩老师问起时你能说出区别就是深度。事务里第二个容易忽视的坑是SqlCommand必须显式传入tran对象,很多人写了事务但命令没挂上去,结果只有最后一个命令在事务里,前面的根本没管。这段代码执行完后,你可以顺手做一个验证动作:在Sql Server Management Studio里看日志文件,观察事务提交后LDF文件的记录情况,想深入的同学可以下载SQL Server Log管理工具或直接用DBCC OPENTRAN查未提交事务,这也是热词里“sql server writelog”这类关注点在实操层的落地体现。

4. 配置SQL Server连接与部署到IIS:4个参数决定能不能本机跑起来

4.1 连接字符串的两种写法与连接池超时问题

从源码库解压出来的Web.config里,connectionStrings节点是第一个要改的地方。课程设计包最常见的连接字符串写法是Data Source=.;Initial Catalog=BookManage;User ID=sa;Password=123456,它假设你的SQL Server跑在本机默认实例上且sa账号密码是初始的。实际本机很可能不是这个情况,你装了SQL Server 2012以上版本时,默认实例名是MSSQLSERVER,但如果你装的是Express版,实例名会带SQLEXPRESS后缀。很多同学卡住的“无法连接到数据库”八成是这里的问题。

我给出两种最常用的连接字符串写法,你按自己环境二选一:

<!-- 方式一:SQL Server身份认证,适合课设演示,账号密码明文写在配置里 --> <connectionStrings> <add name="BookDb" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=BookManage;User ID=sa;Password=你的密码;Pooling=True;Connect Timeout=5" providerName="System.Data.SqlClient"/> </connectionStrings> <!-- 方式二:Windows集成认证,适合本机开发,不需要管密码 --> <connectionStrings> <add name="BookDb" connectionString="Data Source=localhost;Initial Catalog=BookManage;Integrated Security=True" providerName="System.Data.SqlClient"/> </connectionStrings>

写完之后有两个参数建议立刻调整:一是Connect Timeout=5,默认15秒等待会让你在数据库服务没启动时页面卡很久,改成5秒能快速暴露问题;二是Pooling=True保持打开即可,但如果遇到连接池耗尽错误,可以在SqlConnection.ClearPool()或者在连接字符串里加上Max Pool Size=200限制。这里还有个容易被忽略的问题:如果你改了数据库登录账号或密码,IIS的工作进程不会立刻反映,因为连接池里的旧连接可能还在占用。改完账号密码后重启一下应用程序池,或者用SqlConnection.ClearAllPools()强制清空,这个玄学问题我之前排查了半小时,结果是连接池缓存。

4.2 部署到IIS的步骤:应用程序池的版本与托管管道模式

把Visual Studio里F5跑得飞快的项目发布到IIS,是课设演示前的最后一道坎。常见的做法是右键项目选择“发布”,目标位置选一个本地文件夹,比如C:\publish\BookManage,然后再把这个文件夹挂到IIS站点下。这里有一个默认配置带来的坑:新建的应用程序池默认是“.NET CLR版本 v4.0.30319”且“托管管道模式=集成”,如果你的项目是用旧版.NET Framework 2.0编译的(很多老课设包是这样),就要把CLR版本改成2.0,否则页面全部抛500错误。判断方法很简单:打开项目里的Web.config,看targetFramework属性;如果看不到这个属性,基本就是2.0时代的老工程。用IIS 7以上版本部署老WebForms项目时,还需要确认IIS角色里安装了“ASP.NET”功能模块,否则站点起来但只返回纯文本代码。如果你只是想在演示前临时跑起来图省事,可以在项目根目录放一个app_offline.htm文件,IIS会立刻把应用程序池停掉并显示这个静态页面,部署完新文件后删掉该文件,站点自动恢复。这是个很实用的“后悔药”,你在线上更新版本时用它避免文件被占用锁死,也不用重启IIS。

部署完成后还有一个细节容易被漏掉:页面里如果使用Image控件上传或展示教材封面,IIS默认只允许静态文件访问,ASP.NET生成动态图片时需要在web.config的system.webServer节点加一个staticContent配置,否则浏览器访问图片路径会返回404。这不是字面意义上的文件不存在,而是IIS把图片请求当作静态文件直接拒绝,报错日志里能看到可疑的404.7状态码。这类权限问题排查起来最花费时间,建议部署完先拿浏览器访问一个带资源文件的页面确认。

4.3 数据库版本兼容:从2008 R2到2019的迁移踩坑

教材订购系统的课设包年代都比较久远,大部分是基于SQL Server 2008 R2或2012开发的,而现在很多同学电脑上装的是2019甚至2022。直接还原2008 R2的.bak到2019上一般没问题,SQL Server向后兼容做得不错,但反向操作就会报“备份集是在较新版本上创建的”错误。如果你需要把高版本库给低版本用,得通过“生成脚本”把表结构和数据分别导出为.sql,在低版本上重新执行。另外一个更隐蔽的问题是兼容级别,2019还原了2008 R2的库之后,数据库的兼容级别还停留在100,很多新语法用不了,但也不至于报错,只是查询计划的生成会走老逻辑。我一般会执行一句:

ALTER DATABASE BookManage SET COMPATIBILITY_LEVEL = 150; -- 150对应SQL Server 2019

还有个来自真实课设的惨痛教训:SQL Server 2012默认开启了密码过期策略,如果你用的sa账户很久没动,某天突然连不上,页面上报“登录失败”但其实密码已经到期。处理办法是先用Windows身份认证登录SSMS,然后在安全性节点里把sa密码重设一遍并取消“强制实施密码策略”勾选。这种问题在热词里经常被搜到,碰到时别急着怀疑代码,先从数据库服务端查起。SQL Server安装时的排序规则建议选Chinese_PRC_CI_AS,如果你装的是英文版且排序规则是Latin1_General_CI_AS,建库时就要显式指定排序规则,否则中文字段排序和模糊查询会把LIKE '%数学%'结果都抓不齐。

5. 常见故障排查:5个高频问题的现象、原因与处理

5.1 登录成功跳转后刷新就回到登录页

现象:输入用户名密码成功进入主页,但一按F5刷新或者再开一个标签页访问,又跳回登录页,有时还伴随着Session state is disabled的报错。

原因:这是Session生命周期问题。默认配置下Session存在进程内(InProc模式),IIS应用程序池一旦回收,所有会话全部失效。你又没有在需要鉴权的每个页面里把Session["UserId"]的判断写全,有些页面允许匿名访问,所以只是部分页面表现为“偶发掉登录”。

处理:先检查每个需要登录的页面Page_Load里是否有统一的登录校验,建议抽一个基类页面来处理;再检查IIS应用程序池的“回收时间”设置,默认1740分钟,但如果你在部署期间反复改过Web.config,每改一次Session就会失效一次。如果课设要求不能丢登录态,可以把Session模式改成StateServer,需要启动ASP.NET状态服务,并在Web.config里配好stateConnectionString,但这会增加复杂度,课设里不推荐,老师问起知道概念即可。

5.2 还原数据库报“无法获得独占访问权”

现象:SSMS还原数据库时报错:“数据库正由其他用户使用,无法获得独占访问权”,有时候还会提示有deadlock victim或者有活动事务。

原因:有其他连接正占用着目标数据库,最常见的是你之前跑过那个ASP.NET站点,连接池还残留着久连接没释放;或者SSMS自身的对象资源管理器开着该库的刷新任务。

处理:执行以下SQL强制断开即可,但注意别在生产环境乱试:

ALTER DATABASE BookManage SET OFFLINE WITH ROLLBACK IMMEDIATE; -- 然后重新还原,还原后会自动回到online状态

如果还原完成之后你发现库一直处于Restoring状态,那是还原后没执行RESTORE DATABASE BookManage WITH RECOVERY,SSMS的向导里记得选WithRecovery,否则库只读不可查询,页面里所有增删改查全部报“数据库处于还原状态”。

5.3 页面报“在建立与服务器的连接时出错”

现象:所有数据库操作页面统一报错,错误信息里能看到provider: SQL Network Interfaces, error: 26 - Error Locating Server/Instance Specified,或者干脆是A network-related or instance-specific error。

原因:这个错误信息基本是误导性的,真实原因大多数是连接字符串里的实例名不对,或者SQL Server没有监听TCP/IP协议。尤其装了SQL Server Express后,服务默认只开启Shared Memory协议,你从IIS进程(独立于SSMS)去连,走不了Shared Memory就会连接失败。

处理:打开“SQL Server配置管理器”,进入SQL Server网络配置,把TCP/IP协议启用,然后重启SQL Server服务。之后用telnet localhost 1433验证端口通不通。最后回到Web.config,确认Data Source里的实例名确实存在,localhost\SQLEXPRESS和localhost是完全不同的两个目标,别指望改名后还能连上。如果你的SQL Server版本是2019以上,注意防火墙默认会拦1433端口,开发机上临时关掉防火墙测试是最快捷的排除手段。

5.4 页面上所有中文显示成问号或乱码

现象:GridView里从数据库读出来的中文正常,但页面上的按钮文本、欢迎语是乱码;或者反过来,页面写死的中文正常,数据库读出来的全是问号。两种乱码方向对应不同原因。

原因:数据库读出来乱码,是建库时排序规则不是中文排序规则,或者字段类型用了VARCHAR而不是NVARCHAR;页面静态中文乱码,是aspx文件保存的编码不是UTF-8,老Visual Studio默认保存为GB2312,IIS解析时又按UTF-8解码,两边对不上。

处理:数据库侧执行SELECT name, collation_name FROM sys.databases,把库的排序规则改成Chinese_PRC_CI_AS最省事;字段层面把所有VARCHAR改成NVARCHAR。页面侧用“文件-高级保存选项”把aspx文件另存为UTF-8带签名(BOM)编码。最可靠的判断方法是:点击aspx页面查看文件编码是否和<meta charset="utf-8">声明一致。这里要顺带提醒,如果你改了数据库排序规则,之前已有的数据会保留旧编码的字节,在新排序规则下照样乱码,正确顺序是先改规则再重新插数据。

5.5 提交订单后库存变负数或订单金额对不上

现象:连续点击提交按钮两次,购物车被清空但订单生成了两份;或者库存记录从10变成了负数,而订单明细里的金额合计跟主表TotalAmount不一致。

原因:第一类问题是没有做按钮防重复,ASP.NET按钮点击后页面会回发,连续快速点击就会多次触发btnSubmitOrder_Click;第二类问题是库存扣减没有条件约束,也就是UPDATE Books SET Stock = Stock - @qty WHERE Stock >= @qty写成了不带WHERE的绝对赋值。第三类金额对不上,大概率是主表先插入TotalAmount=0,明细插入后没有回写。

处理:在提交方法入口处加一个if (Session["OrderSubmitted"] == true) return;并用ViewState关掉按钮的多次回发;库存扣减一定要加上WHERE Stock >= @qty,如果更新的影响行数为0,说明库存不足,立刻抛出异常让事务回滚。至于金额,我常用的做法是把TotalAmount的计算放在插入明细的循环里去累加,全部插入完成后在同一个事务里执行UPDATE回写,千万别在前面提前填一个估算值。这三处都做到,基本不会再出现数据不一致的翻车现场。

6. 纵深一点:用存过过程兜底库存并发,把“课设”做成“能答辩”的版本

整套系统跑通之后,如果你想在验收里拉开和同学的差距,我建议你把“提交订单”这一步从C#代码里的多条SQL改成一条存储过程。这不是炫技,而是数据库课程设计最该体现的能力——把数据完整性规则放在数据库层而不是应用层。我在给学生改答辩素材时发现,凡是能当场说清楚“为什么要把库存扣减放到存储过程里用事务包裹”的,老师普遍会多问几轮,因为这是真实系统里并发控制的雏形。

下面这条存储过程你可以直接替换第3章的后置代码逻辑:

CREATE PROCEDURE usp_CreateOrder @UserId INT, @OrderNo NVARCHAR(32), @ResultMsg NVARCHAR(100) OUTPUT AS BEGIN SET NOCOUNT ON; DECLARE @OrderId INT; DECLARE @Err INT = 0; BEGIN TRY BEGIN TRAN; -- 插入订单主表,拿到新订单ID INSERT INTO Orders (UserId, OrderNo, TotalAmount, Status) VALUES (@UserId, @OrderNo, 0, 0); SET @OrderId = SCOPE_IDENTITY(); SET @Err = @@ERROR; -- 从购物车表批量生成明细,并检查库存 INSERT INTO OrderDetails (OrderId, BookId, Quantity, UnitPrice) SELECT @OrderId, BookId, Quantity, Price FROM CartItems WHERE UserId = @UserId; SET @Err = @@ERROR; IF @Err <> 0 BEGIN SET @ResultMsg = '明细插入失败'; ROLLBACK TRAN; RETURN -1; END -- 按照购物车逐行扣减库存,使用行锁避免超卖 DECLARE @BookId INT, @Qty INT; DECLARE cart_cursor CURSOR FOR SELECT BookId, Quantity FROM CartItems WHERE UserId = @UserId; OPEN cart_cursor; FETCH NEXT FROM cart_cursor INTO @BookId, @Qty; WHILE @@FETCH_STATUS = 0 BEGIN UPDATE Books SET Stock = Stock - @Qty WHERE BookId = @BookId AND Stock >= @Qty; IF @@ROWCOUNT = 0 BEGIN SET @ResultMsg = '库存不足:教材ID ' + CAST(@BookId AS NVARCHAR(10)); CLOSE cart_cursor; DEALLOCATE cart_cursor; ROLLBACK TRAN; RETURN -2; END FETCH NEXT FROM cart_cursor INTO @BookId, @Qty; END CLOSE cart_cursor; DEALLOCATE cart_cursor; -- 回写金额并清理该用户的购物车 UPDATE Orders SET TotalAmount = (SELECT SUM(Quantity * UnitPrice) FROM OrderDetails WHERE OrderId = @OrderId) WHERE OrderId = @OrderId; DELETE FROM CartItems WHERE UserId = @UserId; COMMIT TRAN; SET @ResultMsg = 'SUCCESS'; RETURN 0; END TRY BEGIN CATCH ROLLBACK TRAN; SET @ResultMsg = ERROR_MESSAGE(); RETURN -99; END CATCH END;

调用的时候,C#端要做的就是传入用户ID和订单号,然后读取返回值判断成功与否。这里的技术重点是Stock >= @Qty这个条件更新:它利用数据库的行级锁在扣减时重新检查一次库存,避免两个并发请求同时通过检查变成负数。游标在课设数据量下没有问题,答辩时可以主动提一句“生产环境会改用基于集合的UPDATE”,这就是你知道套路的表现。关于报告里的ER图,我最后给一条建议——画ER图时不要只画框和线,把每个关系上的基数标出来,学生到订单是1:N,订单到订单明细是1:N,这两条线是报告里最常被追问的地方。数据库课程设计这套东西,价值不在代码量多少,而在你能不能讲清楚一张表为什么这样拆、一条事务为什么这样包。如果你是自己做课程设计,别急着上手写页面,先花一晚上把表和存储过程磨好,后续所有界面都是顺水推舟的事;如果你是帮别人过这套课设,也把数据库层查一遍再去调aspx,这是最省时间的路径。希望这些白纸黑字的踩坑记录能帮你少熬两个夜,至少当年我要是早知道第4章那几个参数,也不会在部署上耗掉一整天。

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

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

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

立即咨询