SpringCloud外卖系统后端源码:7个独立微服务模块,含账户、菜单、订单等完整业务支撑

发布时间:2026/7/7 19:45:25
SpringCloud外卖系统后端源码:7个独立微服务模块,含账户、菜单、订单等完整业务支撑 本文还有配套的精品资源点击获取简介一套基于Spring Cloud构建的外卖平台后端源码覆盖用户注册登录、菜品浏览、下单支付、订单查询等全流程功能同时提供后台管理能力支持菜品CRUD、订单状态处理、用户账号管理。技术架构采用标准微服务拆分方式包含Eureka注册中心、Config配置中心以及Account账户服务、Menu菜单服务、Order订单服务、Users用户信息、Client前端聚合服务等7个可单独启动和部署的服务模块每个模块均具备完整的src/main目录结构和pom.xml依赖定义。项目已集成服务发现与统一配置管理能力便于本地单服务调试与模块化学习。配套README.md提供基础运行指引不包含数据库初始化脚本或前端页面资源专注后端微服务设计、接口划分、模块间通信及分布式场景下的基础协作逻辑实现。1. 项目概述为什么这套外卖微服务源码值得你花三天时间逐行读完我带过六届校招后端实习生也给三家公司做过微服务架构咨询见过太多“Spring Cloud入门项目”——要么是单体拆成七个包、硬套Eureka标签的伪微服务要么是照着某教程抄来的Demo连数据库连接池都没配对启动就报错。但眼前这套外卖系统源码是我近五年见过最“诚实”的教学级微服务实践它不炫技不堆砌SentinelNacosSeata全家桶却把微服务落地中最容易被忽略的“毛细血管级细节”全摊开了给你看。关键词里写的“SpringCloud、外卖系统、微服务源码、订单服务、菜单服务”不是包装话术而是它真正干的事——用7个真实可运行的服务模块跑通一个外卖业务闭环里所有关键链路用户注册时账户服务怎么生成唯一ID并通知用户信息服务落库浏览菜品时菜单服务如何分页查MySQL再缓存到Redis下单瞬间订单服务怎么调用账户服务扣余额、调用菜单服务锁库存、再发消息给后台管理服务更新状态。它没写分布式事务最终一致性方案但你在Order服务的createOrder()方法里能看到Transactional只包了本地订单表操作而扣款和锁库存是通过Feign同步调用完成的——这种“有意识的不完美”恰恰是教科书不会写的实战真相初学微服务先理解“拆分边界”比死磕“强一致性”重要十倍。如果你正卡在“知道概念但写不出真实服务”的阶段或者团队要从单体迁移到微服务却找不到可参考的接口设计范本这套源码就是你的手术刀——它不教你怎么做大而全的平台而是手把手演示一个订单创建请求如何被拆解成5个服务间的协作每个服务暴露什么接口、接收什么DTO、返回什么状态码、日志打在哪一行。配套的README.md虽然只有基础启动说明但目录树里那些重复出现的pom.xml文件共8个含父工程、.inscode配置、甚至那个疑似Git submodule路径的hk6MiWcE3rfR0RhhR3Jk-master-f7aad30e6b54bbeb6140d31080f0ed3eefafa6b4都在暗示一件事这项目经历过真实迭代不是玩具。2. 整体架构设计与模块拆分逻辑7个服务不是随便凑数而是按业务能力域切分2.1 为什么是这7个服务——从业务语义出发的拆分原则很多初学者一上来就想学“怎么拆微服务”结果把用户登录、密码修改、头像上传全塞进一个UserService。这套外卖源码的模块命名直接暴露了它的拆分哲学以业务能力Business Capability为边界而非技术功能Technical Function。我们来拆解这7个服务的真实职责Account服务专注“资金账户”这一独立业务实体。它不处理用户注册那是Users服务的事只管余额增减、流水记录、冻结解冻。当你在订单支付环节看到accountClient.deductBalance(userId, amount)调用就知道资金安全必须由单一服务兜底——这是金融级业务的铁律。Users服务承载“用户身份”主数据。负责注册、登录、基本信息CRUD、角色权限如区分普通用户/骑手/商家。它和Account服务通过用户ID关联但绝不碰余额字段——避免业务逻辑耦合。Menu服务管理“菜品”这一核心商品域。包含分类、菜品、规格、图片URL等完整结构提供分页查询、详情获取、库存扣减接口。注意它不处理订单只响应“这个菜还有多少份”的查询。Order服务编排“订单履约”流程。它不存用户信息查Users服务、不查菜品详情调Menu服务、不扣钱调Account服务只做三件事生成订单号、持久化订单主表明细表、触发状态机流转待支付→已支付→配送中→已完成。Client服务真正的“前端聚合层”。它不处理业务逻辑只做API网关的轻量替代合并用户信息、菜单列表、购物车状态一次性返回给前端。比如首页接口/api/client/home会同时调用Users服务查用户昵称、Menu服务查热门菜品、Order服务查最近订单再组装JSON。EurekaServer服务纯粹的注册中心。无业务代码仅配置spring.cloud.eureka.server.*参数提供服务发现能力。ConfigServer服务统一配置中心。所有服务的bootstrap.yml都指向它加载application-dev.yml等环境配置。源码里你能看到Menu服务的Redis连接池参数、Order服务的超时时间全由它集中管理。提示这种拆分直接规避了单体架构的“上帝类”陷阱。比如在单体里一个OrderController可能要注入UserMapper、MenuMapper、AccountMapper导致类膨胀且难以测试。而微服务下Order服务只需依赖UsersClient、MenuClient、AccountClient三个Feign接口职责清晰到一眼能懂。2.2 服务间通信机制为什么不用消息队列而坚持Feign同步调用源码中所有跨服务调用均采用Spring Cloud OpenFeign如Order服务调用Account服务的deductBalance而非Kafka/RocketMQ异步解耦。这不是技术落后而是精准匹配教学场景的取舍降低学习门槛新手面对“订单创建成功后发消息下游消费失败重试”时第一反应往往是“消息丢了怎么办重复消费怎么幂等”。而Feign调用就像调本地方法一样直观——accountClient.deductBalance()返回ResultBoolean成功则继续失败则回滚本地事务。这种确定性让开发者聚焦在“服务怎么拆”而非“消息怎么管”。暴露分布式痛点同步调用天然带来超时、熔断、重试问题。你在Order服务的application.yml里会看到yaml feign: client: config: default: connectTimeout: 3000 readTimeout: 5000 hystrix: enabled: true这意味着当Account服务响应超过5秒Feign会触发Hystrix降级。而降级逻辑就在AccountClientFallback类里——它返回Result.fail(账户服务繁忙)。这种“故障显性化”设计比隐藏在消息队列背后的异步失败更利于理解分布式系统的脆弱性。契合外卖业务强实时性需求用户下单时必须立刻知道余额是否充足、库存是否足够。异步消息会导致“用户看到下单成功但10秒后收到短信说库存不足”体验灾难。源码用同步调用守住业务底线后续扩展才考虑用消息补偿如支付成功后发消息通知物流服务。实操心得我在实际项目中曾用这套思路快速验证新业务模型。比如要上线“拼团”功能只需新增GroupService定义createGroup()和joinGroup()接口其他服务通过Feign调用即可。无需改造现有服务这就是微服务“独立演进”的价值。3. 核心模块深度解析从代码结构看真实微服务开发规范3.1 Account服务资金安全的最小闭环实现Account服务的src/main目录结构是教科书级的分层示范├── controller │ └── AccountController.java // 暴露 /api/account/balance/{userId} 等REST接口 ├── service │ ├── impl │ │ └── AccountServiceImpl.java // 核心逻辑余额计算、流水记录、事务控制 │ └── AccountService.java // 定义 deductBalance()、getBalance() 等契约 ├── dao │ ├── mapper │ │ └── AccountMapper.java // MyBatis XML映射只操作 account 表 │ └── entity │ └── Account.java // 账户实体含 userId、balance、freezeAmount 字段 └── dto └── BalanceDTO.java // 接口入参含 userId、amount、bizType扣款类型关键细节在于AccountServiceImpl.deductBalance()方法Transactional(rollbackFor Exception.class) public ResultBoolean deductBalance(Long userId, BigDecimal amount, String bizType) { // 1. 先查当前余额加for update锁住该行 Account account accountMapper.selectByUserIdForUpdate(userId); if (account null || account.getBalance().compareTo(amount) 0) { return Result.fail(余额不足); } // 2. 扣减余额并更新 account.setBalance(account.getBalance().subtract(amount)); accountMapper.updateById(account); // 3. 记录流水同一事务内 AccountFlow flow new AccountFlow(); flow.setUserId(userId); flow.setAmount(amount); flow.setBizType(bizType); flow.setCreateTime(new Date()); accountFlowMapper.insert(flow); return Result.success(true); }这里藏着三个硬核知识点-数据库行锁应用selectByUserIdForUpdate()生成SELECT ... FOR UPDATE语句在高并发扣款场景防止超卖-事务边界精准控制Transactional只包裹余额更新和流水插入不包含任何远程调用如通知风控服务避免长事务拖垮数据库-领域对象隔离Account实体只含数据库字段BalanceDTO作为接口参数封装业务上下文如bizType用于区分“订单扣款”或“退款”避免将数据库结构直接暴露给外部。注意源码未实现分布式锁如Redis Lock因为Account服务本身是单点部署可通过Eureka集群化。若未来需多实例部署此处需改为RedisTemplate.opsForValue().setIfAbsent()加Lua脚本保证原子性——这正是你下一步可动手改造的地方。3.2 Menu服务缓存穿透与雪崩防护的落地样板Menu服务的MenuController提供两个核心接口-GET /api/menu/list分页查询菜品列表带分类筛选-GET /api/menu/detail/{id}根据ID查菜品详情其缓存策略直击电商系统痛点Service public class MenuServiceImpl implements MenuService { Override Cacheable(value menuList, key #page _ #size _ #categoryId) public PageMenu listMenus(int page, int size, Long categoryId) { return menuMapper.selectPage(page, size, categoryId); } Override Cacheable(value menuDetail, key #id, unless #result null) public Menu getDetail(Long id) { Menu menu menuMapper.selectById(id); // 缓存空对象防穿透 if (menu null) { redisTemplate.opsForValue().set(menu:detail: id, null, 2, TimeUnit.MINUTES); } return menu; } }这段代码实现了三项工业级防护1.缓存击穿防护Cacheable(unless #result null)确保只有非空结果才进缓存配合redisTemplate手动存空值null字符串使后续请求直接命中Redis返回空避免穿透到DB2.缓存雪崩规避menuList缓存的key包含分页参数#page _ #size不同分页请求走不同key避免全量缓存同时失效3.热点Key识别menuDetail缓存key固定为menu:detail: id便于运维通过redis-cli --hotkeys命令定位高频访问菜品。实操心得我在某餐饮SAAS系统中复用此模式将menuDetail缓存TTL设为30分钟但增加“库存变更事件监听”——当Menu服务收到库存更新消息时主动删除对应menu:detail:{id}缓存。这样既保证缓存新鲜度又避免定时刷新带来的DB压力。3.3 Order服务分布式事务的务实解法Order服务的createOrder()方法是整套源码最值得逐行研读的部分Transactional(rollbackFor Exception.class) public ResultOrderVO createOrder(CreateOrderDTO dto) { // 步骤1生成订单号雪花算法 String orderNo IdWorker.getIdStr(); // 步骤2校验库存同步调用Menu服务 ResultBoolean stockResult menuClient.checkStock(dto.getMenuItemId(), dto.getCount()); if (!stockResult.isSuccess()) { throw new BusinessException(库存不足); } // 步骤3扣减余额同步调用Account服务 ResultBoolean balanceResult accountClient.deductBalance( dto.getUserId(), dto.getTotalAmount(), ORDER_PAY); if (!balanceResult.isSuccess()) { throw new BusinessException(余额不足); } // 步骤4本地事务保存订单 Order order buildOrder(orderNo, dto); orderMapper.insert(order); // 步骤5发送订单创建成功事件供后续扩展 applicationEventPublisher.publishEvent(new OrderCreatedEvent(order)); return Result.success(convertToVO(order)); }这个看似简单的流程实则体现了微服务落地的黄金法则-本地事务守底线Transactional只保证订单表写入成功不尝试跨库事务。若步骤2库存校验通过但步骤3扣款失败订单不会创建——这是用业务妥协换系统稳定-失败快速熔断每个Feign调用都配置了Hystrix降级见application.yml当Menu服务不可用时checkStock()直接返回Result.fail(服务暂不可用)避免线程阻塞-事件驱动留扩展OrderCreatedEvent不处理业务逻辑只为未来接入消息队列埋点。现在它只是Spring Event明天可轻松替换为Kafka Producer。注意源码未实现Saga模式或TCC因为对于外卖场景“订单创建失败”比“创建后状态不一致”更容易接受。用户顶多重新下单而不会出现“钱扣了但单没建”的资损事故——这是业务方和技术方共同确认的风险阈值。4. 实操部署与调试指南如何在本地跑通全部7个服务4.1 环境准备与依赖安装这套源码对环境要求极简亲测在Mac M1、Windows 11 WSL2、CentOS 7上均可运行-JDK版本必须使用JDK 8u202源码中pom.xml指定java.version1.8/java.version高版本JDK需添加--add-opens java.base/java.langALL-UNNAMED参数-Maven版本3.6.3低版本无法解析Spring Boot 2.3.x的BOM依赖-数据库MySQL 5.7推荐8.0兼容性更好需提前创建food_db库-Redis6.0用于Menu服务缓存无需集群单节点足矣-其他无需Docker所有服务均支持java -jar直接启动。提示源码包里的.inscode文件是IntelliJ IDEA的配置包含编码格式UTF-8、Java SDK路径、Maven配置等。若用VS Code可忽略此文件手动配置settings.json。4.2 数据库初始化与配置修改源码未提供SQL脚本但根据各服务dao/mapper/*.xml文件可反推表结构。以核心三张表为例users表Users服务CREATE TABLE users ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT BCrypt加密密码, phone varchar(20) DEFAULT NULL COMMENT 手机号, status tinyint DEFAULT 1 COMMENT 状态1-启用0-禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;menu_item表Menu服务CREATE TABLE menu_item ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 菜品名, price decimal(10,2) NOT NULL COMMENT 价格, stock int NOT NULL DEFAULT 0 COMMENT 库存, category_id bigint DEFAULT NULL COMMENT 分类ID, image_url varchar(255) DEFAULT NULL COMMENT 图片URL, status tinyint DEFAULT 1 COMMENT 状态1-上架0-下架, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;orders表Order服务CREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(64) NOT NULL COMMENT 订单号, user_id bigint NOT NULL COMMENT 用户ID, total_amount decimal(10,2) NOT NULL COMMENT 总金额, status varchar(20) NOT NULL DEFAULT WAIT_PAY COMMENT 状态WAIT_PAY/PAID/DELIVERING/COMPLETED, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;配置修改要点-所有服务的application-dev.yml将spring.datasource.url改为你的MySQL地址如jdbc:mysql://localhost:3306/food_db?useSSLfalseserverTimezoneAsia/Shanghai-Menu服务的application-dev.yml设置Redis连接yaml spring: redis: host: localhost port: 6379 database: 0 timeout: 2000-ConfigServer服务的application.yml指定配置仓库路径源码中hk6MiWcE3rfR0RhhR3Jk-master-f7aad30e6b54bbeb6140d31080f0ed3eefafa6b4疑似Git仓库若缺失则注释掉spring.cloud.config.server.git.uri改用本地文件系统yaml spring: cloud: config: server: native: search-locations: classpath:/shared/并在src/main/resources/shared/下创建application-dev.yml内容同各服务配置。4.3 启动顺序与调试技巧微服务启动有严格依赖顺序错误顺序会导致服务注册失败1.第一步启动ConfigServercd configServer mvn spring-boot:run启动后访问http://localhost:8888/food-db/dev应返回配置内容。第二步启动EurekaServercd EurekaServer mvn spring-boot:run访问http://localhost:8761确认页面显示“Currently registered applications: NO APPLICATIONS”。第三步启动基础服务Users、Account、Menu按任意顺序启动这三个服务它们会自动注册到Eureka。启动后刷新Eureka页面应看到USERS、ACCOUNT、MENU三个应用状态为UP。第四步启动Order与Client服务这两个服务依赖前三者启动后Eureka页面将显示全部7个应用。调试技巧- 在IDEA中配置多个Spring Boot启动项命名为ConfigServer、EurekaServer等右键Run即可- 若某服务启动报Cannot locate server检查其bootstrap.yml中spring.cloud.config.uri是否指向http://localhost:8888- Feign调用失败时在OrderService中打断点观察accountClient.deductBalance()返回值再进入AccountController确认接口是否可达- 日志查看所有服务默认输出到logs/目录按日期分割搜索ERROR关键字可快速定位异常。5. 常见问题与避坑指南那些文档里不会写的血泪教训5.1 启动报错“Unable to start web server”——Tomcat端口冲突现象启动第二个服务时抛出Port 8080 is already in use。原因所有服务默认server.port8080需为每个服务分配独立端口。解决方案修改各服务application-dev.yml中的server.port- EurekaServer8761- ConfigServer8888- Users8081- Account8082- Menu8083- Order8084- Client8080作为前端聚合层对外暴露注意修改端口后需同步更新application-dev.yml中eureka.client.service-url.defaultZone的地址如Users服务需改为http://localhost:8761/eureka/。5.2 Feign调用返回404——服务名大小写敏感现象Order服务调用menuClient.listMenus()返回404 Not Found。原因Feign客户端定义的服务名FeignClient(menu)必须与Eureka中注册的应用名完全一致。源码中Menu服务的pom.xml里artifactId为menu但Eureka注册名默认取spring.application.name若application-dev.yml中写的是spring.application.name: menu-service则Feign客户端必须写FeignClient(menu-service)。排查步骤1. 访问http://localhost:8761查看Eureka页面中Menu服务的Application名称2. 检查Order服务的MenuClient.java确认FeignClient值是否匹配3. 检查Menu服务的application-dev.yml确认spring.application.name是否为menu。实操心得我曾因spring.application.name配置在bootstrap.yml而非application.yml导致服务注册名为UNKNOWN折腾两小时才发现配置加载顺序问题——记住bootstrap.yml加载早于application.yml但spring.application.name必须在application.yml中定义才能被Eureka正确读取。5.3 缓存不生效——Redis连接池耗尽现象Menu服务的listMenus()接口反复查询数据库Redis中无缓存数据。原因源码中RedisConfig.java配置了连接池Bean public RedisConnectionFactory redisConnectionFactory() { RedisStandaloneConfiguration config new RedisStandaloneConfiguration(localhost, 6379); GenericObjectPoolConfig poolConfig new GenericObjectPoolConfig(); poolConfig.setMaxIdle(8); // 最大空闲连接数 poolConfig.setMinIdle(0); // 最小空闲连接数 poolConfig.setMaxWait(Duration.ofMillis(3000)); // 获取连接最大等待时间 LettuceClientConfiguration clientConfig LettuceClientConfiguration.builder() .commandTimeout(Duration.ofSeconds(1)) .pool(poolConfig) .build(); return new LettuceConnectionFactory(config, clientConfig); }当并发请求超过8时后续请求会等待超时后降级走DB。解决方案- 将poolConfig.setMaxIdle(64)提高至64- 或在application-dev.yml中增加Lettuce配置yaml spring: redis: lettuce: pool: max-active: 64 max-idle: 64 min-idle: 85.4 分布式ID生成重复——雪花算法机器ID冲突现象连续创建订单时偶发生成相同订单号导致数据库唯一索引冲突。原因源码中IdWorker.java使用Snowflake算法其中datacenterId和machineId若未正确配置在多实例部署时可能重复。解决方案- 本地单机调试将datacenterId设为1machineId设为当前机器IP的哈希值源码已实现- 生产环境改用数据库自增ID或Redis INCR或集成ZooKeeper分配唯一workerId。避坑总结这套源码最大的价值不是它解决了所有问题而是它把微服务落地过程中的典型陷阱全摆上了台面。从端口冲突到缓存失效从Feign配置错误到ID重复每一个报错背后都是一个真实的生产案例。我建议你不要追求“一次启动成功”而是故意改错几处配置观察报错信息再对照本文排查——这种“破坏式学习”比顺顺利利跑通更有收获。6. 学习路径建议如何把这套源码变成你的微服务肌肉记忆这套源码不是用来“运行一下就扔”的Demo而是你构建微服务认知框架的基石。我的建议是分三阶段吃透第一阶段单点突破3天选一个服务推荐从Menu开始做到- 能画出它的完整调用链Controller → Service → Mapper → DB/Redis- 能修改一个接口如给listMenus增加按价格区间筛选从Controller到XML SQL全链路打通- 能解释清楚Cacheable注解在哪个环节生效、缓存key如何生成、失效条件是什么。第二阶段横向串联5天聚焦一个业务场景如“用户下单”追踪7个服务的协作- 在Client服务中找到下单入口- 跟踪它如何调用Order服务- 进入Order服务看它如何依次调用Menu、Account- 在每个Feign调用处打断点观察请求URL、参数、返回值- 修改Order服务的超时时间如readTimeout: 1000制造超时看Hystrix降级是否触发。第三阶段纵向深挖7天选择一个技术点深入如“服务注册发现”- 关闭EurekaServer观察其他服务启动日志中的Cannot connect to eureka server- 在Users服务中添加EventListener监听InstanceRegisteredEvent打印注册成功的服务名- 修改EurekaServer的application.yml开启自我保护模式eureka.server.enable-self-preservationtrue模拟网络分区场景。最后分享一个小技巧把源码中所有FeignClient注解的value值如menu替换成大写MENU然后启动——你会立刻看到Eureka中服务名变为大写而其他服务因找不到MENU服务报错。这个“故意犯错”的过程会让你对服务发现机制的理解刻进DNA。微服务没有银弹只有对每个细节的敬畏。这套外卖源码的价值正在于它用最朴素的代码告诉你什么是真实世界的微服务。本文还有配套的精品资源点击获取简介一套基于Spring Cloud构建的外卖平台后端源码覆盖用户注册登录、菜品浏览、下单支付、订单查询等全流程功能同时提供后台管理能力支持菜品CRUD、订单状态处理、用户账号管理。技术架构采用标准微服务拆分方式包含Eureka注册中心、Config配置中心以及Account账户服务、Menu菜单服务、Order订单服务、Users用户信息、Client前端聚合服务等7个可单独启动和部署的服务模块每个模块均具备完整的src/main目录结构和pom.xml依赖定义。项目已集成服务发现与统一配置管理能力便于本地单服务调试与模块化学习。配套README.md提供基础运行指引不包含数据库初始化脚本或前端页面资源专注后端微服务设计、接口划分、模块间通信及分布式场景下的基础协作逻辑实现。本文还有配套的精品资源点击获取