简介:一份基于微服务架构的Java在线教育平台设计与实现源码包,面向正在学习微服务与Spring Cloud的Java开发者,可帮助理解如何将课程、用户、测评、作业、社区等业务模块拆分为独立服务并完成接口协作。压缩包共189个文件、约156KB,其中128个Java源文件构成各服务核心逻辑,19个XML与9个properties文件提供配置支撑,另含31个zbak备份文件与一篇md说明文档,结构紧凑便于研读。已有55人学习过该资源。通过学习可掌握服务注册发现、负载均衡、断路器、分布式链路追踪等微服务治理组件的落地写法,以及RESTful API、OAuth2/JWT安全认证等实践。预览中可见课程、教师、统计等核心业务模块的源码实现,覆盖课程发布、教学管理和数据统计典型场景,适合作为课程设计或项目实战的参考模板。
1. 微服务架构的 Java 在线教育平台:为什么这套设计与实现值得你拆开看
答辩老师问了我一个问题,我当场没接住:“你下单的时候如果课程库存扣了,但订单没生成,你的系统怎么保证两边一致?”那一刻我才意识到,做一个能跑通的前后端 Demo 并不难,难的是用微服务架构把在线教育平台的订单、课程、用户、支付这些业务串起来,还能在数据一致性上站得住脚。这份资源就是一套基于微服务架构的 Java 在线教育平台设计与实现,代码层面从网关到业务服务全部拆开,涵盖 Nacos 注册中心、OpenFeign 远程调用、Redis 缓存、RabbitMQ 延时关单和 Seata 分布式事务。适合三类人:拿它做毕业设计或课程设计的在校生;想从单体项目转向微服务的 Java 工程师;以及正在准备微服务架构面试、想找一份能落地复现的源码包的人。它能回答的不只是“怎么跑起来”,还有“跑起来之后怎么向别人讲清架构”。
2. 服务拆分与技术选型:六大业务模块的边界、端口与依赖版本
2.1 在线教育平台为什么必须拆微服务
在线教育平台的业务天然适合微服务,因为它的模块边界太清楚了。用户、课程、订单、支付、学习记录、评论通知,每个模块的访问量和变更频率都不一样。比如课程详情是读多写少,支付和订单是强一致场景,学习记录则需要高吞吐写入。如果把这些东西全塞进一个 Spring Boot 单体,到了课程大促或者答辩演示高并发的时候,一个慢 SQL 就能把整站拖死;更麻烦的是,团队多人协作时每改一段代码都要重新构建整个应用。
我把平台拆成了六个服务:网关服务负责统一入口和鉴权路由,认证服务管登录态与 Token,用户服务管学员信息,课程服务管课程与章节内容,订单服务管下单与状态流转,支付服务管支付回调与对账。每个服务独立部署、独立数据库,服务之间只通过 OpenFeign 接口通信。这个拆分粒度不是越细越好,而是按业务边界和事务边界来切。课程与章节放在同一个服务里,因为它们的读写在同一事务里;订单与支付拆开,因为支付回调需要处理外部结果,不能阻塞下单主链路。
2.2 技术选型:这套平台为什么选 Spring Cloud Alibaba 全家桶
现在做 Java 微服务项目,主流路线就是 Spring Cloud Alibaba。Nacos 同时承担注册中心和配置中心,比 Eureka 加 Config 的组合少维护两个组件;OpenFeign 做声明式远程调用,比 RestTemplate 直观;网关直接用 Spring Cloud Gateway,性能和断言配置都比 Zuul 顺眼;缓存用 Redis,消息用 RabbitMQ,分布式事务接入 Seata。资源仓库里的 pom 就是按这套组合配好的,Spring Boot 用的 2.7.x 系列,Spring Cloud Alibaba 用的 2021.x 系列,JDK 默认配了 1.8。
选型逻辑有一条主线:尽量贴近一线互联网公司的生产组合,又别引入太重的东西。比如分布式事务有人用 TCC、有人用 Saga,但课程设计答辩时你能把 Seata AT 模式的自动回滚讲清楚,比手写一堆补偿接口更让人信服。中间件能 Docker 跑的一律 Docker 跑,MySQL、Redis、RabbitMQ 都给了容器编排文件,免得装环境就劝退一半人。持久层选了 MyBatis-Plus,不是为了炫技,是因为单表 CRUD 和分页代码能少写很多,把精力留给微服务本身。
2.3 端口规划与服务注册:部署前先看懂这张表
服务模块、端口和注册名的对应关系必须提前定死。Nacos 默认暴露 8848,网关对外是 9527,下面这张端口分配表是资源包里的标准配置,也是我在本地一直沿用的,部署和排查问题的时候第一步就是核对端口有没有被占用、服务名拼没拼错。
| 服务模块 | 端口 | Nacos 注册名 | 主要职责 |
|---|---|---|---|
| gateway-server | 9527 | gateway-server | 统一入口、路由转发、Token 鉴权 |
| auth-server | 9001 | auth-service | 用户认证、Token 签发 |
| user-server | 9002 | user-service | 学员信息、注册登录 |
| course-server | 9003 | course-service | 课程、章节、讲师、库存 |
| order-server | 9004 | order-service | 订单创建、状态流转、关单 |
| pay-server | 9005 | pay-service | 支付回调、对账 |
每个服务都要往 resources 里放一个 bootstrap.yml,里面写上 spring.application.name 和 Nacos 地址。我遇到过很多人把应用名写在 application.yml 里,结果 Nacos 控制台显示的服务名和 Feign 里 @FeignClient 指定的 name 对不上,接口调用直接报找不到服务。这个坑在第 5 章还会专门说,这里先记住一条:Nacos 里的服务名 = bootstrap.yml 的 spring.application.name = @FeignClient 的 name,三者必须完全一致。
2.4 网关路由配置:把请求正确转发到六个服务
网关是外部请求进入微服务集群的唯一入口。前端调 /api/course/getById,网关就把请求转发给 course-service;调 /api/order/create,就转发给 order-service。下面是资源包里 gateway-server 的 application.yml 核心配置:
spring: application: name: gateway-server cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: online-edu gateway: routes: - id: course-route uri: lb://course-service predicates: - Path=/api/course/** filters: - StripPrefix=1 - id: order-route uri: lb://order-service predicates: - Path=/api/order/** filters: - StripPrefix=1 - id: auth-route uri: lb://auth-service predicates: - Path=/api/auth/** filters: - StripPrefix=1 redis: host: 127.0.0.1 port: 6379关键点是 uri 里的 lb://course-service。lb 表示走负载均衡,后面跟的是 Nacos 注册中心的服务名,不是 IP 地址。Predicates 配置的是路由匹配规则,Path=/api/course/** 表示所有以 /api/course/ 开头的请求都走这条路由。StripPrefix=1 是把第一段路径去掉再转发,也就是请求到 course-service 后,路径变成 /getById,不用再带 /course 前缀。鉴权用的全局过滤器写了放行白名单,比如 /api/auth/login 和课程列表接口不需要 Token,下单和下架接口必须带 Token,这部分的判断逻辑在返还码和状态码上也做了区分,方便前端统一处理。
3. 课程下单与订单关单:OpenFeign 调用、Redis 扣库存和延时消息
3.1 下单主流程的代码骨架
下单是一个跨服务的业务链:订单服务收到下单请求后,要去用户服务确认用户存在,去课程服务确认课程在售,再扣减课程库存,最后在本地生成订单并发送延时消息等待支付。下面这段代码是 OrderService 里 createOrder 的核心实现,也是整个平台业务逻辑最集中的地方:
@Slf4j @Service public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> { private final UserClient userClient; private final CourseClient courseClient; private final RedisTemplate<String, Object> redisTemplate; private final OrderMapper orderMapper; private final RabbitTemplate rabbitTemplate; public OrderResult createOrder(CreateOrderDTO dto) { // 1. 调用 user-service 查询用户是否存在 UserDTO user = userClient.getById(dto.getUserId()); if (user == null) { throw new BizException(500, "用户不存在"); } // 2. 调用 course-service 查询课程信息 CourseDTO course = courseClient.getById(dto.getCourseId()); if (course == null || course.getStatus() != 1) { throw new BizException(500, "课程不存在或已下架"); } // 3. Redis Hash 原子扣减库存,避免超卖 Long remain = redisTemplate.opsForHash() .increment("course:stock:" + dto.getCourseId(), "stock", -1); if (remain < 0) { redisTemplate.opsForHash() .increment("course:stock:" + dto.getCourseId(), "stock", 1); throw new BizException(500, "课程库存不足"); } // 4. 生成本地订单 Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setCourseId(dto.getCourseId()); order.setAmount(course.getPrice()); order.setStatus(0); // 0=待支付, 1=已支付, 2=已关闭, 3=已退款 orderMapper.insert(order); // 5. 发送延时消息,30 分钟后未支付自动关单 rabbitTemplate.convertAndSend("order.exchange", "order.delay.key", order.getOrderNo(), message -> { message.getMessageProperties().setDelay(30 * 60 * 1000); return message; }); log.info("订单创建成功 orderNo={}", order.getOrderNo()); return new OrderResult(order.getOrderNo()); } }这里的库存扣减用的是 Redis 的 Hash 结构,key 是 course:stock:课程ID,里面存一个 stock 字段。opsForHash().increment 是原子操作,并发下单时不会出现两个请求都读到库存为 1 的情况。扣减后如果剩余小于 0,要立刻把库存加回来再抛异常,这一步是防止超卖的关键。generateOrderNo 生成订单号用的规则是“时间戳 + 用户ID后四位 + 随机数”,保证并发下订单号不重复。延时消息设置的是 30 分钟,这个值在资源包里是个常量,改成 5 分钟就能在演示时更快看到关单效果。
3.2 OpenFeign 客户端:服务间调用就是这么写的
订单服务要调用户服务和课程服务,靠的是 OpenFeign。在 order-server 里定义一个 CourseClient 接口,写法如下:
@FeignClient(name = "course-service", path = "/api/course") public interface CourseClient { @GetMapping("/{id}") CourseDTO getById(@PathVariable("id") Long id); @PostMapping("/stock/deduct") void deductStock(@RequestParam("courseId") Long courseId, @RequestParam("count") Integer count); @PostMapping("/stock/release") void releaseStock(@RequestParam("courseId") Long courseId, @RequestParam("count") Integer count); }name 必须和课程服务在 Nacos 里的注册名完全一致,path 要和课程服务 Controller 的 RequestMapping 前缀一致。Feign 的工作原理是启动时根据 name 去 Nacos 拉取服务实例列表,然后通过动态代理帮你发 HTTP 请求,你写的接口方法签名会拼出真实的 URL。所以 CourseDTO、UserDTO 这些对象不能在两个服务里各写各的,资源包里单独放了一个 common-dto 模块,订单服务通过 Maven 依赖引入。
这里有个细节:Feign 的接口要加 @FeignClient 才能被扫描,启动类还需要 @EnableFeignClients 注解,扫描路径默认是启动类所在包。如果你的 FeignClient 放在别的包路径下,启动时会报找不到对应 bean,需要在 @EnableFeignClients 里指定 basePackages。资源包的代码里已经配好了 basePackages = "com.edu.order.feign",自己加新接口时记得保持包路径一致。
3.3 RabbitMQ 延时关单:用死信队列给订单发“后悔药”
订单创建后如果用户一直不支付,订单就一直占着库存,这不行。常见的做法是用 RabbitMQ 死信队列实现延时关单,消息 30 分钟后过期,过期后自动投递到死信队列,由一个消费者消费并关闭订单。配置类核心代码如下:
@Configuration public class RabbitDelayConfig { @Bean public Queue orderDelayQueue() { return QueueBuilder.durable("order.delay.queue") .withArgument("x-dead-letter-exchange", "order.exchange") .withArgument("x-dead-letter-routing-key", "order.close.key") .build(); } @Bean public Queue orderCloseQueue() { return QueueBuilder.durable("order.close.queue").build(); } @Bean public Binding delayBinding() { return BindingBuilder.bind(orderDelayQueue()) .to(new TopicExchange("order.exchange")) .with("order.delay.key"); } @Bean public Binding closeBinding() { return BindingBuilder.bind(orderCloseQueue()) .to(new TopicExchange("order.exchange")) .with("order.close.key"); } }这个方案的核心是给 order.delay.queue 设置两个参数:x-dead-letter-exchange 指定死信消息去哪个交换机,x-dead-letter-routing-key 指定死信消息的路由键。生产者发消息时不设置 TTL,而是在发送时给每条消息设置 setDelay,这样不同订单可以灵活控制超时时间。消息在 delay queue 里躺够 30 分钟,变成死信后被投递到 order.exchange,路由键换成 order.close.key,closeBinding 把消息送进 order.close.queue,关单消费者在那里接消息。
关单消费者的逻辑是:根据 orderNo 查询订单,如果状态还是待支付,就更新为已关闭,同时远程调用 course-service 的 releaseStock 释放库存;如果订单已经支付了,就直接忽略消息并确认。这个“先查状态再更新”的动作一定要在数据库层面做条件更新,比如 UPDATE order SET status=2 WHERE order_no=? AND status=0,避免关单和支付的并发竞争。
4. 分布式事务与数据一致性:Seata AT 模式的落地与订单状态机兜底
4.1 微服务下单产生的数据一致性问题
下单流程同时涉及订单服务写订单表、课程服务扣库存表,两个操作在各自服务里都有自己的本地事务。本地事务只能保证自己库里的操作原子性,跨库就失效了。假设订单写成功但远程扣库存失败,或者库存扣了但订单没生成,都会造成数据对不上。这就是面试官常问的“Java 怎么保证数据一致性”在微服务场景下的标准提问方式。
解决跨服务一致性有几种常用方案。本地消息表要自己建表写扫描任务,TCC 要写 try-confirm-cancel 三组接口,Seata AT 模式则在业务代码无侵入的情况下帮我们做回滚。对课程设计和中小型项目来说,Seata AT 的性价比最高。它在第一阶段把每个分支事务的 undo_log 写进业务库,第二阶段全局事务提交时就顺手删除这些日志,回滚时就反过来执行补偿 SQL。
| 方案 | 对业务代码侵入 | 一致性强度 | 需要额外维护的组件 |
|---|---|---|---|
| 本地消息表 | 中等,要写消息表和定时任务 | 最终一致 | 无 |
| TCC | 高,每个接口写三组逻辑 | 强一致 | 无 |
| Seata AT | 低,一个注解搞定 | 强一致 | TC Server、每个库建 undo_log 表 |
| RabbitMQ 事务消息 | 低 | 最终一致 | 消息中间件 |
4.2 引入 Seata:依赖、配置和全局事务注解
资源包采用 Seata 的 AT 模式,核心思路是:在发起全局事务的方法上标注 @GlobalTransactional,Seata 会在方法执行过程中协调所有参与者的分支事务。下面是在订单服务中开启全局事务的最简实现:
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class) public void createOrderWithStock(CreateOrderDTO dto) { // 远程调用 course-service 扣减数据库库存 courseClient.deductStock(dto.getCourseId(), 1); // 本地插入订单记录 orderMapper.insert(buildOrder(dto)); // 模拟异常:如果这里抛错,Seata 会同步回滚上面扣掉的库存 if (dto.getCourseId() == 999L) { throw new RuntimeException("模拟创建订单失败"); } }@GlobalTransactional 的 name 属性是这个全局事务的标识符,在 Seata Server 控制台能看到它;rollbackFor 指定哪些异常触发回滚。这里双注解的原因是 @GlobalTransactional 负责全局管理,@Transactional 负责本地事务。两个注解都加上之后,远程扣库存和本地插订单被包进同一个全局事务,任意一步抛异常,Seata 都会根据 undo_log 反向生成补偿 SQL 把库存加回来。
Seata 的服务端单独运行,资源包里用 Docker 方式部署。连接到 Nacos 后,客户端通过 application.yml 指定事务分组名,如下:
seata: enabled: true application-id: order-server tx-service-group: online-edu-group registry: type: nacos nacos: server-addr: 127.0.0.1:8848 application: seata-servertx-service-group 必须和 Seata Server 端的配置一致,否则客户端找不到服务端,启动时不会报错,但全局事务调用时会一直卡在超时。每个业务数据库都要建 undo_log 表,表结构和 Seata 官方给的建表语句一致,遗漏这张表会导致回滚时报“undo_log 表不存在”。
4.3 定时任务扫单:给一致性再加一道保险
即便有 Seata,关单和支付回调解读还要防止极端情况漏处理,比如 RabbitMQ 重启期间消息丢失、支付回调在重试次数内没送达。资源包里还做了定时任务兜底:order-server 启动一个 @Scheduled 任务,每隔两分钟扫描订单表中状态为待支付且创建时间超过 30 分钟的订单,批量关闭并释放库存。
这个操作必须基于条件更新去做。核心 SQL 是 update order_info set status = 2 where order_no = ? and status = 0,只有返回值等于 1 才说明这次关闭操作是当前系统完成的,才去调 releaseStock 释放库存。如果更新行数为 0,说明订单在定时任务执行之前已经完成支付或已经关闭,直接跳过。用数据库条件更新代替先查后改,可以避免并发情况下重复关单或重复释放库存。
我这里再解释一下为什么定时任务和延时消息要同时存在。延时消息负责缩短“从下单到关单”的延迟,通常能做到秒级触发;定时任务做低速兜底,负责找回丢失消息。两个方案并行时一定要保证释放库存的接口是幂等的,course-service 的 releaseStock 方法实现是库存先加 count,再把加超的阈值压回 totalStock,这样就算同一订单被关两次,库存也不会超过初始总数。
5. 本地部署与常见问题排查:从启动到演示容易翻车的五个坑
5.1 启动顺序错了,服务之间谁也找不到谁
拿到资源包后先别急着点启动。正确的启动顺序是 MySQL、Redis、RabbitMQ、Nacos、Seata Server 这些中间件先起来,再启动业务服务。业务服务里先启动 auth-server、user-server、course-server,确认它们在 Nacos 控制台出现后,再启动 order-server 和 pay-server,最后启动 gateway-server。如果反过来,gateway 先启动了,路由断言里指向的 lb://course-service 没有可用实例,请求进来会直接报 503。
5.2 坑一:服务一直注册不上 Nacos,控制台空白
现象:服务启动日志没有报错,但 Nacos 控制台服务列表是空的。原因大概率是 bootstrap.yml 里的 namespace 和 Nacos 控制台不一致。Nacos 默认命名空间是 public,如果代码里配了 namespace: online-edu,但控制台没有创建这个命名空间,服务会注册到不存在的命名空间下,页面当然看不到。解决方法是去 Nacos 控制台新建命名空间,把命名空间 ID 填进 bootstrap.yml;或者直接把 namespace 这行删掉,让服务注册到 public。这个问题在带命名空间的项目里非常常见,每次新拉代码我都会先确认这行。
5.3 坑二:Feign 调用报 Load balancer does not have available server
现象:服务都注册上去了,但调接口时返回“Load balancer does not have available server for client: course-service”。原因有两种:第一种是 course-service 真的没注册上,按 5.2 排查;第二种是 2021 年之后的 Spring Cloud 移除了 Ribbon,Feign 默认没有负载均衡器实现,需要单独引入 spring-cloud-starter-loadbalancer 依赖。资源包的 pom 里已经加了,但如果你在自己项目里复刻代码时漏掉这个依赖,就会踩中。解决方法是检查 order-server 的 pom 是否包含 spring-cloud-starter-loadbalancer,加上后重启即可。
5.4 坑三:MySQL 8 连接报 Public Key Retrieval is not allowed
现象:服务启动时数据源初始化抛异常,提示“Public Key Retrieval is not allowed”。原因是 MySQL 8 默认使用 caching_sha2_password 认证插件,连接时如果 SSL 未启用,驱动拒绝自动获取公钥。解决方法是修改 JDBC URL,加上 allowPublicKeyRetrieval=true&useSSL=false,比如 jdbc:mysql://127.0.0.1:3306/edu?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai。serverTimezone 必须带上,否则还会出现时间差 8 小时的问题。数据库建表脚本在 resources/sql 目录下,执行时注意选择 utf8mb4 字符集,课程标题和学员评价里的 emoji 才能正常存。
5.5 坑四:JDK 版本切换后报“源发行版 17 需要目标发行版 17”
现象:Maven 编译时控制台输出“java: 警告: 源发行版 17 需要目标发行版 17”。这个报错在 IDEA 里尤其多,原因是你机器的 JDK、Maven 编译器的 source/target、IDEA 的 Project SDK 三处版本不一致。比如 pom 里写了 <java.version>1.8</java.version>,但 IDEA 的 Project Structure 里 Project SDK 选成了 JDK 17,编译时就会拿 17 的 javac 去按 8 的源级别解析。解决方法是把 pom 的 java.version 改成 1.8(资源包默认就是 1.8),再将 IDEA 的 Project SDK、Module SDK、Settings → Java Compiler 里的 Target bytecode version 全部切到 1.8,最后执行 mvn clean compile。如果你刻意用 JDK 17,就把前三处统一改成 17。
5.6 坑五:Seata 回滚失效,库存和订单数据对不上
现象:下单时报错了,但课程库存没有恢复。大概率是三个原因之一:业务库没有建 undo_log 表、tx-service-group 分组不一致、数据源没有用 DataSourceProxy 代理。前两个按第 4 章的配置核对;第三个要特别留意,Seata AT 模式下业务数据源必须被代理,否则 Seata 拦截不到 SQL 执行。资源包在 order-server 和 course-server 里都配置了 SeataDataSourceConfig,用 @Configuration 把 DataSource 包装成 DataSourceProxy 返回。复刻代码时不要嫌麻烦去掉这个配置类,去掉之后事务回滚就变成“静默失效”,业务不报错但数据就是不对。
6. 验证平台的最终手段:健康检查脚本、压测与链路追踪
6.1 一键健康检查脚本:演示前跑一遍
每次答辩或演示前,我会先跑一个脚本确认各个服务实例在线、下单接口响应正常。下面这段脚本是我压箱底的习惯,直接放在项目根目录,改成你的业务参数就能用:
#!/bin/bash # 检查六个服务在 Nacos 中的存活实例数 SERVICES=("auth-service" "user-service" "course-service" "order-service" "pay-service" "gateway-server") for s in ${SERVICES[@]}; do COUNT=$(curl -s "http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=$s" | jq '.hosts | length') echo "$s 实例数: $COUNT" done # 用真实登录 Token 调一次下单接口,打印 HTTP 状态码 TOKEN=$(curl -s -X POST "http://127.0.0.1:9527/api/auth/login" \ -H "Content-Type: application/json" \ -d '{"username":"demo","password":"123456"}' | jq -r '.data.token') BODY="{\"userId\":1,\"courseId\":2}" CODE=$(curl -s -o /dev/null -w "%{http_code}" \ -X POST "http://127.0.0.1:9527/api/order/create" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" -d "$BODY") echo "下单接口 HTTP 状态: $CODE"脚本用 curl 直接打 Nacos OpenAPI 查实例数,jq 解析 JSON,环境里没有 jq 的话先 brew install jq 或 apt install jq。下单前先走一次登录接口拿 Token,避免网关鉴权拦截导致误判。看到五个业务服务实例数都是 1、下单接口返回 200,再上台演示才稳。
6.2 压测与链路追踪:把性能数字和调用链一起亮出来
如果你想让答辩更有说服力,可以用 JMeter 对下单接口做一次简单的 100 并发压测,看 TPS 和错误率。压测前要注意关掉定时关单任务,否则压测产生的待支付订单会被批量关闭,导致数据看起来乱。链路追踪方面,资源包里集成了 Spring Cloud Sleuth 和 Zipkin,启动 Zipkin 后用 docker compose up -d zipkin 拉起来,一次下单请求的完整调用链——从网关到订单服务再到课程服务、再走 Redis 和 RabbitMQ——都能在 Zipkin 的界面里看到。当你能在几十秒内把“下单请求跨了哪几个服务、每一步耗时多少”展示出来,比任何架构图都有说服力。
从那以后,我每次做完微服务项目,都会强制走一遍这个流程:先跑健康检查脚本确认服务在线,再用 JMeter 压一遍核心接口,最后开 Zipkin 追踪一次完整调用链,三步全过才敢拿出来见人。这套流程也让我后来调线上问题时不再像无头苍蝇一样乱翻日志,而是直接通过链路追踪定位慢节点。项目的源码、SQL 脚本和 Seata 配置都在资源包里,照着启动顺序搭起来,再结合第 5 章的排错清单,你也能一个下午把它跑通。希望帮到你。
本文还有配套的精品资源,点击获取