☰
ASP.NET电商网站源码实战:技术架构与订单链路全解析
2026/10/23 13:16:34 网站建设 项目流程

简介:面向网页开发初学者与ASP.NET课程设计学生,这份电子商务网站示例项目演示了基于微软.NET框架的动态站点基本结构。压缩包共59个文件,主要包括页面文件、用户控件、后台代码、样式表及Access数据库等类型,整体仅400KB,结构精简且便于下载分析。项目覆盖商品展示、购物车、用户注册登录、订单处理等典型电商流程,能够看到身份验证、状态管理、数据库读写与服务器控件绑定等ASP.NET核心应用的落地写法;同时页面母版、主题样式与Ajax局部刷新等细节也对初学前端交互有参考价值。已有143人学习浏览,适合期末作业或课程实践;通过阅读源码可快速理解小型商城从数据表设计到页面布局、再到业务流程封装的完整链路,并对异常处理、缓存与部署配置等进阶主题产生直观认知,是入门ASP.NET Web开发的实用素材。

1. 项目概览:一套能跑起来的 ASP.NET 电商网站源码

接手一套 ASP.NET 电商网站源码,最常见的翻车现场其实不是功能写完没有,而是环境配不通:数据库连接串写错、Session 一直在丢、视图路径差个大小写,随便一项都能让浏览器只剩一片白屏。这套源码是典型的 ASP.NET 电子商务网站项目,前台有商品分类、关键字搜索、购物车、下单结算,后台有商品管理、订单处理和会员列表。它不追求微服务那种大而全,而是把 MVC 三层分层、EF Core 数据访问、Cookie 认证这些经典手段老老实实摆在代码里。

想学 C# Web 的人照跑一遍,能摸清一个请求从进 Controller 到返回 View 的完整链路;准备拿来做课程设计或小规模商城的人,可以直接把它当脚手架改。下一章先把架构和数据库拆开看,知道每层在哪,后面改起来才不慌。

2. 技术架构拆解:三层分层、数据库表与依赖设计

看这套电商源码,我的习惯是先看架构,再看目录,最后才看数据库。打开解决方案后先扫一眼 Controllers 和 Models 的分布,再打开 appsettings.json 看连接串,基本就能判断这个项目是随手堆的,还是按套路分的层。

2.1 技术栈与选型逻辑

这套源码的技术栈比较保守,但恰好适合电商这种页面多、状态多、业务链路长的场景。

方向方案选型逻辑
后端框架ASP.NET Core MVC(.NET 6)Controller + View 天然适合服务端渲染商品页
数据访问EF Core + Code First一条命令生成数据库,跑 Demo 不用手写建表脚本
数据库SQL Server LocalDB / Express本地开发零配置,连接串简单
前端Bootstrap 5 + jQuery不需要 Node 构建链,改完刷新就能看效果
登录态Cookie Authentication电商后台需要传 Cookie,比 JWT 少一截对接成本
会话存储内存 Session演示购物车够用,后续可换 Redis

这里有个容易被低估的设计点:没有拆前后端分离,也没有上消息队列。对一份拿来跑通业务闭环的电商源码来说,服务端渲染能省掉一堆联调时间,商品列表、购物车页面直接 Razor 输出,改动成本低,这对初学者理解数据流特别友好。如果换成 Vue + Web API 两套工程,新手很容易死在跨域和 Token 刷新上。

2.2 项目目录结构与分层边界

打开源码后的目录长这样:

WebShop/ ├── Controllers/ │ ├── HomeController.cs │ ├── ProductController.cs │ ├── CartController.cs │ ├── OrderController.cs │ └── AccountController.cs ├── Models/ │ ├── Entities/ # EF Core 实体 │ │ ├── Product.cs │ │ ├── Category.cs │ │ ├── CartItem.cs │ │ ├── Order.cs │ │ ├── OrderItem.cs │ │ └── User.cs │ ├── ViewModels/ # 页面专用模型 │ │ ├── ProductListViewModel.cs │ │ └── OrderSubmitViewModel.cs │ └── ShopDbContext.cs # 数据库上下文 ├── Views/ │ ├── Home/ Product/ Cart/ Order/ Account/ │ └── Shared/ ├── wwwroot/ │ ├── css/ js/ images/ ├── Program.cs ├── Startup.cs └── appsettings.json

Controllers 只负责接收请求、拼 ViewModel、返回视图;业务逻辑直接写在 Controller 里是目前源码省事的地方,但对后面要扩展的人是个隐患。Entities 目录放 EF Core 实体,ViewModels 放页面需要的数据结构,这样分的好处是实体和页面字段解耦——比如 Order 实体里存的是创建时间,而 OrderSubmitViewModel 里是收货人电话和地址,两者不互相污染。

wwwroot 是静态资源根目录,Bootstrap 的 css 和 jQuery 都在这,改样式不需要重新编译,刷新即生效,对小团队和教学场景很友好。

2.3 数据库表设计与订单状态流转

电商最核心的不是商品表,而是订单和库存的关系。这套源码一共六张主表:

表名说明关键字段
Users会员与后台管理员Id, UserName, PasswordHash, Role
Products商品Id, Name, Price, Stock, CategoryId
Categories商品分类Id, Name, SortOrder
CartItems购物车行项目Id, ProductId, Quantity
Orders订单主表Id, OrderNo, UserId, TotalAmount, Status
OrderItems订单明细Id, OrderId, ProductId, Price, Quantity

订单主表和订单明细分开,是电商设计的常规操作:主表存收货信息、总金额、状态,明细表存每个商品的快照价格。注意 OrderItems 里存的是下单那一刻的商品价格,而不是去关联 Products 表实时取价——商品后面改价,历史订单的金额不能跟着变。这是电商系统一个很重要的设计细节,很多新手会在这里返工。

订单状态在源码里是一个枚举:

public enum OrderStatus { Pending = 0, // 已提交,待支付 Paid = 1, // 已支付 Shipped = 2, // 已发货 Completed = 3, // 已完成 Cancelled = 4 // 已取消 }

状态流转就是 Pending -> Paid -> Shipped -> Completed,Cancelled 可以在前三个阶段由管理后台或超时逻辑触发。理解这条链路之后,后台订单管理页面的设计就顺理成章了,无非是 Status 字段的切换。

3. 核心模块实现:从商品筛选到订单提交的完整链路

套电商源码的关键路径是:用户浏览商品 -> 加购物车 -> 提交订单 -> 扣库存。我按这条链路把三个核心 Controller 的代码拆开讲,每一段都能直接抄进自己的项目里改。

3.1 商品列表:分页、分类与关键字搜索

商品列表是最常见的模块,但做到“能搜、能筛、能翻页”三件事同时成立,需要控制好查询条件的组合。ProductController 里的 Index 方法是这样写的:

public async Task<IActionResult> Index(int page = 1, int categoryId = 0, string keyword = "") { IQueryable<Product> query = _db.Products.AsNoTracking(); if (categoryId > 0) query = query.Where(p => p.CategoryId == categoryId); if (!string.IsNullOrWhiteSpace(keyword)) query = query.Where(p => p.Name.Contains(keyword) || p.Description.Contains(keyword)); var total = await query.CountAsync(); var pageSize = 12; var skip = (page - 1) * pageSize; var products = await query .OrderByDescending(p => p.Id) .Skip(skip) .Take(pageSize) .ToListAsync(); var vm = new ProductListViewModel { Products = products, CurrentPage = page, TotalPages = (int)Math.Ceiling(total / (double)pageSize), CategoryId = categoryId, Keyword = keyword }; return View(vm); }

逻辑拆开看:先构造 IQueryable,这是懒加载的查询对象,还没真正访问数据库。两次 Where 分别处理分类和关键字,都是叠加条件,不破坏原查询。然后 CountAsync 和 ToListAsync 才是真正发 SQL 的地方,这样分页能拿到总数和当前页数据。

参数需要注意三点:page 默认 1,但传入 0 或负数时 Skip 会抛异常,最好入口处做一次保护;categoryId 用 0 表示“全部分类”,因为在数据库里 Id 从 1 开始,0 不会撞到真实数据;keyword 是字符串,要走 Contains 的 SQL 翻译,EF Core 会翻译成 LIKE,如果用户输入了 % 或 _,有些场景会变成通配符,常规做法是在生成查询前对特殊字符做转义。

页面上翻页链接就是把这三个参数拼接起来:

<a asp-action="Index" asp-route-page="@(vm.CurrentPage - 1)" asp-route-categoryId="@vm.CategoryId" asp-route-keyword="@vm.Keyword">上一页</a>

把筛选条件原样带到下一页,否则翻到第 2 页筛选条件就丢了,这是列表页最常见的交互 bug。

3.2 购物车:Session 临时态与会话丢失问题

购物车模块没有单独建表落库,而是用 Session 存一份 CartItem 列表。这么做的好处是免登录也能加购物车,代价是用户清了浏览器缓存购物车就没了。对演示项目来说这是可接受的取舍。

public IActionResult AddToCart(int productId, int quantity = 1) { var product = _db.Products.Find(productId); if (product == null || product.Stock < quantity) return Json(new { success = false, message = "商品不存在或库存不足" }); var cart = HttpContext.Session.Get<List<CartItem>>("Cart") ?? new List<CartItem>(); var existing = cart.FirstOrDefault(c => c.ProductId == productId); if (existing != null) existing.Quantity += quantity; else cart.Add(new CartItem { ProductId = product.Id, ProductName = product.Name, Price = product.Price, Quantity = quantity }); HttpContext.Session.Set("Cart", cart); return Json(new { success = true, message = "已加入购物车", total = cart.Sum(c => c.Quantity) }); }

这段代码有两个关键决定。一是取 Session 时用了 null 合并运算符,第一次进页面时 Session 里没有 Cart,就 new 一个空列表,避免判空写一堆 if。二是同商品重复加入时只改 Quantity,不新增行,购物车角标 total 是所有数量之和,这个交互符合用户直觉。

Session.Set 方法接收的是对象,底层会做二进制序列化,所以 CartItem 必须标记 [Serializable] 或者用 System.Text.Json 序列化成字符串再存,否则第二次取出来反序列化会报错。常见的血泪经验是:本地调试好端端的,发布到 IIS 后购物车频繁丢,原因多半是 Session 的 Mode 变成了 Off 或 StateServer 没启动。检查 Web.config 里 sessionState 节点,开发环境用 InProc,多台服务器部署再考虑 StateServer 或 Redis。

3.3 订单提交:事务、库存扣减与订单号生成

订单提交是整个系统里最需要谨慎的模块,因为涉及多张表的写入,一旦中途出错,就会出现订单有了但库存没扣,或者库存扣了但订单明细丢失的脏数据。源码里用数据库事务包住整个操作:

[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Submit(OrderSubmitViewModel vm) { var cart = HttpContext.Session.Get<List<CartItem>>("Cart"); if (cart == null || cart.Count == 0) return BadRequest("购物车为空"); using var transaction = await _db.Database.BeginTransactionAsync(); try { var order = new Order { OrderNo = "ORD" + DateTime.Now.ToString("yyyyMMddHHmmss") + Guid.NewGuid().ToString("N").Substring(0, 4), UserId = _currentUser.Id, ReceiverName = vm.ReceiverName, ReceiverPhone = vm.ReceiverPhone, ReceiverAddress = vm.ReceiverAddress, TotalAmount = cart.Sum(c => c.Price * c.Quantity), Status = OrderStatus.Pending, CreatedAt = DateTime.Now }; _db.Orders.Add(order); await _db.SaveChangesAsync(); foreach (var item in cart) { _db.OrderItems.Add(new OrderItem { OrderId = order.Id, ProductId = item.ProductId, Price = item.Price, Quantity = item.Quantity }); var product = await _db.Products.FindAsync(item.ProductId); product.Stock -= item.Quantity; } await _db.SaveChangesAsync(); HttpContext.Session.Remove("Cart"); await transaction.CommitAsync(); return RedirectToAction("Detail", "Order", new { id = order.Id }); } catch { await transaction.RollbackAsync(); return View("Error"); } }

两次 SaveChanges 是刻意为之:第一次先拿到自增的 OrderId,第二次写明细和扣库存。如果一次 SaveChanges 把主表和明细一起提交,EF Core 也能自动回填导航属性,但在事务里分两步更好排查——第一步失败说明订单主表数据有问题,第二步失败说明商品库存或明细有问题。

订单号没有用数据库自增 Id 直接展示,而是拼了时间戳加四位随机数。这样订单号有可读性,能看出下单时间,同时避免客户直接通过订单号猜测订单总量。Guid 的截断取前四位在并发量不高时碰撞概率可接受,但如果要上线正式环境,建议改成 Redis 自增序列,或者用数据库 Sequence 保证不重复。

4. 运行部署与常见问题排查

代码拆明白了,接下来要把环境跑通。这一章按“装环境 -> 初始化数据库 -> 填坑”的顺序来,最后几条踩坑记录是从真实运行场景里提取的,照着排查能省不少时间。

4.1 环境准备与启动步骤

本地运行需要装三样东西:.NET 6 SDK、Visual Studio 2022 或 VS Code、SQL Server Express LocalDB。LocalDB 一般随 Visual Studio 安装附赠,不需要额外配置账号密码,连接串里用 Windows 身份认证即可。

打开命令行进入源码目录,依次执行:

dotnet restore dotnet ef database update dotnet run

第一条命令还原 NuGet 依赖包,第二条命令根据 Code First 的迁移文件创建数据库,第三条启动站点。如果提示 dotnet ef 不是可识别的命令,先装全局工具:

dotnet tool install --global dotnet-ef

启动后浏览器访问https://localhost:5001,首页能看到商品列表就说明环境通了。如果页面报 500,优先看控制台窗口的异常堆栈,大概率是数据库连接串没对上。

4.2 数据库初始化与种子数据

空数据库登录后台是没有商品可看的,源码里推荐在 Program.cs 里加一段种子数据逻辑,开发环境启动时自动填充分类、商品和管理员账号:

if (app.Environment.IsDevelopment()) { using var scope = app.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService<ShopDbContext>(); DbInitializer.Seed(db); }

DbInitializer 的 Seed 方法做了两次判断:先查 Categories 有没有数据,没有才插入;再查 Users,没有才创建默认管理员。这样每次启动不会重复插入种子数据,也保证你手动改过的数据不会被覆盖。

默认管理员账号密码在 Seed 方法里写死,一般是 admin / 123456,登录后台后第一件事就是改密码。如果密码带特殊字符导致连接串解析失败,优先在 appsettings.json 里检查是否被转义。

4.3 高频踩坑记录

这套源码跑下来,我整理了出现频率最高的四类问题,每一条都是“现象 -> 原因 -> 解决”的结构。

问题一:数据库创建失败,报错找不到 LocalDB 实例

现象是执行update-database时提示无法连接到数据库。最常见原因是本机根本没装 SQL Server Express LocalDB,Visual Studio 安装时勾选的组件不全,或者装了但服务没启动。解决方法是先打开命令行跑sqllocaldb info查看实例列表,如果没有实例就执行sqllocaldb create MSSQLLocalDB;装过完整版 SQL Server 的可以直接把连接串换成Server=.;Database=ShopDB;Trusted_Connection=True;用本机默认实例。

问题二:登录后刷新就变成未登录状态,Session 一直丢

现象是后台登录成功跳转后,再点一个页面就跳回登录页。原因一般是会话状态配置不对,或者 Cookie 的 SameSite 属性导致浏览器拒绝写入。先检查 Program.cs 里有没有调用AddSession()和app.UseSession(),少了任何一个 Session 都不会工作。确认这两行都在之后,再看 Cookie 配置,开发环境下把 SameSite 设为 Lax:

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.Cookie.SameSite = SameSiteMode.Lax; options.Cookie.HttpOnly = true; options.ExpireTimeSpan = TimeSpan.FromHours(2); });

问题三:访问 /Product/Index 返回 404 但页面文件明明存在

Controller 里明明写了 Index 方法,访问却 404,而且页面文件 Views/Product/Index.cshtml 也在。这个坑通常是 Views 目录里的子文件夹名和 Controller 名不完全一致,比如 Controller 叫 ProductController,但文件夹叫 Products,多了一个 s。ASP.NET Core 查找视图按 Controller 名精确匹配文件夹,对不上就返回 404,而且很多时候还不报具体错误,像个黑匣子。解决办法是右键点击返回 View 的方法,选择“Add View”,让系统自动生成正确路径。

问题四:商品名称中文显示成乱码

现象是 Razor 页面里的中文正常,从数据库读出来的中文全是问号。原因多半是数据表创建时的字符集不支持中文,或者连接串没指定 UTF-8。SQL Server 默认行为通常没问题,但如果你手动改过数据库排序规则,就要检查是否为 Chinese_PRC_CI_AS,并在连接串里加上Encrypt=False避免证书问题影响连接。

5. 二次开发:从 Controller 里抽 Service,把 Session 换成 Redis

源码能跑通只是第一步,真正要拿它做二次开发,第一件该做的事是把 Controller 里的业务逻辑抽到 Service 层。现在 CartController 的 AddToCart 里混着库存判断、Session 读写、JSON 返回三件事,代码短看不出问题,等你要加优惠券、积分、限购条件时,Controller 会膨胀到几百行。

我的改造思路是新建一个 Services 目录,先给商品和订单各建一个接口:

public interface IOrderService { Task<OrderResult> SubmitAsync(OrderSubmitViewModel vm, int userId); Task<IEnumerable<Order>> GetUserOrdersAsync(int userId); }

然后把 OrderController 里的事务、库存扣减、订单号生成全部搬进实现类。Controller 调 service,页面只关心 ViewModel。这个拆分最大的价值不是代码好看,而是能被单元测试直接调用——你不用启动整个网站就能测试下单逻辑,库存不足、订单号重复这类边界情况都能快速验证。

第二步是替换 Session。内存 Session 在单机演示没问题,但你要是部署到多台服务器,用户第一次请求落在服务器 A,第二次被负载均衡分到服务器 B,购物车就没了。常规做法是引入 Redis,在 Program.cs 里把 Session 存储改成:

builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = "localhost:6379"; }); builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(30); });

AddStackExchangeRedisCache 之后,Session 的数据自动存到 Redis,多台服务器共享一份购物车数据。注意 NuGet 包要装Microsoft.Extensions.Caching.StackExchangeRedis,只装 Redis 客户端库是无效的。

从那时起,我每次接电商类项目都会强制先做一件事:把 Controller 里的业务动作清单列出来,凡是超过十行的逻辑,一律迁移到 Service 层再谈其他。这个习惯帮我躲过了好几次“改一个需求要翻三遍 Controller”的窘境。希望这几条改造思路对你有用,下载下来的源码最好先跑通购物车链路,再动手加第一个功能,祝你顺利。

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

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

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

立即咨询