☰
ASP.NET网上书店课设源码拆解:B/S架构、购物车与后台管理全解析
2026/10/22 15:16:26 网站建设 项目流程

简介:一份基于C#和Sql Server的网上书店管理系统,采用ASP.NET与B/S架构,适合课程设计、毕业设计及Web开发初学者参考。系统清晰分离前台与后台:普通用户可浏览商品、注册登录、加入购物车,员工则通过后台管理新闻和产品信息,页面内置书籍广告轮播,整体风格统一、导航完善。压缩包内共262个文件,以aspx网页和cs后台逻辑代码为核心,辅以js/css前端交互与样式、jpg/png图片素材,并包含mdf/ldf数据库文件,可对应Sql Server数据表结构,资源完整度高。包体大小16.51MB,已有149人学习下载。通过该资源可快速理解网上书店的典型功能模块划分、ASP.NET页面生命周期、SqlServer数据库设计以及前后台交互流程,适合对照参考界面布局、代码组织方式与数据库关系,也可用于毕业设计答辩前的整体梳理和功能复盘。

1. 一套能直接答辩的 ASP.NET 网上书店:先说它解决了什么

如果你正在找一份能跑、能改、能写进课程设计报告里的 ASP.NET 项目,这套基于 C# 和 Sql Server 的网上书店系统值得花十分钟看完。它不是什么微服务、不是前后端分离,就是最经典的 Web Forms 三层结构:前台给普通用户逛书、加购物车、下订单,后台给员工维护书籍和订单,两套页面共用一个数据库,整套代码拿下来改改连接字符串就能在本机跑起来。我拆过不少这类课设资源,这套的页面命名非常直白——index.aspx 是前台入口,houtaiguanli.aspx 是后台入口,chanping.aspx 管产品,gouwuche.aspx 管购物车,几乎不用看文档就能猜到每个文件干什么。对正在赶课程设计或者想快速复用一个 B/S 结构电商 Demo 的开发者来说,它的价值在于:架构简单到能一眼看懂,同时又覆盖了登录鉴权、购物车、后台 CRUD 这些必问的答辩考点。

2. 从页面文件反推系统设计:B/S 结构的两套模板与数据流

2.1 十个页面文件的分工:前台与后台的物理隔离

拿到这套资源的第一件事不是急着跑起来,而是先把 Web 项目根目录下的页面文件全部过一遍。从输入的文件清单看,整套系统一共十个核心页面,我按业务域给你拆开:

  • 前台用户直接访问的页面:index.aspx(首页)、chanping.aspx(书籍产品列表)、gongsijieshao.aspx(公司介绍)、zhuce.aspx(用户注册)、denglu.aspx(用户登录)、gouwuche.aspx(购物车)、dg.aspx(订单管理)、gerenzhongxin.aspx(个人中心)、xiugaimima.aspx(修改密码)
  • 后台员工专用入口:houtaiguanli.aspx(后台管理首页)

注意一个设计细节:普通用户从前台首页进入,员工从后台首页进入,这意味着这套系统做了两个模板页(MasterPage)。前台模板页面向浏览者,包含导航条、书籍广告轮播、页脚;后台模板页面向运营员工,布局偏功能菜单,比如产品管理、订单处理、资讯发布。这种双模板设计在课程设计答辩里非常加分,因为评委一眼就能看出你理解了母版页的复用价值,而不是每个页面都复制粘贴同样的导航代码。

2.2 数据库连接与 Web.config:第一个要改的地方

所有的 B/S 系统,第一步一定是把数据库连接串配好。这套系统的数据库是 Sql Server,数据库文件通常以 .mdf 或 .bak 的形式放在项目目录的 App_Data 或 Database 文件夹下。连接串写在 Web.config 里,一般是这样的结构:

<connectionStrings> <add name="BookShopConnectionString" connectionString="Data Source=.;Initial Catalog=BookShop;User ID=sa;Password=123456;" providerName="System.Data.SqlClient" /> </connectionStrings>

这里有几个参数要解释清楚。Data Source 是你的 Sql Server 实例名,如果用本机默认实例就写一个点号.,如果用命名实例要写成服务器名\实例名,比如localhost\SQLEXPRESS。Initial Catalog 是数据库名,和你在 Sql Server Management Studio 里看到的数据库名称要完全一致。User ID 和 Password 是登录账号,如果是自己本机开发测试,建议直接用 sa 账号,如果不想开 sa,也可以改成 Windows 身份验证模式,把整段 connectionString 换成Data Source=.;Initial Catalog=BookShop;Integrated Security=True;。

常见做法是先把数据库文件附加到 Sql Server 里,附加成功后右键数据库查看属性里的名称,然后把名称填进 Initial Catalog。这一步卡住的人最多,后面避坑章我会细说。

2.3 数据访问层:SqlConnection 与 SqlDataReader 的典型用法

这套系统没有用 EF 或者 SqlSugar 这类 ORM,就是最原始的 ADO.NET,直接拼 SQL 语句执行。我拆开代码后发现它的数据访问套路非常统一,每个页面需要数据时就新建连接、打开连接、执行命令、填充数据控件。典型的商品列表加载代码是这个模式:

string connStr = ConfigurationManager.ConnectionStrings["BookShopConnectionString"].ConnectionString; using (SqlConnection conn = new SqlConnection(connStr)) { conn.Open(); string sql = "SELECT BookId, BookName, Price, CoverImage FROM Books WHERE IsOnSale = 1 ORDER BY PublishDate DESC"; SqlCommand cmd = new SqlCommand(sql, conn); SqlDataReader reader = cmd.ExecuteReader(); // 绑定到前端 Repeater 或 DataList Repeater1.DataSource = reader; Repeater1.DataBind(); reader.Close(); }

这段代码是这套系统里出现频率最高的模式,理解它就能看懂八成页面。using 语句块确保 SqlConnection 用完自动释放,避免连接泄漏。ExecuteReader 返回的是只进流的 SqlDataReader,适合列表绑定,但不适合在循环里做二次查询,因为 DataReader 占用着连接通道。如果你需要在绑定过程中拿关联数据,要么先读进 DataTable 再释放连接,要么用 SqlDataAdapter 填充 DataSet。

参数说明:Repeater 控件适合做简单的列表循环,如果你想做两列三列的商品网格布局,通常用 DataList 并设置 RepeatColumns 属性,这套系统的 chanping.aspx 页面里会有具体实现。前台首页的书籍广告轮播一般用 JavaScript 控制图片切换,后台只需要把广告图路径存到数据库或者直接写在页面上即可。

3. 前台购物全流程:登录、加购、下单的代码拆解

3.1 用户登录与 Session 鉴权:明文密码的取舍

denglu.aspx 是前台登录页,核心逻辑是拿用户输入的用户名密码去 Users 表里比对,比对成功就写 Session,然后跳转到个人中心。代码逻辑大概是这样的:

string sql = "SELECT * FROM Users WHERE UserName='" + txtUserName.Text.Trim() + "' AND Password='" + txtPassword.Text.Trim() + "'"; SqlCommand cmd = new SqlCommand(sql, conn); SqlDataReader reader = cmd.ExecuteReader(); if (reader.Read()) { Session["UserId"] = reader["UserId"].ToString(); Session["UserName"] = reader["UserName"].ToString(); Response.Redirect("gerenzhongxin.aspx"); } else { lblMessage.Text = "用户名或密码错误"; }

这里有一个必须吐槽的点:字符串拼接 SQL 的写法在课程设计里非常常见,但这是最典型的 SQL 注入漏洞。你登录框里输入' OR '1'='1' --就能绕过密码校验。这套系统作为课程设计可以这么写,但你拿去面试、或者自己扩展成真实项目,一定要改成参数化查询。后面避坑章我会给出改造前后的对比代码。

Session 在这里承担了「登录凭证」的角色,UserId 和 UserName 写进 Session 后,个人中心的欢迎语、订单列表的归属判断都从 Session 里取值。Session 的默认过期时间是 20 分钟,实际开发中如果用户长时间挂着页面不操作,Session 会失效,代码里要做 Session 为 null 的判空,否则会抛 NullReferenceException。

3.2 购物车的两种实现:Session 存表还是存 Cookie

这套系统的购物车页面是 gouwuche.aspx。ASP.NET 时代购物车最常见的实现方式有两种:一种是把购物车数据放在 Session 里存成一个 DataTable,另一种是存成 Cookie 的 JSON 字符串。这套系统用的是 Session + DataTable 方案,我拆解它的核心步骤:

// 获取 Session 中的购物车,若为空则新建一个 DataTable DataTable dtCart = Session["Cart"] as DataTable; if (dtCart == null) { dtCart = new DataTable(); dtCart.Columns.Add("BookId", typeof(int)); dtCart.Columns.Add("BookName", typeof(string)); dtCart.Columns.Add("Price", typeof(decimal)); dtCart.Columns.Add("Quantity", typeof(int)); Session["Cart"] = dtCart; } // 添加商品:判断是否已存在,存在则数量加 1 DataRow[] rows = dtCart.Select("BookId=" + bookId); if (rows.Length > 0) { rows[0]["Quantity"] = (int)rows[0]["Quantity"] + 1; } else { DataRow newRow = dtCart.NewRow(); newRow["BookId"] = bookId; newRow["BookName"] = bookName; newRow["Price"] = price; newRow["Quantity"] = 1; dtCart.Rows.Add(newRow); }

这个方案的优点是代码直观,不需要额外的数据表,页面刷新前购物车数据一直存在服务器 Session 里,用户感知是「加购后数量立刻变了」。缺点是 Session 存 DataTable 会占用服务器内存,而且如果用户中途关闭浏览器,Session 清空,购物车就没了。真实电商系统会做成数据库购物车表,但课程设计用 Session 完全够用,答辩时把这个方案的优劣说清楚,反而是加分项。

参数说明:dtCart.Select("BookId=" + bookId) 里的过滤条件用的是 DataTable 的 Select 方法,语法类似 SQL 的 WHERE,但注意这里拼接的是整数类型的 bookId,不存在注入问题。如果是字符串类型的筛选条件,记得加单引号,比如BookName='C#入门经典'。

3.3 下订单与订单详情:事务处理的关键场景

用户从购物车点击结算,跳转到 dg.aspx 生成订单。订单生成的业务逻辑涉及两张表:订单主表(Orders)和订单明细表(OrderDetails),主表存订单号、用户 ID、总金额、下单时间、订单状态,明细表存每个商品的数量和单价。这两张表必须用事务保证数据一致性,否则会出现主表写了、明细表没写的脏数据。

using (SqlTransaction tran = conn.BeginTransaction()) { try { string insertOrder = "INSERT INTO Orders (UserId, TotalAmount, OrderDate, Status) VALUES (@uid, @total, GETDATE(), '已提交'); SELECT SCOPE_IDENTITY();"; SqlCommand cmdOrder = new SqlCommand(insertOrder, conn, tran); cmdOrder.Parameters.AddWithValue("@uid", Convert.ToInt32(Session["UserId"])); cmdOrder.Parameters.AddWithValue("@total", totalAmount); int orderId = Convert.ToInt32(cmdOrder.ExecuteScalar()); foreach (DataRow row in dtCart.Rows) { string insertDetail = "INSERT INTO OrderDetails (OrderId, BookId, Price, Quantity) VALUES (@oid, @bid, @price, @qty)"; SqlCommand cmdDetail = new SqlCommand(insertDetail, conn, tran); cmdDetail.Parameters.AddWithValue("@oid", orderId); cmdDetail.Parameters.AddWithValue("@bid", row["BookId"]); cmdDetail.Parameters.AddWithValue("@price", row["Price"]); cmdDetail.Parameters.AddWithValue("@qty", row["Quantity"]); cmdDetail.ExecuteNonQuery(); } tran.Commit(); Session["Cart"] = null; // 清空购物车 } catch (Exception ex) { tran.Rollback(); // 记录异常日志 } }

注意这段代码里我用的是参数化写法——这是我拆完原项目后给你的改进版本,因为原项目的字符串拼接写法在订单这种多表写入场景里风险太大。SCOPE_IDENTITY() 是取当前会话插入的自增 ID 的关键,取到订单主表的 OrderId 后,才能把它作为外键写入明细表,这一步是父子表写入的固定套路。

4. 后台管理模块:houtaiguanli.aspx 的角色控制与数据维护

4.1 后台入口的权限校验:Session 判断是否员工登录

后台管理页 houtaiguanli.aspx 在 Page_Load 事件里必然有一段权限判断代码,逻辑上是判断当前 Session 里有没有员工登录标记。这套系统的员工表和前台用户表是分开的,后台页面顶部通常会写这样的代码:

protected void Page_Load(object sender, EventArgs e) { if (Session["EmployeeId"] == null) { // 未登录,跳转到后台登录页 Response.Redirect("denglu.aspx?type=admin"); } }

业务逻辑解释:前台 denglu.aspx 登录时,会根据传入的 type 参数决定查 Users 表还是 Employees 表。这样设计的好处是一个登录页可以复用两套账号体系,但代价是登录逻辑里多了一个分支判断。实际的判断方式一般是先检查 Users 表,如果没匹配再查 Employees 表——这种「先查用户再查员工」的顺序在数据量大时会有性能损耗,课程设计规模下无感知,但答辩时可以提出用 UserType 字段区分角色,这属于可扩展的改进点。

后台产品管理页(chanping.aspx 在后台的对应功能)通常包含一个 GridView 控件,展示所有书籍的 ID、书名、价格、库存、上下架状态,并提供编辑和删除按钮。GridView 的编辑功能有两种模式:一种是点击编辑行后每个单元格变成 TextBox 让你直接改,另一种是跳转到独立编辑页面。这套系统用的是前者,它的代码如下:

protected void GridView1_RowUpdating(object sender, GridViewUpdateEventArgs e) { int bookId = Convert.ToInt32(GridView1.DataKeys[e.RowIndex].Value); string bookName = ((TextBox)GridView1.Rows[e.RowIndex].Cells[1].Controls[0]).Text; string price = ((TextBox)GridView1.Rows[e.RowIndex].Cells[2].Controls[0]).Text; string sql = "UPDATE Books SET BookName=@name, Price=@price WHERE BookId=@id"; // 执行参数化更新 GridView1.EditIndex = -1; BindBookList(); // 重新绑定数据 }

这段代码里 GridView1.DataKeys 是获取当前行主键值的标准方式,必须在 GridView 的 DataKeyNames 属性里先声明主键列名,比如DataKeyNames="BookId",否则 DataKeys 是空的,取不到值。取单元格里 TextBox 值的方式容易踩坑,因为 Cells[1] 的索引可能受模板列影响偏移,更好的方式是 FindControl,但 FindControl 只能找到模板列里的控件,对 BoundField 不生效。这是 GridView 编辑功能的老大难问题,很多初学者在这里折腾半天,我在避坑章里会专门讲。

5. 避坑指南:连接失败、路径漂移与登录绕过的血泪经验

5.1 数据库附加失败:.mdf 文件提示权限不足

现象:用 Sql Server Management Studio 附加项目里的 BookShop.mdf 文件时,弹出类似「无法打开物理文件,拒绝访问」的错误。

原因:SQL Server 服务账号没有 MDF 文件所在文件夹的读取权限。尤其是从网上下载的项目文件夹通常放在桌面、下载目录或者解压后的子目录里,这些路径的 ACL 权限默认不授予 SQL Server 服务账号。

解决:把整个项目文件夹移动到非系统盘的根目录下,比如C:\BookShopProject\,然后右键文件夹 → 属性 → 安全 → 编辑 → 添加MSSQLSERVER(或者你的 SQL Server 服务账号名)并赋予完全控制权限,再重新附加。如果还不行,用管理员身份打开 SSMS 再试一次。

5.2 Web.config 连接串写错,页面直接报 500

现象:运行后页面报「建立与服务器的连接失败」或「在建立与服务器的连接时出错」。

原因:Data Source 写错是最常见的。如果你本机装的 Sql Server 是 Express 版,实例名不是默认的,写成Data Source=.必然连不上。另外 Initial Catalog 的数据库名必须和附加后的逻辑名称一致,而不是 .mdf 文件名。

解决:打开 SSMS 连接数据库后,在对象资源管理器里看数据库名称,把完整名称复制进连接串。同时在 Web.config 里检查有没有多余的转义字符。我一般会先在 SSMS 里用 sa 账号测试能连上,再回过来改 Web.config,这样能快速定位是数据库服务问题还是连接串问题。

5.3 母版页相对路径失效:CSS 和图片全部丢失

现象:前台 index.aspx 使用了母版页,页面能打开,但所有 CSS 样式丢失、图片显示小红叉。

原因:这是 ASP.NET Web Forms 的经典坑。母版页里的<link href="css/style.css">和<img src="images/logo.png">用的是相对路径,而内容页和母版页不在同一个目录层级,浏览器解析相对路径时就会 404。比如内容页在子目录里,而母版页里写的是../images/banner.jpg,访问时路径就乱了。

解决:母版页和用户控件里一律用绝对根路径,写成<%=ResolveUrl("~/css/style.css")%>,或者在 src 和 href 里直接写href="/css/style.css"(前提是应用部署在站点根目录)。ResolveUrl 是 Web Forms 老项目最稳妥的路径解析方案,它会自动把~/解析成当前应用的根目录,不管页面嵌多深都不会出错。

5.4 字符串拼接 SQL:登录框输入注入语句直接被绕过

现象:在登录页面用户名框输入' or '1'='1' --,密码随便填,结果直接登录成功了。

原因:代码里用了SELECT * FROM Users WHERE UserName='" + txtUserName.Text + "' AND Password='" + txtPassword.Text + "'",你输入的内容被原样拼进 SQL,注释符--把后面的条件全部注掉了,这条 SQL 恒为真。

解决:务必将所有登录、查询、更新、删除的 SQL 改成参数化写法。这是这套系统最大的安全隐患,也是你答辩时必须主动提到的改进点。改造后的登录代码我在 3.1 节后面已经给过示例,核心就是用Parameters.AddWithValue替代字符串拼接。

5.5 GridView 取不到单元格控件值:编辑功能改了没反应

现象:点击 GridView 的编辑按钮后,行变成可编辑状态,也输入了新内容,但点更新后数据没变。

原因:RowUpdating 事件里通过GridView1.Rows[e.RowIndex].Cells[1].Controls[0]取控件时索引对应不上。如果那一列是 BoundField,取到的可能是 DataControlLiteralCell 里的 LiteralControl,而不是 TextBox。这是 GridView 编辑的老牌玄学问题。

解决:在 GridView 的列定义里,把要编辑的列都改成 TemplateField,然后在编辑模板里放 TextBox,并在 RowUpdating 事件里用((TextBox)GridView1.Rows[e.RowIndex].FindControl("txtBookName")).Text来取值。这样找控件是按 ID 找,不会因为列索引偏移而找不到。改完记得把 GridView 的 DataKeyNames 设置为BookId。

6. 把课设升级成能讲出口的项目:参数化改造与商品图片上传

对于课程设计而言,这套系统能跑通就算合格,但如果想拿高分,或者想在面试时把这个项目讲出深度,我建议你优先做两件事:一是把登录和订单里的 SQL 拼接全部改成参数化查询,二是给后台产品管理加上图片上传功能。这两个改动不用动数据库结构,代码量不大,但能直接展示你对安全性、文件上传这两个高频面试考点的掌握程度。

先看参数化改造的完整示例。以登录为例,改造前是字符串拼接,改造后应该是这样:

protected void btnLogin_Click(object sender, EventArgs e) { string connStr = ConfigurationManager.ConnectionStrings["BookShopConnectionString"].ConnectionString; using (SqlConnection conn = new SqlConnection(connStr)) { conn.Open(); string sql = "SELECT * FROM Users WHERE UserName=@username AND Password=@password"; SqlCommand cmd = new SqlCommand(sql, conn); cmd.Parameters.AddWithValue("@username", txtUserName.Text.Trim()); cmd.Parameters.AddWithValue("@password", txtPassword.Text.Trim()); SqlDataReader reader = cmd.ExecuteReader(); if (reader.Read()) { Session["UserId"] = reader["UserId"].ToString(); Session["UserName"] = reader["UserName"].ToString(); Response.Redirect("gerenzhongxin.aspx"); } else { lblMessage.Text = "用户名或密码错误"; } reader.Close(); } }

这段代码的好处是:参数类型是强类型的,AND 和 OR 的边界由 SqlCommand 自动处理,你再输入' or '1'='1也只是被当成一个普通字符串去比对,数据库里没有这条记录自然登录失败。注意 AddWithValue 虽然好用,但它的参数类型推断有时不够精确,比如日期类型和 decimal 类型可能会被推断成 nvarchar 导致隐式转换;严谨的写法是用cmd.Parameters.Add("@price", SqlDbType.Decimal).Value = price;,这样能避免类型推断带来的隐性坑。

再看图片上传。原系统的商品表里有一个 CoverImage 字段存的是图片路径字符串,前台chanping.aspx用Image控件来显示。后台的管理页如果要加上传功能,需要在编辑模板里加一个 FileUpload 控件,然后在更新事件里处理:

FileUpload fileUpload = (FileUpload)GridView1.Rows[e.RowIndex].FindControl("fuCoverImage"); if (fileUpload.HasFile) { string fileName = Guid.NewGuid().ToString() + Path.GetExtension(fileUpload.FileName); string savePath = Server.MapPath("~/images/books/" + fileName); fileUpload.SaveAs(savePath); // 更新数据库时把相对路径写入 CoverImage 字段 string relativePath = "~/images/books/" + fileName; // 执行 UPDATE Books SET CoverImage=@cover WHERE BookId=@id }

这里有两个我在实际项目中踩过的坑,一并提醒你。第一,文件保存路径必须做合法性校验,只允许 .jpg、.png、.gif 这类白名单后缀,否则别人传一个 .aspx 文件就能在你服务器上执行任意代码,这是文件上传漏洞的经典利用方式。第二,用 Guid.NewGuid() 重命名文件是防止文件名冲突的常见做法,但如果你想让文件名可读性更好,可以用时间戳加随机数:DateTime.Now.ToString("yyyyMMddHHmmss") + "_" + new Random().Next(1000, 9999) + ".jpg"。另外记得在 Web 服务器层面限制上传文件大小,配置 Web.config 里的 maxRequestLength。

从那以后我每次帮别人调这套课设源码,都强制先把登录和订单这两处 SQL 拼接改成参数化,再谈其他功能——因为这是成本最低、但演示效果最好的一个升级点。无论你是准备直接拿去交作业,还是想在原基础上加功能,这套系统的骨架都足够清晰透明,改起来不会有无从下手的感觉。希望帮到你。

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

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

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

立即咨询