SpringBoot校园外卖系统架构解析:从多端协同到订单状态机

📅 发布时间:2026/10/21 1:09:55
SpringBoot校园外卖系统架构解析:从多端协同到订单状态机
简介基于SpringBoot的校园外卖服务系统设计与实现项目包面向Java后端学习者和毕业设计学生帮助读者掌握外卖类业务系统的完整落地流程。项目围绕用户、商家、订单、配送四大模块展开覆盖系统架构设计、数据库表关系、业务接口实现、SpringSecurity安全认证以及Docker部署测试等内容能够从需求分析到项目上线提供可参考的工程实践。后端采用SpringBoot整合SpringDataJPA与MySQL完成数据持久化配合SpringSecurity实现认证授权整体结构分层清晰、易于扩展。压缩包共793个文件大小28.84MB以Java源码、Vue组件、JavaScript脚本、CSS样式等前后端文件为主体同时包含SQL数据库脚本、HTML页面、配置文件及bat快捷部署脚本资源结构清晰便于直接导入开发工具运行和二次开发。已有41人学习使用。资源内含完整源码、数据库初始化脚本和说明文档对照工程目录可理解外卖场景下用户、商家、订单和配送模块的实现细节。从数据库建表脚本到前端页面、后端服务接口整套代码按模块划分便于按需研读是学习SpringBoot整合实践、开展课程设计或毕业设计的实用参考资料。1. SpringBoot校园外卖服务系统这份资源能帮你补齐哪块短板做校园外卖这类业务系统难点从来不在增删改查而在「多端协同」和「订单状态流转」。一份合格的SpringBoot校园外卖系统源码通常包含用户端、商家端、管理端三个后台外加一套供小程序或App调用的REST API核心要解决的是认证鉴权怎么做、购物车怎么算钱、订单超时怎么处理、支付回调怎么验签。如果你正在做毕业设计、课设或者想快速搭一套可演示的微服务单体项目这份资源的参考价值是实打实的。我自己拿到第一件事不是跑起来而是先拆它的模块边界和表结构因为这两处决定后续所有功能能否稳定扩展。下面按我的拆解顺序把这份资源的重点、参数和坑一次性讲清楚。2. 系统整体架构与模块划分先看清代码边界再动手改2.1 技术选型与模块边界拆这种项目我习惯先把依赖和模块结构列出来不然改到一半容易晕。这套系统按典型单体分层设计技术栈集中在Spring Boot生态内并辅以中间件解决订单类业务的高频读写问题。代码分层通常是用户端模块登录注册、浏览菜品、购物车、下单、订单查询、地址管理。对应controller包名通常是user接口路径前缀是/api/user。商家端模块菜品上下架、库存修改、订单接单/拒单、营业统计。路径前缀/api/merchant。管理端模块用户管理、商家审核、分类管理、全局订单查询。路径前缀/api/admin。公共模块统一返回体、异常处理、JWT工具、拦截器、Redis工具。基础设施模块MyBatis-Plus的Mapper层、Service层、配置类、定时任务。选型逻辑一般是认证用JWT Redis做token续期ORM用MyBatis-Plus减少单表CRUD代码订单超时关闭用延迟队列简单点就用定时任务扫表复杂一点上RabbitMQ死信队列。这套系统里通常能同时看到这两种方案的代码对理解订单系统演进很有帮助。需要重点关注的是角色边界。如果用户端、商家端的管理员都能操作同一个订单实体的更新接口但没有做角色校验那就会出现商家把用户订单标记为已完成的问题。拆包时先看拦截器里是否区分了USER、MERCHANT、ADMIN三种角色。2.2 目录结构与启动流程拿到源码先不要急着改先在IDE里看一遍包结构。比较合理的工程结构如下campus-delivery/ ├── src/main/java/com/example/delivery/ │ ├── config/ # 拦截器、Redis、WebMvc配置 │ ├── controller/ # 按角色拆包的controller │ ├── service/ # 业务逻辑层 │ ├── mapper/ # MyBatis-Plus的Mapper接口 │ ├── entity/ # 数据库实体 │ ├── dto/ # 前端传参对象 │ ├── vo/ # 返回给前端的视图对象 │ ├── common/ # 统一返回体、异常码、全局异常处理 │ ├── utils/ # JWT工具、日期工具 │ └── task/ # 定时任务订单超时处理 ├── src/main/resources/ │ ├── mapper/ # XML文件复杂SQL写这里 │ ├── application.yml # 主配置 │ ├── application-dev.yml │ └── db/ # 初始化SQL脚本 └── pom.xml这个目录结构有两个值得学习的点。第一是dto和vo分开避免直接把数据库实体暴露给前端即使需求里有时图省事会直接返回entity但拆包做二次开发时建议坚持这个习惯后面改字段名会感谢自己。第二是common包里放全局异常处理而不是在每个controller写try/catch。统一异常处理通常用RestControllerAdvice它能让参数校验失败、业务异常、未知异常分别返回可读的信息。启动流程上也有些需要注意的细节。这套系统因为依赖Redis和MySQL启动前务必先保证本地这两个服务可用。数据库初始化常见的做法是把SQL脚本放在src/main/resources/db目录Spring Boot配置了spring.sql.init.modealways后首次启动会自动建表。如果脚本里包含DROP TABLE IF EXISTS那么每次重启都会清空数据如果你有手工插入的测试数据记得在改配置时保留never模式。2.3 数据库设计九张核心表之间的关系表结构是这份资源最值得看的部分。一张订单相关的表字段怎么设计、状态值怎么定义直接反映作者对业务的理解程度。通常这套系统会有九张左右的表核心表为表名核心字段作用userid、phone、password、role、status用户账号角色区分用户/商家addressid、user_id、detail、phone、is_default用户收货地址categoryid、merchant_id、name、sort菜品分类dishid、merchant_id、category_id、price、status菜品信息shopping_cartid、user_id、dish_id、number购物车条目ordersid、order_no、user_id、merchant_id、amount、status、address_id订单主表order_detailid、order_id、dish_id、dish_name、price、number订单快照merchantid、name、license、status商家信息adminid、username、password管理端账号订单表是整个系统的核心几个关键字段设计要仔细推敲订单号order_no一般不用自增id做主键暴露给用户用时间戳随机数生成唯一订单号长度通常20位。如果是并发量大的场景可以用Redis自增拼接日期但课设和演示项目里够用就行。状态status典型状态有PENDING_PAYMENT、PAID、ACCEPTED、DELIVERING、COMPLETED、CANCELLED、REFUNDED。这套系统里如果用整数表示常见的映射是0待支付、1已支付、2已接单、3配送中、4已完成、5已取消、6已退款。金额字段建议用decimal类型避免float或double的精度问题。前端传金额时接口层最好以分为单位接收整数或者用字符串类型接收后在后端解析否则容易出现前端浮点运算导致金额差一分钱的问题。表设计上还有一个常见争议点order_detail表里要不要冗余dish_name和price字段大多数入门项目会直接关联dish表去查菜品名称和价格但这样就丢失了历史快照。如果商家后来改了菜品价格订单详情里的价格也会跟着变这不符合财务审计要求。合格的系统会在order_detail里冗余一份菜品名称和下单时价格这份资源如果做到了这点那说明作者对订单系统的理解是到位的。3. 核心流程落地从登录鉴权到订单状态机3.1 JWT登录与拦截器配置校园外卖系统的所有接口几乎都要登录态才能访问JWT是这类项目最常见的方案。登录接口拿到手机号和密码后校验通过签发一个token返回给前端。前端后续请求在Header里带上Authorization: Bearer token后端通过拦截器统一解析。核心拦截器代码逻辑通常是这样的Component public class JwtInterceptor implements HandlerInterceptor { Autowired private StringRedisTemplate redisTemplate; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求和其他放行路径 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String authHeader request.getHeader(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { throw new BusinessException(401, 未登录或token缺失); } String token authHeader.substring(7); // 解析token校验签名并获取用户id和角色 Claims claims JwtUtil.parseToken(token); Integer userId claims.get(userId, Integer.class); String role claims.get(role, String.class); // 关键校验token是否已在Redis中过期或被主动注销 String redisKey login:token: userId; String cachedToken redisTemplate.opsForValue().get(redisKey); if (cachedToken null || !cachedToken.equals(token)) { throw new BusinessException(401, 登录已过期请重新登录); } // 将用户信息放入请求上下文供后续controller使用 request.setAttribute(userId, userId); request.setAttribute(role, role); return true; } }这段代码里最容易被忽略的是Redis校验这一步。如果没有这个校验用户退出登录后旧token依然有效因为JWT本身是无状态的只有配合Redis做服务端状态记录才能真正做到主动失效。这就是为什么Stpter这类依赖JWT的项目基本都配了Redis不是因为它快而是为了管理token的生命周期。拦截器配置还有一个坑白名单路径。登录接口、注册接口、图片访问路径不需要拦截直接在WebMvcConfig里放行。常见做法是registry.addInterceptor(jwtInterceptor) .addPathPatterns(/**) .excludePathPatterns( /api/user/login, /api/user/register, /api/user/captcha, /images/**, /error );注意/error路径也放行了否则过滤器抛出异常时Spring Boot跳到错误页面的请求又会被拦截导致错误详情看不清。3.2 点餐与购物车实现购物车的典型实现是把数据存到数据库表里而不是存在客户端。这样用户在A设备加购、B设备下单时可以同步也方便后续做“购物车失效菜品提示”。加购接口的几个关键逻辑点菜品是否存在且为在售状态。购物车里是否已有相同菜品有则数量累加。单一菜品库存是否足够不够则拒绝加购。Service层常见的写法是Override public void addToCart(CartAddDTO dto) { // 参数校验菜品id不能为空、数量必须大于0 if (dto.getDishId() null || dto.getNumber() null || dto.getNumber() 0) { throw new BusinessException(参数不合法); } // 菜品状态校验 Dish dish dishMapper.selectById(dto.getDishId()); if (dish null || dish.getStatus() ! 1) { throw new BusinessException(菜品已下架); } // 菜品库存校验注意判断条件是当前库存与购物车数量之和 int currentStock dish.getStock(); if (currentStock dto.getNumber()) { throw new BusinessException(库存不足当前剩余 currentStock 份); } // 查询购物车是否已有该菜品 QueryWrapperCart wrapper new QueryWrapper(); wrapper.eq(user_id, dto.getUserId()); wrapper.eq(dish_id, dto.getDishId()); Cart cartItem cartMapper.selectOne(wrapper); if (cartItem null) { // 没有则新增一条记录 Cart cart new Cart(); cart.setUserId(dto.getUserId()); cart.setDishId(dto.getDishId()); cart.setNumber(dto.getNumber()); cartMapper.insert(cart); } else { // 已有则数量累加 cartItem.setNumber(cartItem.getNumber() dto.getNumber()); cartMapper.updateById(cartItem); } }这段代码的库存判断容易忽略一个点如果当前库存是5份购物车里已经有4份再加购2份直接拿currentStock dto.getNumber()判断会误判为允许加购实际超过总库存。正确做法是拿“购物车已有数量 本次加购数量”和库存比较即cartItem.getNumber() dto.getNumber() currentStock时才拒绝。很多电商系统的超卖问题就出在这个判断条件上。此外点餐接口还涉及到“菜品列表按分类展示”。通常通过category_id关联查询返回VO里包含categoryName和一个dishList。批量查询时注意避免for循环查库应该用selectBatchIds或者一次联表查出所有菜品后再按分类分组。如果这份资源里是循环查你接手后建议改成一次性查询数据量大一点差距就非常明显。3.3 订单状态机与支付回调模拟订单接口是这个项目最复杂的部分核心涉及两步创建订单和状态流转。创建订单的流程从购物车中取出本次勾选的菜品校验库存和价格计算订单总金额生成订单主记录和订单详情记录然后清空购物车已选的条目。这里面有个环节容易翻车前端传给后端的是菜品id数组还是购物车条目id数组如果是购物车条目id数组后端可以直接查出每条购物车对应的菜品然后依次扣库存。如果是直接传菜品id和数量后端还要防重即同一菜品在数组里出现多次数量应该合并而不是重复插入订单详情。订单金额的计算以后端为准。即使前端传了一个总价后端也要根据数据库里的菜品价格重新算一遍再对比前端传的值不一致就提示刷新页面而不是直接入库。否则前端改一下传参就能低价下单。订单状态机的流转逻辑建议单独写在Service层避免controller里到处都是状态判断public void updateOrderStatus(Long orderId, Integer targetStatus, String role) { // 加锁防止并发状态更新 Order order orderMapper.selectByIdForUpdate(orderId); if (order null) { throw new BusinessException(订单不存在); } int current order.getStatus(); // 状态机校验只有合法迁移才允许变更 MapInteger, ListInteger allowedTransitions new HashMap(); allowedTransitions.put(0, Arrays.asList(1, 5)); // 待支付 - 已支付/取消 allowedTransitions.put(1, Arrays.asList(2, 5)); // 已支付 - 接单/取消 allowedTransitions.put(2, Arrays.asList(3)); // 接单 - 配送中 allowedTransitions.put(3, Arrays.asList(4)); // 配送中 - 已完成 ListInteger allowed allowedTransitions.get(current); if (allowed null || !allowed.contains(targetStatus)) { throw new BusinessException(当前订单状态不允许变更为目标状态); } order.setStatus(targetStatus); orderMapper.updateById(order); }这里有两个值得关注的点。第一是selectByIdForUpdate即SELECT ... FOR UPDATE行级锁防止两个请求同时把订单从0状态改成不同状态。MyBatis-Plus里自定义这个方法要在Mapper接口上写SQL注解或XML。第二是状态机不允许跳跃式变更比如从待支付直接改为已完成这在多端协同的多角色场景下尤为重要。支付回调模拟方面现在的项目为了方便演示通常提供两种方式一种是前端点击“模拟支付”按钮直接调用后端/api/order/payMock接口后端直接把订单置为已支付状态另一种是比较完整的支付回调接口内部用RSA2或MD5签名校验要求回调请求带签名参数后端先验签再更新订单。做毕业设计的话第一种足够做企业外包演示或者需要展示项目深度建议保留第二种。验签的关键在于不能信任回调请求的参数本身而应该用接收到的原始参数重新计算签名拿到本地私钥或公钥做对比。哪怕这套系统只是模拟这个流程也建议保留。订单超时未支付自动关闭一般用两种实现定时任务扫描待支付订单并更新状态或者用延迟队列。定时任务写法简单但会有分钟级延迟且要处理好分页避免一次扫太多数据。如果源码里用的是Spring Scheduling的Scheduled(cron 0 */1 * * * ?)大概每分钟扫一次扫描条件是status 0 AND create_time NOW() - INTERVAL 15 MINUTE。要注意一旦扫到订单就批量更新状态脚本要控制一次扫描的数量比如LIMIT 100防止首次运行时把历史积累的大量超时订单一次性锁住导致数据库压力集中。4. 部署与配置多环境Profile与打包发布4.1 application.yml三套环境配置拿到这个SpringBoot校园外卖系统源码后第一件事往往是黑匣子阶段——项目起不来也不知道哪里起不来。这时先从配置文件开始排查就对了。常规情况下src/main/resources下会有application.yml、application-dev.yml、application-test.yml、application-prod.yml。这是Spring Boot多环境Profile的标准拆分法。dev环境的关键配置可以这样参考server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_delivery?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000 redis: host: localhost port: 6379 database: 0 timeout: 5000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 data: redis: repositories: enabled: false mybatis-plus: mapper-locations: classpath:/mapper/*.xml type-aliases-package: com.example.delivery.entity configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jwt: secret: your-secret-key-at-least-32-characters-long expire-hours: 24这里每个参数都要解释清楚不然改错就是坑serverTimezoneAsia/Shanghai不设置的话自定义插入时间和数据库时间的时区差值会导致时间显示异常。useSSLfalse本地连接MySQL时避免SSL握手报错。maximum-pool-size10这个参数控制数据库连接池最大连接数。本地开发10就够如果部署到服务器上并发高一点建议调整到20或30。太小会报连接池满太大对本地MySQL来说是浪费。lettuce.poolRedis客户端lettuce的连接池参数max-active8意味着同一时间最多8个Redis连接。整体连接数大会出现Unable to connect to Redis之前经常是max-active太小加上连接泄漏导致。logic-delete-field: deletedMyBatis-Plus的逻辑删除全局配置。所有实体表如果有deleted字段所有查询自动加WHERE deleted 0所有删除自动改成UPDATE deleted 1。接手的项目如果代码里出现deleted字段而配置文件里没有开逻辑删除需要自己注意。启动时指定环境只需在IDE里设置虚拟机参数-Dspring.profiles.activedev或者打包后用java -jar campus-delivery.jar --spring.profiles.activedev。命令行指定优先级是最高的。4.2 打包跳过测试与数据初始化mvn clean package -DskipTests -Pdev-DskipTests跳过测试用例执行-Pdev激活Maven Profile对应的构建配置。很多项目在pom.xml里配置了profiles对应用户说不同环境的过滤规则。这里注意-Pdev和spring.profiles.activedev是两回事前者是Maven构建时的Profile后者是Spring Boot运行时的Profile。如果pom.xml里没配置profiles-Pdev不会起作用但要确保Spring Boot项目的application.yml里没有把spring.profiles.active写死成prod否则本地打包上线后会发现连接的是生产数据库这个错误极难排查。数据初始化通常有两种方式schema.sql和data.sql放到resources目录下配合spring.sql.init.modealways使用。特点在于表结构重建快适合开发和演示。数据被清空是常态别把重要的手工数据放在这里更不能和Flyway混用在同一个环境否则可能出现初始化脚本重复执行的问题。db/init.sql手动执行mysql -uroot -p init.sql。这种方式稳定可控适合上线部署避免每次启动都重置数据。对于课设和内部演示推荐第二种方式手动导入一次。原因在于spring.sql.init.modealways的机制会在每次启动都执行脚本如果脚本里含INSERT而表里数据已经存在就会报主键冲突。这也是拆包后第一波常见的报错来源。4.3 Docker Compose一键起依赖如果本地没有安装MySQL和Redis或者维护环境麻烦Compose是演示环境的短期解决方案。version: 3.8 services: mysql: image: mysql:8.0 container_name: campus-mysql environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: campus_delivery ports: - 3306:3306 volumes: - ./init.sql:/docker-entrypoint-initdb.d/init.sql command: --default-authentication-pluginmysql_native_password redis: image: redis:7.0 container_name: campus-redis ports: - 6379:6379 volumes: - ./redis-data:/data./init.sql挂载到/docker-entrypoint-initdb.d目录下MySQL容器首次启动时会自动执行该脚本完成建库建表和数据初始化。command参数中--default-authentication-pluginmysql_native_password是为了兼容MySQL 8.0默认的认证插件因为有些JDBC驱动版本比较旧时使用caching_sha2_password会连接不上。如果你是新生代驱动如mysql-connector-j8.0.21的部分更新版本可以去掉这个参数直接用默认认证插件密码在初始化环境中设置更安全。Redis容器没有配置appendonly或save数据在容器重建后会丢。如果只是开发调试无所谓但演示完发现Redis里存的验证码和token全没了不要慌就是这个原因。5. 常见问题排查五个最容易翻车的点5.1 前端传参为JSON但后端对象接收为空现象接口调用成功但Controller里的DTO对象有一半字段为null尤其在用axios或postman调用时。原因多半是前端只设置了Content-Type为application/json但实际发送的是一个奇怪的字符串或者后端controller入参缺少RequestBody注解。还有一种情况是Spring Boot 3.x默认使用Jackson前端传的字段名是orderNo后端DTO字段是order_no。Jackson默认不会自动做驼峰和下划线的互转字段名对不上就全部null。解决先在application.yml里配置全局命名策略spring.jackson.property-naming-strategySNAKE_CASE如果约定前端全部用下划线或者保持所有字段用驼峰并统一前端。更直接的做法是利用IDE调试在Controller第一行打断点看RequestBody OrderDTO解析后的值如果字段全是null但对象不是null多半就是字段名映射问题。处理方式是给DTO字段加JsonProperty(order_no)逐个对齐或者统一约定命名风格。5.2 明明登录成功但请求业务接口报401现象用户调登录接口返回了token复制进Authorization请求头后业务接口依然报401 未登录或token已过期。原因最常见的是token字符串前多了Bearer前缀。前端可能把token直接放进Header而后端拦截器要求的是Authorization: Bearer token格式代码里用authHeader.startsWith(Bearer )判断少了这个前缀直接被判定为无效。另外如果Redis中token的key设计成login:token:userId而这里userId取的类型不对也会匹配不上比如从claims里取到的是Integer或String不一致。解决后端在拦截器日志里增加一分钟的调试观察把authHeader打印出来看一眼实际值。前端在请求拦截器里统一加上config.headers[Authorization] Bearer getToken()。注意Bearer和token之间有且仅有一个空格。顺便检查Redis里是否存了token如果没存说明登录接口里写Redis那一步没执行去查登录代码有没有走到redisTemplate.opsForValue().set。5.3 下单成功后商户端看不到新订单现象用户端已成功提交订单套餐金额也扣了但商户端订单列表始终为空。原因典型原因是订单表有merchant_id字段但创建订单的Service层没有从菜品信息里反查出当前订单属于哪个商家默认存了一个null或0。商户端查询订单时where merchant_id ?查不到。也有可能是数据库里的关联字段名映射问题比如merchantId和merchant_id没对应上。解决创建订单时根据购物车里的第一个菜品或者当前所选菜品的商家信息把merchant_id正确写入订单主表。如果是一个订单包含多个商家的菜品那设计上应为每个商家拆分开单即一个购物车提交后按merchant_id分组生成多张订单。这一点你拿到这套源码后要重点看它到底是支持一单多商家还是只支持单商家下单。如果是前者订单明细表里要有order_id关联但一个订单不能属于多个商户必须拆单。排查步骤建议先查数据库确认订单记录的merchant_id是否为null再用商户账号的id手动拼一条SQL查询看列表能否查出数据。能查出问题在查询条件查不出问题在落库逻辑。5.4 支付回调后订单未变成已支付现象前端使用模拟支付功能页面上弹窗提示支付成功但订单状态仍是待支付。原因可能是模拟支付接口更新了Redis或缓存中的订单状态但数据库里的订单状态没有同步更新。还有一种情况是支付回调接口路径与controller映射不一致导致请求被拦截器拦截或404但前端以为成功。解决第一步打开控制台Network看模拟支付请求是否返回200第二步如果返回200直接看数据库里orders表的status字段是否真的被更新。如果数据库为0说明后端更新代码没执行多半是状态机校验没过——因为模拟支付入口调用时当前订单状态可能是5已取消状态机不允许从5再跳到1。模拟支付接口在更新状态前通常会先调用一遍查询接口判断订单是否存在且处于待支付状态。这里最容易被忽略的是Transactional事务没有生效比如方法被内部类调用而不是通过Spring代理调用。自己实现时务必通过OrderService.pay(...)这样从外部传入bean方式调用不要用this.pay()否则事务注解无效异常时状态不会回滚下次还会遇到脏数据。5.5 菜品图片上传后访问404现象管理端上传菜品图片成功后前端访问图片URL地址一直是404数据库里面保存的是一串本地文件路径。原因这就是本地存储和静态资源映射没配好。图片被存到了服务器磁盘某个绝对路径而Spring Boot的默认静态资源目录是classpath:/static/磁盘路径不在这个范围内Tomcat不会直接暴露给你。解决在application.yml里配置自定义静态资源映射spring: web: resources: static-locations: file:${upload.dir}/然后在配置类里把/images/**映射到磁盘目录Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${upload.dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceLocations(file: uploadDir /); } }同时确认上传的代码是保存到${upload.dir}目录下而且目录要提前手动创建好否则文件保存时会报找不到路径。开发环境下建议把upload.dir配置成./upload相对工作目录比较好理解。项目打包后如果不指定绝对路径它是相对于jar包所在目录的位置这一点容易在服务部署后被迷惑。6. 进阶验证用自动化脚本跑通完整订单链路有些时候写完代码觉得没问题但上线前心里没底。这时候可以用一个简单的Shell脚本把登录、加购、下单、支付、接单、配送、完成的完整链路自动跑一遍。方法是把后端的HTTP接口串起来脚本里逐步调用校验每个步骤的返回状态和订单状态。#!/bin/bash BASE_URLhttp://localhost:8080/api echo 1. 用户登录 LOGIN_RESP$(curl -s -X POST $BASE_URL/user/login \ -H Content-Type: application/json \ -d {phone:13800000000,password:123456}) TOKEN$(echo $LOGIN_RESP | jq -r .data.token) echo 用户token获取结果: $TOKEN echo 2. 查询菜品列表 DISH_RESP$(curl -s -X GET $BASE_URL/user/dish/list?categoryId1 \ -H Authorization: Bearer $TOKEN) DISH_ID$(echo $DISH_RESP | jq -r .data.list[0].id) echo 第一个菜品ID: $DISH_ID echo 3. 加入购物车 curl -s -X POST $BASE_URL/user/cart/add \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {\dishId\:$DISH_ID,\number\:1} echo 加购完成 echo 4. 提交订单 ORDER_RESP$(curl -s -X POST $BASE_URL/user/order/submit \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {addressId:1,remark:快点送}) ORDER_ID$(echo $ORDER_RESP | jq -r .data.orderId) echo 新订单ID: $ORDER_ID echo 5. 模拟支付 curl -s -X POST $BASE_URL/user/order/payMock \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {\orderId\:$ORDER_ID} echo 支付完成 echo 6. 查询订单状态 ORDER_STATUS$(curl -s -X GET $BASE_URL/user/order/detail?orderId$ORDER_ID \ -H Authorization: Bearer $TOKEN) echo $ORDER_STATUS | jq .data.status这个脚本用了jq解析JSON建议在Linux或macOS环境下执行。如果没有jq可以用Python的json.tool替换或者直接在脚本里用grep加正则提取字段。脚本的价值在于每次改完订单状态机后跑一遍能快速发现链路哪里断了。比如支付后状态如果是0而不是1那就是状态更新处有bug脚本十秒之内就能暴露问题。完成这个验证后再配合日志埋点检查慢SQL和异常。日志框架里把com.example.delivery.mapper的日志级别调到DEBUG就能看到MyBatis-Plus自动生成的所有SQL语句和参数排查懒查询和N1问题特别有用logging: level: com.example.delivery.mapper: debug把订单相关的接口完整走一遍然后再去改逻辑时就有底气得多。有一回我在这套模拟项目里调整完超时订单定时任务的逻辑心里拿不准会不会出现重复关单的问题就是用这个脚本连续跑三遍同时监控数据库里的状态流转记录确认没有一条订单被重复更新到取消状态才敢把代码合并上去。从那以后我每次用这套项目做演示前都强制自己先把这个链路脚本完整跑一遍发现订单状态异常就先用日志定位不急着动代码。希望它能帮你省掉一部分到处打听“项目怎么跑”的时间也建议你把这种验证思路移植到别的工作里去。本文还有配套的精品资源点击获取