首页 / 资讯中心 / 文章详情

ASP.NET电商网站全链路实战:库存并发、支付幂等与订单状态机

ASP.NET电商网站全链路实战:库存并发、支付幂等与订单状态机 ★ FEATURED ARTICLE
简介这份 ASP.NET 电子商务网站项目面向学习 Web 开发的学生与初学者可作为课程设计、期末作业或小型电商系统练习的参考案例帮助理解从页面展示到后台数据管理的完整开发流程。压缩包共 59 个文件约 400KB包含 7 个 aspx 页面、12 个 cs 后台代码、4 个 ascx 用户控件、4 个 mdb Access 数据库以及 css 样式、sitemap 站点地图和 config 配置等另有较多 gif、jpg 图片资源整体结构保留了典型 Web Forms 项目的组织方式。已有 143 人学习读者可从中参考商品展示、用户注册登录、购物车与订单处理等模块的页面组织、控件复用和数据库连接思路也可借此梳理 ASP.NET 状态管理、身份验证与站点配置的基本用法适合用来对照自己的课程作业并查漏补缺。1. 从一笔“对不上账”的订单说起答辩前一晚购物车里加了两件商品结算页金额却少算了一件后台把商品改成下架前台列表还挂着两个人同时点最后一件库存两边都下单成功。这类问题通常与代码写得多不多无关根源多在于需求边界一开始就没定死。这套 ASP.NET 电子商务网站方案瞄准的是“能跑通一笔完整交易”这件事商品与 SKU 分离、购物车双态、下单事务扣库存、支付回调幂等、后台订单状态机一条链路从头走到尾。它适合两类人要交毕设或课程设计、需要一套能演示能答辩的完整站点的同学以及要出一份可交付电商方案的从业者。后面按需求、库表、架构、核心模块、避坑、验收的顺序推进每一段都落到能直接抄的结构和代码上。2. 需求拆解与数据库设计八张表撑起一条交易链路电商站最容易失控的地方不是页面多而是数据关系乱。先把角色边界划清楚再把表定死后面写控制器和视图基本是体力活。我一般会要求自己在动第一行代码之前先在纸上把这八张表画出来画不出来就说明需求还没想明白。2.1 三类角色与用例边界买家侧的用例其实只有六个浏览分类、搜索商品、看商品详情选规格、加购物车、下单支付、查我的订单。运营侧是商品上下架、SKU 与库存维护、订单发货。管理员侧只保留账号与权限、订单异常处理。把这三类拆开的意义在于它直接决定了后面控制器怎么切分、权限过滤器挂在哪一层。很多同学一上来就做“万能后台”结果商品编辑页里塞了订单发货按钮权限判断写在 Razor 视图里答辩时老师一问“这个按钮谁能点”就答不上来。比较稳妥的做法是按角色划分区域前台Areas/Shop、后台Areas/Admin两边共用领域层但各自有独立的控制器和视图。用例还要标注“谁触发、写哪张表、失败时回滚什么”。比如“下单”这个用例触发者是买家写 Order、OrderItem、ProductSku扣库存、Payment生成待支付流水任一步失败必须整体回滚。这张用例表在写代码时会反复用到比任何 UML 图都实用。提示把“库存扣减”单独标成一条用例而不是塞在“下单”里答辩时被问到并发问题你能直接翻到这一页。2.2 八张核心表与字段取舍商品和 SKU 必须分开。Product存标题、主图、分类、状态这些与规格无关的信息ProductSku存具体规格、价格、库存。只做单规格商品的站点可以合并但只要涉及“颜色/容量/套餐”合并的表一定会在下单时出问题。-- 商品主表只放与规格无关的公共信息 CREATE TABLE Product ( Id INT IDENTITY(1,1) PRIMARY KEY, CategoryId INT NOT NULL, Title NVARCHAR(120) NOT NULL, Subtitle NVARCHAR(200) NULL, MainImage NVARCHAR(300) NULL, Status TINYINT NOT NULL DEFAULT 0, -- 0 草稿 1 上架 2 下架 CreatedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME() ); -- SKU 表价格和库存挂在这一层行版本用于乐观并发 CREATE TABLE ProductSku ( Id INT IDENTITY(1,1) PRIMARY KEY, ProductId INT NOT NULL, SpecText NVARCHAR(120) NOT NULL, -- 例如 颜色:黑;容量:256G Price DECIMAL(10,2) NOT NULL, Stock INT NOT NULL, RowVersion ROWVERSION -- 并发令牌 ); -- 订单主表金额一律 DECIMAL禁止 FLOAT CREATE TABLE [Order] ( Id BIGINT IDENTITY(1,1) PRIMARY KEY, OrderNo VARCHAR(32) NOT NULL UNIQUE, UserId INT NOT NULL, TotalAmount DECIMAL(12,2) NOT NULL, Status TINYINT NOT NULL, -- 0 待支付 1 已支付 2 已发货 3 已完成 4 已取消 CreatedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME() ); -- 支付流水OutTradeNo 唯一索引是幂等的第一道防线 CREATE TABLE Payment ( Id BIGINT IDENTITY(1,1) PRIMARY KEY, OrderNo VARCHAR(32) NOT NULL, OutTradeNo VARCHAR(64) NOT NULL, Amount DECIMAL(12,2) NOT NULL, PaidAt DATETIME2 NULL ); CREATE UNIQUE INDEX UX_Payment_OutTradeNo ON Payment(OutTradeNo);字段取舍上有三个血泪经验。第一金额全用DECIMAL(12,2)FLOAT和DOUBLE在累加时会给你演出“差一分钱”的玄学。第二订单状态用TINYINT加常量枚举不要用字符串否则后面改文案要全表更新。第三OrderNo单独生成而不是拿自增主键自增 ID 会暴露订单量也不方便按日期分表。剩下四张表是Category分类带ParentId做两级、CartItem登录用户购物车、OrderItem订单明细快照商品标题与单价、AppUser账号与角色。OrderItem一定要存商品标题和下单时单价不要只存SkuId——商品改价改名之后历史订单必须还能显示当时的价格。2.3 领域模型与 EF Core 映射表定完实体类基本是照抄但有两处必须手动配置金额精度和并发令牌。EF Core 默认会把decimal映射成decimal(18,2)多数场景够用如果你自定义了精度就要在OnModelCreating里显式声明否则迁移生成的列类型可能和手工建表脚本对不上。public class ProductSku { public int Id { get; set; } public int ProductId { get; set; } public string SpecText { get; set; } string.Empty; public decimal Price { get; set; } public int Stock { get; set; } // 映射到 SQL Server 的 rowversion 列EF 用它做乐观并发 public byte[] RowVersion { get; set; } Array.Emptybyte(); public Product Product { get; set; } null!; } public class ShopDbContext : DbContext { public DbSetProduct Products SetProduct(); public DbSetProductSku Skus SetProductSku(); public DbSetOrder Orders SetOrder(); protected override void OnModelCreating(ModelBuilder b) { b.EntityProductSku(e { e.Property(x x.Price).HasPrecision(10, 2); e.Property(x x.RowVersion).IsRowVersion(); // 并发令牌 e.HasIndex(x x.ProductId); }); b.EntityOrder(e { e.Property(x x.TotalAmount).HasPrecision(12, 2); e.HasIndex(x x.OrderNo).IsUnique(); // 订单号唯一 }); } }IsRowVersion()这一行的作用是EF 在UPDATE时会自动把RowVersion加进WHERE条件更新影响 0 行就抛DbUpdateConcurrencyException。它解决的是“两个人同时改同一行”的问题但对“同时下单抢库存”还不够——那个要靠第 4 章的条件更新语句。迁移命令按顺序走一遍就能生成库dotnet ef migrations add InitShop -p ./src/ECommerce.Infrastructure -s ./src/ECommerce.Web dotnet ef database update -p ./src/ECommerce.Infrastructure -s ./src/ECommerce.Web-p指向包含DbContext的项目-s指向启动项目两个参数写反是新手最常踩的坑报错信息还特别绕。生成完记得打开迁移文件扫一眼确认decimal精度和唯一索引都进去了再执行database update。3. 总体架构与分层ASP.NET 项目怎么切才不糊成一团默认模板生成的 MVC 项目只有Controllers / Models / Views三个文件夹做课程作业够用做一套能交付的电商站就不够了。业务一多控制器里会同时出现 SQL 查询、库存计算、发邮件、拼 HTML改一个需求要翻五个文件。分层的目标不是好看是让“改价格规则”这件事只动一个地方。3.1 五层结构与依赖方向我一般切成五个项目Web控制器、视图、过滤器、Application用例服务、DTO、校验、Domain实体、枚举、领域规则、InfrastructureEF Core、仓储实现、外部支付网关、Tests。依赖方向只有一个Web → Application → DomainInfrastructure 实现 Domain 定义的接口由 DI 容器在启动时注入。关键约束是 Domain 不引用任何 ORM 和框架包。这样订单状态机、金额计算这些核心规则可以脱离数据库单测答辩现场演示“取消已发货订单会被拒绝”时跑一个单元测试比点十次页面有说服力。Application层用“用例服务”而不是“万能仓储”。比如OrderAppService只暴露CreateOrder、PayCallback、Ship、Cancel四个方法控制器里就是一行调用。很多项目的控制器动辄三百行本质是把用例逻辑写在了 HTTP 层换一个入口比如后台批量发货就得复制一遍。3.2 请求管道与中间件顺序ASP.NET Core 的中间件是顺序敏感的注册顺序错了会出现“静态文件 404”“登录后还是游客”“异常页不生效”这类问题排查起来像黑匣子。下面这段是电商站比较稳的顺序var app builder.Build(); // 1. 异常处理必须最外层才能兜住后面所有中间件 app.UseExceptionHandler(/Home/Error); app.UseStatusCodePagesWithReExecute(/Home/StatusCode/{0}); // 2. HTTPS 跳转放在静态文件之前避免静态资源被反复重定向 app.UseHttpsRedirection(); // 3. 静态文件放在路由之前命中就直接返回不进 MVC app.UseStaticFiles(); // 4. 路由 → 认证 → 授权三者顺序不能换 app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); // 5. 自定义中间件记录请求耗时与订单号方便后续排查 app.Use(async (ctx, next) { var sw Stopwatch.StartNew(); await next(); if (sw.ElapsedMilliseconds 500) app.Logger.LogWarning(慢请求 {Path} {Ms}ms, ctx.Request.Path, sw.ElapsedMilliseconds); }); app.MapControllerRoute(areas, {area:exists}/{controllerHome}/{actionIndex}/{id?}); app.MapControllerRoute(default, {controllerHome}/{actionIndex}/{id?}); app.Run();几个参数的含义{area:exists}里的exists是路由约束只有存在的 Area 才会匹配避免和前台控制器同名冲突UseStatusCodePagesWithReExecute让 404 走统一页面而不是白屏那个自定义中间件只打印超过 500ms 的请求生产环境日志量可控。一个常见误用是把UseStaticFiles()写在UseAuthorization()后面结果后台管理页面的 CSS 也要过一遍权限校验未登录时样式全丢。另一个是把UseAuthentication()和UseAuthorization()顺序写反表现是所有[Authorize]标记的页面全部 401但登录明明是成功的。3.3 依赖注入与配置管理连接字符串、支付密钥、图片存储路径这些东西一律进appsettings.json代码里只读IOptionsT。硬编码连接字符串的项目部署到另一台机器上必定翻车。{ ConnectionStrings: { ShopDb: Server.;DatabaseShopDb;Trusted_ConnectionTrue;TrustServerCertificateTrue }, Payment: { Gateway: Sandbox, NotifyUrl: https://example.com/pay/notify, TimeoutSeconds: 10 }, Paging: { DefaultSize: 20, MaxSize: 60 } }// 按生命周期注册DbContext 用 Scoped配置用 Options 模式 builder.Services.AddDbContextShopDbContext(o o.UseSqlServer(builder.Configuration.GetConnectionString(ShopDb))); builder.Services.ConfigurePaymentOptions(builder.Configuration.GetSection(Payment)); builder.Services.ConfigurePagingOptions(builder.Configuration.GetSection(Paging)); // 用例服务与仓储接口在 Domain实现在 Infrastructure builder.Services.AddScopedIOrderAppService, OrderAppService(); builder.Services.AddScopedIProductRepository, ProductRepository(); builder.Services.AddScopedICartService, CookieCartService();AddScoped意味着每个 HTTP 请求一个实例DbContext必须用它写成AddSingleton会出现“上下文已被释放”的报错。Paging.MaxSize这个上限很必要——前端传pageSize100000时不加限制一条查询就能把内存打满这是我在压测时踩过的坑。4. 核心模块实现购物车、下单、支付回调、商品列表前面三章是地基这一章是真正会被老师或甲方点开看的部分。四个模块按交易顺序排列每个都给出可直接运行的实现和参数说明改改命名空间就能进项目。4.1 购物车Cookie 与数据库双态未登录用户用 Cookie 存购物车登录后合并进数据库这是电商站的标准做法。只做数据库购物车的话游客加购会直接失败只做 Cookie 的话换设备就丢。双态方案的关键是“合并策略”同一个 SKU 数量相加超出库存则截断到库存上限。public interface ICartService { TaskListCartLine GetLinesAsync(); Task AddAsync(int skuId, int qty); Task MergeToUserAsync(int userId); // 登录成功后调用 } public class CookieCartService : ICartService { private const string CookieName shop_cart; private readonly IHttpContextAccessor _http; private readonly ShopDbContext _db; // Cookie 里存 skuId:qty|skuId:qty 这种紧凑格式省字节也省解析 public async Task AddAsync(int skuId, int qty) { var map ReadCookie(); map[skuId] map.GetValueOrDefault(skuId) qty; WriteCookie(map); await Task.CompletedTask; } public async Task MergeToUserAsync(int userId) { var map ReadCookie(); foreach (var (skuId, qty) in map) { var sku await _db.Skus.FindAsync(skuId); if (sku is null) continue; // 商品已删除则丢弃 var finalQty Math.Min(qty, sku.Stock); // 数量不能超过库存 var item await _db.CartItems .FirstOrDefaultAsync(x x.UserId userId x.SkuId skuId); if (item is null) _db.CartItems.Add(new CartItem { UserId userId, SkuId skuId, Qty finalQty }); else item.Qty Math.Min(item.Qty finalQty, sku.Stock); } await _db.SaveChangesAsync(); _http.HttpContext!.Response.Cookies.Delete(CookieName); // 合并完清掉 } }Math.Min(qty, sku.Stock)这一句是防呆游客在 Cookie 里攒了 10 件登录时库存只剩 3 件合并后应该是 3 而不是 10。Cookie 要设HttpOnly true和SameSite Lax前者防脚本读取后者保证从外部链接跳回来时 Cookie 还在。购物车还有一个容易忽略的点加购时要不要校验商品状态。我的做法是加购只校验 SKU 是否存在下单时才校验Product.Status 1。原因是运营临时下架商品时用户购物车里的东西不应该凭空消失而是在结算页给出“该商品已下架”的提示——这是体验问题也是答辩时容易被追问的细节。4.2 下单事务与库存扣减下单是整个系统里唯一必须“要么全成要么全败”的操作。流程是校验库存 → 写订单主表 → 写订单明细 → 扣库存 → 生成待支付流水。五步在一个事务里任何一步抛异常全部回滚。库存扣减不能写成“先查库存再更新”两个请求同时读到库存 1都判断够都更新结果卖了两件。正确写法是带条件的原子更新用受影响行数判断是否成功public async TaskOrderResult CreateOrderAsync(int userId, ListCartLine lines) { await using var tx await _db.Database.BeginTransactionAsync(); var order new Order { OrderNo GenerateOrderNo(), // 形如 20240512000001 随机位 UserId userId, Status 0, CreatedAt DateTime.UtcNow }; decimal total 0; foreach (var line in lines) { var sku await _db.Skus.AsNoTracking() .FirstOrDefaultAsync(x x.Id line.SkuId); if (sku is null || sku.Stock line.Qty) return OrderResult.Fail($商品 {line.SkuId} 库存不足); // 关键条件更新库存不足时影响行数为 0不依赖先查后改 var affected await _db.Database.ExecuteSqlInterpolatedAsync($ UPDATE ProductSku SET Stock Stock - {line.Qty} WHERE Id {line.SkuId} AND Stock {line.Qty}); if (affected 0) return OrderResult.Fail($商品 {line.SkuId} 扣减失败请重试); total sku.Price * line.Qty; order.Items.Add(new OrderItem { SkuId line.SkuId, Title sku.SpecText, // 快照商品改名不影响历史订单 UnitPrice sku.Price, Qty line.Qty }); } order.TotalAmount total; _db.Orders.Add(order); await _db.SaveChangesAsync(); await tx.CommitAsync(); return OrderResult.Ok(order.OrderNo); }ExecuteSqlInterpolatedAsync会自动把参数转成 SQL 参数不要用字符串拼接否则就是注入漏洞。affected 0是并发失败信号直接返回“请重试”比抛异常友好。事务用await using保证异常时释放。订单号生成有个小技巧yyyyMMddHHmmss加 6 位随机数长度 20既按时间有序又基本不会撞。用自增 ID 当订单号的话别人下单两次就能推算出你一天卖了多少单。注意BeginTransactionAsync之后如果提前return事务会随await using自动回滚但如果你在 using 块外捕获了异常又继续执行事务可能已经处于不可用状态。养成“事务块内不做 try-catch 吞异常”的习惯。4.3 支付回调的幂等处理支付平台的回调是“至少一次”语义同一笔支付可能通知你三次。没有幂等处理的系统会重复加库存、重复改状态甚至把已发货订单改回已支付。幂等的实现分两层数据库唯一索引兜底业务代码做状态机守卫。public async Taskbool HandleNotifyAsync(PayNotifyDto dto) { // 第一层验签防止伪造回调具体算法按支付方文档 if (!_signer.Verify(dto)) return false; // 第二层OutTradeNo 唯一索引重复插入会抛异常捕获后视为已处理 var exists await _db.Payments.AnyAsync(x x.OutTradeNo dto.OutTradeNo); if (exists) return true; var order await _db.Orders.FirstOrDefaultAsync(x x.OrderNo dto.OrderNo); if (order is null) return false; // 第三层状态机守卫只有待支付的订单才能变成已支付 if (order.Status ! 0) return true; order.Status 1; _db.Payments.Add(new Payment { OrderNo dto.OrderNo, OutTradeNo dto.OutTradeNo, Amount dto.Amount, PaidAt DateTime.UtcNow }); await _db.SaveChangesAsync(); return true; }三层防护各管一件事验签管“是不是真的支付方发的”唯一索引管“同一条通知只落一次库”状态机管“已发货的订单不能被改回去”。少任何一层压测或联调时都会出问题。回调接口还要注意两点一是必须返回支付方要求的格式通常是纯文本success返回 JSON 会被判定为失败并持续重试二是回调接口不能要求登录态必须允许匿名访问否则支付方永远调不通。这两点在[AllowAnonymous]和返回值上各改一行就能解决但每年都有项目栽在这里。4.4 商品列表分页、筛选与查询优化列表页是访问量最大的页面也是最容易写出 N1 查询的地方。常见的错误写法是查出商品列表后在视图里循环访问item.Skus.Min(x x.Price)200 条商品就是 201 次查询页面加载三秒起。正确做法是一次性投影出列表需要的字段public async TaskPagedResultProductCardDto QueryAsync(ProductQuery q) { var size Math.Clamp(q.PageSize, 1, _paging.MaxSize); // 上限保护 var query _db.Products.AsNoTracking() .Where(x x.Status 1); // 只查上架商品 if (q.CategoryId 0) query query.Where(x x.CategoryId q.CategoryId); if (!string.IsNullOrWhiteSpace(q.Keyword)) query query.Where(x x.Title.Contains(q.Keyword)); var total await query.CountAsync(); var items await query .OrderByDescending(x x.CreatedAt) .Skip((q.PageIndex - 1) * size) .Take(size) .Select(x new ProductCardDto { Id x.Id, Title x.Title, Image x.MainImage, // 子查询取最低价SQL 层完成不产生 N1 MinPrice x.Skus.Min(s (decimal?)s.Price) ?? 0, TotalStock x.Skus.Sum(s (int?)s.Stock) ?? 0 }) .ToListAsync(); return new PagedResultProductCardDto(items, total, q.PageIndex, size); }AsNoTracking()关掉变更跟踪纯读场景能省 20% 到 30% 的时间。Select投影只取列表需要的四五个字段比查完整实体再映射轻得多。Math.Clamp挡住pageSize99999这种恶意参数。Contains在 SQL Server 上会翻译成LIKE %关键词%走不了索引商品量上万后明显变慢。课程设计阶段够用如果要做全文检索通常的做法是加一张关键词表或者接搜索引擎但那属于加分项不必在毕设阶段硬上。5. 避坑与排查五个最容易翻车的点这一章是我带项目时反复遇到的清单每一条都对应过真实的报错或答辩现场被问住的瞬间。现象、原因、解决按顺序写遇到问题可以直接对号入座。5.1 库存卖成负数现象并发下单后ProductSku.Stock出现 -1或者卖出数量大于初始库存。原因代码写成“先SELECT查库存判断够不够再UPDATE减库存”。两个请求在同一时刻读到库存 1都判断通过都执行减法。解决改成带条件的原子更新UPDATE ... SET Stock Stock - qty WHERE Id id AND Stock qty用ExecuteSqlInterpolatedAsync的返回值判断影响行数为 0 就说明抢失败了。如果业务允许也可以给Stock加CHECK (Stock 0)约束做最后一道保险让数据库直接拒绝脏数据。5.2 列表页慢得像卡住现象商品列表页打开要三到五秒日志里同一请求打了上百条 SQL。原因EF Core 的导航属性在循环里被逐个访问或者开启了延迟加载每次item.Skus都触发一次查询。解决列表查询一律AsNoTracking()加Select投影需要的字段在 SQL 层一次算完。排查时打开 EF 的 SQL 日志看一次请求发了几条语句超过五条就该改。开发环境可以在OnConfiguring里加LogTo(Console.WriteLine)一眼就能看出问题。5.3 结算金额差一分钱现象购物车三件商品单价相加和订单总额对不上差 0.01。原因金额字段用了float或double二进制浮点无法精确表示 0.1 这类十进制小数累加后产生误差。解决数据库用DECIMAL(12,2)C# 用decimalEF Core 里显式HasPrecision。运费、优惠券、积分抵扣一律用decimal参与运算最后统一Math.Round(total, 2, MidpointRounding.AwayFromZero)。已经上线改不动的表至少要在应用层做一次四舍五入对齐。5.4 用户登录后购物车空了现象本地调试一切正常部署到服务器后用户加购完过一会儿购物车就清空或者登录后加购直接丢失。原因购物车和登录态存在进程内 Session 里应用池回收或换一台服务器Session 就没了。解决购物车按第 4 章的双态方案游客存 Cookie、登录后入库。登录态如果必须用 Session就改成分布式缓存并把数据序列化出去。部署到多台机器时还要在负载均衡上开粘性会话否则用户第二次请求就落到另一台机器上表现就是“刚登录又变游客”。5.5 支付回调把订单状态改乱了现象同一笔订单被加了两次库存或者已经发货的订单状态突然退回“已支付”。原因回调接口没有任何幂等判断支付方重试一次就执行一次业务或者状态流转没有守卫任何状态都能被改成任意状态。解决OutTradeNo建唯一索引插入重复直接捕获异常当作已处理业务代码里判断order.Status ! 0就跳过。订单状态机建议单独写一个CanTransit(from, to)方法所有改状态的地方都走它取消订单、发货、退款共用一套规则测试也好写。6. 验收与答辩怎么证明这套站真的跑通了验收不是把首页点一遍就完事。我带过的项目里最容易在答辩现场出问题的往往是“演示到一半第二个人同时下单库存错了”这种需要并发才能复现的场景。所以验收清单要按“单点功能、并发行为、异常路径”三类分开准备。先按功能清单逐项过每项都要能指出对应的表和数据变化验收项操作步骤通过标准商品上下架后台改状态为下架前台列表消失直接访问详情页提示已下架购物车合并游客加 2 件登录库存 1 件购物车显示 1 件Cookie 被清除下单扣库存下单 1 件ProductSku.Stock减 1Order与OrderItem各新增记录库存不足库存 1 件时下 2 件返回“库存不足”订单表无新记录支付回调重复同一OutTradeNo调两次第二次返回成功Payment表仍只有一条状态机守卫对已发货订单再调支付回调订单状态保持“已发货”不变并发行为单独验一次。写个简单脚本用 20 个并发请求抢同一件库存 1 的商品跑完检查Stock是否为 0、订单数是否为 1# 用 ab 做并发下单-c 并发数-n 总请求数-p 携带下单参数 ab -n 20 -c 20 -p order.json -T application/json \ -H Cookie: shop_authYOUR_TOKEN \ http://localhost:5000/api/order/create跑完到数据库里SELECT Stock FROM ProductSku WHERE Id 1再SELECT COUNT(*) FROM [Order]。如果库存是 0 且订单只有一条说明条件更新那一步真的生效了如果库存变负或者订单出现多条回到 5.1 重看。这个脚本我一般会留在仓库的scripts/目录里答辩被问到并发就直接跑一遍比口头解释有说服力。异常路径也要走一遍支付回调返回错误格式、下单时商品刚好被下架、后台删除一个有订单关联的商品。这些路径不要求页面好看但必须不抛 500 白屏。我通常的做法是给每个控制器加一个全局异常过滤器把未处理异常记日志并跳转到统一错误页答辩时演示一个“故意造出来的异常”反而加分。答辩讲的时候别从技术栈开始讲。先讲一条完整交易链路用户怎么找到商品、怎么加购、下单时系统做了什么、支付回调怎么保证不重复。讲完这条线再打开代码指到第 4 章那三段核心代码说明为什么库存要条件更新、为什么回调要三层防护。老师追问细节时把避坑清单里的现象和原因说出来比背概念管用。从那以后我每次交付这类项目都会强制自己先跑一遍“并发抢库存 重复回调”这两个用例跑通了再谈界面美化。页面做得再漂亮账对不上就是不及格。这套顺序和清单你可以直接拿去改希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站