) originalPayload);
+ }
+
+ // 添加查询参数到 query 字段
+ enriched.put("query", new LinkedHashMap<>(queryParams));
+
+ return enriched;
+ }
}
diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/StartStepHandler.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/StartStepHandler.java
index db6d83e9..4fe73b23 100644
--- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/StartStepHandler.java
+++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/StartStepHandler.java
@@ -18,10 +18,19 @@ import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_START_EXECUTION_FAILED;
/**
* Handler for START orchestration nodes.
+ *
+ * 增强功能:START 节点的映射结果会存入 context.requestBody,
+ * 使得后续 HTTP 步骤可以自动继承这些公共参数,避免重复配置。
+ *
+ *
注意:不将参数存入 context.variables,避免与后续步骤的响应结果冲突。
+ * variables 应该保留给步骤执行结果使用。
*/
@Component
@RequiredArgsConstructor
@@ -40,11 +49,22 @@ public class StartStepHandler implements ApiStepHandler {
Instant start = Instant.now();
Object snapshotBefore = GatewayExpressionHelper.snapshotRequest(payload);
try {
- ExpressionSpec spec = ExpressionSpecParser.parse(stepDefinition.getStep().getRequestMappingExpr(), ExpressionTypeEnum.JSON);
+ ExpressionSpec spec = ExpressionSpecParser.parse(
+ stepDefinition.getStep().getRequestMappingExpr(),
+ ExpressionTypeEnum.JSON
+ );
if (spec != null) {
- Object evaluated = expressionExecutor.evaluate(spec, payload, payload.getRequestBody(), headers);
+ // 将查询参数合并到 payload 中,使表达式可以通过 $.query.k 访问
+ // 这样和 HTTP 步骤的映射逻辑保持一致
+ Object enrichedPayload = enrichPayloadWithQuery(payload.getRequestBody(), payload.getRequestQueryParams());
+
+ Object evaluated = expressionExecutor.evaluate(spec, payload, enrichedPayload, headers);
+
+ // 应用上下文变更(包括 requestBody、requestQuery 等)
+ // 支持字段级映射,将外部字段名转换为内部字段名
GatewayExpressionHelper.applyContextMutations(payload, evaluated, true, false);
}
+
payload.addStepResult(ApiStepResult.builder()
.stepId(stepDefinition.getStep().getId())
.stepType(stepDefinition.getStep().getType())
@@ -70,4 +90,26 @@ public class StartStepHandler implements ApiStepHandler {
return payload;
};
}
+
+ /**
+ * 将查询参数合并到 payload 中,使表达式可以通过 $.query.k 访问
+ * 这样可以避免 JSONata 环境变量访问的限制
+ */
+ private Object enrichPayloadWithQuery(Object originalPayload, Map queryParams) {
+ if (queryParams == null || queryParams.isEmpty()) {
+ return originalPayload;
+ }
+
+ Map enriched = new LinkedHashMap<>();
+
+ // 如果原始 payload 是 Map,复制所有字段
+ if (originalPayload instanceof Map) {
+ enriched.putAll((Map) originalPayload);
+ }
+
+ // 添加查询参数到 query 字段
+ enriched.put("query", new LinkedHashMap<>(queryParams));
+
+ return enriched;
+ }
}
diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiAnonymousUserService.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiAnonymousUserService.java
index 788b0498..ae04eabe 100644
--- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiAnonymousUserService.java
+++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiAnonymousUserService.java
@@ -40,6 +40,9 @@ public class ApiAnonymousUserService {
private final AdminUserApi adminUserApi;
private final OAuth2TokenCommonApi oauth2TokenApi;
+ private static final int RETRY_ATTEMPTS = 10;
+ private static final Duration RETRY_DELAY = Duration.ofSeconds(5);
+
private LoadingCache> cache;
@PostConstruct
@@ -105,18 +108,33 @@ public class ApiAnonymousUserService {
if (details == null) {
return Optional.empty();
}
- try {
- OAuth2AccessTokenCreateReqDTO req = buildAccessTokenRequest(details);
- OAuth2AccessTokenRespDTO resp = oauth2TokenApi.createAccessToken(req).getCheckedData();
- if (resp == null || !StringUtils.hasText(resp.getAccessToken())) {
- log.warn("[ANONYMOUS] 获取用户 {} 的访问令牌失败: 响应为空", details.getUserId());
- return Optional.empty();
+ OAuth2AccessTokenCreateReqDTO req = buildAccessTokenRequest(details);
+ Exception lastException = null;
+ for (int attempt = 1; attempt <= RETRY_ATTEMPTS; attempt++) {
+ try {
+ OAuth2AccessTokenRespDTO resp = oauth2TokenApi.createAccessToken(req).getCheckedData();
+ if (resp == null || !StringUtils.hasText(resp.getAccessToken())) {
+ log.warn("[ANONYMOUS] 获取用户 {} 的访问令牌失败: 响应为空", details.getUserId());
+ return Optional.empty();
+ }
+ return Optional.of(resp.getAccessToken());
+ } catch (Exception ex) {
+ lastException = ex;
+ if (attempt < RETRY_ATTEMPTS) {
+ log.warn("[ANONYMOUS] 获取用户 {} 的访问令牌失败,开始第 {} 次重试,原因:{}",
+ details.getUserId(), attempt, ex.getMessage());
+ try {
+ Thread.sleep(RETRY_DELAY.toMillis());
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ log.error("[ANONYMOUS] 获取用户 {} 的访问令牌重试被中断", details.getUserId());
+ return Optional.empty();
+ }
+ }
}
- return Optional.of(resp.getAccessToken());
- } catch (Exception ex) {
- log.error("[ANONYMOUS] 获取用户 {} 的访问令牌时发生异常", details.getUserId(), ex);
- return Optional.empty();
}
+ log.error("[ANONYMOUS] 获取用户 {} 的访问令牌时发生异常", details.getUserId(), lastException);
+ return Optional.empty();
}
private OAuth2AccessTokenCreateReqDTO buildAccessTokenRequest(AnonymousUserDetails details) {
diff --git a/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/file/dto/FileCreateReqDTO.java b/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/file/dto/FileCreateReqDTO.java
index 4260da92..6bfce374 100644
--- a/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/file/dto/FileCreateReqDTO.java
+++ b/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/file/dto/FileCreateReqDTO.java
@@ -21,4 +21,6 @@ public class FileCreateReqDTO {
@NotEmpty(message = "文件内容不能为空")
private byte[] content;
+ @Schema(description = "是否可下载(true是,false否)", example = "true")
+ private Boolean downloadable;
}
diff --git a/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/file/dto/FileRespDTO.java b/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/file/dto/FileRespDTO.java
index 58af4c9a..3b318d0c 100644
--- a/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/file/dto/FileRespDTO.java
+++ b/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/file/dto/FileRespDTO.java
@@ -37,4 +37,7 @@ public class FileRespDTO {
@Schema(description = "文件下载次数")
private Integer downloadCount;
+ @Schema(description = "是否可下载(true是,false否)")
+ private Boolean downloadable;
+
}
\ No newline at end of file
diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/file/FileController.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/file/FileController.java
index d4b971a9..3e812f1b 100644
--- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/file/FileController.java
+++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/file/FileController.java
@@ -3,6 +3,7 @@ package com.zt.plat.module.infra.controller.admin.file;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
+import com.zt.plat.framework.common.enums.VerifyCodeSendType;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.pojo.PageResult;
@@ -21,6 +22,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
@@ -43,18 +45,48 @@ import static com.zt.plat.module.infra.framework.file.core.utils.FileTypeUtils.w
@Slf4j
public class FileController {
+ @Value("${zt.file.preview-base-url:}")
+ private String previewBaseUrl;
+
@Resource
private FileService fileService;
@GetMapping("/get")
@Operation(summary = "获取文件预览地址", description = "根据 fileId 返回文件预览 url(kkfile)")
- public CommonResult getPreviewUrl(@RequestParam("fileId") Long fileId) {
+ public CommonResult getPreviewUrl(@RequestParam("fileId") Long fileId,
+ @RequestParam(value = "code", required = false) String code,
+ HttpServletRequest request) throws Exception {
FileDO fileDO = fileService.getActiveFileById(fileId);
if (fileDO == null) {
return CommonResult.error(HttpStatus.NOT_FOUND.value(), "文件不存在");
}
+
+ // 统计下载次数
+ fileService.incDownloadCount(fileId);
+
// FileDO 转换为 FileRespVO
FileRespVO fileRespVO = BeanUtils.toBean(fileDO, FileRespVO.class);
+
+ // 加密文件:塞入“临时解密预览 URL”
+ if (Boolean.TRUE.equals(fileRespVO.getIsEncrypted())) { // FileDO 通过 aesIv 判断加密
+
+ if (cn.hutool.core.util.StrUtil.isBlank(code)) {
+ return CommonResult.error(HttpStatus.BAD_REQUEST.value(), "加密文件预览需要验证码 code");
+ }
+ // 验证通过:发放给 kkfile 用的短期 token(kkfile 不带登录态)
+ Long userId = getLoginUserId();
+ boolean flag = fileService.verifyCode(fileId, userId, code);
+ if(!flag){
+ return CommonResult.customize(null, HttpStatus.INTERNAL_SERVER_ERROR.value(), "验证码错误");
+ }
+
+ String token = fileService.generatePreviewToken(fileId, userId);
+
+ String baseUrl = buildPublicBaseUrl(request); // 见下方函数
+ String decryptUrl = baseUrl + "/admin-api/infra/file/preview-decrypt?fileId=" + fileId + "&token=" + token;
+ fileRespVO.setUrl(decryptUrl);
+ }
+
return success(fileRespVO);
}
@@ -162,15 +194,32 @@ public class FileController {
}
@GetMapping("/generate-download-code")
@Operation(summary = "获取下载验证码")
- public CommonResult preDownloadEncrypt(@RequestParam("fileId") Long fileId) {
+ public CommonResult preDownloadEncrypt(@RequestParam("fileId") Long fileId,
+ @RequestParam(value = "sendType", required = false) String sendType // 可选:SMS / E_OFFICE
+ ) {
Long userId = getLoginUserId();
+
+ // 解析 sendType(允许为空)
+ VerifyCodeSendType sendTypeEnum = null;
+ if (sendType != null && !sendType.trim().isEmpty()) {
+ try {
+ sendTypeEnum = VerifyCodeSendType.valueOf(sendType.trim().toUpperCase());
+ } catch (IllegalArgumentException ex) {
+ return CommonResult.error(HttpStatus.BAD_REQUEST.value(),
+ "sendType 参数不合法,可选:SMS / E_OFFICE");
+ }
+ }
+
FileDO activeFileById = fileService.getActiveFileById(fileId);
if (activeFileById == null) {
return CommonResult.error(HttpStatus.NOT_FOUND.value(), "文件不存在");
}
+
FileRespVO fileRespVO = BeanUtils.toBean(activeFileById, FileRespVO.class);
try {
- fileService.generateFileVerificationCode(fileId, userId);
+ String code = fileService.generateFileVerificationCode(fileId, userId);
+ if(sendTypeEnum != null)
+ fileService.sendVerifyCode(code, sendTypeEnum); // 发送验证码
return CommonResult.customize(fileRespVO, HttpStatus.OK.value(), "验证码已生成,请使用验证码下载文件");
} catch (ServiceException e) {
return CommonResult.customize(fileRespVO, HttpStatus.OK.value(), e.getMessage());
@@ -187,4 +236,53 @@ public class FileController {
}
return CommonResult.customize(null, HttpStatus.OK.value(), "验证码校验通过");
}
+
+ @GetMapping("/preview-decrypt")
+ @PermitAll
+ @TenantIgnore
+ @Operation(summary = "加密文件预览解密流(供 kkfile 拉取)")
+ public void previewDecrypt(@RequestParam("fileId") Long fileId,
+ @RequestParam("token") String token,
+ HttpServletResponse response) throws Exception {
+
+ boolean ok = fileService.verifyPreviewToken(fileId, token);
+ if (!ok) {
+ response.setStatus(HttpStatus.FORBIDDEN.value());
+ return;
+ }
+
+ FileDO fileDO = fileService.getActiveFileById(fileId);
+ if (fileDO == null) {
+ response.setStatus(HttpStatus.NOT_FOUND.value());
+ return;
+ }
+
+ // byte[] content = fileService.getDecryptedBytes(fileId);
+ response.setHeader("Cache-Control", "no-store");
+ response.setContentType(fileDO.getType());
+
+ String filename = java.net.URLEncoder.encode(fileDO.getName(), java.nio.charset.StandardCharsets.UTF_8);
+ response.setHeader("Content-Disposition", "inline; filename*=UTF-8''" + filename);
+
+ // cn.hutool.core.io.IoUtil.write(response.getOutputStream(), true, content);
+ fileService.writeDecryptedToStream(fileId, response.getOutputStream());
+ }
+
+ private String buildPublicBaseUrl(HttpServletRequest request) {
+ if (previewBaseUrl != null && !previewBaseUrl.isBlank()) {
+ return previewBaseUrl.endsWith("/")
+ ? previewBaseUrl.substring(0, previewBaseUrl.length() - 1)
+ : previewBaseUrl;
+ }
+ // 兜底:从请求推断
+ String scheme = request.getHeader("X-Forwarded-Proto");
+ if (scheme == null) scheme = request.getScheme();
+
+ String host = request.getHeader("X-Forwarded-Host");
+ if (host == null) host = request.getHeader("Host");
+ if (host == null) host = request.getServerName() + ":" + request.getServerPort();
+
+ return scheme + "://" + host;
+ }
+
}
diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/file/vo/file/FileRespVO.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/file/vo/file/FileRespVO.java
index 6d4d12f4..77d3d443 100644
--- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/file/vo/file/FileRespVO.java
+++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/file/vo/file/FileRespVO.java
@@ -23,8 +23,8 @@ import java.util.Date;
@Accessors(chain = true)
public class FileRespVO {
public String getUrl() {
- // 加密附件不返回 url
- if (Boolean.TRUE.equals(this.isEncrypted)) {
+ // 不可下载 或 加密附件不返回 url
+ if (Boolean.FALSE.equals(this.downloadable) || Boolean.TRUE.equals(this.isEncrypted)) {
return null;
}
// 如果 url 已经是临时下载地址(如预签名 URL),直接返回
@@ -62,8 +62,8 @@ public class FileRespVO {
private String previewUrl;
public String getPreviewUrl() {
- // 加密附件不返回 previewUrl
- if (Boolean.TRUE.equals(this.isEncrypted)) {
+ // 不可下载不返回 previewUrl
+ if (Boolean.FALSE.equals(this.downloadable) ) {
return null;
}
// 仅当 url 不为空时生成
@@ -75,7 +75,15 @@ public class FileRespVO {
if (onlinePreview == null || onlinePreview.isEmpty()) {
return null;
}
- String presignedUrl = this.getUrl();
+ // 添加加密文件预览逻辑
+ String presignedUrl = null;
+ if (Boolean.TRUE.equals(this.isEncrypted)) {
+ if (url != null && (url.startsWith("http://") || url.startsWith("https://"))) {
+ presignedUrl = url;
+ }
+ }else{
+ presignedUrl = this.getUrl();
+ }
if (presignedUrl == null || presignedUrl.isEmpty()) {
return null;
}
@@ -102,4 +110,6 @@ public class FileRespVO {
@Schema(description = "下载次数")
private Integer downloadCount;
+ @Schema(description = "是否可下载(true是,false否)")
+ private Boolean downloadable;
}
diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/dataobject/file/FileDO.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/dataobject/file/FileDO.java
index c82c0e3c..d1be1f72 100644
--- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/dataobject/file/FileDO.java
+++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/dataobject/file/FileDO.java
@@ -70,6 +70,11 @@ public class FileDO extends BaseDO {
*/
private Integer downloadCount;
+ /**
+ * 是否可下载(true是,false否)
+ */
+ private Boolean downloadable;
+
/**
* 是否加密
*
diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/mysql/file/FileMapper.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/mysql/file/FileMapper.java
index 9857cdc2..1cd9807c 100644
--- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/mysql/file/FileMapper.java
+++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/mysql/file/FileMapper.java
@@ -9,6 +9,8 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
+import java.util.List;
+
/**
* 文件操作 Mapper
*
@@ -34,7 +36,16 @@ public interface FileMapper extends BaseMapperX {
return selectFirstOne(FileDO::getHash, hash);
}
+ /**
+ * 根据configId和path查找文件信息
+ * @param configId
+ * @param path
+ * @return
+ */
+ default List selectByConfigIdAndPath(Long configId, String path){
+ return selectList(FileDO::getConfigId, configId, FileDO::getPath, path);
+ };
- @Update("UPDATE INFRA_FILE SET DOWNLOAD_COUNT = DOWNLOAD_COUNT + 1 WHERE CONFIG_ID = #{configId} AND PATH = #{path}")
- int incDownloadCount(@Param("configId") Long configId, @Param("path") String path);
+ @Update("UPDATE INFRA_FILE SET DOWNLOAD_COUNT = DOWNLOAD_COUNT + 1 WHERE ID = #{fileId}")
+ int incDownloadCount(@Param("fileId") Long fileId);
}
diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/redis/RedisKeyConstants.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/redis/RedisKeyConstants.java
index 8ffde8c1..dcaf93ff 100644
--- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/redis/RedisKeyConstants.java
+++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/redis/RedisKeyConstants.java
@@ -6,4 +6,7 @@ package com.zt.plat.module.infra.dal.redis;
public class RedisKeyConstants {
public static final String FILE_VERIFICATION_CODE = "infra:file:verification_code:%d:%d";
public static final String FILE_VERIFICATION_CODE_USER_SET = "infra:file:verification_code:user:%d";
+
+ // 加密文件预览token
+ public static final String FILE_PREVIEW_TOKEN = "infra:file:preview-token:%s";
}
diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/framework/rpc/config/RpcConfiguration.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/framework/rpc/config/RpcConfiguration.java
index 352e5313..b978f66b 100644
--- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/framework/rpc/config/RpcConfiguration.java
+++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/framework/rpc/config/RpcConfiguration.java
@@ -2,11 +2,12 @@ package com.zt.plat.module.infra.framework.rpc.config;
import com.zt.plat.module.system.api.permission.PermissionApi;
import com.zt.plat.module.system.api.permission.RoleApi;
+import com.zt.plat.module.system.api.sms.SmsSendApi;
import com.zt.plat.module.system.api.user.AdminUserApi;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;
@Configuration(value = "infraRpcConfiguration", proxyBeanMethods = false)
-@EnableFeignClients(clients = {PermissionApi.class, RoleApi.class, AdminUserApi.class})
+@EnableFeignClients(clients = {PermissionApi.class, RoleApi.class, AdminUserApi.class, SmsSendApi.class })
public class RpcConfiguration {
}
diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileService.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileService.java
index 6624810c..52d959b1 100644
--- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileService.java
+++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileService.java
@@ -1,5 +1,6 @@
package com.zt.plat.module.infra.service.file;
+import com.zt.plat.framework.common.enums.VerifyCodeSendType;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
import com.zt.plat.module.infra.controller.admin.file.vo.file.FilePageReqVO;
@@ -10,6 +11,8 @@ import com.zt.plat.module.infra.dal.dataobject.file.FileDO;
import jakarta.validation.constraints.NotEmpty;
import lombok.SneakyThrows;
+import java.io.OutputStream;
+
/**
* 文件 Service 接口
*
@@ -72,6 +75,14 @@ public interface FileService {
*/
String generateFileVerificationCode(Long fileId, Long userId);
+
+ /**
+ * 发送验证码
+ * @param code 验证码
+ * @param verifyCodeSendType 发送类型
+ */
+ void sendVerifyCode(String code, VerifyCodeSendType verifyCodeSendType);
+
/**
* 校验验证码并返回解密后的文件内容
*/
@@ -119,4 +130,31 @@ public interface FileService {
* @param path
*/
void incDownloadCount(Long configId, String path);
+
+ /**
+ * 更新文件下载次数
+ * @param fileId
+ */
+ void incDownloadCount(Long fileId);
+
+ /**
+ * 临时生成文件预览token
+ * @param fileId 文件ID
+ * @param userId 用户ID
+ * @return 临时token
+ */
+ String generatePreviewToken(Long fileId, Long userId);
+
+ /**
+ * 验证文件预览token
+ * @param fileId 文件ID
+ * @param token 用户ID
+ * @return 临时token
+ */
+ boolean verifyPreviewToken(Long fileId, String token);
+
+ /**
+ * 校验预览 token 后,将文件内容解密并写入输出流(用于预览)
+ */
+ void writeDecryptedToStream(Long fileId, OutputStream outputStream) throws Exception;
}
diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileServiceImpl.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileServiceImpl.java
index 14f42c77..d890a6c2 100644
--- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileServiceImpl.java
+++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileServiceImpl.java
@@ -2,12 +2,17 @@ package com.zt.plat.module.infra.service.file;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.common.annotations.VisibleForTesting;
+import com.zt.plat.framework.common.enums.VerifyCodeSendType;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.common.util.object.BeanUtils;
+import com.zt.plat.framework.security.core.LoginUser;
+import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils;
import com.zt.plat.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
import com.zt.plat.module.infra.controller.admin.file.vo.file.FilePageReqVO;
import com.zt.plat.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
@@ -20,6 +25,9 @@ import com.zt.plat.module.infra.framework.file.core.client.FileClient;
import com.zt.plat.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO;
import com.zt.plat.module.infra.framework.file.core.utils.FileTypeUtils;
import com.zt.plat.module.infra.util.VerificationCodeUtil;
+import com.zt.plat.module.system.api.permission.RoleApi;
+import com.zt.plat.module.system.api.sms.SmsSendApi;
+import com.zt.plat.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
@@ -30,8 +38,9 @@ import org.springframework.stereotype.Service;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
+import java.io.OutputStream;
import java.security.SecureRandom;
-import java.util.Base64;
+import java.util.*;
import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN;
import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -50,6 +59,15 @@ public class FileServiceImpl implements FileService {
private String aesKey;
@Value("${zt.verify-code:}")
private String fixedVerifyCode;
+ // 加密文件预览token过期时间
+ @Value("${zt.file.preview-expire-seconds:300}")
+ private Integer previewExpireSeconds;
+
+ @Resource
+ private RoleApi roleApi;
+
+ @Resource
+ private SmsSendApi smsSendApi;
@Resource
private StringRedisTemplate stringRedisTemplate;
@@ -62,6 +80,31 @@ public class FileServiceImpl implements FileService {
return VerificationCodeUtil.generateCode(codeKey, userSetKey, stringRedisTemplate);
}
+ @Override
+ public void sendVerifyCode(String code, VerifyCodeSendType verifyCodeSendType) {
+ if(verifyCodeSendType == null) return;
+ LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
+ Assert.notNull(loginUser,"用户未登录或权限不足!");
+ if(loginUser == null) return;
+ if (VerifyCodeSendType.SMS.equals(verifyCodeSendType)) {
+ Map templateParams = new HashMap<>();
+ templateParams.put("code",code);
+ SmsSendSingleToUserReqDTO smsSendReqDTO = new SmsSendSingleToUserReqDTO();
+ if(loginUser.getInfo().get(LoginUser.INFO_KEY_PHONE)!=null)
+ smsSendReqDTO.setMobile(loginUser.getInfo().get(LoginUser.INFO_KEY_PHONE));
+ smsSendReqDTO.setUserId(loginUser.getId());
+ smsSendReqDTO.setTemplateCode("test_02");
+ smsSendReqDTO.setTemplateParams(templateParams);
+ smsSendApi.sendSingleSmsToAdmin(smsSendReqDTO);
+ return;
+ }
+
+ if (VerifyCodeSendType.E_OFFICE.equals(verifyCodeSendType)) {
+ // TODO 预留实现接口
+ return;
+ }
+ }
+
@Override
public byte[] verifyCodeAndGetFile(Long fileId, Long userId, String code) throws Exception {
// 开发模式下,验证码直接获取配置进行比对
@@ -336,7 +379,46 @@ public class FileServiceImpl implements FileService {
@Override
public void incDownloadCount(Long configId, String path) {
- fileMapper.incDownloadCount(configId, path);
+ List fileList = fileMapper.selectByConfigIdAndPath(configId, path);
+ if(fileList != null && !fileList.isEmpty())
+ incDownloadCount(fileList.get(0).getId());
}
+ @Override
+ public void incDownloadCount(Long fileId) {
+ fileMapper.incDownloadCount(fileId);
+ }
+
+ @Override
+ public String generatePreviewToken(Long fileId, Long userId) {
+ // 你也可以加:validateFileExists(fileId)
+ String token = UUID.randomUUID().toString().replace("-", "");
+ String key = String.format(RedisKeyConstants.FILE_PREVIEW_TOKEN, token);
+ stringRedisTemplate.opsForValue().set(key, String.valueOf(fileId),
+ java.time.Duration.ofSeconds(previewExpireSeconds));
+ return token;
+ }
+
+ @Override
+ public boolean verifyPreviewToken(Long fileId, String token) {
+ String key = String.format(RedisKeyConstants.FILE_PREVIEW_TOKEN, token);
+ String val = stringRedisTemplate.opsForValue().get(key);
+ if (val == null || !val.equals(String.valueOf(fileId))) {
+ return false;
+ }
+ // 可选:单次使用更安全
+ // stringRedisTemplate.delete(key);
+ return true;
+ }
+
+ @Override
+ public void writeDecryptedToStream(Long fileId, OutputStream os) throws Exception {
+ FileDO fileDO = getActiveFileById(fileId);
+ if (fileDO == null) {
+ throw exception(FILE_NOT_EXISTS);
+ }
+
+ byte[] decrypted = getDecryptedBytes(fileId);
+ IoUtil.write(os, true, decrypted);
+ }
}
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/dept/DeptApi.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/dept/DeptApi.java
index abc53972..da378c52 100644
--- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/dept/DeptApi.java
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/dept/DeptApi.java
@@ -2,6 +2,7 @@ package com.zt.plat.module.system.api.dept;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.util.collection.CollectionUtils;
+import com.zt.plat.framework.common.util.object.BeanUtils;
import com.zt.plat.module.system.api.dept.dto.*;
import com.zt.plat.module.system.enums.ApiConstants;
import io.swagger.v3.oas.annotations.Operation;
@@ -15,6 +16,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
+import static com.zt.plat.framework.common.pojo.CommonResult.success;
+
@FeignClient(name = ApiConstants.NAME) // TODO ZT:fallbackFactory =
@Tag(name = "RPC 服务 - 部门")
public interface DeptApi {
@@ -86,6 +89,11 @@ public interface DeptApi {
@Parameter(name = "userId", description = "用户编号", example = "1", required = true)
CommonResult> getCompanyDeptInfoListByUserId(@RequestParam("userId") Long userId);
+ @GetMapping(PREFIX+"/up-find-company-node")
+ @Operation(summary = "获取公司节点信息", description = "通过部门编号,向上追溯部门信息,直到上级部门是公司,返回追溯到的部门信息列表")
+ @Parameter(name = "deptId", description = "部门编号", required = true, example = "1024")
+ CommonResult> upFindCompanyNode(@RequestParam("deptId") Long deptId);
+
// ========== 数据同步专用接口 ==========
@PostMapping(PREFIX + "/sync")
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApi.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApi.java
index 9181a071..84851de0 100644
--- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApi.java
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApi.java
@@ -46,18 +46,18 @@ public interface IWorkIntegrationApi {
@PostMapping(PREFIX + "/hr/subcompany/page")
@Operation(summary = "获取 iWork 分部列表")
- CommonResult listSubcompanies(@RequestBody IWorkOrgPageReqDTO reqDTO);
+ CommonResult listSubcompanies(@RequestBody IWorkSubcompanyQueryReqDTO reqDTO);
@PostMapping(PREFIX + "/hr/department/page")
@Operation(summary = "获取 iWork 部门列表")
- CommonResult listDepartments(@RequestBody IWorkOrgPageReqDTO reqDTO);
+ CommonResult listDepartments(@RequestBody IWorkDepartmentQueryReqDTO reqDTO);
@PostMapping(PREFIX + "/hr/job-title/page")
@Operation(summary = "获取 iWork 岗位列表")
- CommonResult listJobTitles(@RequestBody IWorkOrgPageReqDTO reqDTO);
+ CommonResult listJobTitles(@RequestBody IWorkJobTitleQueryReqDTO reqDTO);
@PostMapping(PREFIX + "/hr/user/page")
@Operation(summary = "获取 iWork 人员列表")
- CommonResult listUsers(@RequestBody IWorkOrgPageReqDTO reqDTO);
+ CommonResult listUsers(@RequestBody IWorkUserQueryReqDTO reqDTO);
}
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkDepartmentQueryReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkDepartmentQueryReqDTO.java
new file mode 100644
index 00000000..7072bb1e
--- /dev/null
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkDepartmentQueryReqDTO.java
@@ -0,0 +1,48 @@
+package com.zt.plat.module.system.api.iwork.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+/**
+ * iWork 部门查询参数。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class IWorkDepartmentQueryReqDTO extends IWorkOrgBaseQueryReqDTO {
+
+ @JsonProperty("departmentcode")
+ @Schema(description = "部门编号")
+ private String departmentCode;
+
+ @JsonProperty("departmentname")
+ @Schema(description = "部门名称")
+ private String departmentName;
+
+ @JsonProperty("subcompanyid1")
+ @Schema(description = "分部 ID")
+ private String subcompanyId1;
+
+ @JsonProperty("created")
+ @Schema(description = "创建时间戳(>=)")
+ private String created;
+
+ @JsonProperty("modified")
+ @Schema(description = "修改时间戳(>=)")
+ private String modified;
+
+ @JsonProperty("canceled")
+ @Schema(description = "封存标志,默认查询非封存数据。1:封存")
+ private String canceled;
+
+ @JsonProperty("custom_data")
+ @Schema(description = "自定义字段列表(逗号分隔)")
+ private String customData;
+
+ @JsonProperty("id")
+ @Schema(description = "OA 部门 ID")
+ private String id;
+}
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkJobTitleQueryReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkJobTitleQueryReqDTO.java
new file mode 100644
index 00000000..553a614c
--- /dev/null
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkJobTitleQueryReqDTO.java
@@ -0,0 +1,32 @@
+package com.zt.plat.module.system.api.iwork.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+/**
+ * iWork 岗位查询参数。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class IWorkJobTitleQueryReqDTO extends IWorkOrgBaseQueryReqDTO {
+
+ @JsonProperty("jobtitlename")
+ @Schema(description = "岗位名称")
+ private String jobTitleName;
+
+ @JsonProperty("created")
+ @Schema(description = "创建时间戳(>=)")
+ private String created;
+
+ @JsonProperty("modified")
+ @Schema(description = "修改时间戳(>=)")
+ private String modified;
+
+ @JsonProperty("id")
+ @Schema(description = "岗位 ID")
+ private String id;
+}
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOrgBaseQueryReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOrgBaseQueryReqDTO.java
new file mode 100644
index 00000000..aeddfdc7
--- /dev/null
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOrgBaseQueryReqDTO.java
@@ -0,0 +1,17 @@
+package com.zt.plat.module.system.api.iwork.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * iWork 人力组织查询基础参数。
+ */
+@Data
+public class IWorkOrgBaseQueryReqDTO {
+
+ @Schema(description = "当前页码", example = "1")
+ private Integer curpage;
+
+ @Schema(description = "每页条数", example = "20")
+ private Integer pagesize;
+}
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOrgPageReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOrgPageReqDTO.java
deleted file mode 100644
index 60fe8231..00000000
--- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOrgPageReqDTO.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.zt.plat.module.system.api.iwork.dto;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-/**
- * iWork 人力组织分页查询通用请求 DTO
- */
-@Data
-public class IWorkOrgPageReqDTO {
-
- @Schema(description = "页码", example = "1", requiredMode = Schema.RequiredMode.REQUIRED)
- private Integer pageNo;
-
- @Schema(description = "每页大小", example = "20", requiredMode = Schema.RequiredMode.REQUIRED)
- private Integer pageSize;
-
- @Schema(description = "关键字过滤")
- private String keyword;
-
-}
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkSubcompanyQueryReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkSubcompanyQueryReqDTO.java
new file mode 100644
index 00000000..0ae343d1
--- /dev/null
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkSubcompanyQueryReqDTO.java
@@ -0,0 +1,40 @@
+package com.zt.plat.module.system.api.iwork.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+/**
+ * iWork 分部查询参数。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class IWorkSubcompanyQueryReqDTO extends IWorkOrgBaseQueryReqDTO {
+
+ @JsonProperty("subcompanycode")
+ @Schema(description = "分部编号")
+ private String subcompanyCode;
+
+ @JsonProperty("subcompanyname")
+ @Schema(description = "分部名称")
+ private String subcompanyName;
+
+ @JsonProperty("modified")
+ @Schema(description = "修改时间戳(>=)")
+ private String modified;
+
+ @JsonProperty("canceled")
+ @Schema(description = "封存标志,默认查询非封存数据。1:封存")
+ private String canceled;
+
+ @JsonProperty("custom_data")
+ @Schema(description = "自定义字段列表(逗号分隔)")
+ private String customData;
+
+ @JsonProperty("id")
+ @Schema(description = "OA 分部 ID")
+ private String id;
+}
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkUserQueryReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkUserQueryReqDTO.java
new file mode 100644
index 00000000..d15b4366
--- /dev/null
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkUserQueryReqDTO.java
@@ -0,0 +1,60 @@
+package com.zt.plat.module.system.api.iwork.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+/**
+ * iWork 人员查询参数。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class IWorkUserQueryReqDTO extends IWorkOrgBaseQueryReqDTO {
+
+ @JsonProperty("workcode")
+ @Schema(description = "人员编号")
+ private String workCode;
+
+ @JsonProperty("subcompanyid1")
+ @Schema(description = "分部 ID")
+ private String subcompanyId1;
+
+ @JsonProperty("departmentid")
+ @Schema(description = "部门 ID")
+ private String departmentId;
+
+ @JsonProperty("jobtitleid")
+ @Schema(description = "岗位 ID")
+ private String jobTitleId;
+
+ @JsonProperty("id")
+ @Schema(description = "人员 ID")
+ private String id;
+
+ @JsonProperty("loginid")
+ @Schema(description = "登录名")
+ private String loginId;
+
+ @JsonProperty("created")
+ @Schema(description = "创建时间戳(>=)")
+ private String created;
+
+ @JsonProperty("modified")
+ @Schema(description = "修改时间戳(>=)")
+ private String modified;
+
+ @JsonProperty("base_custom_data")
+ @Schema(description = "基本信息自定义字段列表(逗号分隔)")
+ private String baseCustomData;
+
+ @JsonProperty("person_custom_data")
+ @Schema(description = "个人信息自定义字段列表(逗号分隔)")
+ private String personCustomData;
+
+ @JsonProperty("work_custom_data")
+ @Schema(description = "工作信息自定义字段列表(逗号分隔)")
+ private String workCustomData;
+}
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowCreateReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowCreateReqDTO.java
index 013eea65..95a10c6d 100644
--- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowCreateReqDTO.java
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowCreateReqDTO.java
@@ -36,9 +36,9 @@ public class IWorkWorkflowCreateReqDTO extends IWorkBaseReqDTO {
@Schema(description = "用印材料附件 URL(必填)")
private String xyywjUrl;
-
- @Schema(description = "用印材料附件文件名(必填)")
- private String xyywjFileName;
+
+ @Schema(description = "业务回调标识(回调分发使用,≤255 字符)")
+ private String bizCallbackKey;
@Schema(description = "用印事项")
private String yysx;
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/push/ExternalPushConfigApi.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/push/ExternalPushConfigApi.java
new file mode 100644
index 00000000..5020a83f
--- /dev/null
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/push/ExternalPushConfigApi.java
@@ -0,0 +1,39 @@
+package com.zt.plat.module.system.api.push;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.module.system.enums.ApiConstants;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+/**
+ * 外部系统推送配置 Feign API
+ *
+ * @author ZT Cloud
+ */
+@FeignClient(name = ApiConstants.NAME)
+@Tag(name = "RPC 服务 - 外部系统推送配置")
+public interface ExternalPushConfigApi {
+
+ String PREFIX = ApiConstants.PREFIX + "/external-push-config";
+
+ /**
+ * 判断是否允许推送到外部系统
+ *
+ * @param companyId 公司编号(可选,为 null 时表示不限制公司)
+ * @param deptId 部门编号(可选,为 null 时只按公司配置判断)
+ * @param businessType 业务类型(可选:PURCHASE/SALE/PRODUCTION,为 null 时表示所有业务类型)
+ * @param externalSystem 外部系统标识(可选:ERP/IWORK/等,为 null 时表示所有外部系统)
+ * @return 是否允许推送(true=允许,false=禁止,默认 true)
+ */
+ @GetMapping(PREFIX + "/is-push-enabled")
+ @Operation(summary = "判断是否允许推送到外部系统")
+ CommonResult isPushEnabled(
+ @RequestParam(value = "companyId", required = false) @Parameter(description = "公司编号") Long companyId,
+ @RequestParam(value = "deptId", required = false) @Parameter(description = "部门编号") Long deptId,
+ @RequestParam(value = "businessType", required = false) @Parameter(description = "业务类型") String businessType,
+ @RequestParam(value = "externalSystem", required = false) @Parameter(description = "外部系统标识") String externalSystem);
+}
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/ErrorCodeConstants.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/ErrorCodeConstants.java
index 41f4e40e..b9231f52 100644
--- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/ErrorCodeConstants.java
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/ErrorCodeConstants.java
@@ -230,4 +230,11 @@ public interface ErrorCodeConstants {
// ========== 门户网站 1-002-033-000 ==========
ErrorCode PORTAL_NOT_EXISTS = new ErrorCode(1_002_033_000, "门户不存在");
+ // ========== 外部系统推送配置 1_002_034_000 ==========
+ ErrorCode EXTERNAL_PUSH_CONFIG_NOT_EXISTS = new ErrorCode(1_002_034_001, "外部系统推送配置不存在");
+ ErrorCode EXTERNAL_PUSH_CONFIG_EXISTS = new ErrorCode(1_002_034_002, "该配置已存在");
+ ErrorCode EXTERNAL_PUSH_CONFIG_COMPANY_INVALID = new ErrorCode(1_002_034_003, "公司编号必须是公司节点(is_company=1)");
+ ErrorCode EXTERNAL_PUSH_CONFIG_DEPT_INVALID = new ErrorCode(1_002_034_004, "部门编号必须是部门节点(is_company=0)");
+ ErrorCode EXTERNAL_PUSH_CONFIG_BUSINESS_TYPE_INVALID = new ErrorCode(1_002_034_005, "业务类型无效,仅支持 PURCHASE/SALE/PRODUCTION");
+
}
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleConditionEnum.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleConditionEnum.java
new file mode 100644
index 00000000..9064c4d6
--- /dev/null
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleConditionEnum.java
@@ -0,0 +1,60 @@
+package com.zt.plat.module.system.enums.permission;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 数据规则条件枚举
+ *
+ * 用于菜单数据规则的条件类型
+ *
+ * @author ZT
+ */
+@Getter
+@AllArgsConstructor
+public enum DataRuleConditionEnum {
+
+ EQ("=", "等于"),
+ NE("!=", "不等于"),
+ GT(">", "大于"),
+ GE(">=", "大于等于"),
+ LT("<", "小于"),
+ LE("<=", "小于等于"),
+ IN("IN", "包含"),
+ NOT_IN("NOT_IN", "不包含"),
+ LIKE("LIKE", "模糊匹配"),
+ LEFT_LIKE("LEFT_LIKE", "左模糊"),
+ RIGHT_LIKE("RIGHT_LIKE", "右模糊"),
+ NOT_LIKE("NOT_LIKE", "不匹配"),
+ IS_NULL("IS_NULL", "为空"),
+ IS_NOT_NULL("IS_NOT_NULL", "不为空"),
+ SQL_RULE("SQL_RULE", "自定义SQL");
+
+ /**
+ * 条件符号
+ */
+ private final String condition;
+
+ /**
+ * 条件描述
+ */
+ private final String description;
+
+ /**
+ * 根据条件符号查找枚举
+ *
+ * @param condition 条件符号
+ * @return 枚举值
+ */
+ public static DataRuleConditionEnum findByCondition(String condition) {
+ if (condition == null) {
+ return null;
+ }
+ for (DataRuleConditionEnum value : values()) {
+ if (value.condition.equals(condition)) {
+ return value;
+ }
+ }
+ return null;
+ }
+}
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleVariableEnum.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleVariableEnum.java
new file mode 100644
index 00000000..1c5881cd
--- /dev/null
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleVariableEnum.java
@@ -0,0 +1,53 @@
+package com.zt.plat.module.system.enums.permission;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 数据规则变量枚举
+ *
+ * 用于菜单数据规则的变量替换
+ *
+ * @author ZT
+ */
+@Getter
+@AllArgsConstructor
+public enum DataRuleVariableEnum {
+
+ USER_ID("#{userId}", "当前用户ID"),
+ USERNAME("#{username}", "当前用户名"),
+ DEPT_ID("#{deptId}", "当前用户部门ID"),
+ DEPT_IDS("#{deptIds}", "当前用户所有部门ID"),
+ ORG_CODE("#{orgCode}", "当前用户组织编码"),
+ TENANT_ID("#{tenantId}", "当前租户ID"),
+ CURRENT_DATE("#{currentDate}", "当前日期"),
+ CURRENT_TIME("#{currentTime}", "当前时间");
+
+ /**
+ * 变量名
+ */
+ private final String variable;
+
+ /**
+ * 变量描述
+ */
+ private final String description;
+
+ /**
+ * 根据变量名查找枚举
+ *
+ * @param variable 变量名
+ * @return 枚举值
+ */
+ public static DataRuleVariableEnum findByVariable(String variable) {
+ if (variable == null) {
+ return null;
+ }
+ for (DataRuleVariableEnum value : values()) {
+ if (value.variable.equals(variable)) {
+ return value;
+ }
+ }
+ return null;
+ }
+}
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/push/BusinessTypeEnum.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/push/BusinessTypeEnum.java
new file mode 100644
index 00000000..441a239a
--- /dev/null
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/push/BusinessTypeEnum.java
@@ -0,0 +1,70 @@
+package com.zt.plat.module.system.enums.push;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 业务类型枚举
+ *
+ * @author ZT Cloud
+ */
+@AllArgsConstructor
+@Getter
+public enum BusinessTypeEnum {
+
+ PURCHASE(1, "PURCHASE", "采购"),
+ SALE(2, "SALE", "销售"),
+ PRODUCTION(3, "PRODUCTION", "生产");
+
+ /**
+ * 类型
+ */
+ private final Integer type;
+
+ /**
+ * 编码
+ */
+ private final String code;
+
+ /**
+ * 名称
+ */
+ private final String name;
+
+ /**
+ * 根据编码获取枚举
+ */
+ public static BusinessTypeEnum valueOfCode(String code) {
+ if (code == null) {
+ return null;
+ }
+ for (BusinessTypeEnum value : BusinessTypeEnum.values()) {
+ if (value.getCode().equals(code)) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 根据类型获取枚举
+ */
+ public static BusinessTypeEnum valueOfType(Integer type) {
+ if (type == null) {
+ return null;
+ }
+ for (BusinessTypeEnum value : BusinessTypeEnum.values()) {
+ if (value.getType().equals(type)) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 验证编码是否有效
+ */
+ public static boolean isValidCode(String code) {
+ return valueOfCode(code) != null;
+ }
+}
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackMessage.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackMessage.java
new file mode 100644
index 00000000..4eb80a2b
--- /dev/null
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackMessage.java
@@ -0,0 +1,23 @@
+package com.zt.plat.module.system.mq.iwork;
+
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
+public class IWorkBizCallbackMessage {
+
+ /** 统一回调主题 */
+ public static final String TOPIC = "SYSTEM_IWORK_BIZ_CALLBACK";
+
+ /** requestId 唯一标识 */
+ private String requestId;
+ /** 业务回调标识 */
+ private String bizCallbackKey;
+ /** 回调负载对象(可为 Map) */
+ private Object payload;
+ /** 当前尝试次数,从 0 开始 */
+ private int attempt;
+ /** 最大尝试次数 */
+ private int maxAttempts;
+}
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackResultMessage.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackResultMessage.java
new file mode 100644
index 00000000..2a34f72c
--- /dev/null
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackResultMessage.java
@@ -0,0 +1,31 @@
+package com.zt.plat.module.system.mq.iwork;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class IWorkBizCallbackResultMessage {
+
+ /** 统一回调结果主题 */
+ public static final String TOPIC = "SYSTEM_IWORK_BIZ_CALLBACK_RESULT";
+
+ /** requestId 唯一标识 */
+ private String requestId;
+ /** 业务回调标识,对应发送方设置的 tag */
+ private String bizCallbackKey;
+ /** 是否成功 */
+ private boolean success;
+ /** 错误消息 */
+ private String errorMessage;
+ /** 当前尝试次数(业务侧可回传) */
+ private int attempt;
+ /** 最大尝试次数 */
+ private int maxAttempts;
+ /** 回调负载(用于 system 端重试再投递) */
+ private Object payload;
+}
diff --git a/zt-module-system/zt-module-system-server-app/src/main/resources/logback-spring.xml b/zt-module-system/zt-module-system-server-app/src/main/resources/logback-spring.xml
index d68b13bb..b592120f 100644
--- a/zt-module-system/zt-module-system-server-app/src/main/resources/logback-spring.xml
+++ b/zt-module-system/zt-module-system-server-app/src/main/resources/logback-spring.xml
@@ -91,6 +91,10 @@
+
+
+
+
@@ -103,8 +107,4 @@
-
-
-
-
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/dept/DeptApiImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/dept/DeptApiImpl.java
index bed2d2b6..8b87fd52 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/dept/DeptApiImpl.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/dept/DeptApiImpl.java
@@ -4,6 +4,8 @@ import com.zt.plat.framework.common.enums.CommonStatusEnum;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.pojo.CompanyDeptInfo;
import com.zt.plat.framework.common.util.object.BeanUtils;
+import com.zt.plat.framework.datapermission.core.annotation.CompanyDataPermissionIgnore;
+import com.zt.plat.framework.datapermission.core.annotation.DeptDataPermissionIgnore;
import com.zt.plat.module.system.api.dept.dto.*;
import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptListReqVO;
import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO;
@@ -78,6 +80,8 @@ public class DeptApiImpl implements DeptApi {
}
@Override
+ @CompanyDataPermissionIgnore
+ @DeptDataPermissionIgnore
public CommonResult getDept(Long id) {
DeptDO dept = deptService.getDept(id);
return success(BeanUtils.toBean(dept, DeptRespDTO.class));
@@ -107,6 +111,12 @@ public class DeptApiImpl implements DeptApi {
return success(BeanUtils.toBean(companyDeptInfos, CompanyDeptInfoRespDTO.class));
}
+ @Override
+ public CommonResult> upFindCompanyNode(Long deptId) {
+ List depts = deptService.upFindCompanyNode(deptId);
+ return success(BeanUtils.toBean(depts, DeptRespDTO.class));
+ }
+
// ========== 数据同步专用接口 ==========
@Override
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApiImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApiImpl.java
index b12f91f9..eefc2e95 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApiImpl.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApiImpl.java
@@ -77,7 +77,7 @@ public class IWorkIntegrationApiImpl implements IWorkIntegrationApi {
// ----------------- 人力组织分页接口 -----------------
@Override
- public CommonResult listSubcompanies(IWorkOrgPageReqDTO reqDTO) {
+ public CommonResult listSubcompanies(IWorkSubcompanyQueryReqDTO reqDTO) {
IWorkSubcompanyQueryReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkSubcompanyQueryReqVO.class);
IWorkHrSubcompanyPageRespVO respVO = orgRestService.listSubcompanies(reqVO);
IWorkHrSubcompanyPageRespDTO respDTO = BeanUtils.toBean(respVO, IWorkHrSubcompanyPageRespDTO.class);
@@ -85,7 +85,7 @@ public class IWorkIntegrationApiImpl implements IWorkIntegrationApi {
}
@Override
- public CommonResult listDepartments(IWorkOrgPageReqDTO reqDTO) {
+ public CommonResult listDepartments(IWorkDepartmentQueryReqDTO reqDTO) {
IWorkDepartmentQueryReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkDepartmentQueryReqVO.class);
IWorkHrDepartmentPageRespVO respVO = orgRestService.listDepartments(reqVO);
IWorkHrDepartmentPageRespDTO respDTO = BeanUtils.toBean(respVO, IWorkHrDepartmentPageRespDTO.class);
@@ -93,7 +93,7 @@ public class IWorkIntegrationApiImpl implements IWorkIntegrationApi {
}
@Override
- public CommonResult listJobTitles(IWorkOrgPageReqDTO reqDTO) {
+ public CommonResult listJobTitles(IWorkJobTitleQueryReqDTO reqDTO) {
IWorkJobTitleQueryReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkJobTitleQueryReqVO.class);
IWorkHrJobTitlePageRespVO respVO = orgRestService.listJobTitles(reqVO);
IWorkHrJobTitlePageRespDTO respDTO = BeanUtils.toBean(respVO, IWorkHrJobTitlePageRespDTO.class);
@@ -101,7 +101,7 @@ public class IWorkIntegrationApiImpl implements IWorkIntegrationApi {
}
@Override
- public CommonResult listUsers(IWorkOrgPageReqDTO reqDTO) {
+ public CommonResult listUsers(IWorkUserQueryReqDTO reqDTO) {
IWorkUserQueryReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkUserQueryReqVO.class);
IWorkHrUserPageRespVO respVO = orgRestService.listUsers(reqVO);
IWorkHrUserPageRespDTO respDTO = BeanUtils.toBean(respVO, IWorkHrUserPageRespDTO.class);
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/push/ExternalPushConfigApiImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/push/ExternalPushConfigApiImpl.java
new file mode 100644
index 00000000..7fac6614
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/push/ExternalPushConfigApiImpl.java
@@ -0,0 +1,28 @@
+package com.zt.plat.module.system.api.push;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.module.system.service.push.ExternalPushConfigService;
+import jakarta.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.RestController;
+
+import static com.zt.plat.framework.common.pojo.CommonResult.success;
+
+/**
+ * 外部系统推送配置 Feign API 实现类
+ *
+ * @author ZT Cloud
+ */
+@RestController
+@Validated
+public class ExternalPushConfigApiImpl implements ExternalPushConfigApi {
+
+ @Resource
+ private ExternalPushConfigService externalPushConfigService;
+
+ @Override
+ public CommonResult isPushEnabled(Long companyId, Long deptId, String businessType, String externalSystem) {
+ Boolean result = externalPushConfigService.isPushEnabled(companyId, deptId, businessType, externalSystem);
+ return success(result);
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthLoginReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthLoginReqVO.java
index 2e4e2813..61d840cf 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthLoginReqVO.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthLoginReqVO.java
@@ -6,7 +6,6 @@ import com.zt.plat.module.system.enums.social.SocialTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotEmpty;
-import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -22,8 +21,8 @@ public class AuthLoginReqVO extends CaptchaVerificationReqVO {
@Schema(description = "账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "ztyuanma")
@NotEmpty(message = "登录账号不能为空")
- @Length(min = 4, max = 16, message = "账号长度为 4-16 位")
- @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
+ @Length(min = 1, max = 16, message = "账号长度为 1-16 位")
+// @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
private String username;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao")
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthTestLoginReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthTestLoginReqVO.java
index 5e9ab950..b43587ee 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthTestLoginReqVO.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthTestLoginReqVO.java
@@ -2,7 +2,6 @@ package com.zt.plat.module.system.controller.admin.auth.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
-import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -19,7 +18,7 @@ public class AuthTestLoginReqVO {
@Schema(description = "账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "ztyuanma")
@NotEmpty(message = "登录账号不能为空")
@Length(min = 4, max = 16, message = "账号长度为 4-16 位")
- @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
+// @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
private String username;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao")
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/DeptController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/DeptController.java
index 54ba9b37..c2da8184 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/DeptController.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/DeptController.java
@@ -165,4 +165,11 @@ public class DeptController {
return success(BeanUtils.toBean(companyDeptInfos, CompanyDeptInfoRespDTO.class));
}
+ @GetMapping("/up-find-company-node")
+ @Operation(summary = "获取公司节点信息", description = "通过部门编号,向上追溯部门信息,直到上级部门是公司,返回追溯到的部门信息列表")
+ @Parameter(name = "deptId", description = "部门编号", required = true, example = "1024")
+ public CommonResult> upFindCompanyNode(@RequestParam("deptId") Long deptId) {
+ List list = deptService.upFindCompanyNode(deptId);
+ return success(BeanUtils.toBean(list, DeptRespVO.class));
+ }
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/IWorkIntegrationController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/IWorkIntegrationController.java
index 88adb502..f3b6f08a 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/IWorkIntegrationController.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/IWorkIntegrationController.java
@@ -3,6 +3,7 @@ package com.zt.plat.module.system.controller.admin.integration.iwork;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.*;
+import com.zt.plat.module.system.service.integration.iwork.IWorkCallbackLogService;
import com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationService;
import com.zt.plat.module.system.service.integration.iwork.IWorkOrgRestService;
import com.zt.plat.module.system.service.integration.iwork.IWorkSyncService;
@@ -19,6 +20,8 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import org.springframework.security.access.prepost.PreAuthorize;
+
import static com.zt.plat.framework.common.pojo.CommonResult.success;
/**
* 提供统一 iWork 流程能力的管理端接口。
@@ -34,6 +37,7 @@ public class IWorkIntegrationController {
private final IWorkIntegrationService integrationService;
private final IWorkOrgRestService orgRestService;
private final IWorkSyncService syncService;
+ private final IWorkCallbackLogService callbackLogService;
@PostMapping("/auth/register")
@Operation(summary = "注册 iWork 凭证,获取服务端公钥与 secret")
@@ -87,6 +91,39 @@ public class IWorkIntegrationController {
return success(integrationService.voidWorkflow(reqVO));
}
+ @PreAuthorize("@ss.hasPermission('system:iwork:log:query')")
+ @PostMapping("/log/page")
+ @Operation(summary = "iWork 回调日志分页查询")
+ public CommonResult> pageLogs(@Valid @RequestBody IWorkCallbackLogPageReqVO reqVO) {
+ com.zt.plat.framework.common.pojo.PageResult page = callbackLogService.page(reqVO);
+ java.util.List mapped = new java.util.ArrayList<>();
+ for (com.zt.plat.module.system.dal.dataobject.iwork.IWorkSealLogDO log : page.getList()) {
+ IWorkCallbackLogRespVO vo = new IWorkCallbackLogRespVO();
+ vo.setId(log.getId());
+ vo.setRequestId(log.getRequestId());
+ vo.setBusinessCode(log.getBusinessCode());
+ vo.setBizCallbackKey(log.getBizCallbackKey());
+ vo.setStatus(log.getStatus());
+ vo.setRetryCount(log.getRetryCount());
+ vo.setMaxRetry(log.getMaxRetry());
+ vo.setLastErrorMessage(log.getLastErrorMessage());
+ vo.setRawCallback(log.getRawCallback());
+ vo.setLastCallbackTime(log.getLastCallbackTime());
+ vo.setCreateTime(log.getCreateTime());
+ vo.setUpdateTime(log.getUpdateTime());
+ mapped.add(vo);
+ }
+ return success(new com.zt.plat.framework.common.pojo.PageResult<>(mapped, page.getTotal(), page.getSummary()));
+ }
+
+ @PreAuthorize("@ss.hasPermission('system:iwork:log:retry')")
+ @PostMapping("/log/retry")
+ @Operation(summary = "iWork 回调手工重试")
+ public CommonResult retry(@Valid @RequestBody IWorkWorkflowVoidReqVO reqVO) {
+ callbackLogService.resetAndDispatch(reqVO.getRequestId());
+ return success(true);
+ }
+
// ----------------- 人力组织接口 -----------------
@PostMapping("/hr/subcompany/page")
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkCallbackLogPageReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkCallbackLogPageReqVO.java
new file mode 100644
index 00000000..0eefd069
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkCallbackLogPageReqVO.java
@@ -0,0 +1,39 @@
+package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
+
+import com.zt.plat.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static com.zt.plat.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - iWork 用印回调日志分页查询")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class IWorkCallbackLogPageReqVO extends PageParam {
+
+ @Schema(description = "requestId")
+ private String requestId;
+
+ @Schema(description = "业务单号")
+ private String businessCode;
+
+ @Schema(description = "业务回调标识")
+ private String bizCallbackKey;
+
+ @Schema(description = "状态")
+ private Integer status;
+
+ @Schema(description = "创建时间范围")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+
+ @Schema(description = "最后回调时间范围")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] lastCallbackTime;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkCallbackLogRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkCallbackLogRespVO.java
new file mode 100644
index 00000000..577908f9
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkCallbackLogRespVO.java
@@ -0,0 +1,24 @@
+package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "iWork 用印回调日志响应 VO")
+@Data
+public class IWorkCallbackLogRespVO {
+
+ private Long id;
+ private String requestId;
+ private String businessCode;
+ private String bizCallbackKey;
+ private Integer status;
+ private Integer retryCount;
+ private Integer maxRetry;
+ private String lastErrorMessage;
+ private String rawCallback;
+ private LocalDateTime lastCallbackTime;
+ private LocalDateTime createTime;
+ private LocalDateTime updateTime;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkDepartmentQueryReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkDepartmentQueryReqVO.java
index 059f0962..d0ae5b07 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkDepartmentQueryReqVO.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkDepartmentQueryReqVO.java
@@ -1,5 +1,6 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
+import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -13,12 +14,35 @@ import lombok.ToString;
@ToString(callSuper = true)
public class IWorkDepartmentQueryReqVO extends IWorkOrgBaseQueryReqVO {
- @Schema(description = "部门编码")
+ @JsonProperty("departmentcode")
+ @Schema(description = "部门编号")
private String departmentCode;
+ @JsonProperty("departmentname")
@Schema(description = "部门名称")
private String departmentName;
- @Schema(description = "所属分部ID")
- private String subcompanyId;
+ @JsonProperty("subcompanyid1")
+ @Schema(description = "分部 ID")
+ private String subcompanyId1;
+
+ @JsonProperty("created")
+ @Schema(description = "创建时间戳(>=)")
+ private String created;
+
+ @JsonProperty("modified")
+ @Schema(description = "修改时间戳(>=)")
+ private String modified;
+
+ @JsonProperty("canceled")
+ @Schema(description = "封存标志,默认查询非封存数据。1:封存")
+ private String canceled;
+
+ @JsonProperty("custom_data")
+ @Schema(description = "自定义字段列表(逗号分隔)")
+ private String customData;
+
+ @JsonProperty("id")
+ @Schema(description = "OA 部门 ID")
+ private String id;
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFileCallbackReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFileCallbackReqVO.java
index 12a5d2b5..cb044d91 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFileCallbackReqVO.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFileCallbackReqVO.java
@@ -2,12 +2,21 @@ package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(name = "IWorkFileCallbackReqVO", description = "iWork 文件回调请求 VO")
@Data
public class IWorkFileCallbackReqVO {
+ @Schema(description = "iWork requestId,唯一标识", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotBlank(message = "requestId 不能为空")
+ private String requestId;
+
+ @Schema(description = "业务回调标识 bizCallbackKey,≤255 字符", example = "seal-flow-callback")
+ @Size(max = 255, message = "bizCallbackKey 长度不能超过 255 字符")
+ private String bizCallbackKey;
+
@Schema(description = "文件下载 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://example.com/file.pdf")
@NotBlank(message = "文件 URL 不能为空")
private String fileUrl;
@@ -19,6 +28,9 @@ public class IWorkFileCallbackReqVO {
@Schema(description = "文件名称,可选", example = "合同附件.pdf")
private String fileName;
- @Schema(description = "OA 单点下载使用的 ssoToken,可选", example = "6102A7C13F09DD6B1AF06CDA0E479AC8...")
- private String ssoToken;
+ @Schema(description = "业务回调状态/结果码,可选")
+ private String status;
+
+ @Schema(description = "原始回调文本(可选,若不传则使用 payload 或请求体序列化)")
+ private String rawBody;
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFullSyncReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFullSyncReqVO.java
index e3793181..74e12911 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFullSyncReqVO.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFullSyncReqVO.java
@@ -1,5 +1,6 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
+import com.fasterxml.jackson.annotation.JsonProperty;
import com.zt.plat.module.system.enums.integration.IWorkSyncEntityTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
@@ -42,6 +43,70 @@ public class IWorkFullSyncReqVO {
@Schema(description = "是否允许更新已存在的本地实体", example = "false")
private Boolean allowUpdate = Boolean.FALSE;
+ @JsonProperty("departmentcode")
+ @Schema(description = "部门编号")
+ private String departmentCode;
+
+ @JsonProperty("departmentname")
+ @Schema(description = "部门名称")
+ private String departmentName;
+
+ @JsonProperty("subcompanycode")
+ @Schema(description = "分部编号")
+ private String subcompanyCode;
+
+ @JsonProperty("subcompanyname")
+ @Schema(description = "分部名称")
+ private String subcompanyName;
+
+ @JsonProperty("subcompanyid1")
+ @Schema(description = "分部 ID")
+ private String subcompanyId1;
+
+ @JsonProperty("jobtitleid")
+ @Schema(description = "岗位 ID")
+ private String jobTitleId;
+
+ @JsonProperty("jobtitlename")
+ @Schema(description = "岗位名称")
+ private String jobTitleName;
+
+ @JsonProperty("workcode")
+ @Schema(description = "人员编号")
+ private String workCode;
+
+ @JsonProperty("loginid")
+ @Schema(description = "登录名")
+ private String loginId;
+
+ @JsonProperty("created")
+ @Schema(description = "创建时间戳(>=)")
+ private String created;
+
+ @JsonProperty("modified")
+ @Schema(description = "修改时间戳(>=)")
+ private String modified;
+
+ @JsonProperty("canceled")
+ @Schema(description = "封存标志,默认查询非封存数据。1:封存")
+ private String canceled;
+
+ @JsonProperty("custom_data")
+ @Schema(description = "自定义字段列表(逗号分隔)")
+ private String customData;
+
+ @JsonProperty("base_custom_data")
+ @Schema(description = "基本信息自定义字段列表(逗号分隔)")
+ private String baseCustomData;
+
+ @JsonProperty("person_custom_data")
+ @Schema(description = "个人信息自定义字段列表(逗号分隔)")
+ private String personCustomData;
+
+ @JsonProperty("work_custom_data")
+ @Schema(description = "工作信息自定义字段列表(逗号分隔)")
+ private String workCustomData;
+
public Set resolveScopes() {
EnumSet defaults = EnumSet.allOf(IWorkSyncEntityTypeEnum.class);
if (scopes == null || scopes.isEmpty()) {
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkJobTitleQueryReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkJobTitleQueryReqVO.java
index 64760eed..191267f9 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkJobTitleQueryReqVO.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkJobTitleQueryReqVO.java
@@ -1,5 +1,6 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
+import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -13,9 +14,19 @@ import lombok.ToString;
@ToString(callSuper = true)
public class IWorkJobTitleQueryReqVO extends IWorkOrgBaseQueryReqVO {
- @Schema(description = "岗位编码")
- private String jobTitleCode;
-
+ @JsonProperty("jobtitlename")
@Schema(description = "岗位名称")
private String jobTitleName;
+
+ @JsonProperty("created")
+ @Schema(description = "创建时间戳(>=)")
+ private String created;
+
+ @JsonProperty("modified")
+ @Schema(description = "修改时间戳(>=)")
+ private String modified;
+
+ @JsonProperty("id")
+ @Schema(description = "岗位 ID")
+ private String id;
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaTokenReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaTokenReqVO.java
index bfbd488f..37df96b0 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaTokenReqVO.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaTokenReqVO.java
@@ -14,6 +14,6 @@ public class IWorkOaTokenReqVO {
@NotBlank(message = "loginId 不能为空")
private String loginId;
- @Schema(description = "应用 appid,未填则使用配置默认值", example = "a17ca6ca-88b0-463e-bffa-7995086bf225")
+ @Schema(description = "应用 appid,已固定使用配置值,无需传递", example = "")
private String appId;
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOrgBaseQueryReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOrgBaseQueryReqVO.java
index 58be39c6..e39d5f02 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOrgBaseQueryReqVO.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOrgBaseQueryReqVO.java
@@ -3,7 +3,6 @@ package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
-import java.util.Map;
/**
* iWork 组织查询基础参数。
@@ -16,7 +15,4 @@ public class IWorkOrgBaseQueryReqVO {
@Schema(description = "每页条数", example = "10")
private Integer pagesize;
-
- @Schema(description = "查询参数(扩展用),将被序列化为 params 传给 iWork")
- private Map params;
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSubcompanyQueryReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSubcompanyQueryReqVO.java
index 781613e7..768e559f 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSubcompanyQueryReqVO.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSubcompanyQueryReqVO.java
@@ -1,5 +1,6 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
+import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -13,9 +14,27 @@ import lombok.ToString;
@ToString(callSuper = true)
public class IWorkSubcompanyQueryReqVO extends IWorkOrgBaseQueryReqVO {
- @Schema(description = "分部编码")
+ @JsonProperty("subcompanycode")
+ @Schema(description = "分部编号")
private String subcompanyCode;
+ @JsonProperty("subcompanyname")
@Schema(description = "分部名称")
private String subcompanyName;
+
+ @JsonProperty("modified")
+ @Schema(description = "修改时间戳(>=)")
+ private String modified;
+
+ @JsonProperty("canceled")
+ @Schema(description = "封存标志,默认查询非封存数据。1:封存")
+ private String canceled;
+
+ @JsonProperty("custom_data")
+ @Schema(description = "自定义字段列表(逗号分隔)")
+ private String customData;
+
+ @JsonProperty("id")
+ @Schema(description = "OA 分部 ID")
+ private String id;
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkUserQueryReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkUserQueryReqVO.java
index 3e4b73fd..5f1fb2f1 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkUserQueryReqVO.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkUserQueryReqVO.java
@@ -1,5 +1,6 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
+import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -13,27 +14,47 @@ import lombok.ToString;
@ToString(callSuper = true)
public class IWorkUserQueryReqVO extends IWorkOrgBaseQueryReqVO {
- @Schema(description = "人员工号")
+ @JsonProperty("workcode")
+ @Schema(description = "人员编号")
private String workCode;
- @Schema(description = "人员姓名")
- private String lastName;
+ @JsonProperty("subcompanyid1")
+ @Schema(description = "分部 ID")
+ private String subcompanyId1;
- @Schema(description = "所属部门ID")
+ @JsonProperty("departmentid")
+ @Schema(description = "部门 ID")
private String departmentId;
- @Schema(description = "所属分部ID")
- private String subcompanyId;
-
- @Schema(description = "所属岗位ID")
+ @JsonProperty("jobtitleid")
+ @Schema(description = "岗位 ID")
private String jobTitleId;
- @Schema(description = "人员状态 (0:试用, 1:正式, 2:临时, 3:试用延期, 4:解聘, 5:离职, 6:退休, 7:无效)")
- private String status;
+ @JsonProperty("id")
+ @Schema(description = "人员 ID")
+ private String id;
- @Schema(description = "手机号")
- private String mobile;
+ @JsonProperty("loginid")
+ @Schema(description = "登录名")
+ private String loginId;
- @Schema(description = "邮箱")
- private String email;
+ @JsonProperty("created")
+ @Schema(description = "创建时间戳(>=)")
+ private String created;
+
+ @JsonProperty("modified")
+ @Schema(description = "修改时间戳(>=)")
+ private String modified;
+
+ @JsonProperty("base_custom_data")
+ @Schema(description = "基本信息自定义字段列表(逗号分隔)")
+ private String baseCustomData;
+
+ @JsonProperty("person_custom_data")
+ @Schema(description = "个人信息自定义字段列表(逗号分隔)")
+ private String personCustomData;
+
+ @JsonProperty("work_custom_data")
+ @Schema(description = "工作信息自定义字段列表(逗号分隔)")
+ private String workCustomData;
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkWorkflowCallbackReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkWorkflowCallbackReqVO.java
new file mode 100644
index 00000000..66cf7d87
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkWorkflowCallbackReqVO.java
@@ -0,0 +1,26 @@
+package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Schema(description = "iWork 流程回调请求")
+@Data
+public class IWorkWorkflowCallbackReqVO {
+
+ @Schema(description = "iWork requestId,唯一标识", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotBlank(message = "requestId 不能为空")
+ private String requestId;
+
+ @Schema(description = "业务单号 (ywxtdjbh)")
+ private String businessCode;
+
+ @Schema(description = "业务回调标识 bizCallbackKey")
+ private String bizCallbackKey;
+
+ @Schema(description = "回调状态/结果码")
+ private String status;
+
+ @Schema(description = "原始回调文本(可截断存储)")
+ private String rawBody;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkWorkflowCreateReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkWorkflowCreateReqVO.java
index 1b2fcc54..2430fc04 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkWorkflowCreateReqVO.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkWorkflowCreateReqVO.java
@@ -34,9 +34,9 @@ public class IWorkWorkflowCreateReqVO extends IWorkBaseReqVO {
@Schema(description = "用印材料附件 URL(必填)")
private String xyywjUrl;
-
- @Schema(description = "用印材料附件文件名(必填)")
- private String xyywjFileName;
+
+ @Schema(description = "业务回调标识(回调分发使用,≤255 字符)")
+ private String bizCallbackKey;
@Schema(description = "用印事项")
private String yysx;
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/MenuDataRuleController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/MenuDataRuleController.java
new file mode 100644
index 00000000..be27f6c4
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/MenuDataRuleController.java
@@ -0,0 +1,77 @@
+package com.zt.plat.module.system.controller.admin.permission;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleRespVO;
+import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleSaveReqVO;
+import com.zt.plat.module.system.convert.permission.MenuDataRuleConvert;
+import com.zt.plat.module.system.dal.dataobject.permission.MenuDataRuleDO;
+import com.zt.plat.module.system.service.permission.MenuDataRuleService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+import static com.zt.plat.framework.common.pojo.CommonResult.success;
+
+/**
+ * 菜单数据规则 Controller
+ *
+ * @author ZT
+ */
+@Tag(name = "管理后台 - 菜单数据规则")
+@RestController
+@RequestMapping("/system/menu-data-rule")
+@Validated
+public class MenuDataRuleController {
+
+ @Resource
+ private MenuDataRuleService menuDataRuleService;
+
+ @PostMapping("/create")
+ @Operation(summary = "创建菜单数据规则")
+ @PreAuthorize("@ss.hasPermission('system:menu:update')")
+ public CommonResult createMenuDataRule(@Valid @RequestBody MenuDataRuleSaveReqVO createReqVO) {
+ return success(menuDataRuleService.createMenuDataRule(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "更新菜单数据规则")
+ @PreAuthorize("@ss.hasPermission('system:menu:update')")
+ public CommonResult updateMenuDataRule(@Valid @RequestBody MenuDataRuleSaveReqVO updateReqVO) {
+ menuDataRuleService.updateMenuDataRule(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除菜单数据规则")
+ @Parameter(name = "id", description = "规则ID", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('system:menu:update')")
+ public CommonResult deleteMenuDataRule(@RequestParam("id") Long id) {
+ menuDataRuleService.deleteMenuDataRule(id);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得菜单数据规则")
+ @Parameter(name = "id", description = "规则ID", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('system:menu:query')")
+ public CommonResult getMenuDataRule(@RequestParam("id") Long id) {
+ MenuDataRuleDO rule = menuDataRuleService.getMenuDataRule(id);
+ return success(MenuDataRuleConvert.INSTANCE.convert(rule));
+ }
+
+ @GetMapping("/list")
+ @Operation(summary = "获得菜单的所有数据规则")
+ @Parameter(name = "menuId", description = "菜单ID", required = true, example = "1")
+ @PreAuthorize("@ss.hasPermission('system:menu:query')")
+ public CommonResult> getMenuDataRuleList(@RequestParam("menuId") Long menuId) {
+ List list = menuDataRuleService.getMenuDataRuleListByMenuId(menuId);
+ return success(MenuDataRuleConvert.INSTANCE.convertList(list));
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/PermissionController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/PermissionController.java
index d84bc342..cd1d2728 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/PermissionController.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/PermissionController.java
@@ -66,6 +66,9 @@ public class PermissionController {
PermissionAssignRoleMenuItemReqVO reqVO = new PermissionAssignRoleMenuItemReqVO();
reqVO.setId(menu.getMenuId());
reqVO.setShowMenu(menu.getShowMenu());
+ // 获取该角色在该菜单下的数据规则ID列表
+ Set dataRuleIds = permissionService.getRoleMenuDataRules(roleId, menu.getMenuId());
+ reqVO.setDataRuleIds(dataRuleIds != null ? new ArrayList<>(dataRuleIds) : null);
return reqVO;
}).collect(Collectors.toSet());
return success(result);
@@ -83,6 +86,10 @@ public class PermissionController {
// 更新菜单的显示状态
permissionService.updateMenuDisplay(reqVO.getRoleId(), reqVO.getMenus());
+
+ // 保存菜单数据规则关联
+ permissionService.assignRoleMenuDataRules(reqVO.getRoleId(), reqVO.getMenus());
+
return success(true);
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/RoleController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/RoleController.java
index 74845b6c..86186e3b 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/RoleController.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/RoleController.java
@@ -11,6 +11,7 @@ import com.zt.plat.module.system.controller.admin.permission.vo.role.RolePageReq
import com.zt.plat.module.system.controller.admin.permission.vo.role.RoleRespVO;
import com.zt.plat.module.system.controller.admin.permission.vo.role.RoleSaveReqVO;
import com.zt.plat.module.system.dal.dataobject.permission.RoleDO;
+import com.zt.plat.framework.datapermission.core.menudatapermission.annotation.PermissionData;
import com.zt.plat.module.system.service.permission.RoleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -78,6 +79,7 @@ public class RoleController {
@GetMapping("/page")
@Operation(summary = "获得角色分页")
@PreAuthorize("@ss.hasPermission('system:role:query')")
+ @PermissionData(pageComponent = "system/role/index")
public CommonResult> getRolePage(RolePageReqVO pageReqVO) {
PageResult pageResult = roleService.getRolePage(pageReqVO);
// 获取所有父级角色信息
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleRespVO.java
new file mode 100644
index 00000000..4b624dea
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleRespVO.java
@@ -0,0 +1,46 @@
+package com.zt.plat.module.system.controller.admin.permission.vo.menudatarule;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 菜单数据规则 Response VO
+ *
+ * @author ZT
+ */
+@Schema(description = "管理后台 - 菜单数据规则 Response VO")
+@Data
+public class MenuDataRuleRespVO {
+
+ @Schema(description = "规则ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "菜单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Long menuId;
+
+ @Schema(description = "规则名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "仅看本部门数据")
+ private String ruleName;
+
+ @Schema(description = "规则字段", example = "dept_id")
+ private String ruleColumn;
+
+ @Schema(description = "规则条件", requiredMode = Schema.RequiredMode.REQUIRED, example = "=")
+ private String ruleConditions;
+
+ @Schema(description = "规则值", requiredMode = Schema.RequiredMode.REQUIRED, example = "#{deptId}")
+ private String ruleValue;
+
+ @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Integer status;
+
+ @Schema(description = "排序", example = "1")
+ private Integer sort;
+
+ @Schema(description = "备注", example = "限制只能查看本部门数据")
+ private String remark;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleSaveReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleSaveReqVO.java
new file mode 100644
index 00000000..d3044877
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleSaveReqVO.java
@@ -0,0 +1,53 @@
+package com.zt.plat.module.system.controller.admin.permission.vo.menudatarule;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+/**
+ * 菜单数据规则创建/修改 Request VO
+ *
+ * @author ZT
+ */
+@Schema(description = "管理后台 - 菜单数据规则创建/修改 Request VO")
+@Data
+public class MenuDataRuleSaveReqVO {
+
+ @Schema(description = "规则ID", example = "1024")
+ private Long id;
+
+ @Schema(description = "菜单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "菜单ID不能为空")
+ private Long menuId;
+
+ @Schema(description = "规则名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "仅看本部门数据")
+ @NotBlank(message = "规则名称不能为空")
+ @Size(max = 100, message = "规则名称长度不能超过 100 个字符")
+ private String ruleName;
+
+ @Schema(description = "规则字段", example = "dept_id")
+ @Size(max = 100, message = "规则字段长度不能超过 100 个字符")
+ private String ruleColumn;
+
+ @Schema(description = "规则条件", requiredMode = Schema.RequiredMode.REQUIRED, example = "=")
+ @NotBlank(message = "规则条件不能为空")
+ @Size(max = 20, message = "规则条件长度不能超过 20 个字符")
+ private String ruleConditions;
+
+ @Schema(description = "规则值", requiredMode = Schema.RequiredMode.REQUIRED, example = "#{deptId}")
+ @NotBlank(message = "规则值不能为空")
+ @Size(max = 500, message = "规则值长度不能超过 500 个字符")
+ private String ruleValue;
+
+ @Schema(description = "状态", example = "1")
+ private Integer status;
+
+ @Schema(description = "排序", example = "1")
+ private Integer sort;
+
+ @Schema(description = "备注", example = "限制只能查看本部门数据")
+ @Size(max = 500, message = "备注长度不能超过 500 个字符")
+ private String remark;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuItemReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuItemReqVO.java
index 0d038266..576274b9 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuItemReqVO.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuItemReqVO.java
@@ -4,6 +4,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
+import java.util.List;
+
@Schema(description = "管理后台 - 赋予角色菜单--菜单列表 Request VO")
@Data
@@ -19,4 +21,7 @@ public class PermissionAssignRoleMenuItemReqVO {
@Schema(description = "是否显示菜单按钮是否点击过(避免大量更新数据,只更新点击过的)")
private Boolean showMenuChanged = false;
+ @Schema(description = "菜单数据规则ID列表", example = "[1, 2, 3]")
+ private List dataRuleIds;
+
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/portal/PortalController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/portal/PortalController.java
index c956f82e..eb8625a7 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/portal/PortalController.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/portal/PortalController.java
@@ -4,6 +4,9 @@ import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.common.util.object.BeanUtils;
import com.zt.plat.framework.excel.core.util.ExcelUtils;
+import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
+import com.zt.plat.module.infra.api.file.FileApi;
+import com.zt.plat.module.infra.api.file.dto.FileRespDTO;
import com.zt.plat.module.system.controller.admin.portal.vo.PortalPageReqVO;
import com.zt.plat.module.system.controller.admin.portal.vo.PortalRespVO;
import com.zt.plat.module.system.controller.admin.portal.vo.PortalSaveReqVO;
@@ -13,6 +16,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
+import jakarta.annotation.security.PermitAll;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -20,7 +24,12 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
+
+import org.springframework.util.StringUtils;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
import static com.zt.plat.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@@ -39,6 +48,9 @@ public class PortalController {
@Resource
private PortalService portalService;
+ @Resource
+ private FileApi fileApi;
+
@PostMapping("/create")
@Operation(summary = "创建门户网站")
@PreAuthorize("@ss.hasPermission('system:portal:create')")
@@ -97,9 +109,60 @@ public class PortalController {
*/
@GetMapping("/list")
@Operation(summary = "获取我的门户列表")
+ @PermitAll
+ @TenantIgnore
public CommonResult> getMyPortalList() {
- Long userId = getLoginUserId();
- List portals = portalService.getPortalListByUserId(userId);
+ Long userId = null;
+ try {
+ userId = getLoginUserId();
+ } catch (Exception ignored) {
+ // 未登录时获取公开门户
+ }
+ List portals = (userId == null)
+ ? portalService.getPublicPortalList()
+ : portalService.getPortalListByUserId(userId);
return success(BeanUtils.toBean(portals, PortalRespVO.class));
}
+
+ /**
+ * 匿名获取公开门户的图标文件信息
+ * 仅允许访问门户中已配置的图标文件
+ */
+ @GetMapping("/public-icon-files")
+ @Operation(summary = "获取公开门户图标文件信息")
+ @PermitAll
+ @TenantIgnore
+ public CommonResult> getPublicPortalIconFiles(@RequestParam("fileIds") List fileIds) {
+ if (fileIds == null || fileIds.isEmpty()) {
+ return success(java.util.Collections.emptyList());
+ }
+
+ List portals = portalService.getPublicPortalList();
+ Set allowedFileIds = new HashSet<>();
+ for (PortalDO portal : portals) {
+ if (portal.getIconType() == null || portal.getIconType() != 2) {
+ continue;
+ }
+ if (!StringUtils.hasText(portal.getIconFileId())) {
+ continue;
+ }
+ try {
+ allowedFileIds.add(Long.parseLong(portal.getIconFileId()));
+ } catch (NumberFormatException ignored) {
+ // ignore invalid fileId
+ }
+ }
+
+ List result = new ArrayList<>();
+ for (Long fileId : fileIds) {
+ if (!allowedFileIds.contains(fileId)) {
+ continue;
+ }
+ CommonResult fileResult = fileApi.getFileInfo(fileId);
+ if (fileResult != null && fileResult.isSuccess() && fileResult.getData() != null) {
+ result.add(fileResult.getData());
+ }
+ }
+ return success(result);
+ }
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/ExternalPushConfigController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/ExternalPushConfigController.java
new file mode 100644
index 00000000..1e3b160b
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/ExternalPushConfigController.java
@@ -0,0 +1,160 @@
+package com.zt.plat.module.system.controller.admin.push;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.framework.common.util.collection.CollectionUtils;
+import com.zt.plat.framework.common.util.object.BeanUtils;
+import com.zt.plat.module.system.controller.admin.push.vo.BusinessTypeRespVO;
+import com.zt.plat.module.system.controller.admin.push.vo.ExternalPushConfigPageReqVO;
+import com.zt.plat.module.system.controller.admin.push.vo.ExternalPushConfigRespVO;
+import com.zt.plat.module.system.controller.admin.push.vo.ExternalPushConfigSaveReqVO;
+import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
+import com.zt.plat.module.system.dal.dataobject.push.ExternalPushConfigDO;
+import com.zt.plat.module.system.enums.push.BusinessTypeEnum;
+import com.zt.plat.module.system.service.dept.DeptService;
+import com.zt.plat.module.system.service.push.ExternalPushConfigService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.zt.plat.framework.common.pojo.CommonResult.success;
+
+/**
+ * 外部系统推送配置 Controller
+ *
+ * @author ZT Cloud
+ */
+@Tag(name = "管理后台 - 外部系统推送配置")
+@RestController
+@RequestMapping("/system/external-push-config")
+@Validated
+public class ExternalPushConfigController {
+
+ @Resource
+ private ExternalPushConfigService externalPushConfigService;
+ @Resource
+ private DeptService deptService;
+
+ @PostMapping("/create")
+ @Operation(summary = "创建推送配置")
+ @PreAuthorize("@ss.hasPermission('system:external-push-config:create')")
+ public CommonResult create(@Valid @RequestBody ExternalPushConfigSaveReqVO createReqVO) {
+ Long id = externalPushConfigService.createExternalPushConfig(createReqVO);
+ return success(id);
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "修改推送配置")
+ @PreAuthorize("@ss.hasPermission('system:external-push-config:update')")
+ public CommonResult update(@Valid @RequestBody ExternalPushConfigSaveReqVO updateReqVO) {
+ externalPushConfigService.updateExternalPushConfig(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除推送配置")
+ @PreAuthorize("@ss.hasPermission('system:external-push-config:delete')")
+ public CommonResult delete(@RequestParam("id") Long id) {
+ externalPushConfigService.deleteExternalPushConfig(id);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获取推送配置详情")
+ @PreAuthorize("@ss.hasPermission('system:external-push-config:query')")
+ public CommonResult get(@RequestParam("id") Long id) {
+ ExternalPushConfigDO entity = externalPushConfigService.getExternalPushConfig(id);
+ ExternalPushConfigRespVO respVO = BeanUtils.toBean(entity, ExternalPushConfigRespVO.class);
+ fillDeptInfo(List.of(respVO));
+ return success(respVO);
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "分页查询推送配置")
+ @PreAuthorize("@ss.hasPermission('system:external-push-config:query')")
+ public CommonResult> page(@Valid ExternalPushConfigPageReqVO reqVO) {
+ PageResult pageResult = externalPushConfigService.getExternalPushConfigPage(reqVO);
+ PageResult result = BeanUtils.toBean(pageResult, ExternalPushConfigRespVO.class);
+ fillDeptInfo(result.getList());
+ return success(result);
+ }
+
+ @GetMapping("/is-push-enabled")
+ @Operation(summary = "判断是否允许推送")
+ @Parameter(name = "companyId", description = "公司编号(为空表示不限制公司)")
+ @Parameter(name = "deptId", description = "部门编号")
+ @Parameter(name = "businessType", description = "业务类型(为空表示所有业务类型)")
+ @Parameter(name = "externalSystem", description = "外部系统标识(为空表示所有外部系统)")
+ @PreAuthorize("@ss.hasPermission('system:external-push-config:query')")
+ public CommonResult isPushEnabled(
+ @RequestParam(value = "companyId", required = false) Long companyId,
+ @RequestParam(value = "deptId", required = false) Long deptId,
+ @RequestParam(value = "businessType", required = false) String businessType,
+ @RequestParam(value = "externalSystem", required = false) String externalSystem) {
+ Boolean result = externalPushConfigService.isPushEnabled(companyId, deptId, businessType, externalSystem);
+ return success(result);
+ }
+
+ @GetMapping("/business-types")
+ @Operation(summary = "获取业务类型列表")
+ public CommonResult> getBusinessTypes() {
+ List result = Arrays.stream(BusinessTypeEnum.values())
+ .map(e -> new BusinessTypeRespVO(e.getType(), e.getCode(), e.getName()))
+ .collect(Collectors.toList());
+ return success(result);
+ }
+
+ /**
+ * 填充公司和部门名称
+ */
+ private void fillDeptInfo(List list) {
+ if (list == null || list.isEmpty()) {
+ return;
+ }
+
+ // 收集所有公司ID和部门ID
+ Set deptIds = list.stream()
+ .flatMap(item -> {
+ Set ids = new java.util.HashSet<>();
+ ids.add(item.getCompanyId());
+ if (item.getDeptId() != null) {
+ ids.add(item.getDeptId());
+ }
+ return ids.stream();
+ })
+ .collect(Collectors.toSet());
+
+ if (deptIds.isEmpty()) {
+ return;
+ }
+
+ // 批量查询部门信息
+ Map deptMap = CollectionUtils.convertMap(
+ deptService.getDeptList(deptIds), DeptDO::getId);
+
+ // 填充名称
+ list.forEach(item -> {
+ DeptDO company = deptMap.get(item.getCompanyId());
+ if (company != null) {
+ item.setCompanyName(company.getName());
+ }
+ if (item.getDeptId() != null) {
+ DeptDO dept = deptMap.get(item.getDeptId());
+ if (dept != null) {
+ item.setDeptName(dept.getName());
+ }
+ }
+ });
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/BusinessTypeRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/BusinessTypeRespVO.java
new file mode 100644
index 00000000..5dc83f66
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/BusinessTypeRespVO.java
@@ -0,0 +1,22 @@
+package com.zt.plat.module.system.controller.admin.push.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Schema(description = "管理后台 - 业务类型 Response VO")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class BusinessTypeRespVO {
+
+ @Schema(description = "类型", example = "1")
+ private Integer type;
+
+ @Schema(description = "编码", example = "PURCHASE")
+ private String code;
+
+ @Schema(description = "名称", example = "采购")
+ private String name;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigBaseVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigBaseVO.java
new file mode 100644
index 00000000..fd385e70
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigBaseVO.java
@@ -0,0 +1,33 @@
+package com.zt.plat.module.system.controller.admin.push.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 外部系统推送配置基础信息")
+@Data
+public class ExternalPushConfigBaseVO {
+
+ @Schema(description = "公司编号(为空表示不限制公司)", example = "1024")
+ private Long companyId;
+
+ @Schema(description = "部门编号(为空表示公司级配置)", example = "2048")
+ private Long deptId;
+
+ @Schema(description = "业务类型(为空表示所有业务类型)", example = "PURCHASE")
+ @Size(max = 32, message = "业务类型长度不能超过 32 个字符")
+ private String businessType;
+
+ @Schema(description = "外部系统标识(为空表示所有外部系统)", example = "ERP")
+ @Size(max = 64, message = "外部系统标识长度不能超过 64 个字符")
+ private String externalSystem;
+
+ @Schema(description = "是否启用推送", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+ @NotNull(message = "推送开关不能为空")
+ private Boolean enablePush;
+
+ @Schema(description = "备注", example = "ERP 采购单推送配置")
+ @Size(max = 512, message = "备注长度不能超过 512 个字符")
+ private String remark;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigPageReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigPageReqVO.java
new file mode 100644
index 00000000..2ac1eb4e
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigPageReqVO.java
@@ -0,0 +1,27 @@
+package com.zt.plat.module.system.controller.admin.push.vo;
+
+import com.zt.plat.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Schema(description = "管理后台 - 外部系统推送配置分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ExternalPushConfigPageReqVO extends PageParam {
+
+ @Schema(description = "公司编号", example = "1024")
+ private Long companyId;
+
+ @Schema(description = "部门编号", example = "2048")
+ private Long deptId;
+
+ @Schema(description = "业务类型", example = "PURCHASE")
+ private String businessType;
+
+ @Schema(description = "外部系统标识", example = "ERP")
+ private String externalSystem;
+
+ @Schema(description = "是否启用推送", example = "true")
+ private Boolean enablePush;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigRespVO.java
new file mode 100644
index 00000000..7e2e5763
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigRespVO.java
@@ -0,0 +1,28 @@
+package com.zt.plat.module.system.controller.admin.push.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 外部系统推送配置 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ExternalPushConfigRespVO extends ExternalPushConfigBaseVO {
+
+ @Schema(description = "配置编号", example = "1024")
+ private Long id;
+
+ @Schema(description = "公司名称", example = "浙江中天建设集团")
+ private String companyName;
+
+ @Schema(description = "部门名称", example = "采购部")
+ private String deptName;
+
+ @Schema(description = "创建时间")
+ private LocalDateTime createTime;
+
+ @Schema(description = "最后更新时间")
+ private LocalDateTime updateTime;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigSaveReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigSaveReqVO.java
new file mode 100644
index 00000000..3a51a5f8
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigSaveReqVO.java
@@ -0,0 +1,14 @@
+package com.zt.plat.module.system.controller.admin.push.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Schema(description = "管理后台 - 外部系统推送配置创建/修改 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ExternalPushConfigSaveReqVO extends ExternalPushConfigBaseVO {
+
+ @Schema(description = "配置编号", example = "1024")
+ private Long id;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSaveReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSaveReqVO.java
index 842ee71c..7d11c31f 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSaveReqVO.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSaveReqVO.java
@@ -10,7 +10,10 @@ import com.zt.plat.module.system.framework.operatelog.core.DeptParseFunction;
import com.zt.plat.module.system.framework.operatelog.core.PostParseFunction;
import com.zt.plat.module.system.framework.operatelog.core.SexParseFunction;
import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.validation.constraints.*;
+import jakarta.validation.constraints.AssertTrue;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
@@ -28,7 +31,7 @@ public class UserSaveReqVO {
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "zt")
@NotBlank(message = "用户账号不能为空")
- @Pattern(regexp = "^[a-zA-Z0-9]+$", message = "用户账号由 数字、字母 组成")
+// @Pattern(regexp = "^[a-zA-Z0-9]+$", message = "用户账号由 数字、字母 组成")
@Size(min = 1, max = 30, message = "用户账号长度为 1-30 个字符")
@DiffLogField(name = "用户账号")
private String username;
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/app/portal/AppPortalController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/app/portal/AppPortalController.java
deleted file mode 100644
index a1386fe7..00000000
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/app/portal/AppPortalController.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.zt.plat.module.system.controller.app.portal;
-
-import com.zt.plat.framework.common.pojo.CommonResult;
-import com.zt.plat.framework.common.util.object.BeanUtils;
-import com.zt.plat.module.system.controller.admin.portal.vo.PortalRespVO;
-import com.zt.plat.module.system.dal.dataobject.portal.PortalDO;
-import com.zt.plat.module.system.service.portal.PortalService;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.annotation.Resource;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import java.util.List;
-
-import static com.zt.plat.framework.common.pojo.CommonResult.success;
-import static com.zt.plat.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
-
-/**
- * 用户端 - 门户网站 Controller
- *
- * @author 中铜数字供应链平台
- */
-@Tag(name = "用户端 - 门户网站")
-@RestController
-@RequestMapping("/system/portal")
-@Validated
-public class AppPortalController {
-
- @Resource
- private PortalService portalService;
-
- /**
- * 获取当前用户可访问的门户列表
- * 此接口无需权限验证,因为已经通过登录验证,
- * 返回的门户列表已经根据用户权限进行了过滤
- */
- @GetMapping("/list")
- @Operation(summary = "获取我的门户列表")
- public CommonResult> getMyPortalList() {
- Long userId = getLoginUserId();
- List portals = portalService.getPortalListByUserId(userId);
- return success(BeanUtils.toBean(portals, PortalRespVO.class));
- }
-
-}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/convert/permission/MenuDataRuleConvert.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/convert/permission/MenuDataRuleConvert.java
new file mode 100644
index 00000000..1841074b
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/convert/permission/MenuDataRuleConvert.java
@@ -0,0 +1,26 @@
+package com.zt.plat.module.system.convert.permission;
+
+import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleRespVO;
+import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleSaveReqVO;
+import com.zt.plat.module.system.dal.dataobject.permission.MenuDataRuleDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+/**
+ * 菜单数据规则 Convert
+ *
+ * @author ZT
+ */
+@Mapper
+public interface MenuDataRuleConvert {
+
+ MenuDataRuleConvert INSTANCE = Mappers.getMapper(MenuDataRuleConvert.class);
+
+ MenuDataRuleDO convert(MenuDataRuleSaveReqVO bean);
+
+ MenuDataRuleRespVO convert(MenuDataRuleDO bean);
+
+ List convertList(List list);
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/iwork/IWorkSealLogDO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/iwork/IWorkSealLogDO.java
new file mode 100644
index 00000000..09e1d325
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/iwork/IWorkSealLogDO.java
@@ -0,0 +1,72 @@
+package com.zt.plat.module.system.dal.dataobject.iwork;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.zt.plat.framework.mybatis.core.dataobject.BaseDO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+
+/**
+ * iWork 用印流程回调日志。
+ */
+@TableName("system_iwork_seal_log")
+@KeySequence("system_iwork_seal_log_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class IWorkSealLogDO extends BaseDO {
+
+ @TableId(type = IdType.ASSIGN_ID)
+ private Long id;
+
+ /**
+ * iWork 返回的请求编号,唯一业务标识。
+ */
+ private String requestId;
+
+ /**
+ * 业务单号(ywxtdjbh)。
+ */
+ private String businessCode;
+
+ /**
+ * 业务回调标识。
+ */
+ private String bizCallbackKey;
+
+ /**
+ * 状态枚举,参考 IWorkCallbackStatusEnum。
+ */
+ private Integer status;
+
+ /**
+ * 已执行的自动/手工重试次数。
+ */
+ private Integer retryCount;
+
+ /**
+ * 最大重试次数(快照)。
+ */
+ private Integer maxRetry;
+
+ /**
+ * 最后一次错误信息。
+ */
+ private String lastErrorMessage;
+
+ /**
+ * 回调原始负载(截断)。
+ */
+ private String rawCallback;
+
+ /**
+ * 最近一次回调时间。
+ */
+ private LocalDateTime lastCallbackTime;
+
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/MenuDataRuleDO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/MenuDataRuleDO.java
new file mode 100644
index 00000000..f8843af7
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/MenuDataRuleDO.java
@@ -0,0 +1,67 @@
+package com.zt.plat.module.system.dal.dataobject.permission;
+
+import com.zt.plat.framework.tenant.core.db.TenantBaseDO;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 菜单数据规则 DO
+ *
+ * @author ZT
+ */
+@TableName("system_menu_data_rule")
+@KeySequence("system_menu_data_rule_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class MenuDataRuleDO extends TenantBaseDO {
+
+ /**
+ * 规则ID
+ */
+ @TableId(type = IdType.ASSIGN_ID)
+ private Long id;
+
+ /**
+ * 菜单ID
+ */
+ private Long menuId;
+
+ /**
+ * 规则名称
+ */
+ private String ruleName;
+
+ /**
+ * 规则字段(数据库列名)
+ */
+ private String ruleColumn;
+
+ /**
+ * 规则条件(=、>、<、IN、LIKE等)
+ */
+ private String ruleConditions;
+
+ /**
+ * 规则值(支持变量如#{userId}、#{deptId})
+ */
+ private String ruleValue;
+
+ /**
+ * 状态(0=禁用 1=启用)
+ */
+ private Integer status;
+
+ /**
+ * 排序
+ */
+ private Integer sort;
+
+ /**
+ * 备注
+ */
+ private String remark;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/RoleMenuDataRuleDO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/RoleMenuDataRuleDO.java
new file mode 100644
index 00000000..f5d9ae2f
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/RoleMenuDataRuleDO.java
@@ -0,0 +1,42 @@
+package com.zt.plat.module.system.dal.dataobject.permission;
+
+import com.zt.plat.framework.tenant.core.db.TenantBaseDO;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 角色菜单数据规则关联 DO
+ *
+ * @author ZT
+ */
+@TableName("system_role_menu_data_rule")
+@KeySequence("system_role_menu_data_rule_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class RoleMenuDataRuleDO extends TenantBaseDO {
+
+ /**
+ * 自增主键
+ */
+ @TableId(type = IdType.ASSIGN_ID)
+ private Long id;
+
+ /**
+ * 角色ID
+ */
+ private Long roleId;
+
+ /**
+ * 菜单ID
+ */
+ private Long menuId;
+
+ /**
+ * 数据规则ID
+ */
+ private Long dataRuleId;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/push/ExternalPushConfigDO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/push/ExternalPushConfigDO.java
new file mode 100644
index 00000000..b42ae1b9
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/push/ExternalPushConfigDO.java
@@ -0,0 +1,76 @@
+package com.zt.plat.module.system.dal.dataobject.push;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.zt.plat.framework.tenant.core.db.TenantBaseDO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 外部系统推送配置 DO
+ *
+ * 用于配置不同公司/部门/业务类型下的外部系统推送开关
+ *
+ * @author ZT Cloud
+ */
+@TableName("system_external_push_config")
+@KeySequence("system_external_push_config_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ExternalPushConfigDO extends TenantBaseDO {
+
+ /**
+ * 主键编号
+ */
+ @TableId(type = IdType.ASSIGN_ID)
+ private Long id;
+
+ /**
+ * 公司编号(可为空)
+ *
+ * 关联 system_dept 表,is_company = 1
+ * 为空表示不限制公司
+ */
+ private Long companyId;
+
+ /**
+ * 部门编号(可为空)
+ *
+ * 关联 system_dept 表,is_company = 0
+ * 为空表示公司级配置
+ */
+ private Long deptId;
+
+ /**
+ * 业务类型(可为空)
+ *
+ * 枚举值:PURCHASE, SALE, PRODUCTION
+ * 为空表示所有业务类型
+ * 枚举 {@link com.zt.plat.module.system.enums.push.BusinessTypeEnum}
+ */
+ private String businessType;
+
+ /**
+ * 外部系统标识(可为空)
+ *
+ * 如:ERP, IWORK
+ * 为空表示所有外部系统
+ * 枚举 {@link com.zt.plat.module.system.enums.dept.ExternalPlatformEnum}
+ */
+ private String externalSystem;
+
+ /**
+ * 是否启用推送
+ *
+ * true:启用推送
+ * false:停用推送
+ */
+ private Boolean enablePush;
+
+ /**
+ * 备注
+ */
+ private String remark;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptMapper.java
index a8416485..00029e99 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptMapper.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptMapper.java
@@ -10,6 +10,8 @@ import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptListReqVO;
import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
import java.util.Collection;
import java.util.List;
@@ -167,4 +169,9 @@ public interface DeptMapper extends BaseMapperX {
);
}
+ @Select("""
+ SELECT sd.* FROM SYSTEM_DEPT sd START WITH sd.id = #{deptId}
+ CONNECT BY PRIOR sd.parent_id = sd.id AND PRIOR sd.is_company <> 1 ;
+ """)
+ List upFindCompanyNode(@Param("deptId") Long deptId);
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/PostMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/PostMapper.java
index 4556ce9a..d1bd29d5 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/PostMapper.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/PostMapper.java
@@ -9,6 +9,7 @@ import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
+import java.util.Collections;
@Mapper
public interface PostMapper extends BaseMapperX {
@@ -35,4 +36,12 @@ public interface PostMapper extends BaseMapperX {
return selectOne(PostDO::getCode, code);
}
+ default List selectByCodes(Collection codes) {
+ if (codes == null || codes.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return selectList(new LambdaQueryWrapperX()
+ .in(PostDO::getCode, codes));
+ }
+
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/iwork/IWorkSealLogMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/iwork/IWorkSealLogMapper.java
new file mode 100644
index 00000000..55ac9ee6
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/iwork/IWorkSealLogMapper.java
@@ -0,0 +1,27 @@
+package com.zt.plat.module.system.dal.mysql.iwork;
+
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
+import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
+import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkCallbackLogPageReqVO;
+import com.zt.plat.module.system.dal.dataobject.iwork.IWorkSealLogDO;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface IWorkSealLogMapper extends BaseMapperX {
+
+ default IWorkSealLogDO selectByRequestId(String requestId) {
+ return selectOne(IWorkSealLogDO::getRequestId, requestId);
+ }
+
+ default PageResult selectPage(IWorkCallbackLogPageReqVO reqVO) {
+ return selectPage(reqVO, new LambdaQueryWrapperX()
+ .eqIfPresent(IWorkSealLogDO::getRequestId, reqVO.getRequestId())
+ .eqIfPresent(IWorkSealLogDO::getBusinessCode, reqVO.getBusinessCode())
+ .eqIfPresent(IWorkSealLogDO::getBizCallbackKey, reqVO.getBizCallbackKey())
+ .eqIfPresent(IWorkSealLogDO::getStatus, reqVO.getStatus())
+ .betweenIfPresent(IWorkSealLogDO::getCreateTime, reqVO.getCreateTime())
+ .betweenIfPresent(IWorkSealLogDO::getLastCallbackTime, reqVO.getLastCallbackTime())
+ .orderByDesc(IWorkSealLogDO::getId));
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuDataRuleMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuDataRuleMapper.java
new file mode 100644
index 00000000..2f8aa889
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuDataRuleMapper.java
@@ -0,0 +1,59 @@
+package com.zt.plat.module.system.dal.mysql.permission;
+
+import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
+import com.zt.plat.module.system.dal.dataobject.permission.MenuDataRuleDO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 菜单数据规则 Mapper
+ *
+ * @author ZT
+ */
+@Mapper
+public interface MenuDataRuleMapper extends BaseMapperX {
+
+ /**
+ * 根据菜单ID查询规则列表
+ *
+ * @param menuId 菜单ID
+ * @return 规则列表
+ */
+ default List selectListByMenuId(Long menuId) {
+ return selectList(MenuDataRuleDO::getMenuId, menuId);
+ }
+
+ /**
+ * 根据角色和菜单查询规则ID列表
+ *
+ * @param roleIds 角色ID集合
+ * @param menuId 菜单ID
+ * @return 规则ID列表
+ */
+ @Select("")
+ List selectRuleIdsByRoleAndMenu(@Param("roleIds") Collection roleIds,
+ @Param("menuId") Long menuId);
+
+ /**
+ * 批量查询菜单的规则
+ *
+ * @param menuIds 菜单ID集合
+ * @return 规则列表
+ */
+ default List selectListByMenuIds(Collection menuIds) {
+ return selectList("menu_id", menuIds);
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuMapper.java
index 60a4af8b..5f9b4a3d 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuMapper.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuMapper.java
@@ -33,4 +33,8 @@ public interface MenuMapper extends BaseMapperX {
return selectOne(MenuDO::getComponentName, componentName);
}
+ default MenuDO selectByComponent(String component) {
+ return selectOne(MenuDO::getComponent, component);
+ }
+
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/RoleMenuDataRuleMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/RoleMenuDataRuleMapper.java
new file mode 100644
index 00000000..25d7e101
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/RoleMenuDataRuleMapper.java
@@ -0,0 +1,43 @@
+package com.zt.plat.module.system.dal.mysql.permission;
+
+import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
+import com.zt.plat.module.system.dal.dataobject.permission.RoleMenuDataRuleDO;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 角色菜单数据规则关联 Mapper
+ *
+ * @author ZT
+ */
+@Mapper
+public interface RoleMenuDataRuleMapper extends BaseMapperX {
+
+ /**
+ * 根据角色ID和菜单ID查询规则关联
+ *
+ * @param roleId 角色ID
+ * @param menuId 菜单ID
+ * @return 规则关联列表
+ */
+ default List selectListByRoleAndMenu(Long roleId, Long menuId) {
+ return selectList(new LambdaQueryWrapper()
+ .eq(RoleMenuDataRuleDO::getRoleId, roleId)
+ .eq(RoleMenuDataRuleDO::getMenuId, menuId));
+ }
+
+ /**
+ * 根据角色ID和菜单ID删除规则关联
+ *
+ * @param roleId 角色ID
+ * @param menuId 菜单ID
+ */
+ default void deleteByRoleAndMenu(Long roleId, Long menuId) {
+ delete(new LambdaQueryWrapper()
+ .eq(RoleMenuDataRuleDO::getRoleId, roleId)
+ .eq(RoleMenuDataRuleDO::getMenuId, menuId));
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/push/ExternalPushConfigMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/push/ExternalPushConfigMapper.java
new file mode 100644
index 00000000..f05cdaf6
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/push/ExternalPushConfigMapper.java
@@ -0,0 +1,92 @@
+package com.zt.plat.module.system.dal.mysql.push;
+
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
+import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
+import com.zt.plat.module.system.controller.admin.push.vo.ExternalPushConfigPageReqVO;
+import com.zt.plat.module.system.dal.dataobject.push.ExternalPushConfigDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 外部系统推送配置 Mapper
+ *
+ * @author ZT Cloud
+ */
+@Mapper
+public interface ExternalPushConfigMapper extends BaseMapperX {
+
+ default PageResult selectPage(ExternalPushConfigPageReqVO reqVO) {
+ LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX()
+ .eqIfPresent(ExternalPushConfigDO::getCompanyId, reqVO.getCompanyId())
+ .eqIfPresent(ExternalPushConfigDO::getBusinessType, reqVO.getBusinessType())
+ .eqIfPresent(ExternalPushConfigDO::getExternalSystem, reqVO.getExternalSystem())
+ .eqIfPresent(ExternalPushConfigDO::getEnablePush, reqVO.getEnablePush());
+
+ // 如果传了 companyId 但没传 deptId,则只查公司级配置(dept_id IS NULL)
+ if (reqVO.getCompanyId() != null && reqVO.getDeptId() == null) {
+ wrapper.isNull(ExternalPushConfigDO::getDeptId);
+ } else if (reqVO.getDeptId() != null) {
+ // 如果传了 deptId,则查指定部门的配置
+ wrapper.eq(ExternalPushConfigDO::getDeptId, reqVO.getDeptId());
+ }
+ // 如果都没传,则查所有配置
+
+ wrapper.orderByDesc(ExternalPushConfigDO::getId);
+ return selectPage(reqVO, wrapper);
+ }
+
+ /**
+ * 通用查询配置方法
+ *
+ * @param companyId 公司ID(null 表示查询 company_id IS NULL 的记录)
+ * @param deptId 部门ID(null 表示查询 dept_id IS NULL 的记录)
+ * @param businessType 业务类型(null 表示查询 business_type IS NULL 的记录)
+ * @param externalSystem 外部系统(null 表示查询 external_system IS NULL 的记录)
+ * @return 配置对象
+ */
+ default ExternalPushConfigDO selectByConfig(Long companyId, Long deptId, String businessType, String externalSystem) {
+ LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX<>();
+
+ if (companyId == null) {
+ wrapper.isNull(ExternalPushConfigDO::getCompanyId);
+ } else {
+ wrapper.eq(ExternalPushConfigDO::getCompanyId, companyId);
+ }
+
+ if (deptId == null) {
+ wrapper.isNull(ExternalPushConfigDO::getDeptId);
+ } else {
+ wrapper.eq(ExternalPushConfigDO::getDeptId, deptId);
+ }
+
+ if (businessType == null) {
+ wrapper.isNull(ExternalPushConfigDO::getBusinessType);
+ } else {
+ wrapper.eq(ExternalPushConfigDO::getBusinessType, businessType);
+ }
+
+ if (externalSystem == null) {
+ wrapper.isNull(ExternalPushConfigDO::getExternalSystem);
+ } else {
+ wrapper.eq(ExternalPushConfigDO::getExternalSystem, externalSystem);
+ }
+
+ return selectOne(wrapper);
+ }
+
+ /**
+ * 查询公司下所有配置
+ */
+ default List selectListByCompanyId(Long companyId) {
+ return selectList(ExternalPushConfigDO::getCompanyId, companyId);
+ }
+
+ /**
+ * 查询部门下所有配置
+ */
+ default List selectListByDeptId(Long deptId) {
+ return selectList(ExternalPushConfigDO::getDeptId, deptId);
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/integration/iwork/config/IWorkProperties.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/integration/iwork/config/IWorkProperties.java
index dde38deb..c7a4e0ee 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/integration/iwork/config/IWorkProperties.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/integration/iwork/config/IWorkProperties.java
@@ -42,6 +42,7 @@ public class IWorkProperties {
private final Client client = new Client();
private final OrgRest org = new OrgRest();
private final Workflow workflow = new Workflow();
+ private final Callback callback = new Callback();
private final Oa oa = new Oa();
@Data
@@ -127,6 +128,26 @@ public class IWorkProperties {
private String sealWorkflowId;
}
+ @Data
+ public static class Callback {
+ /**
+ * 业务回调重试配置。
+ */
+ private final Retry retry = new Retry();
+ }
+
+ @Data
+ public static class Retry {
+ /**
+ * 最大重试次数,默认 3。
+ */
+ private int maxAttempts = 3;
+ /**
+ * 重试延迟(秒),默认 5。
+ */
+ private int delaySeconds = 5;
+ }
+
@Data
public static class Oa {
/**
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/job/sync/SyncIWorkOrgChangeJob.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/job/sync/SyncIWorkOrgChangeJob.java
new file mode 100644
index 00000000..0e2c23e2
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/job/sync/SyncIWorkOrgChangeJob.java
@@ -0,0 +1,40 @@
+package com.zt.plat.module.system.job.sync;
+
+import com.xxl.job.core.handler.annotation.XxlJob;
+import com.zt.plat.framework.tenant.core.job.TenantJob;
+import com.zt.plat.module.system.service.sync.SyncIWorkOrgChangeService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * 用于定时同步 iWork 当日变更的组织数据
+ * 同步时间:每日23:00
+ */
+@Component
+@Slf4j
+public class SyncIWorkOrgChangeJob {
+
+ @Resource
+ private SyncIWorkOrgChangeService syncIWorkOrgChangeService;
+
+ /**
+ * 执行定时任务
+ * 配置执行频率:每日23:00时执行一次
+ * cron表达式:0 0 23 * * ?
+ */
+ @XxlJob("syncIWorkOrgChangeJob")
+ @TenantJob
+ public void execute() {
+ log.info("[syncIWorkOrgChangeJob][开始执行同步 iWork 当日变更组织任务]");
+ try {
+ int processedCount = syncIWorkOrgChangeService.process();
+ if (processedCount > 0) {
+ log.info("[syncIWorkOrgChangeJob][同步任务执行完成,处理了 {} 条记录]", processedCount);
+ }
+ } catch (Exception e) {
+ log.error("[syncIWorkOrgChangeJob][同步任务执行失败]", e);
+ throw e;
+ }
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/job/sync/SyncIWorkUserChangeJob.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/job/sync/SyncIWorkUserChangeJob.java
new file mode 100644
index 00000000..602c8402
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/job/sync/SyncIWorkUserChangeJob.java
@@ -0,0 +1,39 @@
+package com.zt.plat.module.system.job.sync;
+
+import com.xxl.job.core.handler.annotation.XxlJob;
+import com.zt.plat.framework.tenant.core.job.TenantJob;
+import com.zt.plat.module.system.service.sync.SyncIWorkUserChangeService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * 用于定时同步iWork当日修改的用户数据
+ * 同步时间:每日23:00
+ */
+@Component
+@Slf4j
+public class SyncIWorkUserChangeJob {
+
+ @Resource
+ private SyncIWorkUserChangeService syncIWorkUserChangeService;
+
+ /**
+ * 执行批量重跑任务
+ * 配置执行频率:每日23:00时执行一次
+ * cron表达式:0 0 23 * * ?
+ */
+ @XxlJob("syncIWorkUserChangeJob")
+ @TenantJob
+ public void execute() {
+ log.info("[syncLogBatchRerunJob][开始执行同步iWork当日变更用户任务]");
+ try{
+ int processedCount = syncIWorkUserChangeService.process();
+ if (processedCount > 0) {
+ log.info("[syncLogBatchRerunJob][同步任务执行完成,处理了 {} 条记录]", processedCount);
+ }
+ } catch (Exception e) {
+ log.error("[syncLogBatchRerunJob][同步iWork当日变更用户任务执行异常]", e);
+ }
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackListener.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackListener.java
new file mode 100644
index 00000000..c92bfe45
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackListener.java
@@ -0,0 +1,66 @@
+package com.zt.plat.module.system.mq.iwork;
+
+import com.zt.plat.module.system.framework.integration.iwork.config.IWorkProperties;
+import com.zt.plat.module.system.service.integration.iwork.IWorkCallbackLogService;
+import com.zt.plat.module.system.mq.iwork.IWorkBizCallbackMessage;
+import com.zt.plat.module.system.mq.iwork.IWorkBizCallbackResultMessage;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 监听业务模块回传的处理结果,并在失败时由 system 模块负责重试投递原始回调消息。
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+@RocketMQMessageListener(topic = IWorkBizCallbackResultMessage.TOPIC, consumerGroup = IWorkBizCallbackResultMessage.TOPIC + "_CONSUMER")
+public class IWorkBizCallbackListener implements RocketMQListener, InitializingBean {
+
+ private final IWorkCallbackLogService logService;
+ private final IWorkProperties properties;
+ private final IWorkBizCallbackProducer producer;
+ private ScheduledExecutorService scheduler;
+
+ @Override
+ public void afterPropertiesSet() {
+ scheduler = Executors.newScheduledThreadPool(1, r -> new Thread(r, "iwork-callback-retry"));
+ }
+
+ @Override
+ public void onMessage(IWorkBizCallbackResultMessage message) {
+ String key = message.getBizCallbackKey();
+ if (message.isSuccess()) {
+ logService.markSuccess(message.getRequestId());
+ return;
+ }
+
+ int attempt = message.getAttempt() + 1;
+ logService.incrementRetry(message.getRequestId());
+ int maxAttempts = message.getMaxAttempts() > 0 ? message.getMaxAttempts() : properties.getCallback().getRetry().getMaxAttempts();
+
+ if (attempt < maxAttempts) {
+ logService.markFailure(message.getRequestId(), message.getErrorMessage(), true, maxAttempts);
+
+ IWorkBizCallbackMessage next = IWorkBizCallbackMessage.builder()
+ .requestId(message.getRequestId())
+ .bizCallbackKey(key)
+ .payload(message.getPayload())
+ .attempt(attempt)
+ .maxAttempts(maxAttempts)
+ .build();
+
+ int delay = properties.getCallback().getRetry().getDelaySeconds();
+ scheduler.schedule(() -> producer.send(next), delay, TimeUnit.SECONDS);
+ } else {
+ logService.markFailure(message.getRequestId(), message.getErrorMessage(), false, maxAttempts);
+ }
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackProducer.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackProducer.java
new file mode 100644
index 00000000..1b526f23
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackProducer.java
@@ -0,0 +1,18 @@
+package com.zt.plat.module.system.mq.iwork;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class IWorkBizCallbackProducer {
+
+ private final RocketMQTemplate rocketMQTemplate;
+
+ public void send(IWorkBizCallbackMessage message) {
+ // 使用 tag=bizCallbackKey,方便业务侧按 key 订阅
+ String destination = IWorkBizCallbackMessage.TOPIC + ":" + message.getBizCallbackKey();
+ rocketMQTemplate.syncSend(destination, message);
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImpl.java
index 5038fde4..d7e43355 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImpl.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImpl.java
@@ -110,6 +110,9 @@ public class AdminAuthServiceImpl implements AdminAuthService {
final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME;
// 校验账号是否存在
AdminUserDO user = userService.getUserByUsername(username);
+ if (user == null) {
+ user = userService.getUserByWorkcode(username);
+ }
if (user == null) {
createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java
index 3996eef8..7f8258e1 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java
@@ -185,4 +185,11 @@ public interface DeptService {
// ========== 数据同步专用接口 ==========
void syncDept(DeptSaveReqVO syncReqVO);
+
+ /**
+ * 向上查找公司节点信息
+ * @param deptId
+ * @return
+ */
+ List upFindCompanyNode(Long deptId);
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java
index 0a5c607e..81fc35bf 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java
@@ -16,13 +16,12 @@ import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
import com.zt.plat.module.system.dal.dataobject.userdept.UserDeptDO;
import com.zt.plat.module.system.dal.mysql.dept.DeptMapper;
import com.zt.plat.module.system.dal.mysql.userdept.UserDeptMapper;
-import com.zt.plat.module.system.service.dept.DeptExternalCodeService;
import com.zt.plat.module.system.dal.redis.RedisKeyConstants;
import com.zt.plat.module.system.enums.dept.DeptSourceEnum;
import com.zt.plat.module.system.service.permission.PermissionService;
-import org.apache.seata.spring.annotation.GlobalTransactional;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
+import org.apache.seata.spring.annotation.GlobalTransactional;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@@ -762,6 +761,7 @@ public class DeptServiceImpl implements DeptService {
return deptIds;
}
+ @DataPermission(enable = false)
private Long resolveNearestCompanyId(Long deptId, Map deptCache) {
DeptDO current = loadDept(deptId, deptCache);
while (current != null) {
@@ -960,4 +960,9 @@ public class DeptServiceImpl implements DeptService {
// 注意:不发布变更事件,避免循环同步
}
+ @Override
+ public List upFindCompanyNode(Long deptId) {
+ return deptMapper.upFindCompanyNode(deptId);
+ }
+
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkCallbackLogService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkCallbackLogService.java
new file mode 100644
index 00000000..c87ba5de
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkCallbackLogService.java
@@ -0,0 +1,21 @@
+package com.zt.plat.module.system.service.integration.iwork;
+
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkCallbackLogPageReqVO;
+import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkWorkflowCallbackReqVO;
+import com.zt.plat.module.system.dal.dataobject.iwork.IWorkSealLogDO;
+
+public interface IWorkCallbackLogService {
+
+ IWorkSealLogDO upsertOnCallback(IWorkWorkflowCallbackReqVO reqVO, int maxRetry, String rawBody);
+
+ void markSuccess(String requestId);
+
+ void markFailure(String requestId, String error, boolean retrying, int maxRetry);
+
+ void incrementRetry(String requestId);
+
+ PageResult page(IWorkCallbackLogPageReqVO reqVO);
+
+ void resetAndDispatch(String requestId);
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/callback/IWorkBizCallbackHandler.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/callback/IWorkBizCallbackHandler.java
new file mode 100644
index 00000000..967d3140
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/callback/IWorkBizCallbackHandler.java
@@ -0,0 +1,16 @@
+package com.zt.plat.module.system.service.integration.iwork.callback;
+
+public interface IWorkBizCallbackHandler {
+
+ /**
+ * 返回 handler 能处理的 bizCallbackKey。
+ */
+ String getBizCallbackKey();
+
+ /**
+ * 处理业务回调。
+ * @param requestId iWork requestId
+ * @param payload 回调负载
+ */
+ void handle(String requestId, Object payload) throws Exception;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/enums/IWorkCallbackStatusEnum.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/enums/IWorkCallbackStatusEnum.java
new file mode 100644
index 00000000..c08a3e1c
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/enums/IWorkCallbackStatusEnum.java
@@ -0,0 +1,20 @@
+package com.zt.plat.module.system.service.integration.iwork.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum IWorkCallbackStatusEnum {
+
+ CREATE_PENDING(0),
+ CREATE_SUCCESS(1),
+ CREATE_FAILED(2),
+ CALLBACK_PENDING(3),
+ CALLBACK_SUCCESS(4),
+ CALLBACK_FAILED(5),
+ CALLBACK_RETRYING(6),
+ CALLBACK_RETRY_FAILED(7);
+
+ private final int status;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkCallbackLogServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkCallbackLogServiceImpl.java
new file mode 100644
index 00000000..d46dbbe6
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkCallbackLogServiceImpl.java
@@ -0,0 +1,130 @@
+package com.zt.plat.module.system.service.integration.iwork.impl;
+
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.framework.common.util.json.JsonUtils;
+import com.zt.plat.framework.common.util.object.ObjectUtils;
+import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkCallbackLogPageReqVO;
+import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkWorkflowCallbackReqVO;
+import com.zt.plat.module.system.dal.dataobject.iwork.IWorkSealLogDO;
+import com.zt.plat.module.system.dal.mysql.iwork.IWorkSealLogMapper;
+import com.zt.plat.module.system.framework.integration.iwork.config.IWorkProperties;
+import com.zt.plat.module.system.mq.iwork.IWorkBizCallbackMessage;
+import com.zt.plat.module.system.mq.iwork.IWorkBizCallbackProducer;
+import com.zt.plat.module.system.service.integration.iwork.IWorkCallbackLogService;
+import com.zt.plat.module.system.service.integration.iwork.enums.IWorkCallbackStatusEnum;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class IWorkCallbackLogServiceImpl implements IWorkCallbackLogService {
+
+ private static final int RAW_MAX = 2000;
+
+ private final IWorkSealLogMapper logMapper;
+ private final IWorkBizCallbackProducer producer;
+ private final IWorkProperties properties;
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public IWorkSealLogDO upsertOnCallback(IWorkWorkflowCallbackReqVO reqVO, int maxRetry, String rawBody) {
+ IWorkSealLogDO existing = logMapper.selectByRequestId(reqVO.getRequestId());
+ IWorkSealLogDO log = Optional.ofNullable(existing).orElseGet(IWorkSealLogDO::new);
+ log.setRequestId(reqVO.getRequestId());
+ log.setBusinessCode(reqVO.getBusinessCode());
+ log.setBizCallbackKey(reqVO.getBizCallbackKey());
+ log.setStatus(IWorkCallbackStatusEnum.CALLBACK_PENDING.getStatus());
+ log.setRetryCount(ObjectUtils.defaultIfNull(log.getRetryCount(), 0));
+ log.setMaxRetry(maxRetry);
+ log.setRawCallback(truncate(rawBody));
+ log.setLastCallbackTime(LocalDateTime.now());
+ if (log.getId() == null) {
+ logMapper.insert(log);
+ } else {
+ logMapper.updateById(log);
+ }
+ return log;
+ }
+
+ @Override
+ public void markSuccess(String requestId) {
+ IWorkSealLogDO log = new IWorkSealLogDO();
+ log.setRequestId(requestId);
+ log.setStatus(IWorkCallbackStatusEnum.CALLBACK_SUCCESS.getStatus());
+ log.setLastErrorMessage(null);
+ log.setLastCallbackTime(LocalDateTime.now());
+ logMapper.update(log, new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper()
+ .eq(IWorkSealLogDO::getRequestId, requestId));
+ }
+
+ @Override
+ public void markFailure(String requestId, String error, boolean retrying, int maxRetry) {
+ IWorkSealLogDO log = new IWorkSealLogDO();
+ log.setRequestId(requestId);
+ log.setStatus(retrying ? IWorkCallbackStatusEnum.CALLBACK_RETRYING.getStatus() : IWorkCallbackStatusEnum.CALLBACK_FAILED.getStatus());
+ log.setLastErrorMessage(error);
+ log.setLastCallbackTime(LocalDateTime.now());
+ log.setMaxRetry(maxRetry);
+ logMapper.update(log, new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper()
+ .eq(IWorkSealLogDO::getRequestId, requestId));
+ }
+
+ @Override
+ public void incrementRetry(String requestId) {
+ IWorkSealLogDO db = logMapper.selectByRequestId(requestId);
+ if (db == null) {
+ return;
+ }
+ IWorkSealLogDO log = new IWorkSealLogDO();
+ log.setId(db.getId());
+ log.setRetryCount(ObjectUtils.defaultIfNull(db.getRetryCount(), 0) + 1);
+ log.setLastCallbackTime(LocalDateTime.now());
+ logMapper.updateById(log);
+ }
+
+ @Override
+ public PageResult page(IWorkCallbackLogPageReqVO reqVO) {
+ return logMapper.selectPage(reqVO);
+ }
+
+ @Override
+ public void resetAndDispatch(String requestId) {
+ IWorkSealLogDO db = logMapper.selectByRequestId(requestId);
+ if (db == null) {
+ return;
+ }
+ IWorkSealLogDO log = new IWorkSealLogDO();
+ log.setId(db.getId());
+ log.setRetryCount(0);
+ log.setStatus(IWorkCallbackStatusEnum.CALLBACK_RETRYING.getStatus());
+ log.setLastCallbackTime(LocalDateTime.now());
+ logMapper.updateById(log);
+
+ int maxAttempts = properties.getCallback().getRetry().getMaxAttempts();
+ Object payload;
+ try {
+ payload = JsonUtils.parseObject(db.getRawCallback(), Object.class);
+ } catch (Exception ex) {
+ payload = db.getRawCallback();
+ }
+ producer.send(IWorkBizCallbackMessage.builder()
+ .requestId(db.getRequestId())
+ .bizCallbackKey(db.getBizCallbackKey())
+ .payload(payload)
+ .attempt(0)
+ .maxAttempts(maxAttempts)
+ .build());
+ }
+
+ private String truncate(String raw) {
+ if (!StringUtils.hasText(raw)) {
+ return raw;
+ }
+ return raw.length() > RAW_MAX ? raw.substring(0, RAW_MAX) : raw;
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkIntegrationServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkIntegrationServiceImpl.java
index 445af73b..d06b0adf 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkIntegrationServiceImpl.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkIntegrationServiceImpl.java
@@ -182,7 +182,7 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
AtomicReference attachmentIdRef = new AtomicReference<>();
- TenantUtils.execute(tenantId, () -> attachmentIdRef.set(saveCallbackAttachment(fileUrl, reqVO.getFileName(), referenceBusinessFile, reqVO.getSsoToken())));
+ TenantUtils.execute(tenantId, () -> attachmentIdRef.set(saveCallbackAttachment(fileUrl, reqVO.getFileName(), referenceBusinessFile)));
return attachmentIdRef.get();
}
@@ -251,14 +251,14 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
return executeOaRequest(request);
}
- private Long saveCallbackAttachment(String fileUrl, String overrideFileName, BusinessFileRespDTO referenceBusinessFile, String ssoToken) {
+ private Long saveCallbackAttachment(String fileUrl, String overrideFileName, BusinessFileRespDTO referenceBusinessFile) {
Long businessId = referenceBusinessFile.getBusinessId();
FileCreateReqDTO fileCreateReqDTO = new FileCreateReqDTO();
fileCreateReqDTO.setName(resolveFileName(overrideFileName, fileUrl));
fileCreateReqDTO.setDirectory(null);
fileCreateReqDTO.setType(null);
- fileCreateReqDTO.setContent(downloadFileBytes(fileUrl, ssoToken));
+ fileCreateReqDTO.setContent(downloadFileBytes(fileUrl));
CommonResult fileResult = fileApi.createFileWithReturn(fileCreateReqDTO);
if (fileResult == null || !fileResult.isSuccess() || fileResult.getData() == null) {
@@ -297,10 +297,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
return businessFile;
}
- private byte[] downloadFileBytes(String fileUrl, String ssoToken) {
- // 如果回调已提供 ssoToken,按需拼接后下载 OA 附件
- String finalUrl = appendSsoTokenIfNeeded(fileUrl, ssoToken);
-
+ private byte[] downloadFileBytes(String fileUrl) {
+ String finalUrl = fileUrl;
OkHttpClient client = okHttpClient();
Request request = new Request.Builder().url(finalUrl).get().build();
try (Response response = client.newCall(request).execute()) {
@@ -317,22 +315,6 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
}
- private String appendSsoTokenIfNeeded(String fileUrl, String ssoToken) {
- // 未提供 token 或 URL 为空,直接返回原链接
- if (!StringUtils.hasText(ssoToken) || !StringUtils.hasText(fileUrl)) {
- return fileUrl;
- }
- // 已包含 ssoToken(不区分大小写)则不重复添加
- String lower = fileUrl.toLowerCase();
- if (lower.contains("ssotoken=")) {
- return fileUrl;
- }
- // 简单拼接查询参数
- return fileUrl.contains("?")
- ? fileUrl + "&ssoToken=" + ssoToken
- : fileUrl + "?ssoToken=" + ssoToken;
- }
-
private String resolveFileName(String overrideName, String fileUrl) {
if (StringUtils.hasText(overrideName)) {
return overrideName;
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkOrgRestServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkOrgRestServiceImpl.java
index f31003a1..0d8973aa 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkOrgRestServiceImpl.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkOrgRestServiceImpl.java
@@ -92,6 +92,18 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService {
if (StringUtils.hasText(reqVO.getSubcompanyName())) {
params.put("subcompanyname", reqVO.getSubcompanyName());
}
+ if (StringUtils.hasText(reqVO.getModified())) {
+ params.put("modified", reqVO.getModified());
+ }
+ if (StringUtils.hasText(reqVO.getCanceled())) {
+ params.put("canceled", reqVO.getCanceled());
+ }
+ if (StringUtils.hasText(reqVO.getCustomData())) {
+ params.put("custom_data", reqVO.getCustomData());
+ }
+ if (StringUtils.hasText(reqVO.getId())) {
+ params.put("id", reqVO.getId());
+ }
JsonNode node = invokeParamsEndpoint(path, params);
return buildSubcompanyPageResp(node);
}
@@ -106,8 +118,23 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService {
if (StringUtils.hasText(reqVO.getDepartmentName())) {
params.put("departmentname", reqVO.getDepartmentName());
}
- if (StringUtils.hasText(reqVO.getSubcompanyId())) {
- params.put("subcompanyid", reqVO.getSubcompanyId());
+ if (StringUtils.hasText(reqVO.getSubcompanyId1())) {
+ params.put("subcompanyid1", reqVO.getSubcompanyId1());
+ }
+ if (StringUtils.hasText(reqVO.getCreated())) {
+ params.put("created", reqVO.getCreated());
+ }
+ if (StringUtils.hasText(reqVO.getModified())) {
+ params.put("modified", reqVO.getModified());
+ }
+ if (StringUtils.hasText(reqVO.getCanceled())) {
+ params.put("canceled", reqVO.getCanceled());
+ }
+ if (StringUtils.hasText(reqVO.getCustomData())) {
+ params.put("custom_data", reqVO.getCustomData());
+ }
+ if (StringUtils.hasText(reqVO.getId())) {
+ params.put("id", reqVO.getId());
}
JsonNode node = invokeParamsEndpoint(path, params);
return buildDepartmentPageResp(node);
@@ -117,12 +144,18 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService {
public IWorkHrJobTitlePageRespVO listJobTitles(IWorkJobTitleQueryReqVO reqVO) {
String path = orgPaths().getJobTitlePage();
Map params = buildBaseParams(reqVO);
- if (StringUtils.hasText(reqVO.getJobTitleCode())) {
- params.put("jobtitlecode", reqVO.getJobTitleCode());
- }
if (StringUtils.hasText(reqVO.getJobTitleName())) {
params.put("jobtitlename", reqVO.getJobTitleName());
}
+ if (StringUtils.hasText(reqVO.getCreated())) {
+ params.put("created", reqVO.getCreated());
+ }
+ if (StringUtils.hasText(reqVO.getModified())) {
+ params.put("modified", reqVO.getModified());
+ }
+ if (StringUtils.hasText(reqVO.getId())) {
+ params.put("id", reqVO.getId());
+ }
JsonNode node = invokeParamsEndpoint(path, params);
return buildJobTitlePageResp(node);
}
@@ -134,11 +167,8 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService {
if (StringUtils.hasText(reqVO.getWorkCode())) {
params.put("workcode", reqVO.getWorkCode());
}
- if (StringUtils.hasText(reqVO.getLastName())) {
- params.put("lastname", reqVO.getLastName());
- }
- if (StringUtils.hasText(reqVO.getSubcompanyId())) {
- params.put("subcompanyid", reqVO.getSubcompanyId());
+ if (StringUtils.hasText(reqVO.getSubcompanyId1())) {
+ params.put("subcompanyid1", reqVO.getSubcompanyId1());
}
if (StringUtils.hasText(reqVO.getDepartmentId())) {
params.put("departmentid", reqVO.getDepartmentId());
@@ -146,14 +176,26 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService {
if (StringUtils.hasText(reqVO.getJobTitleId())) {
params.put("jobtitleid", reqVO.getJobTitleId());
}
- if (StringUtils.hasText(reqVO.getStatus())) {
- params.put("status", reqVO.getStatus());
+ if (StringUtils.hasText(reqVO.getId())) {
+ params.put("id", reqVO.getId());
}
- if (StringUtils.hasText(reqVO.getMobile())) {
- params.put("mobile", reqVO.getMobile());
+ if (StringUtils.hasText(reqVO.getLoginId())) {
+ params.put("loginid", reqVO.getLoginId());
}
- if (StringUtils.hasText(reqVO.getEmail())) {
- params.put("email", reqVO.getEmail());
+ if (StringUtils.hasText(reqVO.getCreated())) {
+ params.put("created", reqVO.getCreated());
+ }
+ if (StringUtils.hasText(reqVO.getModified())) {
+ params.put("modified", reqVO.getModified());
+ }
+ if (StringUtils.hasText(reqVO.getBaseCustomData())) {
+ params.put("base_custom_data", reqVO.getBaseCustomData());
+ }
+ if (StringUtils.hasText(reqVO.getPersonCustomData())) {
+ params.put("person_custom_data", reqVO.getPersonCustomData());
+ }
+ if (StringUtils.hasText(reqVO.getWorkCustomData())) {
+ params.put("work_custom_data", reqVO.getWorkCustomData());
}
JsonNode node = invokeParamsEndpoint(path, params);
return buildUserPageResp(node);
@@ -161,9 +203,6 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService {
private Map buildBaseParams(IWorkOrgBaseQueryReqVO reqVO) {
Map params = new HashMap<>();
- if (reqVO.getParams() != null) {
- params.putAll(reqVO.getParams());
- }
if (reqVO.getCurpage() != null) {
params.put("curpage", reqVO.getCurpage());
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncProcessorImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncProcessorImpl.java
index 3c92a184..a32be23a 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncProcessorImpl.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncProcessorImpl.java
@@ -13,8 +13,11 @@ import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrUs
import com.zt.plat.module.system.controller.admin.user.vo.user.UserSaveReqVO;
import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
import com.zt.plat.module.system.dal.dataobject.dept.PostDO;
+import com.zt.plat.module.system.dal.dataobject.dept.UserPostDO;
import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO;
+import com.zt.plat.module.system.dal.dataobject.userdept.UserDeptDO;
import com.zt.plat.module.system.dal.mysql.dept.PostMapper;
+import com.zt.plat.module.system.dal.mysql.dept.UserPostMapper;
import com.zt.plat.module.system.dal.mysql.user.AdminUserMapper;
import com.zt.plat.module.system.enums.common.SexEnum;
import com.zt.plat.module.system.enums.dept.DeptSourceEnum;
@@ -24,6 +27,7 @@ import com.zt.plat.module.system.service.dept.DeptService;
import com.zt.plat.module.system.service.dept.PostService;
import com.zt.plat.module.system.service.integration.iwork.IWorkSyncProcessor;
import com.zt.plat.module.system.service.user.AdminUserService;
+import com.zt.plat.module.system.service.userdept.UserDeptService;
import com.zt.plat.module.system.util.sync.SyncVerifyUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -33,6 +37,7 @@ import org.springframework.util.DigestUtils;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
@Slf4j
@Service
@@ -47,8 +52,10 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
private final DeptService deptService;
private final PostService postService;
private final PostMapper postMapper;
+ private final UserPostMapper userPostMapper;
private final AdminUserService adminUserService;
private final AdminUserMapper adminUserMapper;
+ private final UserDeptService userDeptService;
private final Map postCache = new ConcurrentHashMap<>();
@@ -322,7 +329,41 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
if (records.isEmpty()) {
return result;
}
+ long batchStart = System.currentTimeMillis();
result.increasePulled(records.size());
+ // 预取已有用户,避免逐条查询
+ long preloadUsersStart = System.currentTimeMillis();
+ Map existingUsers = new HashMap<>();
+ List userIds = records.stream()
+ .map(IWorkHrUserPageRespVO.User::getId)
+ .filter(Objects::nonNull)
+ .map(Integer::longValue)
+ .distinct()
+ .toList();
+ if (!userIds.isEmpty()) {
+ List users = adminUserMapper.selectBatchIds(userIds);
+ if (CollUtil.isNotEmpty(users)) {
+ users.forEach(user -> existingUsers.put(user.getId(), user));
+ }
+ }
+ long preloadUsersMs = System.currentTimeMillis() - preloadUsersStart;
+
+ // 预取岗位,避免逐条按编码查询
+ long preloadPostsStart = System.currentTimeMillis();
+ List postCodes = records.stream()
+ .map(IWorkHrUserPageRespVO.User::getJobtitleid)
+ .filter(Objects::nonNull)
+ .map(this::buildJobCode)
+ .distinct()
+ .toList();
+ if (!postCodes.isEmpty()) {
+ List posts = postMapper.selectByCodes(postCodes);
+ if (CollUtil.isNotEmpty(posts)) {
+ posts.forEach(post -> postCache.put(buildPostCacheKey(post.getCode()), post));
+ }
+ }
+ long preloadPostsMs = System.currentTimeMillis() - preloadPostsStart;
+
for (IWorkHrUserPageRespVO.User user : records) {
if (user == null) {
continue;
@@ -344,7 +385,7 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
CommonStatusEnum status = inactive ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE;
// 直接沿用 iWork 原始密码,避免重复格式化造成校验偏差
String externalPassword = trimToNull(user.getPassword());
- AdminUserDO existing = adminUserMapper.selectById(user.getId());
+ AdminUserDO existing = user.getId() == null ? null : existingUsers.get(user.getId().longValue());
UserSyncOutcome outcome;
if (existing == null) {
if (!options.isCreateIfMissing()) {
@@ -377,6 +418,9 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
result.withMessage("同步人员失败: " + ex.getMessage());
}
}
+ long totalMs = System.currentTimeMillis() - batchStart;
+ log.info("[iWork] 人员批次同步完成 size={} preloadUsersMs={} preloadPostsMs={} totalMs={}",
+ records.size(), preloadUsersMs, preloadPostsMs, totalMs);
return result;
}
//TODO
@@ -494,12 +538,49 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
String externalPassword) {
UserSaveReqVO req = buildUserSaveReq(source, username, deptId, postId, status);
req.setId(existing.getId());
+ Long iworkDeptId = resolveIWorkDeptId(deptId);
+ req.setDeptIds(null);
+ req.setPostIds(null);
boolean disabledChanged = CommonStatusEnum.isDisable(status.getStatus()) && CommonStatusEnum.isEnable(existing.getStatus());
- adminUserService.updateUser(req);
- syncPassword(existing, externalPassword);
+ boolean infoChanged = isUserInfoChanged(existing, req);
+ boolean passwordChanged = isPasswordChanged(existing, externalPassword);
+ boolean deptChanged = syncIWorkUserDeptRelations(existing.getId(), iworkDeptId);
+ boolean postChanged = syncIWorkUserPostRelations(existing.getId(), postId);
+
+ if (!infoChanged && !passwordChanged && !deptChanged && !postChanged) {
+ return new UserSyncOutcome(SyncAction.SKIPPED, false, existing.getId());
+ }
+
+ if (infoChanged) {
+ adminUserService.updateUser(req);
+ }
+ if (passwordChanged) {
+ syncPassword(existing, externalPassword);
+ }
return new UserSyncOutcome(SyncAction.UPDATED, disabledChanged, existing.getId());
}
+ private boolean isUserInfoChanged(AdminUserDO existing, UserSaveReqVO req) {
+ if (existing == null || req == null) {
+ return false;
+ }
+ return !Objects.equals(existing.getUsername(), req.getUsername())
+ || !Objects.equals(existing.getWorkcode(), req.getWorkcode())
+ || !Objects.equals(existing.getNickname(), req.getNickname())
+ || !Objects.equals(existing.getRemark(), req.getRemark())
+ || !Objects.equals(existing.getEmail(), req.getEmail())
+ || !Objects.equals(existing.getMobile(), req.getMobile())
+ || !Objects.equals(existing.getSex(), req.getSex())
+ || !Objects.equals(existing.getStatus(), req.getStatus());
+ }
+
+ private boolean isPasswordChanged(AdminUserDO existing, String externalPassword) {
+ if (existing == null || StrUtil.isBlank(externalPassword)) {
+ return false;
+ }
+ return !StrUtil.equals(externalPassword, existing.getPassword());
+ }
+
private DeptSaveReqVO buildSubcompanySaveReq(IWorkHrSubcompanyPageRespVO.Subcompany data,
Long deptId,
Long parentId,
@@ -550,8 +631,9 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
req.setWorkcode(resolveWorkcode(source));
req.setNickname(limitLength(StrUtil.blankToDefault(source.getLastname(), username), 30));
req.setRemark(buildUserRemark(source));
- if (deptId != null) {
- req.setDeptIds(singletonSet(deptId));
+ Long iworkDeptId = resolveIWorkDeptId(deptId);
+ if (iworkDeptId != null) {
+ req.setDeptIds(singletonSet(iworkDeptId));
}
if (postId != null) {
req.setPostIds(singletonSet(postId));
@@ -566,6 +648,74 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
return req;
}
+ private boolean syncIWorkUserDeptRelations(Long userId, Long iworkDeptId) {
+ if (userId == null) {
+ return false;
+ }
+ List relations = userDeptService.getValidUserDeptListByUserIds(Collections.singleton(userId));
+ Set existingDeptIds = relations.stream()
+ .map(UserDeptDO::getDeptId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ Set existingIworkDeptIds = new LinkedHashSet<>();
+ if (CollUtil.isNotEmpty(existingDeptIds)) {
+ Map deptMap = deptService.getDeptMap(existingDeptIds);
+ existingIworkDeptIds = deptMap.values().stream()
+ .filter(this::isIWorkDept)
+ .map(DeptDO::getId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ }
+ Set desiredIworkDeptIds = iworkDeptId == null
+ ? Collections.emptySet()
+ : Collections.singleton(iworkDeptId);
+ if (existingIworkDeptIds.equals(desiredIworkDeptIds)) {
+ return false;
+ }
+ Collection toDelete = CollUtil.subtract(existingIworkDeptIds, desiredIworkDeptIds);
+ Collection toAdd = CollUtil.subtract(desiredIworkDeptIds, existingIworkDeptIds);
+ if (CollUtil.isNotEmpty(toDelete)) {
+ userDeptService.deleteUserDeptByUserIdAndDeptIds(userId, toDelete);
+ }
+ if (CollUtil.isNotEmpty(toAdd)) {
+ userDeptService.batchCreateUserDept(Collections.singletonList(
+ new UserDeptDO().setUserId(userId).setDeptId(iworkDeptId)));
+ }
+ return true;
+ }
+
+ private boolean syncIWorkUserPostRelations(Long userId, Long postId) {
+ if (userId == null || postId == null) {
+ return false;
+ }
+ List relations = userPostMapper.selectListByUserId(userId);
+ Set existingPostIds = relations.stream()
+ .map(UserPostDO::getPostId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ if (existingPostIds.contains(postId)) {
+ return false;
+ }
+ userPostMapper.insertBatch(Collections.singletonList(
+ new UserPostDO().setUserId(userId).setPostId(postId)));
+ return true;
+ }
+
+ private Long resolveIWorkDeptId(Long deptId) {
+ if (deptId == null) {
+ return null;
+ }
+ DeptDO dept = deptService.getDept(deptId);
+ return isIWorkDept(dept) ? deptId : null;
+ }
+
+ private boolean isIWorkDept(DeptDO dept) {
+ if (dept == null) {
+ return false;
+ }
+ return Objects.equals(dept.getDeptSource(), DeptSourceEnum.IWORK.getSource());
+ }
+
private void mergeDeptDefaults(DeptSaveReqVO target, DeptDO existing) {
target.setCode(StrUtil.blankToDefault(target.getCode(), existing.getCode()));
target.setShortName(StrUtil.blankToDefault(target.getShortName(), existing.getShortName()));
@@ -729,7 +879,12 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
if (StrUtil.isBlank(statusFlag)) {
return false;
}
- return !"0".equals(statusFlag.trim());
+ Integer status = parseInteger(statusFlag);
+ if (status == null) {
+ return false;
+ }
+ // iWork 状态:0试用、1正式、2临时、3试用延期、4解聘、5离职、6退休、7无效
+ return status >= 4;
}
private Integer resolveSex(String sexFlag) {
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncServiceImpl.java
index 95ac0cdc..dfb327a1 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncServiceImpl.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncServiceImpl.java
@@ -11,14 +11,13 @@ import com.zt.plat.module.system.service.integration.iwork.IWorkSyncProcessor;
import com.zt.plat.module.system.service.integration.iwork.IWorkSyncService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.EnumSet;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import static com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationErrorCodeConstants.IWORK_ORG_REMOTE_FAILED;
@@ -217,11 +216,14 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
int pagesLimit = reqVO.getMaxPages() == null ? Integer.MAX_VALUE : reqVO.getMaxPages();
int processedPages = 0;
for (int page = startPage; processedPages < pagesLimit; page++) {
+ long pageStart = System.currentTimeMillis();
BatchExecution execution = executor.execute(page, pageSize);
+ long pageMs = System.currentTimeMillis() - pageStart;
if (execution == null || execution.totalPulled == 0) {
break;
}
processedPages++;
+ log.info("[iWork] 全量同步 {} 页={} pulled={} costMs={}", type, page, execution.totalPulled, pageMs);
IWorkSyncBatchStatVO batchStat = new IWorkSyncBatchStatVO();
batchStat.setEntityType(type);
batchStat.setPageNumber(page);
@@ -258,15 +260,18 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
if (query == null || reqVO == null) {
return;
}
+ copyQueryParameters(reqVO, query); // 设置查询条件
if (StrUtil.isBlank(reqVO.getId())) {
return;
}
- Map params = query.getParams();
- if (params == null) {
- params = new HashMap<>();
- query.setParams(params);
+ applyQueryId(query, reqVO.getId());
+ }
+
+ private void copyQueryParameters(IWorkFullSyncReqVO reqVO, IWorkOrgBaseQueryReqVO query) {
+ BeanUtils.copyProperties(reqVO, query);
+ if (query instanceof IWorkUserQueryReqVO userQuery) {
+ userQuery.setDepartmentId(reqVO.getDepartmentCode()); // 设置部门编号
}
- params.put("id", reqVO.getId());
}
/**
@@ -385,12 +390,28 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
if (StrUtil.isBlank(reqVO.getId())) {
return;
}
- Map params = query.getParams();
- if (params == null) {
- params = new HashMap<>();
- query.setParams(params);
+ applyQueryId(query, reqVO.getId());
+ }
+
+ private void applyQueryId(IWorkOrgBaseQueryReqVO query, String id) {
+ if (query == null || StrUtil.isBlank(id)) {
+ return;
+ }
+ if (query instanceof IWorkSubcompanyQueryReqVO subcompanyQuery) {
+ subcompanyQuery.setId(id);
+ return;
+ }
+ if (query instanceof IWorkDepartmentQueryReqVO departmentQuery) {
+ departmentQuery.setId(id);
+ return;
+ }
+ if (query instanceof IWorkJobTitleQueryReqVO jobTitleQuery) {
+ jobTitleQuery.setId(id);
+ return;
+ }
+ if (query instanceof IWorkUserQueryReqVO userQuery) {
+ userQuery.setId(id);
}
- params.put("id", reqVO.getId());
}
@@ -403,11 +424,14 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
int pagesLimit = reqVO.getMaxPages() == null ? Integer.MAX_VALUE : reqVO.getMaxPages();
int processedPages = 0;
for (int page = startPage; processedPages < pagesLimit; page++) {
+ long pageStart = System.currentTimeMillis();
BatchExecution execution = executor.execute(page, pageSize);
+ long pageMs = System.currentTimeMillis() - pageStart;
if (execution == null || execution.totalPulled == 0) {
break;
}
processedPages++;
+ log.info("[iWork] 手动同步 {} 页={} pulled={} costMs={}", type, page, execution.totalPulled, pageMs);
IWorkSyncBatchStatVO batchStat = new IWorkSyncBatchStatVO();
batchStat.setEntityType(type);
batchStat.setPageNumber(page);
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/OAuth2TokenServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/OAuth2TokenServiceImpl.java
index eee1404e..33552e9f 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/OAuth2TokenServiceImpl.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/OAuth2TokenServiceImpl.java
@@ -203,6 +203,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
.put(LoginUser.INFO_KEY_TENANT_ID, user.getTenantId().toString())
.put(LoginUser.INFO_KEY_USERNAME, user.getUsername())
.put(LoginUser.INFO_KEY_PHONE, user.getMobile())
+ .put(LoginUser.INFO_KEY_WORK_CODE, user.getWorkcode())
.put(LoginUser.INFO_KEY_POST_IDS, CollUtil.isEmpty(user.getPostIds()) ? "[]" : JsonUtils.toJsonString(user.getPostIds()))
.build();
} else if (userType.equals(UserTypeEnum.MEMBER.getValue())) {
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/MenuDataRuleService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/MenuDataRuleService.java
new file mode 100644
index 00000000..e49d10d8
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/MenuDataRuleService.java
@@ -0,0 +1,72 @@
+package com.zt.plat.module.system.service.permission;
+
+import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleSaveReqVO;
+import com.zt.plat.module.system.dal.dataobject.permission.MenuDataRuleDO;
+
+import jakarta.validation.Valid;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 菜单数据规则 Service 接口
+ *
+ * @author ZT
+ */
+public interface MenuDataRuleService {
+
+ /**
+ * 创建菜单数据规则
+ *
+ * @param createReqVO 创建信息
+ * @return 规则ID
+ */
+ Long createMenuDataRule(@Valid MenuDataRuleSaveReqVO createReqVO);
+
+ /**
+ * 更新菜单数据规则
+ *
+ * @param updateReqVO 更新信息
+ */
+ void updateMenuDataRule(@Valid MenuDataRuleSaveReqVO updateReqVO);
+
+ /**
+ * 删除菜单数据规则
+ *
+ * @param id 规则ID
+ */
+ void deleteMenuDataRule(Long id);
+
+ /**
+ * 获取菜单数据规则
+ *
+ * @param id 规则ID
+ * @return 规则信息
+ */
+ MenuDataRuleDO getMenuDataRule(Long id);
+
+ /**
+ * 获取菜单的所有数据规则
+ *
+ * @param menuId 菜单ID
+ * @return 规则列表
+ */
+ List getMenuDataRuleListByMenuId(Long menuId);
+
+ /**
+ * 获取用户在指定菜单下的有效数据规则
+ *
+ * @param userId 用户ID
+ * @param menuId 菜单ID
+ * @return 规则列表
+ */
+ List getUserMenuDataRules(Long userId, Long menuId);
+
+ /**
+ * 批量获取菜单的数据规则(带缓存)
+ *
+ * @param menuIds 菜单ID列表
+ * @return 菜单ID -> 规则列表的映射
+ */
+ Map> getMenuDataRuleMap(Collection menuIds);
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PageComponentMappingService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PageComponentMappingService.java
new file mode 100644
index 00000000..f62f9bc1
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PageComponentMappingService.java
@@ -0,0 +1,56 @@
+package com.zt.plat.module.system.service.permission;
+
+import cn.hutool.core.util.StrUtil;
+import com.zt.plat.module.system.dal.dataobject.permission.MenuDO;
+import com.zt.plat.module.system.dal.mysql.permission.MenuMapper;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 页面组件映射服务
+ * 根据pageComponent查询对应的菜单ID
+ *
+ * @author ZT
+ */
+@Service
+@Slf4j
+public class PageComponentMappingService {
+
+ @Resource
+ private MenuMapper menuMapper;
+
+ /**
+ * 根据页面组件路径获取菜单ID
+ *
+ * @param pageComponent 页面组件路径,如:system/role/index
+ * @return 菜单ID,如果未找到返回null
+ */
+ public Long getMenuIdByPageComponent(String pageComponent) {
+ if (StrUtil.isBlank(pageComponent)) {
+ return null;
+ }
+
+ log.debug("[getMenuIdByPageComponent][查询pageComponent: {}]", pageComponent);
+
+ // 使用精确匹配查询菜单
+ MenuDO menu = menuMapper.selectByComponent(pageComponent);
+
+ if (menu != null) {
+ log.debug("[getMenuIdByPageComponent][找到匹配菜单: ID={}, Name={}, Component={}]",
+ menu.getId(), menu.getName(), menu.getComponent());
+ // 兼容达梦数据库,ID可能是Integer类型,需要转换为Long
+ Object id = menu.getId();
+ if (id instanceof Number) {
+ return ((Number) id).longValue();
+ }
+ return (Long) id;
+ }
+
+ log.warn("[getMenuIdByPageComponent][未找到匹配的菜单: {}]", pageComponent);
+ return null;
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionService.java
index 7a23c24e..6d570ca7 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionService.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionService.java
@@ -86,6 +86,23 @@ public interface PermissionService {
*/
Set getMenuRoleIdListByMenuIdFromCache(Long menuId);
+ /**
+ * 批量设置角色-菜单-规则关联
+ *
+ * @param roleId 角色编号
+ * @param menuDataRules 菜单和规则的映射关系
+ */
+ void assignRoleMenuDataRules(Long roleId, Collection menuDataRules);
+
+ /**
+ * 获取角色在指定菜单下已选择的数据规则ID列表
+ *
+ * @param roleId 角色编号
+ * @param menuId 菜单编号
+ * @return 数据规则ID列表
+ */
+ Set getRoleMenuDataRules(Long roleId, Long menuId);
+
// ========== 用户-角色的相关方法 ==========
/**
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java
index ce302e9e..96820ed6 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java
@@ -76,6 +76,8 @@ public class PermissionServiceImpl implements PermissionService {
private RoleMenuMapper roleMenuMapper;
@Resource
private UserRoleMapper userRoleMapper;
+ @Resource
+ private com.zt.plat.module.system.dal.mysql.permission.RoleMenuDataRuleMapper roleMenuDataRuleMapper;
private RoleService roleService;
@Resource
@@ -221,6 +223,45 @@ public class PermissionServiceImpl implements PermissionService {
}
}
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void assignRoleMenuDataRules(Long roleId, Collection menuDataRules) {
+ if (CollUtil.isEmpty(menuDataRules)) {
+ return;
+ }
+
+ // 遍历每个菜单,更新其数据规则关联
+ for (PermissionAssignRoleMenuItemReqVO menuDataRule : menuDataRules) {
+ Long menuId = menuDataRule.getId();
+ List dataRuleIds = menuDataRule.getDataRuleIds();
+
+ // 删除该角色在该菜单下的旧规则关联
+ roleMenuDataRuleMapper.deleteByRoleAndMenu(roleId, menuId);
+
+ // 如果有新规则,则插入
+ if (CollUtil.isNotEmpty(dataRuleIds)) {
+ List entities =
+ dataRuleIds.stream().map(ruleId -> {
+ com.zt.plat.module.system.dal.dataobject.permission.RoleMenuDataRuleDO entity =
+ new com.zt.plat.module.system.dal.dataobject.permission.RoleMenuDataRuleDO();
+ entity.setRoleId(roleId);
+ entity.setMenuId(menuId);
+ entity.setDataRuleId(ruleId);
+ return entity;
+ }).collect(Collectors.toList());
+ roleMenuDataRuleMapper.insertBatch(entities);
+ }
+ }
+ }
+
+ @Override
+ public Set getRoleMenuDataRules(Long roleId, Long menuId) {
+ List list =
+ roleMenuDataRuleMapper.selectListByRoleAndMenu(roleId, menuId);
+ return CollectionUtils.convertSet(list,
+ com.zt.plat.module.system.dal.dataobject.permission.RoleMenuDataRuleDO::getDataRuleId);
+ }
+
@Override
@Transactional(rollbackFor = Exception.class)
@Caching(evict = {
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/impl/MenuDataRuleServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/impl/MenuDataRuleServiceImpl.java
new file mode 100644
index 00000000..da8cbef8
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/impl/MenuDataRuleServiceImpl.java
@@ -0,0 +1,109 @@
+package com.zt.plat.module.system.service.permission.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleSaveReqVO;
+import com.zt.plat.module.system.convert.permission.MenuDataRuleConvert;
+import com.zt.plat.module.system.dal.dataobject.permission.MenuDataRuleDO;
+import com.zt.plat.module.system.dal.mysql.permission.MenuDataRuleMapper;
+import com.zt.plat.module.system.service.permission.MenuDataRuleService;
+import com.zt.plat.module.system.service.permission.PermissionService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import jakarta.annotation.Resource;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static com.zt.plat.module.system.enums.ErrorCodeConstants.*;
+
+/**
+ * 菜单数据规则 Service 实现类
+ *
+ * @author ZT
+ */
+@Service
+@Validated
+@Slf4j
+public class MenuDataRuleServiceImpl implements MenuDataRuleService {
+
+ @Resource
+ private MenuDataRuleMapper menuDataRuleMapper;
+
+ @Resource
+ private PermissionService permissionService;
+
+ @Override
+ @CacheEvict(value = "menuDataRule", key = "#createReqVO.menuId")
+ public Long createMenuDataRule(MenuDataRuleSaveReqVO createReqVO) {
+ MenuDataRuleDO rule = MenuDataRuleConvert.INSTANCE.convert(createReqVO);
+ menuDataRuleMapper.insert(rule);
+ return rule.getId();
+ }
+
+ @Override
+ @CacheEvict(value = "menuDataRule", key = "#updateReqVO.menuId")
+ public void updateMenuDataRule(MenuDataRuleSaveReqVO updateReqVO) {
+ validateMenuDataRuleExists(updateReqVO.getId());
+ MenuDataRuleDO updateObj = MenuDataRuleConvert.INSTANCE.convert(updateReqVO);
+ menuDataRuleMapper.updateById(updateObj);
+ }
+
+ @Override
+ public void deleteMenuDataRule(Long id) {
+ MenuDataRuleDO rule = validateMenuDataRuleExists(id);
+ menuDataRuleMapper.deleteById(id);
+ }
+
+ @Override
+ public MenuDataRuleDO getMenuDataRule(Long id) {
+ return menuDataRuleMapper.selectById(id);
+ }
+
+ @Override
+ @Cacheable(value = "menuDataRule", key = "#menuId")
+ public List getMenuDataRuleListByMenuId(Long menuId) {
+ return menuDataRuleMapper.selectListByMenuId(menuId);
+ }
+
+ @Override
+ public List getUserMenuDataRules(Long userId, Long menuId) {
+ Set roleIds = permissionService.getUserRoleIdListByUserId(userId);
+ if (CollUtil.isEmpty(roleIds)) {
+ return Collections.emptyList();
+ }
+
+ List allRules = getMenuDataRuleListByMenuId(menuId);
+ if (CollUtil.isEmpty(allRules)) {
+ return Collections.emptyList();
+ }
+
+ List ruleIds = menuDataRuleMapper.selectRuleIdsByRoleAndMenu(roleIds, menuId);
+
+ // 如果角色没有关联任何规则,返回空列表(不应用任何过滤)
+ if (CollUtil.isEmpty(ruleIds)) {
+ return Collections.emptyList();
+ }
+
+ return allRules.stream()
+ .filter(rule -> ruleIds.contains(rule.getId()) && rule.getStatus() == 1)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public Map> getMenuDataRuleMap(Collection menuIds) {
+ List rules = menuDataRuleMapper.selectListByMenuIds(menuIds);
+ return rules.stream().collect(Collectors.groupingBy(MenuDataRuleDO::getMenuId));
+ }
+
+ private MenuDataRuleDO validateMenuDataRuleExists(Long id) {
+ MenuDataRuleDO rule = menuDataRuleMapper.selectById(id);
+ if (rule == null) {
+ throw exception(MENU_NOT_EXISTS);
+ }
+ return rule;
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/portal/PortalService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/portal/PortalService.java
index 192b1851..acc8724b 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/portal/PortalService.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/portal/PortalService.java
@@ -60,4 +60,11 @@ public interface PortalService {
*/
List getPortalListByUserId(Long userId);
+ /**
+ * 获得公开门户列表(无需登录)
+ *
+ * @return 门户列表
+ */
+ List getPublicPortalList();
+
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/portal/PortalServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/portal/PortalServiceImpl.java
index 1c30986a..902b5a21 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/portal/PortalServiceImpl.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/portal/PortalServiceImpl.java
@@ -126,6 +126,11 @@ public class PortalServiceImpl implements PortalService {
return portalMapper.selectListByPermissions(permissions);
}
+ @Override
+ public List getPublicPortalList() {
+ return portalMapper.selectListByPermissions(Collections.emptyList());
+ }
+
@VisibleForTesting
public PortalDO validatePortalExists(Long id) {
if (id == null) {
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/push/ExternalPushConfigService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/push/ExternalPushConfigService.java
new file mode 100644
index 00000000..904a474e
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/push/ExternalPushConfigService.java
@@ -0,0 +1,53 @@
+package com.zt.plat.module.system.service.push;
+
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.module.system.controller.admin.push.vo.ExternalPushConfigPageReqVO;
+import com.zt.plat.module.system.controller.admin.push.vo.ExternalPushConfigSaveReqVO;
+import com.zt.plat.module.system.dal.dataobject.push.ExternalPushConfigDO;
+import jakarta.validation.Valid;
+
+/**
+ * 外部系统推送配置 Service 接口
+ *
+ * @author ZT Cloud
+ */
+public interface ExternalPushConfigService {
+
+ /**
+ * 创建推送配置
+ */
+ Long createExternalPushConfig(@Valid ExternalPushConfigSaveReqVO createReqVO);
+
+ /**
+ * 修改推送配置
+ */
+ void updateExternalPushConfig(@Valid ExternalPushConfigSaveReqVO updateReqVO);
+
+ /**
+ * 删除推送配置
+ */
+ void deleteExternalPushConfig(Long id);
+
+ /**
+ * 获取推送配置详情
+ */
+ ExternalPushConfigDO getExternalPushConfig(Long id);
+
+ /**
+ * 分页查询推送配置
+ */
+ PageResult getExternalPushConfigPage(ExternalPushConfigPageReqVO reqVO);
+
+ /**
+ * 判断是否允许推送(核心业务逻辑)
+ *
+ * 优先级:部门配置 > 公司配置 > 默认允许
+ *
+ * @param companyId 公司编号(必填)
+ * @param deptId 部门编号(可选)
+ * @param businessType 业务类型(必填)
+ * @param externalSystem 外部系统标识(必填)
+ * @return 是否允许推送(true=允许,false=禁止,默认 true)
+ */
+ Boolean isPushEnabled(Long companyId, Long deptId, String businessType, String externalSystem);
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/push/ExternalPushConfigServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/push/ExternalPushConfigServiceImpl.java
new file mode 100644
index 00000000..b36aa03c
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/push/ExternalPushConfigServiceImpl.java
@@ -0,0 +1,280 @@
+package com.zt.plat.module.system.service.push;
+
+import cn.hutool.core.util.StrUtil;
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.framework.common.util.object.BeanUtils;
+import com.zt.plat.module.system.controller.admin.push.vo.ExternalPushConfigPageReqVO;
+import com.zt.plat.module.system.controller.admin.push.vo.ExternalPushConfigSaveReqVO;
+import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
+import com.zt.plat.module.system.dal.dataobject.push.ExternalPushConfigDO;
+import com.zt.plat.module.system.dal.mysql.dept.DeptMapper;
+import com.zt.plat.module.system.dal.mysql.push.ExternalPushConfigMapper;
+import com.zt.plat.module.system.enums.push.BusinessTypeEnum;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static com.zt.plat.module.system.enums.ErrorCodeConstants.*;
+
+/**
+ * 外部系统推送配置 Service 实现类
+ *
+ * @author ZT Cloud
+ */
+@Service
+@Validated
+public class ExternalPushConfigServiceImpl implements ExternalPushConfigService {
+
+ @Resource
+ private ExternalPushConfigMapper externalPushConfigMapper;
+ @Resource
+ private DeptMapper deptMapper;
+
+ @Override
+ public Long createExternalPushConfig(ExternalPushConfigSaveReqVO createReqVO) {
+ // 参数规范化
+ normalizeRequest(createReqVO);
+
+ // 业务校验
+ validateForCreateOrUpdate(null, createReqVO);
+
+ // 创建配置
+ ExternalPushConfigDO entity = BeanUtils.toBean(createReqVO, ExternalPushConfigDO.class);
+ if (entity.getEnablePush() == null) {
+ entity.setEnablePush(true);
+ }
+ externalPushConfigMapper.insert(entity);
+ return entity.getId();
+ }
+
+ @Override
+ public void updateExternalPushConfig(ExternalPushConfigSaveReqVO updateReqVO) {
+ // 参数规范化
+ normalizeRequest(updateReqVO);
+
+ // 校验存在
+ validateExists(updateReqVO.getId());
+ // 业务校验
+ validateForCreateOrUpdate(updateReqVO.getId(), updateReqVO);
+
+ // 更新配置
+ ExternalPushConfigDO updateObj = BeanUtils.toBean(updateReqVO, ExternalPushConfigDO.class);
+ externalPushConfigMapper.updateById(updateObj);
+ }
+
+ @Override
+ public void deleteExternalPushConfig(Long id) {
+ validateExists(id);
+ externalPushConfigMapper.deleteById(id);
+ }
+
+ @Override
+ public ExternalPushConfigDO getExternalPushConfig(Long id) {
+ return externalPushConfigMapper.selectById(id);
+ }
+
+ @Override
+ public PageResult getExternalPushConfigPage(ExternalPushConfigPageReqVO reqVO) {
+ return externalPushConfigMapper.selectPage(reqVO);
+ }
+
+ @Override
+ public Boolean isPushEnabled(Long companyId, Long deptId, String businessType, String externalSystem) {
+ // 规范化参数
+ String normalizedBusinessType = StrUtil.isNotBlank(businessType) ? businessType.trim().toUpperCase() : null;
+ String normalizedExternalSystem = StrUtil.isNotBlank(externalSystem) ? externalSystem.trim().toUpperCase() : null;
+
+ // 优先级 1:部门级配置(如果 deptId 不为空),递归查找父部门
+ if (deptId != null) {
+ ExternalPushConfigDO deptConfig = findDeptConfigRecursive(
+ companyId, deptId, normalizedBusinessType, normalizedExternalSystem);
+ if (deptConfig != null) {
+ return deptConfig.getEnablePush() != null ? deptConfig.getEnablePush() : true;
+ }
+ }
+
+ // 优先级 2:公司级配置(dept_id 为 null)
+ if (companyId != null) {
+ ExternalPushConfigDO companyConfig = externalPushConfigMapper.selectByConfig(
+ companyId, null, normalizedBusinessType, normalizedExternalSystem);
+ if (companyConfig != null) {
+ return companyConfig.getEnablePush() != null ? companyConfig.getEnablePush() : true;
+ }
+ }
+
+ // 优先级 3:全局配置(company_id 和 dept_id 都为空)
+ ExternalPushConfigDO globalConfig = externalPushConfigMapper.selectByConfig(
+ null, null, normalizedBusinessType, normalizedExternalSystem);
+ if (globalConfig != null) {
+ return globalConfig.getEnablePush() != null ? globalConfig.getEnablePush() : true;
+ }
+
+ // 优先级 4:没有配置,默认允许推送
+ return true;
+ }
+
+ /**
+ * 递归查找部门配置(包括父部门)
+ *
+ * @param companyId 公司ID
+ * @param deptId 部门ID
+ * @param businessType 业务类型
+ * @param externalSystem 外部系统
+ * @return 配置对象,如果找不到返回 null
+ */
+ private ExternalPushConfigDO findDeptConfigRecursive(Long companyId, Long deptId,
+ String businessType, String externalSystem) {
+ if (deptId == null) {
+ return null;
+ }
+
+ // 查询当前部门的配置
+ ExternalPushConfigDO config = externalPushConfigMapper.selectByConfig(
+ companyId, deptId, businessType, externalSystem);
+ if (config != null) {
+ return config;
+ }
+
+ // 查询当前部门信息,获取父部门ID
+ DeptDO dept = deptMapper.selectById(deptId);
+ if (dept == null || dept.getParentId() == null || dept.getParentId() == 0L) {
+ // 没有父部门了,返回 null
+ return null;
+ }
+
+ // 检查父部门是否是公司节点
+ DeptDO parentDept = deptMapper.selectById(dept.getParentId());
+ if (parentDept != null && Boolean.TRUE.equals(parentDept.getIsCompany())) {
+ // 父部门是公司,不再向上查找(公司级配置在外层处理)
+ return null;
+ }
+
+ // 递归查找父部门的配置
+ return findDeptConfigRecursive(companyId, dept.getParentId(), businessType, externalSystem);
+ }
+
+ //==================== 私有方法 ====================
+
+ /**
+ * 校验配置是否存在
+ */
+ private ExternalPushConfigDO validateExists(Long id) {
+ if (id == null) {
+ throw exception(EXTERNAL_PUSH_CONFIG_NOT_EXISTS);
+ }
+ ExternalPushConfigDO entity = externalPushConfigMapper.selectById(id);
+ if (entity == null) {
+ throw exception(EXTERNAL_PUSH_CONFIG_NOT_EXISTS);
+ }
+ return entity;
+ }
+
+ /**
+ * 业务校验
+ */
+ private void validateForCreateOrUpdate(Long id, ExternalPushConfigSaveReqVO reqVO) {
+ // 1. 如果指定公司,校验公司存在且是公司节点
+ if (reqVO.getCompanyId() != null) {
+ DeptDO company = deptMapper.selectById(reqVO.getCompanyId());
+ if (company == null) {
+ throw exception(DEPT_NOT_FOUND);
+ }
+ if (!Boolean.TRUE.equals(company.getIsCompany())) {
+ throw exception(EXTERNAL_PUSH_CONFIG_COMPANY_INVALID);
+ }
+ }
+
+ // 2. 如果指定部门,校验部门存在且不是公司节点
+ if (reqVO.getDeptId() != null) {
+ DeptDO dept = deptMapper.selectById(reqVO.getDeptId());
+ if (dept == null) {
+ throw exception(DEPT_NOT_FOUND);
+ }
+ if (Boolean.TRUE.equals(dept.getIsCompany())) {
+ throw exception(EXTERNAL_PUSH_CONFIG_DEPT_INVALID);
+ }
+ }
+
+ // 3. 如果指定业务类型,校验业务类型有效性
+ if (StrUtil.isNotBlank(reqVO.getBusinessType())) {
+ if (!BusinessTypeEnum.isValidCode(reqVO.getBusinessType())) {
+ throw exception(EXTERNAL_PUSH_CONFIG_BUSINESS_TYPE_INVALID);
+ }
+ }
+
+ // 4. 校验唯一性:同一租户+公司+部门+业务类型+外部系统的配置唯一
+ ExternalPushConfigDO existing = externalPushConfigMapper.selectByConfig(
+ reqVO.getCompanyId(), reqVO.getDeptId(),
+ reqVO.getBusinessType(), reqVO.getExternalSystem());
+
+ if (existing != null && (id == null || !existing.getId().equals(id))) {
+ throw exception(EXTERNAL_PUSH_CONFIG_EXISTS);
+ }
+ }
+
+ /**
+ * 参数规范化
+ */
+ private void normalizeRequest(ExternalPushConfigSaveReqVO reqVO) {
+ if (reqVO == null) {
+ return;
+ }
+
+ // 如果 companyId 为空但 deptId 不为空,自动查找并填充顶级公司ID
+ if (reqVO.getCompanyId() == null && reqVO.getDeptId() != null) {
+ Long topCompanyId = findTopCompanyId(reqVO.getDeptId());
+ if (topCompanyId != null) {
+ reqVO.setCompanyId(topCompanyId);
+ }
+ }
+
+ if (StrUtil.isNotBlank(reqVO.getBusinessType())) {
+ reqVO.setBusinessType(reqVO.getBusinessType().trim().toUpperCase());
+ }
+ if (StrUtil.isNotBlank(reqVO.getExternalSystem())) {
+ reqVO.setExternalSystem(reqVO.getExternalSystem().trim().toUpperCase());
+ }
+ }
+
+ /**
+ * 查找部门的顶级公司ID
+ *
+ * @param deptId 部门ID
+ * @return 顶级公司ID,如果不存在返回 null
+ */
+ private Long findTopCompanyId(Long deptId) {
+ if (deptId == null) {
+ return null;
+ }
+
+ DeptDO dept = deptMapper.selectById(deptId);
+ if (dept == null) {
+ return null;
+ }
+
+ // 递归向上查找,直到找到公司节点或者parentId为空/0
+ Long currentDeptId = deptId;
+ while (currentDeptId != null) {
+ DeptDO currentDept = deptMapper.selectById(currentDeptId);
+ if (currentDept == null) {
+ break;
+ }
+
+ // 如果当前节点是公司,返回其ID
+ if (Boolean.TRUE.equals(currentDept.getIsCompany())) {
+ return currentDept.getId();
+ }
+
+ // 如果没有父节点或父节点为0,结束查找
+ if (currentDept.getParentId() == null || currentDept.getParentId() == 0L) {
+ break;
+ }
+
+ // 继续向上查找
+ currentDeptId = currentDept.getParentId();
+ }
+
+ return null;
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkOrgChangeService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkOrgChangeService.java
new file mode 100644
index 00000000..532a3b1d
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkOrgChangeService.java
@@ -0,0 +1,13 @@
+package com.zt.plat.module.system.service.sync;
+
+/**
+ * 定时同步 iWork 组织变更服务
+ */
+public interface SyncIWorkOrgChangeService {
+
+ /**
+ * 执行同步
+ * @return 拉取记录数量
+ */
+ int process();
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkOrgChangeServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkOrgChangeServiceImpl.java
new file mode 100644
index 00000000..3b81c13f
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkOrgChangeServiceImpl.java
@@ -0,0 +1,41 @@
+package com.zt.plat.module.system.service.sync;
+
+import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkFullSyncReqVO;
+import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkFullSyncRespVO;
+import com.zt.plat.module.system.service.integration.iwork.IWorkSyncService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+
+@Service
+public class SyncIWorkOrgChangeServiceImpl implements SyncIWorkOrgChangeService {
+
+ @Resource
+ private IWorkSyncService iWorkSyncService;
+
+ @Override
+ public int process() {
+ IWorkFullSyncReqVO reqVO = new IWorkFullSyncReqVO();
+ reqVO.setPageSize(10);
+ ZoneId zone = ZoneId.of("Asia/Shanghai");
+ String startOfToday = LocalDate.now(zone)
+ .atStartOfDay(zone)
+ .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+ reqVO.setModified(startOfToday);
+ IWorkFullSyncRespVO subcompanyResp = iWorkSyncService.fullSyncSubcompanies(reqVO);
+ IWorkFullSyncRespVO departmentResp = iWorkSyncService.fullSyncDepartments(reqVO);
+ return countPulled(subcompanyResp) + countPulled(departmentResp);
+ }
+
+ private int countPulled(IWorkFullSyncRespVO respVO) {
+ if (respVO == null || respVO.getBatches() == null) {
+ return 0;
+ }
+ return respVO.getBatches().stream()
+ .mapToInt(batch -> batch.getPulled() == null ? 0 : batch.getPulled())
+ .sum();
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkUserChangeService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkUserChangeService.java
new file mode 100644
index 00000000..dc3f3418
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkUserChangeService.java
@@ -0,0 +1,14 @@
+package com.zt.plat.module.system.service.sync;
+
+/**
+ * 同步iWork当日修改的用户数据
+ */
+public interface SyncIWorkUserChangeService {
+
+ /**
+ * 同步入口
+ * @return
+ */
+ int process();
+
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkUserChangeServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkUserChangeServiceImpl.java
new file mode 100644
index 00000000..7592823c
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkUserChangeServiceImpl.java
@@ -0,0 +1,37 @@
+package com.zt.plat.module.system.service.sync;
+
+import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkFullSyncReqVO;
+import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkFullSyncRespVO;
+import com.zt.plat.module.system.service.integration.iwork.IWorkSyncService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+
+@Service
+public class SyncIWorkUserChangeServiceImpl implements SyncIWorkUserChangeService {
+
+ @Resource
+ private IWorkSyncService iWorkSyncService;
+
+ @Override
+ public int process() {
+ IWorkFullSyncReqVO reqVO = new IWorkFullSyncReqVO();
+ reqVO.setPageSize(10);
+ // 设置修改日期的查询条件为当日0时起
+ ZoneId zone = ZoneId.of("Asia/Shanghai");
+ String startOfToday = LocalDate.now(zone)
+ .atStartOfDay(zone)
+ .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+ reqVO.setModified(startOfToday);
+ IWorkFullSyncRespVO respVO = iWorkSyncService.fullSyncUsers(reqVO);
+ if(respVO!=null && respVO.getBatches()!=null) {
+ return respVO.getBatches().stream()
+ .mapToInt(b -> b.getPulled() == null ? 0 : b.getPulled())
+ .sum();
+ }
+ return 0;
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/userdept/UserDeptService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/userdept/UserDeptService.java
index 26c728b7..7a4e027c 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/userdept/UserDeptService.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/userdept/UserDeptService.java
@@ -70,6 +70,13 @@ public interface UserDeptService {
*/
void deleteUserDeptByUserId(Long userId);
+ /**
+ * 根据用户ID与部门ID集合删除用户与部门关系
+ * @param userId 用户ID
+ * @param deptIds 部门ID集合
+ */
+ void deleteUserDeptByUserIdAndDeptIds(Long userId, Collection deptIds);
+
/**
* 批量创建用户与部门关系
* @param createReqVOList 创建信息列表
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/userdept/UserDeptServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/userdept/UserDeptServiceImpl.java
index 8c71a020..bbc78645 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/userdept/UserDeptServiceImpl.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/userdept/UserDeptServiceImpl.java
@@ -3,6 +3,7 @@ package com.zt.plat.module.system.service.userdept;
import cn.hutool.core.collection.CollUtil;
import com.zt.plat.framework.common.util.object.BeanUtils;
import com.zt.plat.framework.security.core.LoginUser;
+import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.zt.plat.module.system.dal.dataobject.userdept.UserDeptDO;
import com.zt.plat.module.system.dal.mysql.userdept.UserDeptMapper;
import jakarta.annotation.Resource;
@@ -128,10 +129,20 @@ public class UserDeptServiceImpl implements UserDeptService {
@Override
public void deleteUserDeptByUserId(Long userId) {
if (userId == null) return;
- userDeptMapper.delete(new com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX()
+ userDeptMapper.delete(new LambdaQueryWrapperX()
.eq(UserDeptDO::getUserId, userId));
}
+ @Override
+ public void deleteUserDeptByUserIdAndDeptIds(Long userId, Collection deptIds) {
+ if (userId == null || CollUtil.isEmpty(deptIds)) {
+ return;
+ }
+ userDeptMapper.delete(new LambdaQueryWrapperX()
+ .eq(UserDeptDO::getUserId, userId)
+ .in(UserDeptDO::getDeptId, deptIds));
+ }
+
@Override
@Transactional(rollbackFor = Exception.class)
public void batchCreateUserDept(List createReqVOList) {
diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkCallbackLogServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkCallbackLogServiceImplTest.java
new file mode 100644
index 00000000..f0287595
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkCallbackLogServiceImplTest.java
@@ -0,0 +1,63 @@
+package com.zt.plat.module.system.service.integration.iwork.impl;
+
+import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkWorkflowCallbackReqVO;
+import com.zt.plat.module.system.dal.dataobject.iwork.IWorkSealLogDO;
+import com.zt.plat.module.system.dal.mysql.iwork.IWorkSealLogMapper;
+import com.zt.plat.module.system.framework.integration.iwork.config.IWorkProperties;
+import com.zt.plat.module.system.mq.iwork.IWorkBizCallbackProducer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+class IWorkCallbackLogServiceImplTest {
+
+ private IWorkSealLogMapper mapper;
+ private IWorkBizCallbackProducer producer;
+ private IWorkProperties properties;
+ private IWorkCallbackLogServiceImpl service;
+
+ @BeforeEach
+ void setup() {
+ mapper = mock(IWorkSealLogMapper.class);
+ producer = mock(IWorkBizCallbackProducer.class);
+ properties = new IWorkProperties();
+ service = new IWorkCallbackLogServiceImpl(mapper, producer, properties);
+ }
+
+ @Test
+ void upsertOnCallback_shouldTruncateRaw() {
+ String longRaw = "x".repeat(2100);
+ IWorkWorkflowCallbackReqVO req = new IWorkWorkflowCallbackReqVO();
+ req.setRequestId("REQ-1");
+ req.setBizCallbackKey("key");
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(IWorkSealLogDO.class);
+ when(mapper.selectByRequestId("REQ-1")).thenReturn(null);
+
+ service.upsertOnCallback(req, 3, longRaw);
+
+ verify(mapper).insert(captor.capture());
+ IWorkSealLogDO saved = captor.getValue();
+ assertThat(saved.getRawCallback()).hasSize(2000);
+ assertThat(saved.getMaxRetry()).isEqualTo(3);
+ }
+
+ @Test
+ void incrementRetry_shouldIncreaseCount() {
+ IWorkSealLogDO existing = new IWorkSealLogDO();
+ existing.setId(1L);
+ existing.setRequestId("REQ-2");
+ existing.setRetryCount(1);
+ when(mapper.selectByRequestId("REQ-2")).thenReturn(existing);
+
+ service.incrementRetry("REQ-2");
+
+ ArgumentCaptor