From 731a4f8b8d9cd60475deae27ec69f0c9ec81563f Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Sat, 19 Sep 2026 23:46:43 +0800 Subject: [PATCH] =?UTF-8?q?seata=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/api/entity/SeataTxProbeEntity.java | 34 ++++++ .../controller/SeataTxProbeController.java | 100 ++++++++++++++++++ .../admin/mapper/SeataTxProbeMapper.java | 9 ++ .../admin/service/SeataTxProbeService.java | 18 ++++ .../service/impl/SeataTxProbeServiceImpl.java | 57 ++++++++++ 5 files changed, 218 insertions(+) create mode 100644 pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SeataTxProbeEntity.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SeataTxProbeController.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/SeataTxProbeMapper.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SeataTxProbeService.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SeataTxProbeServiceImpl.java diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SeataTxProbeEntity.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SeataTxProbeEntity.java new file mode 100644 index 000000000..dfcea8536 --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SeataTxProbeEntity.java @@ -0,0 +1,34 @@ +package com.bsd.pjcloud.admin.api.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.activerecord.Model; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +@Data +@TableName("seata_tx_probe") +@EqualsAndHashCode(callSuper = true) +@Schema(description = "Seata事务探测") +public class SeataTxProbeEntity extends Model { + + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键") + private Long id; + + @Schema(description = "本次探测标记") + private String mark; + + @Schema(description = "commit / rollback") + private String caseType; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "创建时间") + private LocalDateTime createTime; +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SeataTxProbeController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SeataTxProbeController.java new file mode 100644 index 000000000..eafb261b9 --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SeataTxProbeController.java @@ -0,0 +1,100 @@ +package com.bsd.pjcloud.admin.controller; + +import com.bsd.pjcloud.admin.service.SeataTxProbeService; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.log.annotation.SysLog; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +/** + * 后台一键探测 Seata:提交应落库,回滚应不落库。 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/seataProbe") +@Tag(description = "seataProbe", name = "Seata事务探测") +@SecurityRequirement(name = HttpHeaders.AUTHORIZATION) +public class SeataTxProbeController { + + private final SeataTxProbeService seataTxProbeService; + + @Operation(summary = "Seata提交探测") + @SysLog("Seata提交探测") + @PostMapping("/commit") + @PreAuthorize("@pms.hasPermission('admin_seata_probe')") + public R> commit() { + try { + Map started = seataTxProbeService.testCommit(); + String mark = String.valueOf(started.get("mark")); + long cnt = seataTxProbeService.countByMark(mark); + Map body = new HashMap<>(started); + body.put("rowCount", cnt); + body.put("pass", cnt == 1); + body.put("message", cnt == 1 + ? "提交成功:已连上 TC,数据已落库(seata_tx_probe)" + : "提交异常:事务看似结束但库中无记录,请查 Seata/undo_log/数据源代理"); + return cnt == 1 ? R.ok(body) : R.failed(body, String.valueOf(body.get("message"))); + } + catch (Exception e) { + log.error("[seataProbe/commit] 失败", e); + return R.failed("提交探测失败:" + e.getMessage()); + } + } + + @Operation(summary = "Seata回滚探测") + @SysLog("Seata回滚探测") + @PostMapping("/rollback") + @PreAuthorize("@pms.hasPermission('admin_seata_probe')") + public R> rollback() { + String mark = null; + String xid = null; + try { + seataTxProbeService.testRollbackExpectThrow(); + return R.failed("回滚探测异常:应抛错触发回滚,但方法正常返回"); + } + catch (IllegalStateException e) { + String msg = e.getMessage(); + if (msg != null && msg.startsWith("SEATA_PROBE_ROLLBACK:")) { + String payload = msg.substring("SEATA_PROBE_ROLLBACK:".length()); + int p = payload.indexOf('|'); + if (p >= 0) { + mark = payload.substring(0, p); + xid = payload.substring(p + 1); + } + else { + mark = payload; + } + } + } + catch (Exception e) { + log.error("[seataProbe/rollback] 非预期异常", e); + return R.failed("回滚探测失败:" + e.getMessage()); + } + if (mark == null) { + return R.failed("回滚探测失败:未拿到 mark"); + } + long cnt = seataTxProbeService.countByMark(mark); + Map body = new HashMap<>(6); + body.put("mark", mark); + body.put("xid", xid); + body.put("caseType", "rollback"); + body.put("rowCount", cnt); + body.put("pass", cnt == 0); + body.put("message", cnt == 0 + ? "回滚成功:已连上 TC,插入被全局回滚,库中无残留" + : "回滚失败:库中仍有记录,全局事务可能未生效(检查数据源代理/Seata)"); + return cnt == 0 ? R.ok(body) : R.failed(body, String.valueOf(body.get("message"))); + } +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/SeataTxProbeMapper.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/SeataTxProbeMapper.java new file mode 100644 index 000000000..2bf92cfc9 --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/SeataTxProbeMapper.java @@ -0,0 +1,9 @@ +package com.bsd.pjcloud.admin.mapper; + +import com.bsd.pjcloud.admin.api.entity.SeataTxProbeEntity; +import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SeataTxProbeMapper extends PjcloudBaseMapper { +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SeataTxProbeService.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SeataTxProbeService.java new file mode 100644 index 000000000..307f369a7 --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SeataTxProbeService.java @@ -0,0 +1,18 @@ +package com.bsd.pjcloud.admin.service; + +import java.util.Map; + +/** + * Seata 全局事务探测:验证 TC 连通与 AT 提交/回滚。 + */ +public interface SeataTxProbeService { + + Map testCommit(); + + /** + * 故意抛错触发全局回滚;调用方捕获异常后查库校验。 + */ + Map testRollbackExpectThrow(); + + long countByMark(String mark); +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SeataTxProbeServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SeataTxProbeServiceImpl.java new file mode 100644 index 000000000..46474aff5 --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SeataTxProbeServiceImpl.java @@ -0,0 +1,57 @@ +package com.bsd.pjcloud.admin.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.bsd.pjcloud.admin.api.entity.SeataTxProbeEntity; +import com.bsd.pjcloud.admin.mapper.SeataTxProbeMapper; +import com.bsd.pjcloud.admin.service.SeataTxProbeService; +import lombok.RequiredArgsConstructor; +import org.apache.seata.core.context.RootContext; +import org.apache.seata.spring.annotation.GlobalTransactional; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class SeataTxProbeServiceImpl implements SeataTxProbeService { + + private final SeataTxProbeMapper seataTxProbeMapper; + + @Override + @GlobalTransactional(name = "seata-probe-commit", rollbackFor = Exception.class) + public Map testCommit() { + String mark = "C-" + UUID.randomUUID().toString().replace("-", ""); + insertRow(mark, "commit", "commit probe"); + Map out = new HashMap<>(4); + out.put("mark", mark); + out.put("xid", RootContext.getXID()); + out.put("caseType", "commit"); + return out; + } + + @Override + @GlobalTransactional(name = "seata-probe-rollback", rollbackFor = Exception.class) + public Map testRollbackExpectThrow() { + String mark = "R-" + UUID.randomUUID().toString().replace("-", ""); + insertRow(mark, "rollback", "rollback probe before throw"); + throw new IllegalStateException("SEATA_PROBE_ROLLBACK:" + mark + "|" + RootContext.getXID()); + } + + @Override + public long countByMark(String mark) { + return seataTxProbeMapper.selectCount(Wrappers.lambdaQuery() + .eq(SeataTxProbeEntity::getMark, mark)); + } + + private void insertRow(String mark, String caseType, String remark) { + SeataTxProbeEntity row = new SeataTxProbeEntity(); + row.setMark(mark); + row.setCaseType(caseType); + row.setRemark(remark); + row.setCreateTime(LocalDateTime.now()); + seataTxProbeMapper.insert(row); + } +}