1. 项目概述与需求拆解1.1 外卖点餐配送系统到底做了什么外卖点餐配送系统说白了就是把用户下单、商家做餐、骑手配送这条链路搬到线上并且用一套后台把所有角色串起来。这个标题里有个容易被忽略的点——员工与忘记密码。也就是说这套系统不只是给C端用户点外卖用的它还有完整的运营后台和员工账号体系。系统里的角色至少包括C端用户浏览菜品、下单、支付、查看订单状态、申请退款。商家端菜品管理、订单接单/拒单、出餐通知。骑手端抢单/接单、取餐、配送、标记送达。平台运营员工审核商家、处理纠纷、运营配置。系统管理员账号权限管理、数据看板、系统配置。在这套系统里员工端忘记密码就是一个非常典型的业务需求——不是只有用户需要找回密码后台运营人员同样需要。而且员工密码重置涉及权限安全不能马虎。我见过很多类似的毕设或实训项目往往把精力全放在C端点餐页面上结果一到员工管理、密码找回这种不起眼的功能就草草了事。实际上这种功能恰恰是面试官和评审老师最爱追问的点你怎么保证重置密码的安全性Token有效期怎么设计验证码怎么防刷这些细节才是项目的含金量所在。1.2 为什么这套系统必须上微服务分布式先说结论一个外卖系统如果只在单机SpringBoot里做代码也能跑通但一旦考虑真实场景——高并发下单、骑手抢单、多端同时在线、持续迭代上线——单体应用就会变成瓶颈。外卖系统天然适合微服务拆分原因是它的业务域足够清晰业务域典型职责拆分收益用户服务C端注册登录、地址管理独立扩展应对大促峰值商家服务店铺信息、菜品管理与用户流量隔离订单服务下单、订单状态流转核心链路重点保障配送服务骑手管理、派单、轨迹独立伸缩抢单场景并发高支付服务支付回调、对账第三方交互隔离故障员工/认证服务后台账号、权限、密码管理安全边界独立审计这不是为了炫技。分布式带来的核心价值是故障隔离和独立扩展。比如中午高峰期订单服务压力大但商家服务可能很闲微服务架构下你可以只给订单服务加副本而不是把整个系统垂直扩容一遍。当然微服务也意味着复杂度转移——服务怎么发现、配置怎么管理、请求怎么路由、链路怎么追踪、事务怎么保证这些都是单体应用压根不用操心的事。标题里写了SpringCloud就是要把这一整套分布式基础设施落地到项目里这也是这个项目真正的学习价值。1.3 适合谁读、读完能落地什么这篇文章适合三类人用SpringBootVue做过单体项目想上一个微服务分布式项目的人。你会发现从单体到微服务核心不是代码量变大而是思维模式变了——你要开始考虑服务边界、网络调用、数据一致性。正在做外卖/电商类毕设或实训项目的人。我会把业务模块拆解、表结构设计、接口契约、状态机设计都讲清楚可以直接参考。准备面试的人。外卖系统是面试中极其常见的业务场景订单状态的流转、分布式事务的处理、并发场景的锁设计都是高频考点。这篇文章里的踩坑经验就是最好的面试素材。读完这篇文章你能收获的不只是怎么搭一个SpringCloud项目而是完整的外卖业务建模思路、微服务拆分方法论、分布式场景的实操解法以及一套可以直接复用的员工密码找回安全方案。2. 整体架构设计与技术选型2.1 模块拆分六个微服务怎么划边界微服务拆分有一个核心原则高内聚、低耦合按业务能力划分而不是按代码层划分。很多人第一次做微服务容易犯的错是拆得太碎——把菜品管理和店铺管理都拆成独立服务结果一个下单接口要调用五六个服务链路长到无法排查。外卖系统的合理拆分方式是上面表格里的六边形用户服务、商家服务、订单服务、配送服务、支付服务、员工/认证服务。每个服务独立数据库服务之间只通过API通信禁止直接操作对方的表。这里有一个很重要的设计决策为什么用户服务和员工服务要分开因为C端用户和后台员工是两套完全不同的账号体系。C端用户用手机号验证码登录后台员工用用户名密码登录权限模型也不同。用户服务面对的是海量C端流量员工服务面对的是少量后台操作两者的安全级别和扩展策略完全不一样。强行合并会让安全审计变得非常困难。订单服务和配送服务为什么要拆想象一个场景用户下单后订单服务要创建订单配送服务要生成配送任务。如果两个服务不拆骑手抢单的并发流量会直接影响下单接口的稳定性。拆开之后订单服务和配送服务各自独立伸缩订单写库慢也不会牵连骑手端刷单。数据库层面每个服务独立库。为了让文章有落地感下面给订单库的核心表举个例子-- 订单主表 CREATE TABLE order_main ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id bigint(20) NOT NULL COMMENT 用户ID, shop_id bigint(20) NOT NULL COMMENT 店铺ID, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, status tinyint(4) NOT NULL COMMENT 订单状态 0待支付 1已支付 2商家接单 3配送中 4已完成 5已取消, address_detail varchar(255) NOT NULL COMMENT 配送地址, create_time datetime NOT NULL, update_time datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_shop_id (shop_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;订单状态用tinyint而不是直接存字符串这是个常见的取舍——存数字节省空间、查询高效但可读性差所以代码里一定要有对应的枚举类。我在实际项目里见过有人把状态直接映射成中文存库当时看着方便后来统计报表时恨不得全都重来。2.2 技术选型SpringBoot SpringCloud Vue的组合逻辑这套技术栈组合是非常成熟的前后端分离 微服务标准方案选它的逻辑很实在。SpringBoot负责微服务的基础开发框架。它解决了Spring配置地狱的问题让一个独立的微服务可以快速启动和部署。每个微服务内部还是传统的三层架构Controller - Service - Mapper开发成本低Java程序员几乎没有学习成本。SpringCloud提供微服务治理全家桶。我用的核心组件包括Spring Cloud Gateway统一入口网关处理鉴权、路由转发、限流。选Gateway而不是Zuul是因为Gateway基于WebFlux性能和吞吐量明显更好而且Spring官方主推。Nacos服务注册与发现 配置中心。比Eureka更强Eureka只管注册发现配置管理还得再搭Config ServerNacos一站式搞定。OpenFeign服务间声明式HTTP调用。写一个接口加几个注解就能完成服务间通信比手动写RestTemplate省太多代码。Sentinel流量控制和熔断降级。外卖系统高峰期的流量是突发的必须有熔断限流保护核心服务。Sleuth Zipkin链路追踪。微服务排障的必备工具否则一个请求串了五六个服务出了问题你都不知道在哪一环。Vue负责前端展示层。我用了Vue3 Element Plus Vite的组合Vue3的组合式API写业务逻辑更清爽Element Plus做后台管理界面特别快。前端工程按角色拆成三个独立应用用户端H5、商家端Web、骑手端App或H5、运营后台Web分项目开发部署。前端和后端的交互全部通过Gateway走HTTP/JSON开发期配置代理解决跨域上线后由Nginx统一转发。这里要点一句很多人初学微服务的误区不要为了微服务而微服务。如果你的项目就两个模块、预估QPS不超过100单体分布式缓存完全够用硬上微服务只会给自己增加运维负担。这套系统上微服务是因为它的业务边界清晰、多端并发场景真实、需要独立扩展这些条件必须满足才值得拆。2.3 分布式组件清单与关键参数我花了大量时间在组件的环境搭建上这里直接给一份可用的选型清单组件版本选型用途关键配置说明Nacos2.2.x注册中心 配置中心启动时设置standalone模式生产环境至少3节点集群Spring Cloud Gateway2021.x统一入口配置路由断言、过滤器链、限流策略OpenFeign内置服务间调用设置连接超时和读取超时避免默认1秒超时导致调用失败Sentinel1.8.x熔断限流降级核心接口设置QPS阈值和熔断策略Zipkin2.x链路追踪配合Sleuth收集调用链数据Redis6.x缓存 分布式锁用于菜品缓存、验证码存储、订单防重复提交RabbitMQ3.x消息队列订单超时未支付取消、派单消息通知MySQL8.x业务数据存储每个微服务独立库建议开启binlog这里要强调一个经验Nacos和Spring Cloud的版本兼容性是个大坑。网上很多教程直接复制官方文档结果启动报各种莫名其妙的错最后排查半天发现是版本不匹配。我的建议是直接在Spring Cloud Alibaba的官方版本说明里查对应关系Spring Boot 2.6.x就配合Spring Cloud 2021.0.x和Spring Cloud Alibaba 2021.0.4.0锁死版本再开发。3. 核心业务模块设计与实现3.1 点餐下单全流程的服务链路用户点餐下单这个流程表面上看就是前端提交一个订单但微服务架构下的完整链路是这样的用户点击下单前端请求Gateway网关解析JWT Token确认用户身份。Gateway根据路由规则把请求转发到订单服务。订单服务先做幂等校验基于用户ID店铺ID最近30秒内的订单查重防止用户重复点提交导致重复下单。订单服务调用商家服务远程获取菜品列表和最新价格校验菜品是否下架、库存是否充足。校验通过后订单服务创建订单数据状态为待支付。订单服务发送MQ消息触发支付服务生成支付单同时启动延迟队列超过15分钟未支付自动取消订单。用户支付成功后支付服务回调订单服务订单状态流转为已支付。这个过程里最有技术含量的两个点远程调用如何保证数据一致以及订单创建如何做幂等。先讲幂等。用户网络不好时往往会疯狂点提交订单如果后端不做幂等控制一次下单就变成三四单。我的实现方案是在订单服务里加一个Redis Key键是order:submit:{userId}:{shopId}值为订单号过期时间设30秒。提交时先尝试写入如果Key已存在则直接返回已存在的订单号。这样既实现了幂等又缓存了用户最近订单前端可以直接跳转支付页。再看远程调用的一致性。订单服务调商家服务校验菜品时如果商家服务超时了怎么办直接报错让用户重新下单还是保存草稿我的选择是核心校验不通过就快速失败让用户重试。因为菜品价格和库存是强约束条件不能为了用户体验用旧数据下单宁可让用户重新选一次也不能产生一笔错误订单。3.2 订单状态机与配送派单逻辑订单状态是外卖系统的骨架我用了状态机来管理而不是在Service层随手改状态。先定义一个枚举public enum OrderStatusEnum { WAIT_PAY(0, 待支付), PAID(1, 已支付), SHOP_ACCEPT(2, 商家已接单), DELIVERING(3, 配送中), COMPLETED(4, 已完成), CANCELLED(5, 已取消); private Integer code; private String desc; // 状态流转合法性校验 public boolean canTransferTo(OrderStatusEnum target) { switch (this) { case WAIT_PAY: return target PAID || target CANCELLED; case PAID: return target SHOP_ACCEPT || target CANCELLED; case SHOP_ACCEPT: return target DELIVERING || target CANCELLED; case DELIVERING: return target COMPLETED; default: return false; } } }为什么用状态机因为订单状态流转有严格的顺序如果不做任何限制一个bug就可能让已完成的订单退回待支付。状态机把合法的流转路径集中定义在一个地方所有修改订单状态的操作都必须经过状态机校验。测试时只需要针对状态机写单元测试不需要把所有业务流程跑一遍省太多事了。配送派单的逻辑我单独说因为这是外卖系统区别于普通电商的核心场景。骑手抢单是一个典型的并发场景。同一个配送任务同时推送给多个骑手谁先抢到就归谁。这个功能如果直接在数据库层面做比如UPDATE delivery_task SET rider_id ? WHERE id ? AND rider_id IS NULL在MySQL默认隔离级别下会出现超卖——两个骑手同时读到rider_id为NULL同时更新成功数据就被覆盖了。我当时先用数据库乐观锁做了一版结果高并发测试下丢单率很高。后来换成了Redis分布式锁 任务状态双重校验的方案public boolean grabOrder(Long taskId, Long riderId) { String lockKey delivery:grab: taskId; String lockValue UUID.randomUUID().toString(); boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS); if (!locked) { return false; // 已经有骑手在抢 } try { // 二次检查任务状态 DeliveryTask task deliveryTaskMapper.selectById(taskId); if (task.getRiderId() ! null || task.getStatus() ! DeliveryStatusEnum.WAIT_GRAB.getCode()) { return false; } int updateRows deliveryTaskMapper.grabTask(taskId, riderId); return updateRows 0; } finally { // Lua脚本释放锁防止误删别人的锁 String script if redis.call(get,KEYS[1]) ARGV[1] then return redis.call(del,KEYS[1]) else return 0 end; redisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList(lockKey), lockValue); } }这个方案里两个关键细节锁的过期时间一定不能太短否则任务还没处理完锁就自动释放了另一个骑手就能趁虚而入释放锁时不能直接DEL要先判断值是不是自己设置的避免把别人的锁删掉。这两个点面试时都属于一问一个准的细节。3.3 员工模块与忘记密码的完整方案标题里特别写了员工 忘记密码我猜有不少同学在这个功能上翻过车。员工忘记密码看起来简单——不就是发个验证码改个密码吗但放到微服务架构和真实业务里至少要回答这几个问题第一员工密码存在哪里员工属于员工/认证服务密码不能明文存。我用的是BCrypt加密而不是MD5。BCrypt自带随机盐同样的密码每次加密结果都不同而且计算复杂度可调暴力破解成本远高于MD5。另外员工密码不能存在用户服务的数据库里否则后台账号和C端用户混在一起权限边界就崩了。第二怎么验证忘记密码的人确实是本人我提供两种方式产品上可以配置手机号验证码员工账号绑定手机号发送验证码校验身份。验证码用Redis存有效期5分钟每个手机号60秒内只能发一次。防刷策略必须做——否则接口泄露后会被短信轰炸验证码成本蹭蹭涨。管理员重置如果员工手机号也换绑了就由管理员在后台发起重置生成一个一次性重置链接发给员工。这个链接带Token有效期2小时用一次就失效。第三重置密码的Token怎么设计这是最容易出安全问题的地方。我的设计如下public class PasswordResetToken { // 业务IDemployeeId private Long targetId; // 随机Token至少32字节用SecureRandom生成 private String token; // 过期时间 private LocalDateTime expireTime; }Token生成不能用UUID简单糊弄UUID虽然随机但包含时间戳信息安全性不够强。要用SecureRandom生成128位随机数再转Base64字符串。Token只存Hash值入库万一数据库泄露攻击者也拿不到有效Token。第四忘记密码的完整流程长什么样我把后端接口的时序理一下1. POST /auth/forgot-password 提交员工账号或手机号 2. 员工服务校验账号存在生成6位数字验证码 3. 验证码存入Rediskeyforgot:code:{phone}过期5分钟 4. 调用短信服务发送验证码 5. POST /auth/verify-code 校验验证码 6. 校验通过后生成PasswordResetToken返回给前端 7. 前端跳转重置密码页提交新密码 Token 8. 员工服务校验Token有效性和过期时间 9. 更新密码BCrypt加密Token立即作废 10. 记录安全日志谁在什么时间从什么IP重置了密码第五安全审计怎么做这一点很多项目都会漏掉。密码重置属于敏感操作必须记录审计日志。我单独建了一张employee_security_log表记录员工ID、操作类型登录成功、登录失败、修改密码、重置密码、IP地址、User-Agent、操作时间。这张表的价值在于一旦发生账号被盗或内部信息泄露可以通过日志还原操作链路。这里必须加粗提醒不要把验证码放在前端代码里写死。我在检查别人项目时真的见过前端写死验证码123456的骚操作虽然调试方便但上线后就是巨大的安全漏洞。验证码必须由服务端生成、存Redis、校验也在服务端前端只负责把用户输入的验证码传给后端。4. 分布式与微服务落地中的硬核问题4.1 服务间调用与鉴权统一微服务架构里服务间调用是常态——订单服务要调商家服务、配送服务要调用户服务。但这个调来调去会带来一个麻烦每个服务都要做鉴权吗我的答案是在网关统一鉴权服务间调用通过内部Token识别。用户带着JWT Token请求进来这个Token只在网关被解析验证一次验证通过后网关把解析出来的用户信息userId、角色等放在请求头里转发给下游服务。下游服务信任网关不重复解析JWT只处理业务。这样做的收益很直接鉴权逻辑只维护一份服务开发专注业务即可不用每写一个接口都拿一套JWT工具类。但这里有一个坑网关到下游是内部调用如果网关被绕过怎么办比如运维把某个服务的端口直接暴露了。我的方案是所有微服务只绑定内网IP通过防火墙和Nacos注册地址双重限制外部无法直接访问。服务间所有请求必须携带内部Token内部服务统一配置的调用凭证下游服务用一个全局过滤器校验这个Token是否存在不存在直接拒绝。实际开发里还有一个高频率出现的问题OpenFeign调用超时。Feign默认连接超时1秒、读取超时1秒这在本地跑可能够用线上一次数据库慢查询就可能超时。我当时被坑得很惨下单链路偶尔超时失败排查半天才发现是Feign超时太短。全局配置如下feign: client: config: default: connectTimeout: 3000 readTimeout: 5000同时一定要配置Feign的熔断降级。否则下游服务挂了上游服务会一直等待超时然后把线程池耗尽引发服务雪崩。我配了Sentinel的Feign降级下游不可用时返回一个友好的错误提示而不是让用户看到一串超时异常堆栈。4.2 分布式事务下单、扣库存、支付、派单怎么保持一致单体应用里一次下单的事务直接包在Transactional里就完事了。但微服务架构下订单服务、库存服务、支付服务各自有独立数据库本地事务根本无法跨库保证一致性。这就是分布式事务问题。我先说结论实际项目里放弃强一致追求最终一致。因为订单系统的高并发场景下强一致方案比如2PC的吞吐量太拉胯而且实现复杂度极高可维护性差。我采用的方案是Seata的AT模式配合本地消息表处理非核心链路。Seata AT模式的好处是对业务代码侵入极小——它就是通过拦截SQL记录数据快照在全局事务提交或回滚时自动补偿。你不用像TCC那样得手写Confirm和Cancel方法。但是Seata AT模式有一个必须要知道的前提它要求全局事务内的所有服务都必须接入同一个Seata Server。如果某个服务是第三方的比如支付服务没法接入Seata怎么办这种情况我换成了事务消息方案支付回调成功的消息发送到RabbitMQ订单服务监听消息消费成功就更新订单状态消费失败就重试。消息中间件充当了不同服务之间的数据一致性协调者。举一个下单扣库存的典型流程1. 订单服务本地开启事务创建订单 写一条锁定库存消息到本地消息表 2. 本地事务提交成功后异步任务把消息发送到MQ 3. 库存服务消费MQ消息执行扣减库存操作 4. 扣减成功回执消息扣减失败重试或进死信队列人工处理 5. 订单服务查看到库存扣减成功更新订单为可支付状态这一步里最关键的设计是下单和写消息必须在一个本地事务里。如果先发消息再写订单消息发出去了订单还没建库存扣了却没订单归属数据就乱了。本地消息表是整个方案可靠性的兜底。我还踩过一个分布式事务的坑回滚时数据不一致。有一次用户下单后支付超时订单服务要回滚但库存服务已经扣减成功了。排查发现是Seata的全局事务超时时间设置太短支付回调比全局事务超时晚到了一步。最后把全局事务超时从30秒调到2分钟并且支付回调增加了重试机制才彻底解决。4.3 分布式锁抢单和库存扣减的并发控制上一节提到骑手抢单用了Redis分布式锁这里展开讲一下分布式锁在库存扣减场景的通用做法。外卖系统的库存场景和电商不太一样电商是商品库存被很多人抢外卖是菜品当日限量比如招牌菜每天只出30份。点餐高峰期多个用户同时下单同一道限量菜必须保证不超卖。我用的是Redis分布式锁 数据库乐观锁的双保险方案public boolean deductStock(Long dishId, Integer quantity) { String lockKey stock:deduct: dishId; String lockValue UUID.randomUUID().toString(); boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, 3, TimeUnit.SECONDS); if (!locked) { // 获取锁失败说明有并发在扣减同一菜品库存直接失败让用户重试 return false; } try { int rows dishStockMapper.deductStockIfEnough(dishId, quantity); return rows 0; } finally { // Lua脚本释放锁 releaseLock(lockKey, lockValue); } }对应的SQL是UPDATE dish_stock SET stock stock - #{quantity} WHERE dish_id #{dishId} AND stock #{quantity}这里有三道防线Redis分布式锁确保同一时刻只有一个线程执行检查库存-扣减库存的操作。SQL条件更新stock #{quantity}保证库存不足时更新失败即使锁被极端情况绕过也不会超卖。数据库行锁InnoDB的更新操作自动锁行多个实例并发更新同一行时由数据库层面保证顺序。很多人会问既然SQL已经用条件更新保证了不超卖为什么还用分布式锁答案是性能。如果没有锁多个请求同时执行UPDATEInnoDB的行锁会让后面的请求排队等待库存扣减在高并发下变成串行。加了Redis锁大部分请求在Redis这一层就被拦下了落到数据库的实际并发量大幅降低数据库压力小很多。还有一个高频问题Redisson的分布式锁和手动用RedisTemplate写锁有什么区别Redisson的RLock是开箱即用的成熟方案自带看门狗自动续期不用自己处理锁超时和误删问题。如果项目里已经引了Redisson直接用它就好省去自己造轮子。我上面的例子用手动实现是为了让读者理解原理生产环境我会优先选Redisson。5. 常见问题与排查经验实录5.1 网关超时与全局异常处理微服务项目上线后第一个高频事故就是网关超时。前端等不到响应、用户反复刷新、后端日志里全是超时异常这种场景我在项目里处理过好几次。网关超时根源是Spring Cloud Gateway默认的响应超时时间非常短一个请求经过网关到下游服务下游处理超过几秒网关就主动断开。而外卖下单链路里订单服务要调用商家服务和支付服务第一次调用往往涉及初始化连接耗时就上去了。我的解决方案是分层设置超时网关到下游spring.cloud.gateway.httpclient.response-timeout设置成10秒。Feign服务间调用connectTimeout 3秒、readTimeout 5秒。数据库超时MySQL连接池中maxLifetime和connectionTimeout合理配置避免线程池堆积。前端Axios超时设置30秒并做超时重试提示。与超时配套的是全局异常处理。微服务每个服务都自己写try-catch会导致大量重复代码我统一写了一个全局异常处理器捕获业务异常、参数校验异常、兜底异常统一返回约定的JSON格式{ code: 500, message: 系统繁忙请稍后重试, traceId: a3f0c9d2e1b845f6 }traceId特别重要它是链路追踪的入口标识。前端报错时用户截图里只要带上这个ID我就能用Zipkin定位到具体是哪个服务、哪个环节出了问题排查效率提升一个量级。5.2 服务之间数据不一致的排查思路微服务架构里最让人头疼的就是数据不一致——订单显示待支付但用户已经付款配送任务显示配送中但订单还是商家接单状态。这种问题往往没有明显的报错日志只有用户投诉或对账时才能发现。我总结出一套排查思路第一步看链路追踪。用Zipkin找到这笔异常订单对应的一次完整调用链路看每一步的耗时和状态码。如果哪一步出现异常或超时问题基本就锁定在这。第二步看MQ消息消费。很多数据不一致是因为消息丢失或重复消费。检查RabbitMQ的死信队列看看有没有消费失败的消息堆积。我遇到过支付回调消息因JSON格式问题消费失败重试3次后进入死信队列导致订单一直是待支付状态——这种问题不看MQ日志根本发现不了。第三步看本地消息表。如果用了本地消息表方案检查这张表的message_status字段看有没有一直停在待发送或发送失败状态的记录。我踩过一次坑本地消息表的定时任务被运维误杀了消息一个都没发出去所有订单卡在待支付状态排查了一上午才定位到。第四步对账兜底。最终手段是每天凌晨跑一个对账任务比对订单服务和支付服务的交易记录发现不一致就自动告警人工介入修复。这个对账任务不能省它是数据一致性的最后一道防线。这里有一个重要的认知微服务架构里不可能消灭数据不一致只能缩短不一致的持续时间。设计目标是把不一致的时间窗口控制在秒级甚至毫秒级而不是追求永远一致。5.3 Vue前端与微服务后端的联调心得前端和后端联调是项目开发里最磨人的环节微服务架构下这个问题被放大了——前端要对接的不止一个后端服务而是通过网关统一暴露的多个API。我的联调经验可以浓缩成三条第一接口契约先行。开发前先定义好每个接口的URL、请求参数、响应结构并维护一份Swagger文档。我在项目里要求所有服务必须开启springdoc接口文档前端根据文档开发后端根据文档测试。没有契约约束前端等接口、后端改接口两边互相猜工期无限拉长。第二前端只认网关地址。开发环境下前端环境变量里配置VITE_API_BASE_URL/api通过Vite的代理转发到本地网关。这样前端代码里不会出现任何后端服务地址上线后只需改代理配置或Nginx转发前端代码一行不用动。Vite开发代理配置大致是这样// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, // 网关地址 changeOrigin: true, pathRewrite: { ^/api: } } } } })第三不要在前端处理任何业务状态流转。我见过有前端代码里写了如果订单状态是2就显示商家已接单这种逻辑后来后端状态加了新枚举前端忘了同步页面显示错乱。正确做法是后端返回状态枚举的code和desc前端只展示desc不做业务判断。状态机逻辑只属于后端前端只管展示。联调中还有一个让我记忆深刻的坑本地跨域问题明明配置了还是报错。后来发现是Cookie跨域——JWT放在请求头里没问题但为了存用户状态我在Token里塞了用户信息Cookie的SameSite属性导致跨域携带不了Cookie。最后把登录态方案改成了前端存储Token、请求头携带Token的模式彻底绕开Cookie跨域问题。写在最后的一些体会这个项目从搭建框架到跑通全流程前后花了大几周时间。回头看我个人最大的感触是微服务真正的难点不在于框架怎么搭而在于业务怎么拆、数据怎么保持一致、问题怎么快速定位。很多人一上来就照抄官方Demo搭了个注册中心和网关以为微服务就入门了结果一写真实业务就卡壳——订单要跨服务查数据、状态要跨服务流转、并发要跨服务控制每个问题都比单体时代复杂一个量级。外卖点餐配送系统作为微服务实践项目确实值得做业务场景足够丰富每一个模块都能挖出有价值的技术点而且技术栈通用性强做完这一套电商、本地生活、即时配送类系统的核心套路基本都能复用。也希望这篇文章里关于员工密码找回、订单状态机、分布式锁的细节能帮你少踩几个坑。