
简介这是一份面向高校《数据库课程设计》的ASP.NETSQL Server教材在线征订系统完整资源包对应选题三“高校课程教材在线征订系统”适合有ASP.NET/Web基础、需要完成课程设计或毕业设计的本科生参考。系统围绕教材科管理员与任课教师两类角色展开管理员维护课程、班级和教材信息审核教师提交的教材订购申请并完成采购、入库、发放及状态更新教师可提交所需教材数量并随时查看申请进度。压缩包共121个文件体积约874KB其中vb源码与aspx页面构成Web前端逻辑resx、xsd、config等用于资源与配置mdf和ldf为SQL Server数据库文件doc报告含设计说明书与实习报告便于整体复盘。已有1700余人学习下载既能帮助理解在线征订系统从订单申请到发放的完整数据流也可作为二次功能扩展或答辩讲解的参考底稿。1. 学校教材订购系统到底解决什么问题从状态流转说起ASP.NET 加 SQL Server 的组合在高校课程设计里几乎是一套“标准答案”式的技术选型但很多同学的选题只做到了“能增删改查”没有真正做出“业务流程”。这份学校教材订购系统的源码和报告最值得拆的地方不是在 GridView 里绑定了一个表而是它把教材订购的完整生命周期做了出来任课教师提交订购申请教材科管理员审核、采购、入库、发放每一步都有独立的页面和对应的状态更新。也就是说教师能看到自己的申请进度管理员能控制哪个环节往下走整个数据不是一口气插进去就完了而是经过五次状态变更才真正结束。这套系统适合两类人一类是做数据库课程设计时选题刚好落在“订单 审核 状态”这类场景上的在校生另一类是刚接触 ASP.NET Web Forms想看一个完整项目怎么组织页面、存储过程、状态字段的人。后面几章我会从数据库设计、页面文件布局、状态流转、常见翻车点这几个方向逐一拆开最后给出一套能拿去答辩演示的验证方案。2. 先看懂系统的数据骨架SQL Server 表结构与关联关系2.1 表拆解七个基础表分别存什么这套系统虽然是课程设计但数据表的设计并没有偷懒。从功能反推至少要包含课程信息表、教师信息表、教材表、订购申请主表和订购明细表也可以合并再加上一个用户表和班级表。以最常见的做法来说订购申请主表存整体状态明细表或子表存具体教材名称、ISBN、单价、数量这些信息这样设计的好处是审核、采购、发放都只改主表的状态字段不动明细数据避免每次更新都要重写一整行。创建表时有一个关键的取舍——订购申请里的教材名称、出版社、ISBN、编者、定价这几个字段到底是从教材表里外键关联还是直接冗余到申请表里。这个系统的做法更偏向冗余。原因很直接教材的“当时定价”是会变的今天订的教材定价 39.8下学期可能调到 42.5如果申请记录里只存了一个教材 ID再去关联教材表历史数据就被污染了。所以存冗余字段反而是合理的设计。你在做类似系统时比如订单 商品也可以沿用这个思路主数据引用外键快照数据直接冗余双轨并行。代码层面这套系统用的是 SQL Server 数据库脚本文件里包含建表语句和初始测试数据。核心的建表逻辑大致是这样-- 创建课程信息表 CREATE TABLE CourseInfo ( CourseID INT PRIMARY KEY IDENTITY(1,1), -- 课程编号自增主键 CourseName NVARCHAR(50) NOT NULL, -- 课程名称 TeacherName NVARCHAR(20) NOT NULL, -- 任课教师 CollegeName NVARCHAR(50) NOT NULL, -- 所属学院 ClassName NVARCHAR(60) NOT NULL -- 学生班级 ); -- 创建教材订购申请表 CREATE TABLE BookOrderInfo ( OrderID INT PRIMARY KEY IDENTITY(1,1), -- 申请编号 CourseID INT NOT NULL, -- 关联课程表 BookName NVARCHAR(100) NOT NULL, -- 教材名称 Publisher NVARCHAR(50) NOT NULL, -- 出版社 AuthorName NVARCHAR(20) NOT NULL, -- 编者 ISBN NVARCHAR(20) NOT NULL, -- 教材ISBN Price DECIMAL(6,2) NOT NULL, -- 定价两位小数 Quantity INT NOT NULL DEFAULT 1, -- 订购数量 OrderStatus TINYINT NOT NULL DEFAULT 0, -- 0待审核 1通过 2不通过 3采购中 4已完成 5已发放 RejectReason NVARCHAR(200) NULL, -- 审核不通过的原因 SubmitTime DATETIME NOT NULL DEFAULT GETDATE(), -- 提交时间 FOREIGN KEY (CourseID) REFERENCES CourseInfo(CourseID) );这段 sql 里有几个设计细节值得说明。OrderStatus 用的 TINYINT 而不是 NVARCHAR是因为状态在代码里要频繁比较用数字比较比字符串比较稳也省存储RejectReason 允许为空是因为只有审核不通过时才需要写理由SubmitTime 用 DEFAULT GETDATE()让数据库来打时间戳防止程序里的时间格式不一致。2.2 一条 JOIN 查询串起全链路数据有了表结构接下来最实用的就是“按教师查进度”和“按课程查申请”这两类查询。系统里教师要能看到自己提交的申请走到哪一步管理员要能看到所有申请并按课程或班级筛选。一个通用的视图或存储过程能大幅减少页面里的重复代码。-- 查询某任课教师提交的所有教材订购申请及当前状态 SELECT c.CourseName, c.TeacherName, c.ClassName, b.BookName, b.Publisher, b.AuthorName, b.ISBN, b.Price, b.Quantity, DATENAME(yyyy, b.SubmitTime) - REPLACE(STR(MONTH(b.SubmitTime), 2), , 0) - REPLACE(STR(DAY(b.SubmitTime), 2), , 0) AS SubmitDate, CASE b.OrderStatus WHEN 0 THEN N待审核 WHEN 1 THEN N审核通过 WHEN 2 THEN N审核不通过 WHEN 3 THEN N采购中 WHEN 4 THEN N订购完成 WHEN 5 THEN N已发放 END AS StatusText FROM BookOrderInfo b INNER JOIN CourseInfo c ON b.CourseID c.CourseID WHERE c.TeacherName TeacherName ORDER BY b.SubmitTime DESC;这里把状态数字转成中文文本这一步放在 SQL 里做而不是放到 C# 页面再去判断能减少代码页面的工作量——绑定 GridView 时直接显示 StatusText 这一列就行后端少写一次循环。如果要按课程筛选某个学院的申请情况把 WHERE 条件换成 c.CollegeName CollegeName 或者按 ClassName 模糊匹配都行。整个查询的核心逻辑就是通过 CourseID 外键把两张表 JOIN 起来这是这套系统里几乎所有数据页面的基础。2.3 数据库脚本导入的常见失败点把 .sql 文件拿到 SQL Server Management StudioSSMS里执行时经常遇到两个问题。第一个是数据库没有先创建直接执行建表脚本会报“数据库不存在”——先在 SSMS 里新建一个名为 BookOrderDB 的数据库名字不一定叫这个以代码里的连接串为准然后选中这个库再执行脚本。第二个是 SQL Server 实例的排序规则问题中文 NVARCHAR 字段如果遇到乱码或长度不够需要把表字段统一改成 NVARCHAR 而不是 VARCHAR并确保创建数据库时选用 Chinese_PRC_CI_AS 排序规则。提示执行 SQL 脚本前先检查 USE 语句。如果你的脚本第一行没有 USE BookOrderDB在执行前手动在 SSMS 顶部切换到目标数据库不然表会建到 master 库里。3. 从文件清单反推系统流程ASP.NET Web Forms 页面职责拆解3.1 源码包里的页面是怎么分工的这套系统的源码文件在解压后是一组 .aspx 页面加上 .aspx.cs 代码文件外加 Global.asax 和 Web.config。很多第一次做 ASP.NET 课程设计的人看到这么多页面会懵其实每个页面是有明确分工的。我拿到源码后第一件事就是把页面名和功能对应起来。从文件命名来看new.aspx 是新增订购申请页update1.aspx 和 update2.aspx 分别对应审核和采购/入库的状态更新book.aspx 是教材信息管理页load.aspx 是页面初始化或者加载数据列表用的history1.aspx 和 2.aspx 是查询历史申请的页面。Global.asax 里处理的是应用程序级的事件比如应用启动时初始化数据库连接、记录全局异常。这些页面和摘要里描述的功能一一对应教师提交申请在 new.aspx管理员审核在 update2.aspx状态流转逻辑通过页面的 Button 点击事件触发。在 Web Forms 里每个 .aspx 页面都有一个同名的 .aspx.cs 文件里面写的是 Page_Load、Button_Click 这类事件函数。页面本身只负责摆放控件TextBox、DropDownList、GridView真正操作数据库的逻辑全部在 .cs 文件里。这种“前台模板 后台逻辑”的结构虽然看起来老但有一个天生的优点页面和操作一一对应课程设计文档里写“功能模块图”时非常直观。3.2 一个典型的页面生命周期提交订购申请下面这段逻辑是 new.aspx.cs 里的核心操作功能是把教师填写的教材信息插入到订购申请表。这里用的是 SqlCommand 参数化方式protected void btnSubmit_Click(object sender, EventArgs e) { string connStr ConfigurationManager.ConnectionStrings[BookOrderDBConnectionString].ConnectionString; string sql INSERT INTO BookOrderInfo (CourseID, BookName, Publisher, AuthorName, ISBN, Price, Quantity, OrderStatus) VALUES (CourseID, BookName, Publisher, AuthorName, ISBN, Price, Quantity, 0); using (SqlConnection conn new SqlConnection(connStr)) { using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(CourseID, Convert.ToInt32(ddlCourse.SelectedValue)); cmd.Parameters.AddWithValue(BookName, txtBookName.Text.Trim()); cmd.Parameters.AddWithValue(Publisher, txtPublisher.Text.Trim()); cmd.Parameters.AddWithValue(AuthorName, txtAuthor.Text.Trim()); cmd.Parameters.AddWithValue(ISBN, txtISBN.Text.Trim()); cmd.Parameters.AddWithValue(Price, Convert.ToDecimal(txtPrice.Text.Trim())); cmd.Parameters.AddWithValue(Quantity, Convert.ToInt32(txtQuantity.Text.Trim())); conn.Open(); int rows cmd.ExecuteNonQuery(); conn.Close(); if (rows 0) { lblMessage.Text 提交成功请等待教材科审核。; ClearForm(); } else { lblMessage.Text 提交失败请检查输入信息。; } } } }这段代码有几个值得抄作业的细节。第一是 AddWithValue 参数化而不是拼字符串这能挡住 SQL 注入也是课程设计答辩时老师必问的点第二是 CourseID 从下拉框里取值而不是手输避免外键约束报错也省去记忆课程编号的麻烦第三是状态字段只在插入时赋 0待审核后面所有流程都靠别的页面去更新不在插入时做状态判断逻辑单一。如果要改造成新增时直接给某个课程订多本教材做法是在页面里加一个 GridView 或 CheckBoxList 让用户勾选多本教材后台循环执行 Insert同时用一个父表记录订单主信息每本教材作为子表记录插入。不过现在的源码是单本教材一次申请作为课程设计足够用了。3.3 审核页面的核心更新状态并记录拒绝原因update2.aspx 对应的是管理员审核功能核心逻辑是更新 OrderStatus 字段。这里关键点是审核通过时要把状态改为 1审核不通过时改为 2 并写 RejectReason。这一段的 C# 逻辑可以看出系统对状态的处理方式protected void btnApprove_Click(object sender, EventArgs e) { UpdateOrderStatus(Convert.ToInt32(hfOrderID.Value), 1, null); } protected void btnReject_Click(object sender, EventArgs e) { string reason txtRejectReason.Text.Trim(); if (string.IsNullOrEmpty(reason)) { lblMessage.Text 审核不通过时必须填写拒绝原因; return; } UpdateOrderStatus(Convert.ToInt32(hfOrderID.Value), 2, reason); } private void UpdateOrderStatus(int orderId, byte newStatus, string reason) { string sql reason null ? UPDATE BookOrderInfo SET OrderStatus Status WHERE OrderID OrderID : UPDATE BookOrderInfo SET OrderStatus Status, RejectReason Reason WHERE OrderID OrderID; using (SqlConnection conn new SqlConnection(connStr)) { using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(Status, newStatus); cmd.Parameters.AddWithValue(OrderID, orderId); if (reason ! null) cmd.Parameters.AddWithValue(Reason, reason); conn.Open(); cmd.ExecuteNonQuery(); } } }把两个按钮的事件都收敛到一个 UpdateOrderStatus 方法里这是一个好习惯。代码复用的同时也避免了“审核通过但状态写成 2”这种因为复制粘贴导致的低级 bug。hfOrderID 是页面上一个 HiddenField 控件用来在 GridView 行选中时暂存当前订单 ID这也是 Web Forms 里常见的传值手段。提示如果用 Session 或 ViewState 传订单 ID页面刷新后容易出问题HiddenField SelectedIndexChanged 事件是课程设计里最稳的做法。4. 教材订购状态机从提交到已发放的流转与权限控制4.1 状态字段设计为什么用数字不用字符串这套系统的七个功能点本质上是一条状态链待审核 → 审核通过/不通过 → 采购中 → 订购完成 → 已发放。数据库里的 OrderStatus 字段用 0 到 5 的数字表示这是一个典型的状态机设计。用数字而不用字符串有三个理由一是比对比稳定不等于“审核通过”和“审核不通过”这种中文字符串的比较不会出错二是数据库存储空间小TINYINT 只占 1 字节三是扩展性强以后如果要加“已退订”状态加一个数字 6 就行不用改动已有的数据。但数字的缺点是不直观。所以查询时要用 CASE 语句把数字翻译成文字这部分前面已经展示过。代码里也可以定义一个枚举类来对应比如public enum OrderStatus { Pending 0, // 待审核 Approved 1, // 审核通过 Rejected 2, // 审核不通过 Purchasing 3, // 采购中 Completed 4, // 订购完成 Distributed 5 // 已发放 }这样在代码里写 if (orderStatus OrderStatus.Purchasing) 而不是 if (orderStatus 3)阅读性和维护性都会好很多答辩时讲状态设计也能多说两句。4.2 状态流转的控制逻辑谁能做什么事从流程上看每个状态变更操作属于不同角色教师提交申请后只能查询进度审核、采购、入库、发放的操作都在管理员端。这套系统没有做复杂的角色权限表而是通过页面访问控制来区分——教师用 new.aspx 和 history1.aspx管理员用 update2.aspx 和 book.aspx。这是一种在课程设计里可以接受的简化方案。真正容易出问题的点是状态跳转的合法性。比如一个申请还在“待审核”状态管理员不应该能直接把它改成“已发放”。虽然从业务逻辑上讲不应该出现但代码里如果只是写一句“UPDATE ... SET OrderStatus 5”就可能出现跳过中间环节的脏数据。建议在更新前做一次当前状态判断// 更新状态前检查当前状态是否合法 string checkSql SELECT OrderStatus FROM BookOrderInfo WHERE OrderID OrderID; int currentStatus 0; using (SqlCommand cmd new SqlCommand(checkSql, conn)) { cmd.Parameters.AddWithValue(OrderID, orderId); currentStatus Convert.ToInt32(cmd.ExecuteScalar()); } // 只有状态为 1审核通过时才允许改为 3采购中 if (currentStatus (int)OrderStatus.Approved newStatus (int)OrderStatus.Purchasing) { // 执行更新 } else { lblMessage.Text 当前状态不允许执行该操作; }这段代码看起来多写了几行但它能挡住两类常见事故一是管理员连续点两次按钮导致状态被二次更新二是页面被恶意构造请求直接把某条记录改成已完成。课程设计里老师经常问“如果流程跳过了怎么办”这一段就是答案。4.3 教师查询进度history1.aspx 的三种实现方式查询进度是教师端唯一需要的功能。最常见的实现方式是 GridView 绑定一个 SqlDataSource页面代码比较简单但查询逻辑写死在 .aspx 里不推荐。好一点的做法是后台代码绑定数据源protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { BindGrid(); } } private void BindGrid() { string sql SELECT c.CourseName, c.TeacherName, c.ClassName, b.BookName, b.Publisher, b.Price, b.Quantity, CASE b.OrderStatus WHEN 0 THEN N待审核 WHEN 1 THEN N审核通过 WHEN 2 THEN N不通过 WHEN 3 THEN N采购中 WHEN 4 THEN N完成 WHEN 5 THEN N已发放 END AS StatusText FROM BookOrderInfo b INNER JOIN CourseInfo c ON b.CourseID c.CourseID WHERE c.TeacherName TeacherName ORDER BY b.SubmitTime DESC; using (SqlConnection conn new SqlConnection(connStr)) { SqlDataAdapter da new SqlDataAdapter(sql, conn); da.SelectCommand.Parameters.AddWithValue(TeacherName, Session[UserName].ToString()); DataTable dt new DataTable(); da.Fill(dt); GridView1.DataSource dt; GridView1.DataBind(); } }这里的 TeacherName 取自 Session登录时写入查询时直接拿来过滤数据。用 Session 而不是再从页面传一次避免了用户修改页面源码来查看别人的数据——虽然课程设计里没人真去搞但这是一个正确的安全意识。4.4 状态更新的“最后一步”发放与入库发放操作通常发生在教材到货之后。管理员把状态从 4订购完成改成 5已发放。这一步和前面的更新操作类似但要注意一个细节发放时往往需要记录发放时间或经手人。如果需要保存这些附加信息就要在表里加字段比如 AddTime 或 HandleBy。源码里如果没加你自己扩展的时候可以直接在 SQL 语句里多加两个赋值字段不影响已有逻辑。5. 避坑指南这套源码里最常见的五个翻车点5.1 页面回发导致 GridView 数据丢失现象点击 GridView 里的按钮后页面刷新表格变空了。原因Page_Load 里绑定了数据但没有检查 IsPostBack每次回发都重新绑定导致事件触发后表格重新加载SelectedIndexChanged 里拿不到选中的行。解决把绑数据的语句放到 Page_Load 的 if (!IsPostBack) 分支里只有首次加载才绑定数据。protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { BindGrid(); // 只在首次加载时绑定 } }5.2 连接字符串写死导致换机器跑不起来现象在自己电脑上能跑换个环境报“建立与 SQL Server 的连接时出错”。原因Web.config 里连接串的服务器名写的是 localhost 或某个开发机的实例名换到别的电脑上 SQL Server 实例名不一样。解决把连接串里的 Data Source 改成 .\SQLEXPRESS 或 (local)并确保 SQL Server 允许 SQL 身份验证或 Windows 身份验证。connectionStrings add nameBookOrderDBConnectionString connectionStringData Source(local);Initial CatalogBookOrderDB;Integrated SecurityTrue providerNameSystem.Data.SqlClient / /connectionStrings5.3 状态值用字符串比较导致中英文不匹配现象明明状态是“审核通过”代码里 if (status 审核通过) 却进不去。原因数据库里存的是数字页面显示的是 CASE 转换后的中文拿转换后的显示文本和常量比较自然匹配不上。解决代码里全部用数字或枚举比较只有 SQL 层做显示转换。5.4 修改 GridView 数据后再次绑定导致编辑失效现象点“编辑”进入编辑模式修改一行数据点“更新”数据没变化。原因EditIndex 在回发后没有被重新设置或者更新后没有重新绑定 GridView。解决在 RowUpdating 事件里执行 UPDATE 语句后把 GridView 的 EditIndex 设回 -1然后重新调用 BindGrid()。protected void GridView1_RowUpdating(object sender, GridViewUpdateEventArgs e) { int orderId Convert.ToInt32(GridView1.DataKeys[e.RowIndex].Value); string newStatus ((DropDownList)GridView1.Rows[e.RowIndex].FindControl(ddlStatus)).SelectedValue; // 执行 UPDATE 语句... GridView1.EditIndex -1; BindGrid(); }5.5 教材订购数量与库存数量对不上现象教材预订了 200 本到货 180 本系统里没有记录发放时数量对不上。原因表里只有 Quantity订购数量字段没有实收数量或发放记录。解决在 BookOrderInfo 表里增加 ArrivedQuantity 字段到货时更新这个字段发放时以实收数量为准。这个坑在验收演示时特别容易被问住提前补上会主动很多。6. 把课程设计升级成答辩能讲的 Demo验证流程与三个加分项拿到源码后不要急着打包交作业先按业务逻辑把整套流程跑一遍。我一般会用以下这套验证路径来确认系统是通的用教师账号登录 → 提交一条新教材订购申请 → 检查状态为“待审核” → 用管理员账号登录 → 在审核页面看到这条申请 → 点击通过 → 状态变为“审核通过” → 继续操作更新状态为“采购中” → 再更新为“订购完成” → 最后更新为“已发放” → 再回到教师账号查询确认可以查到这条申请并且状态已同步。这整个流程如果都能走通系统主链路就是完整的答辩时照着讲一遍就够了。三个加分项分别说。第一在验证状态流转时准备一条“审核不通过”的数据展示拒绝原因如何传给教师端因为这能证明你不是只有单一流程还有分支处理。第二把数据库脚本里的测试数据改成你们学校自己的课程和班级名称用代称也可以关键是要贴合你报告里的场景描述演示时老师会觉得你是真正理解这套系统的。第三在报告里画一张状态流转图把“待审核 → 审核通过 → 采购中 → 订购完成 → 已发放”和“审核不通过 → 终止”画清楚这是数据库设计说明里最直观的加分内容。关于代码里的一个小习惯我每次拿到这类源码都会先做两件事第一步是全局搜索 Response.Write把调试用的输出清理掉第二步是把所有 SQL 语句统一改成参数化顺手把连接串从代码里挪到 Web.config。这两步做完项目基本从“能跑的课程设计”变成“像样的课程设计”。当时我从一个学长手里拿到这套系统的第一版源码时也是先跑通了全流程再对着状态字段做了半天边界测试后来每次做课设都强制自己走一遍这套验证流程从那以后交出去的东西再没被老师打过回票。希望这份拆解能帮你把教材订购系统真正跑起来不只是解压看一眼就搁置而是能在答辩台上把每一步讲清楚。本文还有配套的精品资源点击获取