diff --git a/script/seata/README.md b/script/seata/README.md
new file mode 100644
index 0000000..71ba19f
--- /dev/null
+++ b/script/seata/README.md
@@ -0,0 +1,37 @@
+# 分布式事务(可选接入)
+
+基于 [Apache Seata](https://seata.apache.org/) 2.6.0 AT 模式的可选接入材料。项目默认**不启用**:本目录不是 Maven 模块,不参与默认构建,主工程的依赖、配置、业务表都不受影响。
+
+接入后,`master`、`slave` 数据源保持不变、不参与全局事务,只有示例的 `seata_order`、`seata_account` 两个数据源由 Dynamic Datasource 包装 Seata 代理。示例 `DemoSeataTradeService#placeOrder` 在同一个进程中,先在订单库创建订单、再在账户库扣减余额,两个库的操作要么都成功,要么都回滚。
+
+Seata Server 的启动、配置、验证及使用限制,请阅读项目文档《分布式事务》,本文只列出材料及分支适用范围:
+
+- 单体项目 ruoyi-vue-pro:
+- 微服务项目 yudao-cloud:
+
+## 1. 材料清单
+
+| 文件 | 说明 | 目标位置 |
+| --- | --- | --- |
+| `sql/mysql/demo_seata.sql` | 创建 2 个演示库,各一张示例表 + `undo_log` 表 | 在 MySQL 中执行 |
+| `dependencies.xml` | Maven 依赖片段 | `yudao-module-system/pom.xml`(全局事务入口所在的模块,`yudao-server` 通过已有的模块依赖传递获得) |
+| `seata.yaml` | `seata_order`、`seata_account` 数据源,以及 Seata 客户端的配置片段 | 合并到 `yudao-server` 的 `application-{profile}.yaml`;`seata: false` 只追加到已经存在的数据源,没有 `slave` 时不要复制 `slave` 段 |
+| `src/main/java/.../system/seatademo/` | 订单 DO、Mapper、Service,以及全局事务入口 `DemoSeataTradeService` | `yudao-module-system/src/main/java` 的同名包 |
+| `src/main/java/.../infra/seatademo/` | 账户 DO、Mapper、Service | `yudao-module-infra/src/main/java` 的同名包 |
+
+订单放在 `yudao-module-system`、账户放在 `yudao-module-infra`,是因为 `yudao-module-system` 已经依赖 `yudao-module-infra`,可以直接调用账户 Service;与微服务项目 yudao-cloud 中「`system-server` 发起、`infra-server` 参与」的示例保持一致。
+
+## 2. 分支适用范围
+
+目前只有 `master-jdk17` 分支提供本目录的材料;`master`、`master-jdk25` 分支请先从 `master-jdk17` 分支的 `script/seata/` 获取,同步到对应分支后,也可以直接从当前分支获取。
+
+本目录的材料适用于 `master`(JDK 8 + Spring Boot 2.7.18)、`master-jdk17`(Spring Boot 3.5.15)、`master-jdk25`(Spring Boot 4.1.2)三个分支,接入步骤相同,兼容要点如下:
+
+| 要点 | 说明 |
+| --- | --- |
+| Seata 坐标 | 只使用 `org.apache.seata:seata-spring-boot-starter:2.6.0`,它自带 Dynamic Datasource 4.6.0 需要的 `io.seata` 兼容类,不要再引入 `io.seata:*` 坐标 |
+| Druid | 排除 Seata 传递的 druid 1.2.25,继续使用项目 Druid starter 提供的版本 |
+| Jackson 2 | Seata 的 undo_log 序列化需要 `jackson-databind`、`jackson-datatype-jsr310`;`master-jdk25` 的 Spring Boot 4 默认使用 Jackson 3,必须添加,其它分支添加后版本不变 |
+| 示例代码 | 只使用 Java 8 语法,使用构造方法注入,不引用 `javax`、`jakarta` 包,三个分支无需修改 |
+
+`@GlobalTransactional` 注解的包名是 `org.apache.seata.spring.annotation`。全局事务入口必须是组件扫描的 `@Service` 等 Bean,Seata 2.6 不会增强 `@Bean` 方法定义的 Bean。
diff --git a/script/seata/dependencies.xml b/script/seata/dependencies.xml
new file mode 100644
index 0000000..359b858
--- /dev/null
+++ b/script/seata/dependencies.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+ org.apache.seata
+ seata-spring-boot-starter
+ 2.6.0
+
+
+
+ com.alibaba
+ druid
+
+
+
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
+
diff --git a/script/seata/seata.yaml b/script/seata/seata.yaml
new file mode 100644
index 0000000..c40b781
--- /dev/null
+++ b/script/seata/seata.yaml
@@ -0,0 +1,45 @@
+# 分布式事务(Seata AT 模式)的配置片段,不能直接作为配置文件使用。
+# 合并到 yudao-server 的 application-{profile}.yaml 中,例如说 application-local.yaml:
+# - spring.datasource.dynamic 下的配置:逐项合并到已有的配置中,master、slave 原有的 url、username、password 等保持不变,只追加 seata: false
+# - seata: false 只追加到当前 profile【已经存在】的数据源上。没有配置 slave 时,不要复制 slave 段,
+# 否则会新建一个没有 url 的 slave 数据源,导致启动失败
+# - seata 下的配置:新增到配置文件的顶层
+# 数据库账号密码通过环境变量传入,不要把真实密码写入配置文件、提交到 Git。
+spring:
+ datasource:
+ dynamic:
+ seata: true # 开启 Dynamic Datasource 的 Seata 支持,由它为每个数据源包装 Seata 的 DataSourceProxy
+ seata-mode: AT # 使用 AT 模式
+ datasource:
+ master:
+ seata: false # 只追加这一行:master 主库不参与 AT,不被 Seata 代理
+ slave:
+ seata: false # 只追加这一行:slave 从库不参与 AT,不被 Seata 代理;没有配置 slave 时,删除整个 slave 段
+ seata_order: # 分布式事务示例的订单库,只有 @DS("seata_order") 的 Mapper、Service 使用
+ url: ${YUDAO_SEATA_ORDER_URL:jdbc:mysql://127.0.0.1:3306/yudao_seata_order?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true}
+ username: ${YUDAO_SEATA_DB_USERNAME:root}
+ password: ${YUDAO_SEATA_DB_PASSWORD:}
+ seata: true # 参与 AT:被 Seata 代理,本地事务提交时注册分支、写入 undo_log
+ seata_account: # 分布式事务示例的账户库,只有 @DS("seata_account") 的 Mapper、Service 使用
+ url: ${YUDAO_SEATA_ACCOUNT_URL:jdbc:mysql://127.0.0.1:3306/yudao_seata_account?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true}
+ username: ${YUDAO_SEATA_DB_USERNAME:root}
+ password: ${YUDAO_SEATA_DB_PASSWORD:}
+ seata: true # 参与 AT:被 Seata 代理,本地事务提交时注册分支、写入 undo_log
+
+seata:
+ enabled: true
+ application-id: ${spring.application.name}
+ tx-service-group: yudao_seata_demo_group # 事务分组,与下面的 vgroup-mapping 对应
+ enable-auto-data-source-proxy: false # 必须关闭:数据源已经由 Dynamic Datasource 代理,避免双重代理
+ client:
+ http:
+ interceptor-enabled: false # 单体项目不需要从 HTTP 请求头 TX_XID 接收 XID;关闭后,外部请求不能通过该请求头加入全局事务
+ service:
+ vgroup-mapping:
+ yudao_seata_demo_group: default # 事务分组 -> Seata Server 集群名
+ grouplist:
+ default: ${YUDAO_SEATA_SERVER_ADDR:127.0.0.1:18091} # Seata Server 地址,端口与启动 Seata Server 时的 SEATA_PORT 一致
+ registry:
+ type: file # 直连上面的 grouplist,仅用于本机演示
+ config:
+ type: file
diff --git a/script/seata/sql/mysql/demo_seata.sql b/script/seata/sql/mysql/demo_seata.sql
new file mode 100644
index 0000000..0b698d5
--- /dev/null
+++ b/script/seata/sql/mysql/demo_seata.sql
@@ -0,0 +1,85 @@
+-- ----------------------------
+-- 分布式事务示例:2 个独立的演示库,各自包含一张业务表 + Seata AT 模式的 undo_log 表
+--
+-- 1. yudao_seata_order:订单库,对应 seata_order 数据源(单体:yudao-server;微服务:system-server 发起方)
+-- yudao_seata_account:账户库,对应 seata_account 数据源(单体:yudao-server;微服务:infra-server 参与方)
+-- 2. 本脚本使用「库名.表名」,可以在同一个 MySQL 实例上直接执行,可重复执行;
+-- 如果两个库在不同的 MySQL 实例上,则分别执行对应库的语句
+-- 3. undo_log 必须和业务表在同一个库中,Seata Server 不需要这张表
+-- 4. 不会修改 master 主库,也不会删除已有的库、表、数据
+-- ----------------------------
+
+CREATE DATABASE IF NOT EXISTS `yudao_seata_order` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+CREATE DATABASE IF NOT EXISTS `yudao_seata_account` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+-- ----------------------------
+-- 订单库:yudao_seata_order
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `yudao_seata_order`.`demo_seata_order` (
+ `id` bigint NOT NULL COMMENT '订单编号(由调用方传入,作为幂等主键)',
+ `user_id` bigint NOT NULL COMMENT '用户编号',
+ `amount` int NOT NULL COMMENT '订单金额,单位:分',
+ `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+ `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+ PRIMARY KEY (`id`) USING BTREE,
+ KEY `idx_user_id` (`user_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '分布式事务示例订单表';
+
+-- ----------------------------
+-- 账户库:yudao_seata_account
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `yudao_seata_account`.`demo_seata_account` (
+ `id` bigint NOT NULL COMMENT '账户编号',
+ `user_id` bigint NOT NULL COMMENT '用户编号',
+ `balance` int NOT NULL DEFAULT 0 COMMENT '余额,单位:分',
+ `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+ `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE KEY `uk_tenant_user` (`tenant_id`, `user_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '分布式事务示例账户表';
+
+-- 演示账户:租户 1、用户 1,初始余额 100 分;已存在时不覆盖(INSERT IGNORE),只影响本演示库
+INSERT IGNORE INTO `yudao_seata_account`.`demo_seata_account` (`id`, `user_id`, `balance`, `tenant_id`) VALUES (1, 1, 100, 1);
+
+-- ----------------------------
+-- undo_log:AT 模式的回滚日志,两个演示库各一张
+--
+-- 表结构来自 Apache Seata 2.6.0 的 script/client/at/db/mysql.sql:
+-- https://github.com/apache/incubator-seata/blob/v2.6.0/script/client/at/db/mysql.sql
+-- Licensed to the Apache Software Foundation (ASF) under the Apache License, Version 2.0
+-- (http://www.apache.org/licenses/LICENSE-2.0)。
+-- 与官方脚本的差异:官方使用单独的 ALTER TABLE 添加 ix_log_created 索引,这里写入 CREATE TABLE,便于重复执行;字段、索引保持一致。
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `yudao_seata_order`.`undo_log`
+(
+ `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
+ `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
+ `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
+ `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
+ `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
+ `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
+ `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
+ UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`),
+ KEY `ix_log_created` (`log_created`)
+) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
+
+CREATE TABLE IF NOT EXISTS `yudao_seata_account`.`undo_log`
+(
+ `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
+ `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
+ `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
+ `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
+ `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
+ `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
+ `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
+ UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`),
+ KEY `ix_log_created` (`log_created`)
+) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
diff --git a/script/seata/src/main/java/cn/iocoder/yudao/module/infra/seatademo/DemoSeataAccountDO.java b/script/seata/src/main/java/cn/iocoder/yudao/module/infra/seatademo/DemoSeataAccountDO.java
new file mode 100644
index 0000000..571a2e3
--- /dev/null
+++ b/script/seata/src/main/java/cn/iocoder/yudao/module/infra/seatademo/DemoSeataAccountDO.java
@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.infra.seatademo;
+
+import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 分布式事务示例账户 DO
+ *
+ * 位于 yudao_seata_account 演示库,对应 seata_account 数据源
+ *
+ * @author 芋道源码
+ */
+@TableName("demo_seata_account")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DemoSeataAccountDO extends TenantBaseDO {
+
+ /**
+ * 账户编号
+ */
+ @TableId
+ private Long id;
+ /**
+ * 用户编号
+ *
+ * 同一个租户下唯一
+ */
+ private Long userId;
+ /**
+ * 余额,单位:分
+ */
+ private Integer balance;
+
+}
diff --git a/script/seata/src/main/java/cn/iocoder/yudao/module/infra/seatademo/DemoSeataAccountMapper.java b/script/seata/src/main/java/cn/iocoder/yudao/module/infra/seatademo/DemoSeataAccountMapper.java
new file mode 100644
index 0000000..4801147
--- /dev/null
+++ b/script/seata/src/main/java/cn/iocoder/yudao/module/infra/seatademo/DemoSeataAccountMapper.java
@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.infra.seatademo;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 分布式事务示例账户 Mapper
+ *
+ * 在 Mapper 上添加 @DS 注解,保证 demo_seata_account 表的 SQL 都使用 seata_account 数据源
+ *
+ * @author 芋道源码
+ */
+@Mapper
+@DS(DemoSeataAccountMapper.DATA_SOURCE)
+public interface DemoSeataAccountMapper extends BaseMapperX {
+
+ /**
+ * 账户库的数据源名,对应 spring.datasource.dynamic.datasource.seata_account 配置项
+ */
+ String DATA_SOURCE = "seata_account";
+
+ default DemoSeataAccountDO selectByUserId(Long userId) {
+ return selectOne(DemoSeataAccountDO::getUserId, userId);
+ }
+
+ /**
+ * 扣减余额
+ *
+ * @param userId 用户编号
+ * @param amount 扣减金额,必须大于 0
+ * @return 更新条数;余额不足或账户不存在时为 0
+ */
+ default int updateBalanceDecr(Long userId, Integer amount) {
+ LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper()
+ .setSql(" balance = balance - " + amount)
+ .eq(DemoSeataAccountDO::getUserId, userId)
+ .ge(DemoSeataAccountDO::getBalance, amount); // 条件扣减:余额足够才更新,避免扣成负数
+ return update(null, updateWrapper);
+ }
+
+}
diff --git a/script/seata/src/main/java/cn/iocoder/yudao/module/infra/seatademo/DemoSeataAccountService.java b/script/seata/src/main/java/cn/iocoder/yudao/module/infra/seatademo/DemoSeataAccountService.java
new file mode 100644
index 0000000..0cfd8c9
--- /dev/null
+++ b/script/seata/src/main/java/cn/iocoder/yudao/module/infra/seatademo/DemoSeataAccountService.java
@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.infra.seatademo;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.Assert;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
+
+/**
+ * 分布式事务示例账户 Service
+ *
+ * 作为全局事务中的一个分支:在 seata_account 数据源上执行本地事务,提交时由 Seata 注册分支、写入 undo_log
+ *
+ * @author 芋道源码
+ */
+@Service
+@DS(DemoSeataAccountMapper.DATA_SOURCE) // 先切换到 seata_account 数据源,再开启 @Transactional 事务
+@RequiredArgsConstructor // 使用构造方法注入,避免 JDK 8 与 JDK 17+ 分支的 @Resource 包名不同
+public class DemoSeataAccountService {
+
+ private final DemoSeataAccountMapper demoSeataAccountMapper;
+
+ @Transactional(rollbackFor = Exception.class)
+ public void deductBalance(Long userId, Integer amount) {
+ Assert.notNull(userId, "用户编号不能为空");
+ Assert.isTrue(amount != null && amount > 0, "扣减金额必须大于 0");
+ int updateCount = demoSeataAccountMapper.updateBalanceDecr(userId, amount);
+ if (updateCount == 0) {
+ // 抛出异常,不要吞掉:本地事务回滚,并由全局事务的发起方回滚其它分支
+ throw invalidParamException("用户({}) 的账户不存在或余额不足", userId);
+ }
+ }
+
+ public DemoSeataAccountDO getAccount(Long userId) {
+ return demoSeataAccountMapper.selectByUserId(userId);
+ }
+
+}
diff --git a/script/seata/src/main/java/cn/iocoder/yudao/module/system/seatademo/DemoSeataOrderDO.java b/script/seata/src/main/java/cn/iocoder/yudao/module/system/seatademo/DemoSeataOrderDO.java
new file mode 100644
index 0000000..6ba0b76
--- /dev/null
+++ b/script/seata/src/main/java/cn/iocoder/yudao/module/system/seatademo/DemoSeataOrderDO.java
@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.module.system.seatademo;
+
+import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 分布式事务示例订单 DO
+ *
+ * 位于 yudao_seata_order 演示库,对应 seata_order 数据源
+ *
+ * @author 芋道源码
+ */
+@TableName("demo_seata_order")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DemoSeataOrderDO extends TenantBaseDO {
+
+ /**
+ * 订单编号
+ *
+ * 由调用方传入,作为幂等主键:重复的下单请求会因主键冲突失败,不会重复扣减余额
+ */
+ @TableId(type = IdType.INPUT)
+ private Long id;
+ /**
+ * 用户编号
+ */
+ private Long userId;
+ /**
+ * 订单金额,单位:分
+ */
+ private Integer amount;
+
+}
diff --git a/script/seata/src/main/java/cn/iocoder/yudao/module/system/seatademo/DemoSeataOrderMapper.java b/script/seata/src/main/java/cn/iocoder/yudao/module/system/seatademo/DemoSeataOrderMapper.java
new file mode 100644
index 0000000..ad695c6
--- /dev/null
+++ b/script/seata/src/main/java/cn/iocoder/yudao/module/system/seatademo/DemoSeataOrderMapper.java
@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.system.seatademo;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import com.baomidou.dynamic.datasource.annotation.DS;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 分布式事务示例订单 Mapper
+ *
+ * 在 Mapper 上添加 @DS 注解,保证 demo_seata_order 表的 SQL 都使用 seata_order 数据源
+ *
+ * @author 芋道源码
+ */
+@Mapper
+@DS(DemoSeataOrderMapper.DATA_SOURCE)
+public interface DemoSeataOrderMapper extends BaseMapperX {
+
+ /**
+ * 订单库的数据源名,对应 spring.datasource.dynamic.datasource.seata_order 配置项
+ */
+ String DATA_SOURCE = "seata_order";
+
+}
diff --git a/script/seata/src/main/java/cn/iocoder/yudao/module/system/seatademo/DemoSeataOrderService.java b/script/seata/src/main/java/cn/iocoder/yudao/module/system/seatademo/DemoSeataOrderService.java
new file mode 100644
index 0000000..9867c16
--- /dev/null
+++ b/script/seata/src/main/java/cn/iocoder/yudao/module/system/seatademo/DemoSeataOrderService.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.system.seatademo;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.Assert;
+
+/**
+ * 分布式事务示例订单 Service
+ *
+ * 作为全局事务中的一个分支:在 seata_order 数据源上执行本地事务,提交时由 Seata 注册分支、写入 undo_log
+ *
+ * @author 芋道源码
+ */
+@Service
+@DS(DemoSeataOrderMapper.DATA_SOURCE) // 先切换到 seata_order 数据源,再开启 @Transactional 事务
+@RequiredArgsConstructor // 使用构造方法注入,避免 JDK 8 与 JDK 17+ 分支的 @Resource 包名不同
+public class DemoSeataOrderService {
+
+ private final DemoSeataOrderMapper demoSeataOrderMapper;
+
+ @Transactional(rollbackFor = Exception.class)
+ public void createOrder(Long id, Long userId, Integer amount) {
+ Assert.notNull(id, "订单编号不能为空");
+ Assert.notNull(userId, "用户编号不能为空");
+ Assert.isTrue(amount != null && amount > 0, "订单金额必须大于 0");
+ DemoSeataOrderDO order = DemoSeataOrderDO.builder().id(id).userId(userId).amount(amount).build();
+ demoSeataOrderMapper.insert(order); // 主键冲突时抛出 DuplicateKeyException,所在的全局事务回滚
+ }
+
+ public DemoSeataOrderDO getOrder(Long id) {
+ return demoSeataOrderMapper.selectById(id);
+ }
+
+}
diff --git a/script/seata/src/main/java/cn/iocoder/yudao/module/system/seatademo/DemoSeataTradeService.java b/script/seata/src/main/java/cn/iocoder/yudao/module/system/seatademo/DemoSeataTradeService.java
new file mode 100644
index 0000000..b7b5de0
--- /dev/null
+++ b/script/seata/src/main/java/cn/iocoder/yudao/module/system/seatademo/DemoSeataTradeService.java
@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.system.seatademo;
+
+import cn.iocoder.yudao.module.infra.seatademo.DemoSeataAccountService;
+import lombok.RequiredArgsConstructor;
+import org.apache.seata.spring.annotation.GlobalTransactional;
+import org.springframework.stereotype.Service;
+
+/**
+ * 分布式事务示例下单 Service,即全局事务的入口(发起方)
+ *
+ * 单体项目中,订单、账户是同一个进程的两个数据源(seata_order、seata_account),直接调用账户 Service
+ *
+ * @author 芋道源码
+ */
+@Service // 使用 @Service 组件扫描注册:Seata 2.6 不会增强 @Bean 方法定义的 Bean,@GlobalTransactional 会失效
+@RequiredArgsConstructor // 使用构造方法注入,避免 JDK 8 与 JDK 17+ 分支的 @Resource 包名不同
+public class DemoSeataTradeService {
+
+ private final DemoSeataOrderService demoSeataOrderService;
+ private final DemoSeataAccountService demoSeataAccountService;
+
+ /**
+ * 下单:创建订单 + 扣减余额,两个数据源的操作要么都成功,要么都回滚
+ *
+ * 注意,这里只添加 @GlobalTransactional,【不要】添加 @Transactional:
+ * 本地事务会在开始时固定一个数据源的连接,导致后续的 @DS 切换失效,两个分支的事务由各自的 Service 开启
+ *
+ * @param orderId 订单编号,由调用方生成,作为幂等主键
+ * @param userId 用户编号
+ * @param amount 订单金额,单位:分
+ */
+ @GlobalTransactional(name = "demo-seata-place-order", rollbackFor = Exception.class)
+ public void placeOrder(Long orderId, Long userId, Integer amount) {
+ // 1. 创建订单:分支一,本地事务提交后,订单已经写入 seata_order 库,同时记录 undo_log
+ demoSeataOrderService.createOrder(orderId, userId, amount);
+ // 2. 扣减余额:分支二;余额不足时抛出异常,Seata 根据 undo_log 删除分支一已经提交的订单
+ demoSeataAccountService.deductBalance(userId, amount);
+ }
+
+}