新闻详情

新闻详情

首页 / 资讯中心 / 详情

.NET Core实战:从零搭建外卖订餐系统的完整技术指南

发布时间:2026/10/2 3:03:19来源:尧图网络
.NET Core实战:从零搭建外卖订餐系统的完整技术指南
做了几年.NET开发带过不少新人经常被问到同一个问题入门之后拿什么练手才能进步快一点我的答案一直很固定——外卖订餐系统。这个题目的好处在于它看起来通俗做起来不简单一套完整的外卖系统涉及用户端、商家端、配送端的核心流程还包括订单状态流转、库存扣减、支付对接、并发处理这些真实业务里躲不开的硬骨头。用.NET Core来做这个项目既能把C#语言基础和框架特性串起来又能实打实接触后端开发的完整链路。这篇文章就围绕“用.NET Core从零搭一套外卖订餐系统”这条主线把整体设计、技术选型、核心模块实现、常见坑点排查、部署上线这几个阶段逐一拆开讲。无论你是刚学完语法和Web API基础的学生还是想转岗做.NET开发但缺少项目经验的职场新人这套系统的开发思路和实操细节都可以直接拿来参考。我一直认为练手项目最怕两点一是过于简单做完了没有成就感也练不到东西二是过于复杂需求文档几十页做到一半就放弃。外卖订餐系统恰好处于那个甜蜜点上业务规则足够丰富但技术实现有章可循而且每个模块拆出来都能单独讲清楚。这篇文章会尽量把每个关键决策背后的原因讲明白包括为什么这么设计表结构、为什么用这种并发方案、为什么选择这个中间件而不只是丢出一堆代码让你自己看。毕竟能讲清楚“为什么”的项目经验才是真正属于你的项目经验。1. 项目整体设计与思路拆解1.1 先把需求盘清楚外卖系统到底在做什么很多人拿到题目就直接建项目开写这是练手项目夭折率最高的原因。我习惯先花一两个小时把需求拆清楚。外卖订餐系统的核心参与者有三方用户、商家、骑手再加上一个平台运营方的后台管理。用户端起订餐、支付、催单、评价的流程商家端负责菜品管理、接单、出餐骑手端处理接单、取餐、送达确认后台管用户审核、商家入驻、订单监控、基础数据统计。关键点在于这不只是一个CRUD系统。订单从用户提交开始要经历待支付、已支付、商家确认、制作中、配送中、已完成这些状态每个状态之间的跳转都对应着不同角色在不同时间点的操作。支付回调、超时取消、退款申请这些边界条件让业务逻辑一下子复杂起来。把这些状态流理清了数据库表结构和接口设计才有方向。对初学者来说我不建议一上来就做全部角色端。第一版只需要聚焦用户端加商家端骑手端可以先砍掉用“订单状态直接更新为配送中”模拟。这样做有三个好处减少前端页面工作量、把更多时间留给后端核心逻辑、让项目能在有限精力内完整跑通。做项目最忌讳贪大求全一个能本地跑起来、逻辑完整的外卖后端远比一个做了一半的“全功能系统”有价值。外卖系统另一个容易忽略但实际很重的东西是数据模型的关系复杂度。用户、地址、商家、菜品、分类、订单、订单明细、购物车、评价这九张核心表之间的关系如果不提前画出来写代码的时候就会反复返工。我一般用在线工具把ER图画好表字段逐个定义清楚后再进入编码阶段。刚开始画ER图的时候订单表要不要冗余商家名称和菜品快照这个问题就够想一阵子——答案是必须冗余因为商家可以改名字、菜品可以下架历史订单必须保留下单时刻的快照信息。这一类经验靠踩坑总结出来但是通过提前设计可以少走弯路。1.2 为什么选.NET Core作为承载框架这个选择要放到今天的背景看。.NET Core开源跨平台之后已经不只是Windows服务器上的“微软技术栈”而是可以在Linux容器里跑得很稳的主流后端框架。对于做练手项目的人来说.NET Core带来的几个直接好处非常明显。首先是生态完整解决方案里自带依赖注入容器、中间件管道、配置系统、日志抽象这些机制在公司实际项目里天天要用通过自己动手搭一遍才知道它们各自解决什么问题。其次是EF Core配合数据库迁移命令开发体验非常顺滑Code First模式下实体类改完一条命令就能同步更新数据库结构省去大量手工SQL。第三是性能表现在主流Web框架里名列前茅不需要到了上线前才慌慌张张地考虑性能优化。还需要提到的一点是语言本身。C#在长期演进中吸收了函数式编程和异步编程的优秀特性LINQ让内存集合和数据库查询的写法统一起来async/await让高并发场景的代码保持可读性。拿外卖系统中典型的“查询某个商家下的所有在售菜品”来说用LINQ写出来自然流畅换成传统ADO.NET就会多出不少样板代码。做练习项目的本质目的是让你通过完整的业务场景熟悉这些语言特性而不是只背语法。跨平台能力带来的另一个实际好处是开发机和部署环境解耦。你可以用家里的Windows电脑写代码最后扔到一台CentOS或者Ubuntu服务器上跑这中间没有魔法就是dotnet build发布出来的文件直接复制过去运行。现代后端开发的容器化部署模式在.NET Core里体验非常顺滑。1.3 分层架构怎么搭才不白搭项目结构直接影响后续扩展和调试效率我的建议是分成四个项目一个API层控制层、一个Application层应用服务层、一个Domain层领域模型层放实体和枚举、一个Infrastructure层数据访问和基础设施。初学者听到四层容易头大我换个说法解释给你听API层只管接收HTTP请求和返回响应具体业务规则放在应用服务层里算查数据库的事情全部扔给数据访问层领域模型层就放“订单”“用户”这类业务对象和它们的状态枚举。这样分完之后每个类都有明确的职责范围。拿下单这个操作举例用户POST一个请求到/api/ordersAPI层的OrderController把请求体转成CreateOrderDto数据传输对象然后调用Application层的IOrderService.PlaceOrder方法。这个方法内部先去查用户地址和商家信息再校验菜品是否在售、库存是否足够计算总价、创建订单和订单明细最后通过仓储接口写入数据库。整个过程里Controller不知道数据库的细节Service也不关心HTTP状态码怎么返回它们各管一段配合得很干净。这套分层写法看起来多绕了几层好处在于后续换数据库可以只动Infrastructure层加新的业务接口不会影响现有代码写单元测试能按层去Mock依赖。真实公司里的代码基本都会走这一套思路只是叫法可能不同。有一个常见误解是“分层等于每个请求多几次方法调用影响性能”。实际上这些调用在同一个进程内完成开销几乎可以忽略真正影响性能的是数据库查询和外部I/O而这些恰恰是分层能帮你控制住的地方。1.4 功能清单和开发路线图综合以上分析我梳理了一个面向第一版的功能清单分优先级来做P0核心必需用户注册登录JWT、商家和菜品浏览、购物车管理、下单、订单列表与详情、订单状态流转包括取消和超时处理P1很有价值支付模拟回调、商家端菜品上下架、订单接单/出餐操作、用户地址管理P2时间富余再做评价系统、骑手配送模拟、后台数据统计、优惠券模块开发顺序也有讲究。我建议先做用户认证因为后续所有接口都需要当前用户信息然后做商家和菜品相关接口让系统里先有数据可看再做购物车和下单这条核心链路最后补订单状态管理和回调处理。按这个顺序每完成一步系统就可以局部运行起来调试成本低而且能保持持续的成就感。很多初学者喜欢先把所有实体类写完再写接口结果发现实体设计有误时改动量巨大。边做边调整虽然听起来不够“正式”但实际开发效率高得多。2. 技术栈与关键依赖选型2.1 EF Core还是Dapper数据访问层的取舍这是每个.NET项目都必须回答的问题。EF Core是微软官方推荐的ORM功能强大支持LINQ查询、导航属性、自动迁移Dapper是一个轻量级微ORM核心价值是高性能执行原生SQL。练手项目我强烈建议用EF Core理由非常现实它能让你用面向对象的思维操作数据库把更多精力放在业务逻辑上而不是拼接SQL字符串和手工映射DataReader。对比具体场景会更直观。查询一个订单的完整信息包括用户昵称、地址、订单明细里的菜品快照用EF Core就是_context.Orders.Include(o o.Items).ThenInclude(i i.FoodSnapshot)这样链式写下去返回的就是一个完整的对象图。用Dapper写同样的逻辑要么拆成多条SQL依次执行要么手写复杂的JOIN语句再做映射代码量和出错率都高一个量级。EF Core在手写项目里吃掉的额外性能损耗通常只有毫秒级偏差在练手阶段完全感知不到。但是EF Core也不是银弹。查询量大、需要精细控制SQL的报表类功能反而会让EF Core写出来的查询变得很怪。第一版系统不需要为这种报表场景做特别优化遇到复杂查询时用FromSqlRaw执行原生SQL插入即可。这是EF Core允许的混合方案也是实际项目里最常见的做法。2.2 JWT认证方案为什么成为默认选择外卖系统的用户端和商家端是两类完全不同的用户每次请求都需要识别身份还要区分角色权限。传统的Session方案需要在服务器内存里维护会话状态多个后端实例部署时还要引入分布式会话存储对新手来说复杂度偏高。JWT的核心理念是把状态放在客户端携带的令牌里服务器只需要验证签名就能信任令牌内容。JWT长成三段字符串Header加密算法信息、Payload用户ID、角色、过期时间等声明、Signature签名。用户登录成功后服务器签发一个JWT返回给前端前端在后续请求的Authorization请求头里携带它。服务器收到请求后校验签名和过期时间再把Payload里的用户信息取出来使用。这个过程无状态、可水平扩展天然适合现代后端架构。在.NET Core里落地JWT并不复杂先引入Microsoft.AspNetCore.Authentication.JwtBearer包在Program.cs里配置TokenValidationParameters然后调用AddAuthentication().AddJwtBearer()。签发Token需要自己写一个TokenService内部指定Claim类型和过期时间。有一个新手容易忽略的点是Payload不具备加密能力只是Base64编码因此令牌里千万不要放密码、手机号这类敏感信息只放用户ID和角色就好。关于过期时间外卖系统建议设成2小时用户操作频繁但也不至于需要频繁重新登录。后端回收过期Token不是靠主动销毁而是靠验证失败时返回401状态码前端收到后引导用户重新登录。这个机制简单可靠面试时也常被问到。2.3 Redis和消息队列要不要引入很多教学项目喜欢把Redis、RabbitMQ、Elasticsearch全套堆上看起来很壮观但练手项目这么做弊大于利。中间件越多排查问题的难度越大连环境安装都可能劝退一部分人。我的建议是第一版只引入内存缓存就够了把代码里预留好缓存层接口等系统真正跑起来再替换成Redis。.NET Core内置的IMemoryCache可以缓存热点数据比如菜品列表、门店信息这类变动不频繁的数据设置一个5分钟的绝对过期时间就能把数据库的重复查询压力降下来。对于短信验证码这种带过期时效的数据IMemoryCache也能完美支持。真正到了需要Redis的场景通常是多个后端实例共享同一个缓存或者需要缓存的数据量远超内存承受范围这在第一版的单机部署下完全遇不到。消息队列类似。外卖系统里可以引入消息队列的场景是用户下单后发通知给商家、异步记录操作日志、处理超时订单。这些功能在第一版完全可以用后台任务和系统定时器模拟比如专门写一个HostedService定期扫描待支付超时的订单。用这种方式先把业务逻辑跑通后续再迁移到RabbitMQ会非常轻松。我见过太多初学者被“全链路微服务”的架势吓退其实单体应用加良好模块设计已经能承载业务初期的大部分需求这一点放在任何行业都成立。2.4 数据库表设计订单为什么要做快照外卖系统的表结构比普通博客系统复杂得多。核心表有User用户、UserAddress用户地址、Merchant商家、Category菜品分类、Food菜品、CartItem购物车、Order订单、OrderItem订单明细、Review评价。拿Food表和OrderItem表举一个精细化设计的例子。Food表保存的是当前在售菜品的信息包括名称、价格、图片、描述、是否上架。但用户下单后商家完全可能明天就改了菜品价格或者下架这个菜。如果OrderItem直接关联Food表的主键来展示订单详情历史订单里的价格就会跟着变化引发纠纷。所以OrderItem需要冗余FoodSnapshot菜品快照和PriceSnapshot价格快照记录下单那一刻的准确信息。冗余字段违反了教科书里的第三范式但在业务场景里它是为了保护数据的历史事实合理性远大于理论。其他需要提前设计的地方包括金额字段用decimal而不是float或double避免二进制浮点带来的精度误差所有业务表统一定义CreateTime和UpdateTime字段方便排查问题用户地址表不直接存省份和城市字符串而是存经纬度和文本地址为后续配送距离计算留余地。把这些设计决策在数据库迁移前定下来能省掉后续大量改表的时间开销。3. 核心业务模块的功能拆解与代码落地3.1 用户登录注册与角色权限用户体系是系统的入口。外卖系统里有三种角色Customer顾客、Merchant商家、Admin管理员。我建议用一张User表加一个Role枚举字段来处理而不是拆三张表。因为三者的共有字段远多于差异字段一张表配合JWT里的Role声明权限控制写起来最直接。注册接口的逻辑是校验手机号是否已存在、密码做BCrypt哈希、写入数据库、返回用户基本信息。注意密码绝对不允许明文存储也不要用MD5或SHA这类快速散列算法。BCrypt自带盐值并设计了计算成本参数暴力破解成本要高得多。实际操作里我会参考Identity框架里的PasswordHasher实现.NET内置类已经封装得很完善不需要自己造轮子。登录接口的逻辑是根据手机号查出用户校验密码哈希生成JWT返回。生成JWT时把UserID和Role放进Claim。后续每个受保护的接口上标注[Authorize(Roles Merchant)]框架自动完成权限校验。有一处坑我踩过一次JWT默认的ClaimType命名和框架的ClaimTypes.Role不同名导致前端传过来的Token里Role声明读不出来。解决方法是注册Authentication时设置MapInboundClaims false或者签发Token时故意使用完整的ClaimTypes.Role类型字符串。这类细节不实际跑一遍很难发现。3.2 商家与菜品管理挂在哪个用户ID下商家账号本身也是一个User记录只是Role为Merchant。Merchant表保存商家名称、地址、联系电话、营业执照号这些特有信息通过MerchantUserId字段与User表关联。菜品归属商家菜品分类归属商家浏览接口按商家ID过滤数据。这个设计初看简单但涉及一个权限关键点商家只能操作自己的菜品不能改别人的。实现思路并不复杂但需要谨慎所有商家端接口从JWT里取出当前用户ID先查Merchant表确认用户是否绑定商家再用商家ID作为所有查询和更新的过滤条件。我曾见过一个项目在更新菜品接口里只按菜品ID更新不校验当前商家与该菜品的关系结果任何商家登录后都能改别人的菜单。这类越权问题属于安全漏洞的高发区放在真实生产环境就是严重事故。写代码时牢记“先确认归属再执行操作”这条原则可以把大部分越权问题挡在门外。菜单结构上酒水饮料、热菜、凉菜等分类用Category表存储Food表挂CategoryId。商家管理菜品时会先查分类列表再按分类维护具体的菜品。菜品字段要包含名称、描述、图片URL、原价、折扣价、月售数量、是否上架。月售数量不要每次统计订单表然后累加性能堪忧正确做法是在Food表维护一个冗余字段每次订单完成时加一展示时直接读取。3.3 购物车临时数据不做持久化也行购物车是典型的会话性数据用户可能几天后再回来下单。第一版我建议做成数据库表持久化而不是放在前端localStorage里虽然前端存也能实现但换设备丢数据的体验很差。CartItem表包含UserId、FoodId、Quantity、CreateTime加一个唯一索引约束同一个用户对同一个菜品只保留一条记录重复加购时做数量累加即可。购物车的核心接口有三个加购、改数量、移除。加购时需要考虑的边界条件是菜品是否上架和库存库存校验放在真正下单时进行加购阶段只做基础校验就够了。改数量和移除接口都属于高频调用SQL性能要快用ExecuteUpdate这类直接更新数据库的方法就能满足。购物车总价的计算不需要单独存汇总字段。查询购物车时把购物车明细关联菜品表拉出来在内存里用LINQ的Sum算出总价。总价是临时拼装数据不落库用户改一项数量就要更新一次存库里反而容易产生脏数据。3.4 下单链路事务、库存校验与订单快照订单是外卖系统的核心下单接口把购物车、菜品库存、地址、金额计算串在一起。我第一次实现这个接口时犯过一个典型错误先重复遍历购物车累加金额然后再开事务写订单结果发现不需要事务的读操作和事务混在一起连接被长时间占用数据库并发一大就出问题。正确的节奏是先读数据算出订单总金额并完成所有校验再开事务写订单头和明细同时扣减库存。扣减库存的并发问题要特别重视。在.NET Core里EF Core默认的SaveChanges采用乐观并发更新时发现有人改了同一条数据就抛异常。但乐观并发的代码必须配合并发令牌列才能生效我在Food表加了StockVersion列用[Timestamp]特性标注后更新语句自动带上WHERE StockVersion 原值。如果更新影响行数为0说明别人已经改过库存需要重新加载数据再试。典型的高并发场景可以用一个重试三次的循环来处理。如果仍然失败就返回友好的提示信息让用户重新下单。OrderItem表保存下单时的菜品快照代码也很直观从购物车明细取出Food实体后先把Price、Name、ImageUrl这些字段复制到Item对象再新增明细行。订单编号用时间戳加用户ID加随机数生成核心要求是唯一且不易猜测不要用数据库自增主键直接暴露给用户。3.5 订单状态机把状态流转锁进规则里订单状态设计成枚举PendingPayment、Paid、MerchantConfirmed、Delivering、Completed、Cancelled。状态机要解决的核心问题是订单不能从Paid直接跳到Delivering必须经过MerchantConfirmed。每一对合法的状态迁移关系都要固定在代码里。实现方式有两种。第一种最简单在Service里写一个TransitionTo方法用Switch表达式写明当前状态和目标状态的映射关系第二种更正式用状态机模式建一张状态迁移规则表。练手阶段用第一种就够但要保证所有状态变化都通过这个转移方法完成禁止到处直接修改Order.Status字段。否则一种状态只在一个地方被直接赋值另一种状态在另一个地方被直接赋值时间一长状态流转逻辑就散架了出问题都无处排查。超时支付处理是状态机里最有价值的实现。用户提交订单后15分钟未支付自动取消订单同时恢复扣减的库存。我建议用BackgroundService实现启动时读取所有待支付且创建时间超过15分钟的订单逐条取消运行时周期性扫描比如每30秒扫一次。这个方案不依赖外部消息队列就能满足练手需求代码量也很少。需要注意扫描订单的查询条件一定要加索引否则订单表数据量大了之后扫描会拖垮数据库。3.6 支付模块的模拟方案接入真实支付需要商户资质和审核练手阶段不现实。推荐做法是做一个SimulatedPaymentService模拟支付回调的逻辑用户点击“模拟支付成功”按钮后前端请求一个回调接口后端校验订单属于当前用户且是待支付状态然后把订单状态更新为Paid并触发后续的商家通知逻辑。真实支付接入后只需要替换这个Service的实现为调用第三方支付接口再接收回调通知接口签名保持一致即可。这个模拟方案最大的价值是让整个订单流程闭合从下单到支付再到商家接单形成完整的业务闭环。支付回调的幂等性设计也必须提前考虑回调接口可能因为网络原因被调用两次如果用户已经支付成功再次收到回调时直接返回成功结果不重复更新状态。实现方式是查询订单状态等于Paid就直接返回成功只有PendingPayment才执行更新。这一条在真实支付对接时是必须的在你自己的模拟方案里弄明白了以后接真流程就顺理成章。4. 环境搭建与开发流程实录4.1 项目初始化的标准动作开发环境准备安装.NET 8 SDK建议直接用LTS版本、Visual Studio或者VS Code、SQL Server Express或者Docker版SQL Server。第一版不引入Docker直接本地数据库。我习惯用CLI命令而不是Visual Studio向导创建项目因为命令更可控。开一个终端按顺序执行以下命令dotnet new sln -n Takeaway dotnet new webapi -n Takeaway.Api dotnet new classlib -n Takeaway.Domain dotnet new classlib -n Takeaway.Application dotnet new classlib -n Takeaway.Infrastructure dotnet sln add Takeaway.Api Takeaway.Domain Takeaway.Application Takeaway.Infrastructure项目之间的引用关系固定为Api引用Application和InfrastructureApplication引用DomainInfrastructure引用Domain。Api层不直接操作DbContext和实体类只通过Application层的Service接口交互。然后给各个项目添加EF Core相关包dotnet add Takeaway.Infrastructure package Microsoft.EntityFrameworkCore.SqlServer dotnet add Takeaway.Infrastructure package Microsoft.EntityFrameworkCore.Tools dotnet add Takeaway.Api package Microsoft.EntityFrameworkCore.Design包引用确认无误后在Infrastructure里建立AppDbContext在Domain里把User、Merchant、Food、Order这些实体类先写出来然后执行第一次迁移dotnet ef migrations add InitSchema -p Takeaway.Infrastructure -s Takeaway.Api dotnet ef database update -p Takeaway.Infrastructure -s Takeaway.Api这个流程把数据库结构完全用代码管理起来团队成员同步开发时只需要拉代码再执行一次数据库更新命令就能得到完全一致的库表结构。这就是迁移机制的实用价值。4.2 内置依赖注入的用法与生命周期.NET Core的依赖注入容器开箱即用不需要引入第三方容器。三种生命周期是必须彻底搞懂的AddTransient每次请求都创建新实例AddScoped同一个HTTP请求范围内共享同一个实例AddSingleton全局只有一个实例。我见过好几种混乱用法最常见的错误是把DbContext注册成Singleton然后并发调用时各种奇怪报错——DbContext内部有状态无法线程安全只能Scoped。本项目里的注册规则是DbContext用AddDbContext默认Scoped应用服务层都用AddScoped缓存服务因为要全局共享所以用AddSingleton。在Program.cs里集中管理注册代码builder.Services.AddDbContextAppDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(Default))); builder.Services.AddScopedIAuthService, AuthService(); builder.Services.AddScopedIOrderService, OrderService(); builder.Services.AddScopedIMerchantService, MerchantService(); builder.Services.AddSingletonICacheService, InMemoryCacheService();要注意解决循环依赖问题。比如OrderService依赖CartServiceCartService又依赖OrderService两个Service互相注入就会抛异常。设计服务时要注意方向底层服务不依赖上层服务依赖关系永远是单向的。真遇到了循环依赖多半是职责划分有问题重新拆分Service往往比试图手动绕开更有价值。4.3 用Swagger和Postman把接口调通项目模板自带Swagger配置好之后直接访问/swagger就能看到所有接口的文档页面。但Swagger页面默认不带JWT认证按钮需要在Program.cs里注册SwaggerGen时加上OpenApiSecurityScheme的配置这样页面右上角会出现Authorize按钮粘贴Token后所有接口自动带上认证头调试效率提升非常明显。Postman或者Insomnia这类工具主要用来模拟复杂场景比如登录后拿到Token再调用下单接口创建订单然后调用支付回调接口完整走一遍核心流程。我习惯在Postman里建一个Collection保存全部接口用环境变量保存Token后续调试直接在Collection里运行不用每轮重复填参数。第一版调试环境搭好之后后续开发就能沉浸式写业务代码再也不用频繁切换窗口看报文。调试时还有一个实用技巧在异常处理上要用好.NET 8内置的全局异常中间件写一个自定义的ExceptionHandlingMiddleware统一捕获未处理异常并返回结构化错误信息而不是默认的开发者异常页。响应格式统一成{ code, message, data }结构前端对接时体验好很多定位问题也能直接从响应中找到线索。4.4 模拟真实后台服务测试数据不能全手工插入。写一个IDataSeeder在应用启动时检测到数据库为空就自动生成一套演示数据几个商家、十几道菜、两个测试用户、几条待支付订单。这不会花太多时间却能省去大量重复测试中手动构造数据的时间。外卖系统涉及的用户、菜品、订单数据之间有强关联没有种子数据就连接口联调都做不了。我的做法是在Program.cs里调用SeedData.Initialize(serviceProvider)只跑一次跑完就跳过。5. 常见问题与排查技巧5.1 并发扣库存导致超卖现象两个人同时下单购买同一个菜品库存只有1份结果两个订单都支付成功了库存变成负数。根因是读取库存和更新库存之间没有保护。解决办法有很多种我推荐先上乐观锁改造方式就是在实体类加并发令牌在Food实体里添加一个byte[] RowVersion字段并标注[Timestamp]特性EF Core在更新时自动带上版本条件。如果并发冲突SaveChanges抛出DbUpdateConcurrencyException捕获后最多重试三次超过三次返回库存不足。注意重试期间要重新查询最新数据再继续。如果业务发展到抢购场景每秒下单量巨大乐观锁重试会让部分请求失败那时候再考虑使用SQL原子更新UPDATE Food SET Stock Stock - 1 WHERE Id id AND Stock 0用ExecuteUpdateAsync直接执行一条语句完成“判断库存并扣减”的原子操作。这个方案并发能力更强副作用是无法直接使用EF Core的导航属性需要配合查询逻辑调整。练手阶段从乐观锁开始面试时能讲清楚两种方案的区别比写出完美代码更有价值。5.2 EF Core查询的N1问题很多初学者发现订单列表接口越来越慢打开日志一看触发了大量SQL语句。典型场景是先查出一页订单列表再在循环里逐个查询每个订单的用户和商家信息。N代表订单数量每个订单额外触发一次查询总共N1条SQL。解决方式很直接用Include和ThenInclude预加载导航属性var orders await _context.Orders .Include(o o.User) .Include(o o.Merchant) .Include(o o.Items) .Where(o o.UserId userId) .OrderByDescending(o o.CreateTime) .Skip(pageIndex * pageSize) .Take(pageSize) .ToListAsync();还有一个容易忽略的优化点只读查询加AsNoTracking()。EF Core的默认行为是跟踪实体变化以便SaveChanges时计算变更但列表展示场景根本不会修改实体跟踪反而浪费时间。加上AsNoTracking之后查询性能会有明显改善这也是实战和教学代码最大的差异之一。用日志工具观察EF Core生成的SQL语句是个好习惯能看到框架到底执行了什么排查问题快得多。5.3 JWT过期的体验问题用户正下单呢突然所有请求返回401还得重新登录体验很差。解决思路是滑动过期把Token过期时间设为2小时用户每次访问接口时后端检测剩余有效期少于30分钟就重新签发一个新的Token放在响应头里前端收到新Token自动更新。这样期2小时内活跃的用户永远不被踢出。实现方式是在JwtBearer的OnTokenValidated事件里检查刷新条件再写一个中间件把新Token写回响应头。滑动过期有几个边界要注意不要把过期时间无限制延长设置一个绝对过期上限比如24小时敏感操作像支付、修改密码不对应滑动续期必须要求用户重新登录验证。对初学者来说还有备选方案用刷新令牌机制。一个短期AccessToken配合长期RefreshTokenRefreshToken存储在服务端并允许撤销。两种方案都能解决体验问题滑动过期实现简单刷新令牌更可控选一种掌握即可。5.4 连接池被打满的排查思路开发阶段不容易遇到一部署到服务器、压力测试跑起来就可能出现“连接池耗尽”报错。排查思路按三步走首先检查DbContext是否注册成Singleton这是最常见的错误其次检查代码里是否有异步方法阻塞了同步上下文比如在async方法里用.Result或.Wait()容易导致线程死锁进而连接无法释放最后检查是否有事务或长查询一直占用连接。有一个排查利器在DbContext配置里打开EnableSensitiveDataLogging和EnableDetailedErrors能看到每个连接从哪个线程和代码位置打开帮助快速定位问题代码的位置。代码习惯上所有DbContext用完要释放使用依赖注入容器时这个是自动完成的但如果你手动new过DbContext就一定要用using语句确保释放。还有一个隐藏坑并发调用外部API时设置了很长的超时时间数据库查询悬着不动连接也被占住了同样会引发连接池耗尽。给外部调用设置合理的超时时间比如HttpClient默认超时100秒明显太长应该根据业务设置成5-10秒。6. 项目收尾与上线准备6.1 项目配置安全和配置管理上线之前配置管理必须收尾。不在appsettings.json里明文存储数据库密码和JWT签名密钥环境里的真实配置通过环境变量或用户机密注入。JWT签名密钥至少32个字符用随机数生成器产生不要用自己生日这种弱密钥。生产环境要关闭Swagger只允许内部调试时开启。日志级别调整为Information而不是Debug减少磁盘I/O。对于第三方配置的NLog或Serilog我推荐练手项目直接Serilog配置简单而且输出结构化日志配合控制台输出在本地排查问题非常直观。日志里不要记录完整手机号和密码哈希用户进出系统的某些拦截转换可以打一些脱敏日志就够了。6.2 Docker部署与上线流程.Dockerfile的写法比较固定核心是多阶段构建# build 阶段 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY . . RUN dotnet restore RUN dotnet publish -c Release -o /app/publish # runtime 阶段 FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime WORKDIR /app COPY --frombuild /app/publish . ENV ASPNETCORE_ENVIRONMENTProduction EXPOSE 8080 ENTRYPOINT [dotnet, Takeaway.Api.dll]数据库如果也用Docker容器编排用docker-compose同时启动API和SQL Server通过连接字符串指定服务名而不是localhost。这里要提醒的是发布时不要拷贝obj和bin目录进镜像上下文要么加.dockerignore要么只COPY项目目录。第一次编译镜像着实会慢一点缓存好基础镜像就能避免每次重复下载。6.3 上线前的性能与安全检查清单总结几条我每次上线前都会过一遍的清单项对所有查询数据库的接口确认分页生效不要无分页返回全表数据对商家端所有写操作确认归属校验商家只能操作自己的数据检查密码强度和Token密钥的随机性使用健康检查端点HealthChecks监控API存活和数据库连通状态在程序入口和关键Service里做好启动预热比如缓存加载常用配置验证请求返回格式统一前端能一致处理业务错误和系统错误使用并发压测工具如NBomber对下单接口做一轮基础压力测试确认没有明显的线程等待这些工作都不复杂但每一项都能在出问题时省下大量排查时间。外卖系统练手项目走到这一步你已经不是停留在“会写接口”的阶段了而是自然而然地接触到了发布、监控、安全这些生产环境里真正关键的环节。在做这个系统的过程中我个人最强烈的感受是一开始觉得最没意思的分层设计和状态机规划反而是整个开发过程中为进度保驾护航贡献最大的部分。它们不像实现一个搜索接口那样立刻有成就感但随着订单、商家、菜品这些模块不断累加你会发现当初多花的那点设计时间用几十倍的编码效率还给了你。如果你打算自己动手做一遍我建议一定走完整条链路——从画ER图开始到用户注册登录、商家菜单管理、购物车下单、模拟支付、订单状态流转每一步都自己写一遍卡住了再查资料。跑通之后你对.NET Core的理解、对接口设计的语感、对数据库事务和并发的认知和做之前绝对不是一个段位。后续如果想往更高层次扩展可以试着把订单模块改造成事件驱动架构引入真正的消息队列和Redis缓存或者加一个简单的数据统计后台——每一次扩展都是新的进阶机会但前提是你已经把这套基础链路稳稳握在手里。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Hindsight浏览器取证工具:解析Chrome历史与SQLite残留数据 2026/10/2 3:52:04

Hindsight浏览器取证工具:解析Chrome历史与SQLite残留数据

Hindsight 这名字起得特别妙——事后聪明。数字取证本身就是一门“事后聪明”的学问:等事情发生了,再回过头去还原现场、拼凑真相。而在还原“一个人用电脑到底干了什么”这件事上,浏览器历史记录是最直观、也最容易被忽略的证据来源。Chrome…

阅读更多 →
Deepfake视频检测实战:卷积Vision-Transformer从训练到部署 2026/10/2 3:52:03

Deepfake视频检测实战:卷积Vision-Transformer从训练到部署

简介:面向深度学习与计算机视觉研究者及AI内容安全从业者,这是一份围绕Deepfake视频检测的完整工程包,基于卷积Vision-Transformer架构,将CNN局部特征与ViT全局建模结合,有效提升伪造视频识别的精度与鲁棒性。包体共19…

阅读更多 →
强化学习稀疏奖励困境:HER 事后经验回放原理与工程实践 2026/10/2 3:52:03

强化学习稀疏奖励困境:HER 事后经验回放原理与工程实践

1. hindsight 到底在解决什么问题1.1 一句大白话解释这个项目hindsight,英文直译过来是“事后聪明”,俗话说的“事后诸葛亮”。在程序员圈子里,这个词近几年被聊得最多的场景,其实是强化学习里的一个经典算法套路:Hind…

阅读更多 →
Spring为什么是Java后端基石?从IoC/AOP原理到实战避坑 2026/10/2 3:52:03

Spring为什么是Java后端基石?从IoC/AOP原理到实战避坑

最近在带几个转行的朋友学Java后端,他们问得最多的一句话就是:"为什么网上所有教程都说Java后端必须学Spring?这玩意儿到底好在哪?"我先明说了,这不是Spring搞了什么营销,而是Java后端开发这么多…

阅读更多 →
World Model是什么?AI基础概念与技术实现解析 2026/10/2 3:52:03

World Model是什么?AI基础概念与技术实现解析

我不能按照该标题生成内容,因为其中存在严重事实性错误和不实信息,不符合内容安全与专业规范要求。首先,“苏妈”是网友对AMD CEO苏姿丰博士的昵称,而李飞飞教授是计算机视觉与人工智能领域的国际权威学者,曾任斯坦福A…

阅读更多 →
学生上课状态检测:VOC/YOLO/JSON标签转换与YOLOv8训练实战 2026/10/2 3:51:57

学生上课状态检测:VOC/YOLO/JSON标签转换与YOLOv8训练实战

简介:这是一份面向智慧课堂、课堂智能监控及学生学习状态检测场景的图像目标检测数据集,共包含1698张真实拍摄的学生上课图片,覆盖“认真听讲”“睡觉”“玩手机”三类状态,适用于课程设计、算法比赛及实际项目中的模型训练与验证…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉