+ * 标记在方法或类上时,匹配的调用会临时忽略公司类型的数据权限规则。 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface CompanyDataPermissionIgnore { + + /** + * 是否开启忽略,默认开启。 + * 支持 Spring EL 表达式,返回 true 时生效。 + */ + String enable() default "true"; +} \ No newline at end of file diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/annotation/DeptDataPermissionIgnore.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/annotation/DeptDataPermissionIgnore.java new file mode 100644 index 00000000..de80a7d1 --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/annotation/DeptDataPermissionIgnore.java @@ -0,0 +1,21 @@ +package com.zt.plat.framework.datapermission.core.annotation; + +import java.lang.annotation.*; + +/** + * 忽略部门数据权限的注解。 + *
+ * 标记在方法或类上时,匹配的调用会临时忽略部门类型的数据权限规则。
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface DeptDataPermissionIgnore {
+
+ /**
+ * 是否开启忽略,默认开启。
+ * 支持 Spring EL 表达式,返回 true 时生效。
+ */
+ String enable() default "true";
+}
\ No newline at end of file
diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/aop/CompanyDataPermissionIgnoreAspect.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/aop/CompanyDataPermissionIgnoreAspect.java
new file mode 100644
index 00000000..ae051a25
--- /dev/null
+++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/aop/CompanyDataPermissionIgnoreAspect.java
@@ -0,0 +1,31 @@
+package com.zt.plat.framework.datapermission.core.aop;
+
+import com.zt.plat.framework.common.util.spring.SpringExpressionUtils;
+import com.zt.plat.framework.datapermission.core.annotation.CompanyDataPermissionIgnore;
+import com.zt.plat.framework.tenant.core.context.CompanyContextHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+
+/**
+ * 公司数据权限忽略切面,基于 {@link CompanyDataPermissionIgnore} 注解。
+ */
+@Aspect
+@Slf4j
+public class CompanyDataPermissionIgnoreAspect {
+
+ @Around("@within(companyDataPermissionIgnore) || @annotation(companyDataPermissionIgnore)")
+ public Object around(ProceedingJoinPoint joinPoint, CompanyDataPermissionIgnore companyDataPermissionIgnore) throws Throwable {
+ boolean oldIgnore = CompanyContextHolder.isIgnore();
+ try {
+ Object enable = SpringExpressionUtils.parseExpression(companyDataPermissionIgnore.enable());
+ if (Boolean.TRUE.equals(enable)) {
+ CompanyContextHolder.setIgnore(true);
+ }
+ return joinPoint.proceed();
+ } finally {
+ CompanyContextHolder.setIgnore(oldIgnore);
+ }
+ }
+}
\ No newline at end of file
diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/aop/DeptDataPermissionIgnoreAspect.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/aop/DeptDataPermissionIgnoreAspect.java
new file mode 100644
index 00000000..4ee9054e
--- /dev/null
+++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/aop/DeptDataPermissionIgnoreAspect.java
@@ -0,0 +1,31 @@
+package com.zt.plat.framework.datapermission.core.aop;
+
+import com.zt.plat.framework.common.util.spring.SpringExpressionUtils;
+import com.zt.plat.framework.datapermission.core.annotation.DeptDataPermissionIgnore;
+import com.zt.plat.framework.tenant.core.context.DeptContextHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+
+/**
+ * 部门数据权限忽略切面,基于 {@link DeptDataPermissionIgnore} 注解。
+ */
+@Aspect
+@Slf4j
+public class DeptDataPermissionIgnoreAspect {
+
+ @Around("@within(deptDataPermissionIgnore) || @annotation(deptDataPermissionIgnore)")
+ public Object around(ProceedingJoinPoint joinPoint, DeptDataPermissionIgnore deptDataPermissionIgnore) throws Throwable {
+ boolean oldIgnore = DeptContextHolder.shouldIgnore();
+ try {
+ Object enable = SpringExpressionUtils.parseExpression(deptDataPermissionIgnore.enable());
+ if (Boolean.TRUE.equals(enable)) {
+ DeptContextHolder.setIgnore(true);
+ }
+ return joinPoint.proceed();
+ } finally {
+ DeptContextHolder.setIgnore(oldIgnore);
+ }
+ }
+}
\ No newline at end of file
diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/db/DataPermissionRuleHandler.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/db/DataPermissionRuleHandler.java
index 0ad24ca5..6f1e3f48 100644
--- a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/db/DataPermissionRuleHandler.java
+++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/db/DataPermissionRuleHandler.java
@@ -10,7 +10,9 @@ import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.schema.Table;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import static com.zt.plat.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck;
@@ -41,6 +43,7 @@ public class DataPermissionRuleHandler implements MultiDataPermissionHandler {
}
// 生成条件
+ final Set
@@ -33,8 +33,8 @@ import org.springframework.stereotype.Component;
@Component
@ConditionalOnProperty(prefix = "zt.databus.sync.client", name = "enabled", havingValue = "true")
@RocketMQMessageListener(
- topic = "${zt.databus.sync.client.mq.topic:databus-sync}-${zt.databus.sync.client.client-code}",
- consumerGroup = "${zt.databus.sync.client.mq.consumer-group:databus-client-consumer}-${zt.databus.sync.client.client-code}"
+ topic = "${zt.databus.sync.client.mq.topic-base:databus-sync}-${zt.databus.sync.client.client-code}",
+ consumerGroup = "${zt.databus.sync.client.mq.consumer-group-prefix:databus-client-consumer}-${zt.databus.sync.client.client-code}"
)
public class DatabusClientConsumer implements RocketMQListener
* 使用条件:
* 1. zt.databus.sync.client.enabled=true
+ * 2. 系统中存在 UserDeptApi 接口(Feign 客户端)
*
- * 注意:由于用户-部门关系通常集成在用户管理中,此实现为占位符。
- * 分公司可以根据实际情况:
- * 1. 自定义实现此接口,直接操作本地数据库
- * 2. 或者通过用户管理 API 间接处理关联关系
+ * 如果分公司需要自定义实现,可以创建自己的 UserDeptSyncService Bean,
+ * 此默认实现会自动失效(@ConditionalOnMissingBean)
*
* @author ZT
*/
@Slf4j
@Service
@ConditionalOnProperty(prefix = "zt.databus.sync.client", name = "enabled", havingValue = "true")
+@ConditionalOnClass(name = "com.zt.plat.module.system.api.userdept.UserDeptApi")
public class UserDeptSyncServiceImpl implements UserDeptSyncService {
+ @Autowired(required = false)
+ private UserDeptApi userDeptApi; // Feign 远程调用接口
+
@Override
public void create(DatabusUserDeptData data) {
- log.info("[UserDeptSync] 收到创建用户-部门关系请求, userId={}, deptId={}",
- data.getUserId(), data.getDeptId());
- log.warn("[UserDeptSync] 用户-部门关系同步服务需要分公司自定义实现,当前为占位符实现");
- // TODO: 分公司需要实现此方法,通过本地 API 或直接数据库操作完成同步
+ if (userDeptApi == null) {
+ log.warn("[UserDeptSync] UserDeptApi未注入,跳过创建用户-部门关系, userId={}", data.getUserId());
+ return;
+ }
+ UserDeptSaveReqDTO dto = buildUserDeptDTO(data);
+ userDeptApi.createUserDept(dto).checkError();
+ log.info("[UserDeptSync] 用户-部门关系创建成功, userId={}, deptId={}", data.getUserId(), data.getDeptId());
}
@Override
public void update(DatabusUserDeptData data) {
- log.info("[UserDeptSync] 收到更新用户-部门关系请求, userId={}, deptId={}",
- data.getUserId(), data.getDeptId());
- log.warn("[UserDeptSync] 用户-部门关系同步服务需要分公司自定义实现,当前为占位符实现");
- // TODO: 分公司需要实现此方法
+ if (userDeptApi == null) {
+ log.warn("[UserDeptSync] UserDeptApi未注入,跳过更新用户-部门关系, userId={}", data.getUserId());
+ return;
+ }
+ UserDeptSaveReqDTO dto = buildUserDeptDTO(data);
+ userDeptApi.updateUserDept(dto).checkError();
+ log.info("[UserDeptSync] 用户-部门关系更新成功, userId={}, deptId={}", data.getUserId(), data.getDeptId());
}
@Override
public void delete(Long id) {
- log.info("[UserDeptSync] 收到删除用户-部门关系请求, id={}", id);
- log.warn("[UserDeptSync] 用户-部门关系同步服务需要分公司自定义实现,当前为占位符实现");
- // TODO: 分公司需要实现此方法
+ if (userDeptApi == null) {
+ log.warn("[UserDeptSync] UserDeptApi未注入,跳过删除用户-部门关系, id={}", id);
+ return;
+ }
+ userDeptApi.deleteUserDept(id).checkError();
+ log.info("[UserDeptSync] 用户-部门关系删除成功, id={}", id);
}
@Override
public void fullSync(DatabusUserDeptData data) {
- log.info("[UserDeptSync] 收到全量同步用户-部门关系请求, userId={}, deptId={}",
- data.getUserId(), data.getDeptId());
- log.warn("[UserDeptSync] 用户-部门关系同步服务需要分公司自定义实现,当前为占位符实现");
- // TODO: 分公司需要实现此方法,逻辑:存在则更新,不存在则插入
+ if (userDeptApi == null) {
+ log.warn("[UserDeptSync] UserDeptApi未注入,跳过全量同步用户-部门关系, userId={}", data.getUserId());
+ return;
+ }
+ UserDeptSaveReqDTO dto = buildUserDeptDTO(data);
+ try {
+ userDeptApi.syncUserDept(dto).checkError();
+ log.info("[UserDeptSync] 用户-部门关系全量同步成功, id={}, userId={}, deptId={}",
+ dto.getId(), dto.getUserId(), dto.getDeptId());
+ } catch (Exception e) {
+ log.error("[UserDeptSync] 用户-部门关系全量同步失败, id={}, userId={}, deptId={}, error={}",
+ dto.getId(), dto.getUserId(), dto.getDeptId(), e.getMessage());
+ throw e;
+ }
+ }
+
+ /**
+ * 构建用户部门关系 DTO(用于 Feign 调用)
+ */
+ private UserDeptSaveReqDTO buildUserDeptDTO(DatabusUserDeptData data) {
+ UserDeptSaveReqDTO dto = new UserDeptSaveReqDTO();
+ dto.setId(data.getId());
+ dto.setUserId(data.getUserId());
+ dto.setDeptId(data.getDeptId());
+ dto.setRemark(data.getRemark());
+ return dto;
}
}
diff --git a/zt-framework/zt-spring-boot-starter-databus-client/src/main/java/com/zt/plat/framework/databus/client/handler/userpost/UserPostSyncServiceImpl.java b/zt-framework/zt-spring-boot-starter-databus-client/src/main/java/com/zt/plat/framework/databus/client/handler/userpost/UserPostSyncServiceImpl.java
index b41d6ac8..47251092 100644
--- a/zt-framework/zt-spring-boot-starter-databus-client/src/main/java/com/zt/plat/framework/databus/client/handler/userpost/UserPostSyncServiceImpl.java
+++ b/zt-framework/zt-spring-boot-starter-databus-client/src/main/java/com/zt/plat/framework/databus/client/handler/userpost/UserPostSyncServiceImpl.java
@@ -1,56 +1,93 @@
package com.zt.plat.framework.databus.client.handler.userpost;
import com.zt.plat.module.databus.api.data.DatabusUserPostData;
+import com.zt.plat.module.system.api.userpost.UserPostApi;
+import com.zt.plat.module.system.api.userpost.dto.UserPostSaveReqDTO;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
/**
- * 用户-岗位关系同步服务实现
+ * 用户-岗位关系同步服务实现(通过 Feign API 调用远程服务)
*
* 使用条件:
* 1. zt.databus.sync.client.enabled=true
+ * 2. 系统中存在 UserPostApi 接口(Feign 客户端)
*
- * 注意:由于用户-岗位关系通常集成在用户管理中,此实现为占位符。
- * 分公司可以根据实际情况:
- * 1. 自定义实现此接口,直接操作本地数据库
- * 2. 或者通过用户管理 API 间接处理关联关系
+ * 如果分公司需要自定义实现,可以创建自己的 UserPostSyncService Bean,
+ * 此默认实现会自动失效(@ConditionalOnMissingBean)
*
* @author ZT
*/
@Slf4j
@Service
@ConditionalOnProperty(prefix = "zt.databus.sync.client", name = "enabled", havingValue = "true")
+@ConditionalOnClass(name = "com.zt.plat.module.system.api.userpost.UserPostApi")
public class UserPostSyncServiceImpl implements UserPostSyncService {
+ @Autowired(required = false)
+ private UserPostApi userPostApi; // Feign 远程调用接口
+
@Override
public void create(DatabusUserPostData data) {
- log.info("[UserPostSync] 收到创建用户-岗位关系请求, userId={}, postId={}",
- data.getUserId(), data.getPostId());
- log.warn("[UserPostSync] 用户-岗位关系同步服务需要分公司自定义实现,当前为占位符实现");
- // TODO: 分公司需要实现此方法,通过本地 API 或直接数据库操作完成同步
+ if (userPostApi == null) {
+ log.warn("[UserPostSync] UserPostApi未注入,跳过创建用户-岗位关系, userId={}", data.getUserId());
+ return;
+ }
+ UserPostSaveReqDTO dto = buildUserPostDTO(data);
+ userPostApi.createUserPost(dto).checkError();
+ log.info("[UserPostSync] 用户-岗位关系创建成功, userId={}, postId={}", data.getUserId(), data.getPostId());
}
@Override
public void update(DatabusUserPostData data) {
- log.info("[UserPostSync] 收到更新用户-岗位关系请求, userId={}, postId={}",
- data.getUserId(), data.getPostId());
- log.warn("[UserPostSync] 用户-岗位关系同步服务需要分公司自定义实现,当前为占位符实现");
- // TODO: 分公司需要实现此方法
+ if (userPostApi == null) {
+ log.warn("[UserPostSync] UserPostApi未注入,跳过更新用户-岗位关系, userId={}", data.getUserId());
+ return;
+ }
+ UserPostSaveReqDTO dto = buildUserPostDTO(data);
+ userPostApi.updateUserPost(dto).checkError();
+ log.info("[UserPostSync] 用户-岗位关系更新成功, userId={}, postId={}", data.getUserId(), data.getPostId());
}
@Override
public void delete(Long id) {
- log.info("[UserPostSync] 收到删除用户-岗位关系请求, id={}", id);
- log.warn("[UserPostSync] 用户-岗位关系同步服务需要分公司自定义实现,当前为占位符实现");
- // TODO: 分公司需要实现此方法
+ if (userPostApi == null) {
+ log.warn("[UserPostSync] UserPostApi未注入,跳过删除用户-岗位关系, id={}", id);
+ return;
+ }
+ userPostApi.deleteUserPost(id).checkError();
+ log.info("[UserPostSync] 用户-岗位关系删除成功, id={}", id);
}
@Override
public void fullSync(DatabusUserPostData data) {
- log.info("[UserPostSync] 收到全量同步用户-岗位关系请求, userId={}, postId={}",
- data.getUserId(), data.getPostId());
- log.warn("[UserPostSync] 用户-岗位关系同步服务需要分公司自定义实现,当前为占位符实现");
- // TODO: 分公司需要实现此方法,逻辑:存在则更新,不存在则插入
+ if (userPostApi == null) {
+ log.warn("[UserPostSync] UserPostApi未注入,跳过全量同步用户-岗位关系, userId={}", data.getUserId());
+ return;
+ }
+ UserPostSaveReqDTO dto = buildUserPostDTO(data);
+ try {
+ userPostApi.syncUserPost(dto).checkError();
+ log.info("[UserPostSync] 用户-岗位关系全量同步成功, id={}, userId={}, postId={}",
+ dto.getId(), dto.getUserId(), dto.getPostId());
+ } catch (Exception e) {
+ log.error("[UserPostSync] 用户-岗位关系全量同步失败, id={}, userId={}, postId={}, error={}",
+ dto.getId(), dto.getUserId(), dto.getPostId(), e.getMessage());
+ throw e;
+ }
+ }
+
+ /**
+ * 构建用户岗位关系 DTO(用于 Feign 调用)
+ */
+ private UserPostSaveReqDTO buildUserPostDTO(DatabusUserPostData data) {
+ UserPostSaveReqDTO dto = new UserPostSaveReqDTO();
+ dto.setId(data.getId());
+ dto.setUserId(data.getUserId());
+ dto.setPostId(data.getPostId());
+ return dto;
}
}
diff --git a/zt-framework/zt-spring-boot-starter-databus-server/src/main/java/com/zt/plat/framework/databus/server/consumer/DatabusUserChangeConsumer.java b/zt-framework/zt-spring-boot-starter-databus-server/src/main/java/com/zt/plat/framework/databus/server/consumer/DatabusUserChangeConsumer.java
index dd3a52e0..43fd8e97 100644
--- a/zt-framework/zt-spring-boot-starter-databus-server/src/main/java/com/zt/plat/framework/databus/server/consumer/DatabusUserChangeConsumer.java
+++ b/zt-framework/zt-spring-boot-starter-databus-server/src/main/java/com/zt/plat/framework/databus/server/consumer/DatabusUserChangeConsumer.java
@@ -34,7 +34,15 @@ public class DatabusUserChangeConsumer implements RocketMQListener
+ * 此类覆盖 Seata 原有的 DataCompareUtils,添加了对达梦数据库 DmdbTimestamp 类型的特殊处理。
+ * 通过将 DmdbTimestamp 转换为 UTC Instant 进行比较,解决时区格式不一致导致的 dirty undo log 问题。
+ *
+ * 问题背景:
+ * - 达梦数据库的 DmdbTimestamp 类型在序列化/反序列化后时区格式不一致
+ * - 例如:beforeImage 为 "2025-12-25 09:38:54.077811 +08:00"
+ * afterImage 为 "2025-12-25 09:38:54.077811"
+ * - 导致 Seata AT 模式回滚时 dirty undo log 检查失败
+ *
+ * 解决方案:
+ * - 当检测到 DmdbTimestamp 类型时,将两个值都转换为 UTC Instant 进行比较
+ * - 这样可以忽略时区格式差异,只比较实际的时间点
+ *
+ * 补丁来源: https://github.com/apache/incubator-seata/pull/7538
+ * 相关 Issue: https://github.com/apache/incubator-seata/issues/7453
+ * 该修复已合并到 Seata 2.x 分支,将在 Seata 2.6.0 正式发布,届时可删除此模块。
+ *
+ * @author Seata Community (PR #7538)
+ */
+public class DataCompareUtils {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DataCompareUtils.class);
+
+ /**
+ * 标识补丁类是否已加载
+ */
+ private static final boolean PATCHED;
+
+ static {
+ PATCHED = true;
+ LOGGER.info("[zt-spring-boot-starter-seata-dm] DataCompareUtils 补丁类已加载,用于解决达梦数据库 DmdbTimestamp 时区问题");
+ }
+
+ private DataCompareUtils() {}
+
+ /**
+ * Is field equals result.
+ *
+ * @param f0 the f 0
+ * @param f1 the f 1
+ * @return the result
+ */
+ public static Result
+ * 供 Databus 调用,获取用户-部门关联数据用于全量/增量同步
+ *
+ * @author ZT
+ */
+@FeignClient(name = "${databus.provider.user-dept.service:system-server}")
+@Tag(name = "RPC 服务 - Databus 用户-部门关系数据提供者")
+public interface DatabusUserDeptProviderApi {
+
+ String PREFIX = "/rpc/databus/user-dept";
+
+ /**
+ * 游标分页查询用户-部门关系数据(用于全量同步)
+ *
+ * @param reqDTO 游标分页请求
+ * @return 用户-部门关系数据分页结果
+ */
+ @PostMapping(PREFIX + "/page-by-cursor")
+ @Operation(summary = "游标分页查询用户-部门关系数据")
+ CommonResult
+ * 供 Databus 调用,获取用户-岗位关联数据用于全量/增量同步
+ *
+ * @author ZT
+ */
+@FeignClient(name = "${databus.provider.user-post.service:system-server}")
+@Tag(name = "RPC 服务 - Databus 用户-岗位关系数据提供者")
+public interface DatabusUserPostProviderApi {
+
+ String PREFIX = "/rpc/databus/user-post";
+
+ /**
+ * 游标分页查询用户-岗位关系数据(用于全量同步)
+ *
+ * @param reqDTO 游标分页请求
+ * @return 用户-岗位关系数据分页结果
+ */
+ @PostMapping(PREFIX + "/page-by-cursor")
+ @Operation(summary = "游标分页查询用户-岗位关系数据")
+ CommonResult> getCopyListByProcessInstanceId(@RequestParam("processInstanceId") String processInstanceId) {
+ List
> getListByIds(@RequestParam("ids") List
> getListByIds(@RequestParam("ids") List
+ * 注意:如果存在多条相同编码的记录,只返回第一条
*
* @param code 部门编码
* @return 部门信息
*/
default DeptDO selectByCode(String code) {
- return selectOne(DeptDO::getCode, code);
+ List