
简介这是一套基于 ASP.NET Core MVC 与 SQL Server 构建的商城系统源码面向具备 C# 与 Web 开发基础、希望学习或二次开发电商项目的开发者与在校学生。项目采用经典 MVC 分层架构数据库选用 SQL Server 2012涵盖首页展示、商品列表、商品详情、购物车、下单、订单管理与用户登录等完整购物流程适合作为课程设计、毕业设计或企业级电商入门的参考案例。压缩包共 413 个文件约 31.31MB包含 35 个 cs 源码文件、17 个 cshtml 视图、24 个 css 与 18 个 js 前端资源以及 56 个 dll 依赖库和大量 jpg、png、webp 商品图片另附 mdf、ldf 数据库文件与 sln、csproj 工程文件结构完整可直接运行。目前已有 1075 人学习下载。通过该资源可掌握 MVC 路由与控制器编写、EF 数据访问、购物车会话管理及订单状态流转等核心技能并借助现成页面与数据库快速搭建可运行的电商原型。1. 从零搭一套能收单的 ASP.NET Core MVC 商城先想清楚这三层怎么咬合很多人第一次接商城类需求脑子里第一反应是“找个开源模板改改”结果拉下来跑不起来——不是 SQL Server 连不上就是迁移脚本对不上版本。我见过太多这样的翻车现场。这套ASP.NET Core MVC SQL Server 商城系统的组合本质上是三层咬合MVC 负责请求怎么进来、怎么渲染出去EF Core 负责把 C# 对象翻译成 SQLSQL Server 负责数据怎么存、怎么保证下单不超卖。三层任何一层松动整个商城就是纸糊的。这篇文章面向的是想用这套技术栈真正落地一个可收单、可管库存、可查订单的商城系统的开发者——不管你是刚学完 MVC 想练手还是接了个中小型电商项目要快速出活。我会把建库、建模型、写控制器、配连接、防并发这些环节拆开讲每一步都给出可复制的代码和参数说明让你少走我当年踩过的弯路。2. 数据库先落地SQL Server 建库、建表与连接字符串的硬参数商城系统的地基是数据库。商品、分类、订单、订单明细、用户、购物车——这六张表基本能撑起一个最小可用商城。我一般先在 SQL Server 里把库和表建好再回头写 C# 模型这样能避免 EF Core 迁移生成一堆意料之外的中间状态。SQL Server 的安装本身有不少坑比如 SQL Server 2012 安装完成但失败、密码到期策略、Express 版和 Developer 版的区别这些在环境准备阶段就要处理掉不然后面连不上库会浪费大量时间。2.1 建库建表六张核心表的字段与约束先建一个名为ShopDb的数据库排序规则用默认的Chinese_PRC_CI_AS即可。下面这张表清单是我做商城项目时反复验证过的最小集合字段类型和约束都经过实际下单流程检验。表名核心字段关键约束CategoriesId, Name, SortOrderId 主键自增ProductsId, CategoryId, Name, Price, Stock, ImageUrl, IsOnSalePrice decimal(18,2)Stock int 非负UsersId, UserName, PasswordHash, Phone, CreatedAtUserName 唯一索引OrdersId, UserId, OrderNo, TotalAmount, Status, CreatedAtOrderNo 唯一索引OrderItemsId, OrderId, ProductId, Quantity, UnitPrice外键指向 Orders 和 ProductsCartsId, UserId, ProductId, QuantityUserIdProductId 联合唯一建表时有一个细节容易被忽略Products.Stock要加CHECK (Stock 0)约束。这个约束是防超卖的最后一道防线后面讲并发时会用到。Orders.Status用 tinyint 存枚举值0 待付款、1 已付款、2 已发货、3 已完成、4 已取消。CREATE DATABASE ShopDb; GO USE ShopDb; GO CREATE TABLE Categories ( Id INT IDENTITY(1,1) PRIMARY KEY, Name NVARCHAR(50) NOT NULL, SortOrder INT NOT NULL DEFAULT 0 ); CREATE TABLE Products ( Id INT IDENTITY(1,1) PRIMARY KEY, CategoryId INT NOT NULL FOREIGN KEY REFERENCES Categories(Id), Name NVARCHAR(100) NOT NULL, Price DECIMAL(18,2) NOT NULL CHECK (Price 0), Stock INT NOT NULL CHECK (Stock 0), ImageUrl NVARCHAR(200) NULL, IsOnSale BIT NOT NULL DEFAULT 1 ); CREATE TABLE Users ( Id INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(50) NOT NULL UNIQUE, PasswordHash NVARCHAR(200) NOT NULL, Phone NVARCHAR(20) NULL, CreatedAt DATETIME2 NOT NULL DEFAULT SYSDATETIME() ); CREATE TABLE Orders ( Id INT IDENTITY(1,1) PRIMARY KEY, UserId INT NOT NULL FOREIGN KEY REFERENCES Users(Id), OrderNo VARCHAR(32) NOT NULL UNIQUE, TotalAmount DECIMAL(18,2) NOT NULL, Status TINYINT NOT NULL DEFAULT 0, CreatedAt DATETIME2 NOT NULL DEFAULT SYSDATETIME() ); CREATE TABLE OrderItems ( Id INT IDENTITY(1,1) PRIMARY KEY, OrderId INT NOT NULL FOREIGN KEY REFERENCES Orders(Id), ProductId INT NOT NULL FOREIGN KEY REFERENCES Products(Id), Quantity INT NOT NULL CHECK (Quantity 0), UnitPrice DECIMAL(18,2) NOT NULL ); CREATE TABLE Carts ( Id INT IDENTITY(1,1) PRIMARY KEY, UserId INT NOT NULL FOREIGN KEY REFERENCES Users(Id), ProductId INT NOT NULL FOREIGN KEY REFERENCES Products(Id), Quantity INT NOT NULL CHECK (Quantity 0), CONSTRAINT UQ_Cart_User_Product UNIQUE (UserId, ProductId) );这段脚本直接丢进 SQL Server Management Studio 执行即可。注意OrderNo用 VARCHAR 而不是 NVARCHAR因为订单号只含数字和字母用 VARCHAR 省一半存储。TotalAmount和UnitPrice用 DECIMAL(18,2) 是金额字段的标准做法别用 FLOAT否则会出现 0.10.2 不等于 0.3 的经典问题。2.2 连接字符串三个必须显式设置的参数建完库回到 ASP.NET Core MVC 项目里配连接字符串。appsettings.json里写{ ConnectionStrings: { ShopDb: Serverlocalhost;DatabaseShopDb;User Idsa;PasswordYourStrongPassw0rd;TrustServerCertificateTrue;MultipleActiveResultSetstrue; } }这里有几个参数值得展开说。TrustServerCertificateTrue在本地开发时必加否则 .NET 6 默认会校验证书连本地 SQL Server 直接报证书链错误。MultipleActiveResultSetstrue允许在同一个连接上同时打开多个结果集商城系统里查订单带明细的场景会用到。如果你用的是 SQL Server Express 版Server 写成localhost\SQLEXPRESS。生产环境不要用 sa 账号单独建一个只对 ShopDb 有读写权限的登录名。提示SQL Server 配置管理器里要把 TCP/IP 协议启用并且 IP 地址选项卡里的 TCP 端口设为 1433否则远程连不上。这个坑我踩过不止一次。3. 用 EF Core 把表映射成模型DbContext 配置与迁移命令数据库建好之后下一步是在 ASP.NET Core MVC 项目里用 EF Core 把表映射成 C# 类。这一步的核心是DbContext的配置——哪些字段要索引、哪些关系要显式声明、decimal 精度怎么设。很多教程只给个DbSet就完事结果跑起来发现金额精度丢了、外键级联删除了不该删的数据。3.1 实体类与 Fluent API 配置先装包。在项目目录下执行dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Tools dotnet add package Microsoft.EntityFrameworkCore.Design三个包各有用处SqlServer 是数据库提供程序Tools 提供dotnet ef命令Design 让迁移工具能在设计时工作。版本要跟你的 .NET 版本匹配.NET 8 项目就装 8.x 的包。然后写实体类和 DbContextpublic class Product { public int Id { get; set; } public int CategoryId { get; set; } public string Name { get; set; } string.Empty; public decimal Price { get; set; } public int Stock { get; set; } public string? ImageUrl { get; set; } public bool IsOnSale { get; set; } public Category Category { get; set; } null!; } public class ShopDbContext : DbContext { public ShopDbContext(DbContextOptionsShopDbContext options) : base(options) { } public DbSetCategory Categories SetCategory(); public DbSetProduct Products SetProduct(); public DbSetUser Users SetUser(); public DbSetOrder Orders SetOrder(); public DbSetOrderItem OrderItems SetOrderItem(); public DbSetCart Carts SetCart(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityProduct(e { e.Property(p p.Price).HasColumnType(decimal(18,2)); e.Property(p p.Name).HasMaxLength(100).IsRequired(); e.HasIndex(p p.CategoryId); }); modelBuilder.EntityOrder(e { e.Property(o o.TotalAmount).HasColumnType(decimal(18,2)); e.Property(o o.OrderNo).HasMaxLength(32).IsRequired(); e.HasIndex(o o.OrderNo).IsUnique(); }); modelBuilder.EntityCart(e { e.HasIndex(c new { c.UserId, c.ProductId }).IsUnique(); }); } }OnModelCreating里做三件事decimal 精度显式声明、字符串长度限制、索引声明。HasColumnType(decimal(18,2))必须写否则 EF Core 默认给 decimal 生成decimal(18,2)但某些版本会生成float导致金额对不上。HasIndex声明索引后迁移脚本会自动生成对应的CREATE INDEX。3.2 迁移命令与数据库更新配好 DbContext 后在Program.cs里注册builder.Services.AddDbContextShopDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(ShopDb)));然后执行迁移dotnet ef migrations add InitialCreate dotnet ef database update第一条命令根据实体类和 Fluent API 配置生成迁移文件放在Migrations文件夹下。第二条把迁移应用到数据库。如果你数据库已经手动建好了表可以用dotnet ef migrations add InitialCreate后手动改迁移文件把CreateTable改成migrationBuilder.Sql(SELECT 1)避免重复建表报错。我一般推荐让 EF Core 全权管理表结构手动建表只在需要精细控制索引和约束时用。注意dotnet ef命令需要全局工具先执行dotnet tool install --global dotnet-ef。如果提示找不到命令检查 PATH 里有没有~/.dotnet/tools。4. 商城核心链路商品列表、购物车、下单的控制器与防超卖模型和数据库通了之后真正决定商城能不能用的是核心链路商品列表怎么查、购物车怎么加、下单怎么保证不超卖。这三个环节里下单是最容易出问题的——两个人同时买最后一件库存处理不好就变成负库存。4.1 商品列表与分页IQueryable 延迟执行的正确用法商品列表页要支持分类筛选和分页。控制器写法public class ProductController : Controller { private readonly ShopDbContext _db; public ProductController(ShopDbContext db) _db db; public async TaskIActionResult Index(int? categoryId, int page 1, int pageSize 12) { var query _db.Products .Where(p p.IsOnSale) .AsNoTracking(); if (categoryId.HasValue) query query.Where(p p.CategoryId categoryId.Value); var total await query.CountAsync(); var items await query .OrderByDescending(p p.Id) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(p new ProductListVm { Id p.Id, Name p.Name, Price p.Price, ImageUrl p.ImageUrl }) .ToListAsync(); ViewBag.Total total; ViewBag.Page page; ViewBag.PageSize pageSize; return View(items); } }这里的关键是AsNoTracking()——列表页只读不写关掉变更追踪能省内存。Select投影到 ViewModel 而不是直接返回实体避免把Stock等敏感字段暴露到页面上。分页用Skip/Take注意Skip的值是(page-1)*pageSizepage 从 1 开始。CountAsync和ToListAsync分两次查因为 Count 不需要排序和投影分开写 SQL 更干净。4.2 下单防超卖乐观并发与事务边界下单是商城系统里唯一不能出错的地方。我见过最典型的翻车是库存 1 件两个用户同时点下单两个订单都创建成功库存变成 -1。解决办法是在扣库存时加条件更新public async TaskIActionResult PlaceOrder(int productId, int quantity) { var userId GetCurrentUserId(); using var tx await _db.Database.BeginTransactionAsync(); var product await _db.Products .FromSqlRaw(SELECT * FROM Products WITH (UPDLOCK, ROWLOCK) WHERE Id {0}, productId) .FirstOrDefaultAsync(); if (product null || product.Stock quantity) { await tx.RollbackAsync(); return Json(new { ok false, msg 库存不足 }); } product.Stock - quantity; var order new Order { UserId userId, OrderNo GenerateOrderNo(), TotalAmount product.Price * quantity, Status 0, CreatedAt DateTime.Now }; _db.Orders.Add(order); await _db.SaveChangesAsync(); _db.OrderItems.Add(new OrderItem { OrderId order.Id, ProductId productId, Quantity quantity, UnitPrice product.Price }); await _db.SaveChangesAsync(); await tx.CommitAsync(); return Json(new { ok true, orderNo order.OrderNo }); }FromSqlRaw里带WITH (UPDLOCK, ROWLOCK)是 SQL Server 的行级锁提示保证在事务提交前其他会话读不到这行、也改不了这行。UPDLOCK在读取时就加更新锁防止其他事务读到旧值后也去更新。ROWLOCK限定锁粒度到行避免锁表。这两个提示配合显式事务是防超卖最直接的手段。GenerateOrderNo()我一般用DateTime.Now.ToString(yyyyMMddHHmmss) 随机4位保证唯一且可读。提示如果你的并发量不大也可以用 EF Core 的乐观并发——给 Product 加一个RowVersion字段SaveChanges 时检查版本号。但高并发下乐观并发会频繁抛异常重试不如悲观锁直接。5. 避坑与排查SQL Server 连接、迁移、并发里的五个血泪教训这一章记录的是我在实际项目里踩过的坑每个都按“现象→原因→解决”写清楚。这些坑不解决商城系统要么跑不起来要么跑起来数据对不上。5.1 连接 SQL Server 报“已成功与服务器建立连接但是在登录前”现象程序启动后连数据库报错信息里有一句“已成功与服务器建立连接但是在登录前…”后面跟着证书或加密相关的错误。原因.NET 6 以后Microsoft.Data.SqlClient 默认启用加密连接本地 SQL Server 用的是自签名证书客户端不信任。解决连接字符串加TrustServerCertificateTrue。如果还不行检查 SQL Server 配置管理器里“强制加密”是否被打开关掉它。生产环境应该配正规证书而不是一直 Trust。5.2 迁移报“数据库中已存在名为 XXX 的对象”现象执行dotnet ef database update时报错说表已存在。原因数据库里手动建过表但 EF Core 的__EFMigrationsHistory表里没有对应记录迁移工具以为要从头建。解决两种方式。一是删掉手动建的表让 EF Core 全权管理二是手动往__EFMigrationsHistory里插入一条记录标记该迁移已应用。我一般选第一种保持迁移历史干净。5.3 下单时库存扣成负数现象并发测试时发现Products.Stock出现负数。原因扣库存用的是“先查再改”模式两个请求同时查到库存 1都认为够都扣。解决用FromSqlRaw加UPDLOCK, ROWLOCK锁行或者用UPDATE Products SET Stock Stock - qty WHERE Id id AND Stock qty这种原子更新检查受影响行数是否为 1。后者更轻量推荐。5.4 decimal 金额在页面上显示成科学计数法现象商品价格 1999.00 在页面上显示成 1.999E3。原因实体里 Price 声明成double或float或者视图里用了Html.DisplayFor但没指定格式。解决实体里用decimalFluent API 里HasColumnType(decimal(18,2))视图里用Model.Price.ToString(F2)或[DisplayFormat(DataFormatString {0:F2})]。5.5 SQL Server 2012 密码到期导致连接失败现象用 sa 账号连 SQL Server 2012突然连不上报登录失败。原因SQL Server 2012 默认对 sa 账号启用密码过期策略到期后账号被锁。解决用 Windows 身份验证登录 SSMS在安全性→登录名→sa→属性里取消“强制实施密码过期”。或者执行ALTER LOGIN sa WITH PASSWORD 新密码, CHECK_POLICY OFF;。SQL Server 2022 也有类似策略新装实例时注意。6. 进阶技巧用 SQL Server 查询计划定位商城慢查询商城系统上线后最先暴露的性能问题通常是商品列表和订单查询变慢。我一般不看代码先看查询计划。SSMS 里开“包含实际执行计划”跑一遍慢查询看哪个节点开销最大。最常见的三种情况缺索引导致全表扫描、隐式类型转换导致索引失效、SELECT * 带回大字段。缺索引的典型信号是执行计划里出现Table Scan或Clustered Index Scan而你的查询明明带了 WHERE 条件。比如SELECT * FROM Products WHERE CategoryId 3 AND IsOnSale 1如果只在 CategoryId 上有索引SQL Server 可能选全表扫描。这时候建一个覆盖索引CREATE NONCLUSTERED INDEX IX_Products_Category_OnSale ON Products (CategoryId, IsOnSale) INCLUDE (Name, Price, ImageUrl);INCLUDE里的字段是查询要返回但不用来过滤的列加进去后查询直接从索引里取数据不用回表。这个索引让商品列表查询从全表扫描变成索引查找我实测在 10 万行数据下从 800ms 降到 15ms。隐式类型转换更隐蔽。比如OrderNo是 VARCHAR(32)但 C# 里传参传了 NVARCHARSQL Server 会把 VARCHAR 列转成 NVARCHAR 再比较索引直接失效。解决办法是确保参数类型和列类型一致EF Core 里用HasColumnType(varchar(32))显式声明。最后一个习惯商城系统的查询永远不要SELECT *。商品表如果有 Description 这种大字段列表页带出来会拖慢整个查询。用投影只取需要的列这个在 EF Core 里用Select就能做到。我做了这么多年商城项目最大的教训是数据库层的问题永远比代码层的问题更难查因为代码报错有堆栈数据库慢查询只有执行计划。每次上线前跑一遍核心查询的执行计划比事后加索引省事得多。希望帮到你。本文还有配套的精品资源点击获取