diff --git a/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/db/TenantDatabaseInterceptor.java b/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/db/TenantDatabaseInterceptor.java index aeb05653..ac2760fa 100644 --- a/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/db/TenantDatabaseInterceptor.java +++ b/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/db/TenantDatabaseInterceptor.java @@ -1,12 +1,12 @@ package com.zt.plat.framework.tenant.core.db; -import com.zt.plat.framework.tenant.config.TenantProperties; -import com.zt.plat.framework.tenant.core.aop.TenantIgnore; -import com.zt.plat.framework.tenant.core.context.TenantContextHolder; import com.baomidou.mybatisplus.core.metadata.TableInfo; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; import com.baomidou.mybatisplus.extension.toolkit.SqlParserUtils; +import com.zt.plat.framework.tenant.config.TenantProperties; +import com.zt.plat.framework.tenant.core.aop.TenantIgnore; +import com.zt.plat.framework.tenant.core.context.TenantContextHolder; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; @@ -69,7 +69,12 @@ public class TenantDatabaseInterceptor implements TenantLineHandler { // 找不到的表,说明不是 zt 项目里的,不进行拦截(忽略租户) TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName); if (tableInfo == null) { - return true; + tableName = tableName.toLowerCase(); + tableInfo = TableInfoHelper.getTableInfo(tableName); + } + if (tableInfo == null) { + tableName = tableName.toLowerCase(); + tableInfo = TableInfoHelper.getTableInfo(tableName); } // 如果继承了 TenantBaseDO 基类,显然不忽略租户 if (TenantBaseDO.class.isAssignableFrom(tableInfo.getEntityType())) { 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 new file mode 100644 index 00000000..9181a071 --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApi.java @@ -0,0 +1,63 @@ +package com.zt.plat.module.system.api.iwork; + +import com.zt.plat.framework.common.pojo.CommonResult; +import com.zt.plat.module.system.api.iwork.dto.*; +import com.zt.plat.module.system.enums.ApiConstants; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * RPC 服务 - iWork 集成 + */ +@FeignClient(name = ApiConstants.NAME, contextId = "iWorkIntegrationApi") +@Tag(name = "RPC 服务 - iWork 集成") +public interface IWorkIntegrationApi { + + String PREFIX = ApiConstants.PREFIX + "/integration/iwork"; + + // ----------------- 认证 / 会话 ----------------- + + @PostMapping(PREFIX + "/auth/register") + @Operation(summary = "注册 iWork 凭证,获取服务端公钥与 secret") + CommonResult register(@RequestBody IWorkAuthRegisterReqDTO reqDTO); + + @PostMapping(PREFIX + "/auth/token") + @Operation(summary = "申请 iWork Token(独立接口)") + CommonResult acquireToken(@RequestBody IWorkAuthTokenReqDTO reqDTO); + + // ----------------- 流程类能力 ----------------- + + @PostMapping(PREFIX + "/user/resolve") + @Operation(summary = "根据外部标识获取 iWork 用户编号") + CommonResult resolveUser(@RequestBody IWorkUserInfoReqDTO reqDTO); + + @PostMapping(PREFIX + "/workflow/create") + @Operation(summary = "发起 iWork 流程") + CommonResult createWorkflow(@RequestBody IWorkWorkflowCreateReqDTO reqDTO); + + @PostMapping(PREFIX + "/workflow/void") + @Operation(summary = "作废 / 干预 iWork 流程") + CommonResult voidWorkflow(@RequestBody IWorkWorkflowVoidReqDTO reqDTO); + + // ----------------- 人力组织分页接口 ----------------- + + @PostMapping(PREFIX + "/hr/subcompany/page") + @Operation(summary = "获取 iWork 分部列表") + CommonResult listSubcompanies(@RequestBody IWorkOrgPageReqDTO reqDTO); + + @PostMapping(PREFIX + "/hr/department/page") + @Operation(summary = "获取 iWork 部门列表") + CommonResult listDepartments(@RequestBody IWorkOrgPageReqDTO reqDTO); + + @PostMapping(PREFIX + "/hr/job-title/page") + @Operation(summary = "获取 iWork 岗位列表") + CommonResult listJobTitles(@RequestBody IWorkOrgPageReqDTO reqDTO); + + @PostMapping(PREFIX + "/hr/user/page") + @Operation(summary = "获取 iWork 人员列表") + CommonResult listUsers(@RequestBody IWorkOrgPageReqDTO reqDTO); + +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkAuthRegisterReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkAuthRegisterReqDTO.java new file mode 100644 index 00000000..aaedd14e --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkAuthRegisterReqDTO.java @@ -0,0 +1,18 @@ +package com.zt.plat.module.system.api.iwork.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * iWork 注册授权请求 DTO(供其他模块通过 Feign 调用 system-server 时使用) + */ +@Data +public class IWorkAuthRegisterReqDTO { + + @Schema(description = "iWork 应用编码", requiredMode = Schema.RequiredMode.REQUIRED) + private String appCode; + + @Schema(description = "iWork 网关地址", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String baseUrl; + +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkAuthRegisterRespDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkAuthRegisterRespDTO.java new file mode 100644 index 00000000..9e8ed341 --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkAuthRegisterRespDTO.java @@ -0,0 +1,18 @@ +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 IWorkAuthRegisterRespDTO { + + @Schema(description = "服务端公钥(Base64)") + private String publicKey; + + @Schema(description = "服务端下发的 secret") + private String secret; + +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkAuthTokenReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkAuthTokenReqDTO.java new file mode 100644 index 00000000..ff0598b6 --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkAuthTokenReqDTO.java @@ -0,0 +1,15 @@ +package com.zt.plat.module.system.api.iwork.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * iWork Token 申请请求 DTO + */ +@Data +public class IWorkAuthTokenReqDTO { + + @Schema(description = "应用编码", requiredMode = Schema.RequiredMode.REQUIRED) + private String appCode; + +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkAuthTokenRespDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkAuthTokenRespDTO.java new file mode 100644 index 00000000..65a82f60 --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkAuthTokenRespDTO.java @@ -0,0 +1,18 @@ +package com.zt.plat.module.system.api.iwork.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * iWork Token 响应 DTO + */ +@Data +public class IWorkAuthTokenRespDTO { + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "过期时间(秒)") + private Long expiresIn; + +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkHrDepartmentPageRespDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkHrDepartmentPageRespDTO.java new file mode 100644 index 00000000..f714a36f --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkHrDepartmentPageRespDTO.java @@ -0,0 +1,31 @@ +package com.zt.plat.module.system.api.iwork.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * iWork 部门分页响应 DTO + */ +@Data +public class IWorkHrDepartmentPageRespDTO { + + @Schema(description = "总条数") + private Long total; + + @Schema(description = "当前页数据") + private List list; + + @Data + public static class Item { + + @Schema(description = "部门编号") + private String id; + + @Schema(description = "部门名称") + private String name; + + } + +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkHrJobTitlePageRespDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkHrJobTitlePageRespDTO.java new file mode 100644 index 00000000..c53f0fe9 --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkHrJobTitlePageRespDTO.java @@ -0,0 +1,31 @@ +package com.zt.plat.module.system.api.iwork.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * iWork 岗位分页响应 DTO + */ +@Data +public class IWorkHrJobTitlePageRespDTO { + + @Schema(description = "总条数") + private Long total; + + @Schema(description = "当前页数据") + private List list; + + @Data + public static class Item { + + @Schema(description = "岗位编号") + private String id; + + @Schema(description = "岗位名称") + private String name; + + } + +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkHrSubcompanyPageRespDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkHrSubcompanyPageRespDTO.java new file mode 100644 index 00000000..a57f92ca --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkHrSubcompanyPageRespDTO.java @@ -0,0 +1,31 @@ +package com.zt.plat.module.system.api.iwork.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * iWork 分部分页响应 DTO + */ +@Data +public class IWorkHrSubcompanyPageRespDTO { + + @Schema(description = "总条数") + private Long total; + + @Schema(description = "当前页数据") + private List list; + + @Data + public static class Item { + + @Schema(description = "分部编号") + private String id; + + @Schema(description = "分部名称") + private String name; + + } + +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkHrUserPageRespDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkHrUserPageRespDTO.java new file mode 100644 index 00000000..39b5af0f --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkHrUserPageRespDTO.java @@ -0,0 +1,31 @@ +package com.zt.plat.module.system.api.iwork.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * iWork 人员分页响应 DTO + */ +@Data +public class IWorkHrUserPageRespDTO { + + @Schema(description = "总条数") + private Long total; + + @Schema(description = "当前页数据") + private List list; + + @Data + public static class Item { + + @Schema(description = "人员编号") + private String id; + + @Schema(description = "人员名称") + private String name; + + } + +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOperationRespDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOperationRespDTO.java new file mode 100644 index 00000000..1ebe46d5 --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOperationRespDTO.java @@ -0,0 +1,21 @@ +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 IWorkOperationRespDTO { + + @Schema(description = "是否成功") + private Boolean success; + + @Schema(description = "iWork 返回的操作编号或实例编号") + private String operationId; + + @Schema(description = "提示信息") + private String message; + +} 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 new file mode 100644 index 00000000..60fe8231 --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOrgPageReqDTO.java @@ -0,0 +1,21 @@ +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/IWorkUserInfoReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkUserInfoReqDTO.java new file mode 100644 index 00000000..dbf8667b --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkUserInfoReqDTO.java @@ -0,0 +1,15 @@ +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 IWorkUserInfoReqDTO { + + @Schema(description = "外部系统中的用户唯一标识", requiredMode = Schema.RequiredMode.REQUIRED) + private String externalUserCode; + +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkUserInfoRespDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkUserInfoRespDTO.java new file mode 100644 index 00000000..f311a1f4 --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkUserInfoRespDTO.java @@ -0,0 +1,18 @@ +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 IWorkUserInfoRespDTO { + + @Schema(description = "iWork 用户编号") + private String userId; + + @Schema(description = "iWork 用户名称") + private String userName; + +} 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 new file mode 100644 index 00000000..3d10a1ae --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowCreateReqDTO.java @@ -0,0 +1,46 @@ +package com.zt.plat.module.system.api.iwork.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 发起 iWork 流程请求 DTO + * + * 与 IWorkWorkflowCreateReqVO 字段一一对应,便于 Feign 调用。 + */ +@Data +public class IWorkWorkflowCreateReqDTO { + + @Schema(description = "用印申请人(iWork 人员 ID)", example = "1001") + private String jbr; + + @Schema(description = "用印部门 ID", example = "2001") + private String yybm; + + @Schema(description = "用印单位(分部 ID)", example = "3001") + private String fb; + + @Schema(description = "申请时间,格式 yyyy-MM-dd", example = "2025-01-01") + private String sqsj; + + @Schema(description = "用印去向") + private String yyqx; + + @Schema(description = "用印依据附件 URL") + private String yyfkUrl; + + @Schema(description = "用印事由或内容摘要") + private String yysy; + + @Schema(description = "用印材料附件 URL(必填)") + private String xyywjUrl; + + @Schema(description = "用印材料附件文件名(必填)") + private String xyywjFileName; + + @Schema(description = "用印事项") + private String yysx; + + @Schema(description = "业务系统单据编号(用于派生流程标题)", example = "DJ-2025-0001") + private String ywxtdjbh; +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowVoidReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowVoidReqDTO.java new file mode 100644 index 00000000..17ab8c34 --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowVoidReqDTO.java @@ -0,0 +1,21 @@ +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 IWorkWorkflowVoidReqDTO { + + @Schema(description = "iWork 实例编号", requiredMode = Schema.RequiredMode.REQUIRED) + private String instanceId; + + @Schema(description = "操作人 iWork 用户编号", requiredMode = Schema.RequiredMode.REQUIRED) + private String operatorUserId; + + @Schema(description = "作废原因") + private String reason; + +} 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 4fbc5184..aaef27c3 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 @@ -61,6 +61,7 @@ public interface ErrorCodeConstants { ErrorCode USER_IMPORT_INIT_PASSWORD = new ErrorCode(1_002_003_009, "初始密码不能为空"); ErrorCode USER_MOBILE_NOT_EXISTS = new ErrorCode(1_002_003_010, "该手机号尚未注册"); ErrorCode USER_REGISTER_DISABLED = new ErrorCode(1_002_003_011, "注册功能已关闭"); + ErrorCode USER_PASSWORD_MODIFY_FORBIDDEN = new ErrorCode(1_002_003_012, "该用户来源不支持修改密码"); // ========== 部门模块 1-002-004-000 ========== ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "当前上级部门已存在同名子部门"); diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/user/PasswordStrategyEnum.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/user/PasswordStrategyEnum.java new file mode 100644 index 00000000..2cf7aa21 --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/user/PasswordStrategyEnum.java @@ -0,0 +1,24 @@ +package com.zt.plat.module.system.enums.user; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 密码处理策略,用于区分本地账户与外部同步账户的密码存储/校验方式。 + */ +@AllArgsConstructor +@Getter +public enum PasswordStrategyEnum { + + /** + * 本地创建或注册用户,使用 Spring Security {@code PasswordEncoder}(BCrypt)。 + */ + LOCAL_BCRYPT("LOCAL_BCRYPT"), + /** + * iWork 同步的 MD5 密文,直接按大写 MD5 存储及校验。 + */ + IWORK_MD5("IWORK_MD5"); + + private final String label; + +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/user/UserSourceEnum.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/user/UserSourceEnum.java index 2299cd0d..e30da7e5 100644 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/user/UserSourceEnum.java +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/user/UserSourceEnum.java @@ -3,6 +3,9 @@ package com.zt.plat.module.system.enums.user; import lombok.AllArgsConstructor; import lombok.Getter; +import java.util.Arrays; +import java.util.Objects; + /** * 用户来源枚举 * @@ -12,9 +15,9 @@ import lombok.Getter; @Getter public enum UserSourceEnum { - EXTERNAL(1, "外部用户"), // 系统创建、注册等方式产生的用户 - SYNC(2, "同步用户"), // 通过 UserSyncService 同步的用户 - IWORK(3, "iWork 用户"); // 通过 iWork 全量/单条同步产生的用户 + EXTERNAL(1, "外部用户", PasswordStrategyEnum.LOCAL_BCRYPT), // 系统创建、注册等方式产生的用户 + SYNC(2, "同步用户", PasswordStrategyEnum.LOCAL_BCRYPT), // 通过 UserSyncService 同步的用户 + IWORK(3, "iWork 用户", PasswordStrategyEnum.IWORK_MD5); // 通过 iWork 全量/单条同步产生的用户 /** * 类型 @@ -24,5 +27,28 @@ public enum UserSourceEnum { * 名字 */ private final String name; + /** + * 默认密码策略 + */ + private final PasswordStrategyEnum passwordStrategy; + + public static UserSourceEnum of(Integer source) { + if (source == null) { + return null; + } + return Arrays.stream(values()) + .filter(item -> Objects.equals(item.source, source)) + .findFirst() + .orElse(null); + } + + public static PasswordStrategyEnum resolvePasswordStrategy(Integer source) { + UserSourceEnum matched = of(source); + return matched == null ? PasswordStrategyEnum.LOCAL_BCRYPT : matched.getPasswordStrategy(); + } + + public boolean isExternal() { + return this == EXTERNAL; + } } \ No newline at end of file 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 new file mode 100644 index 00000000..b12f91f9 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApiImpl.java @@ -0,0 +1,111 @@ +package com.zt.plat.module.system.api.iwork; + +import com.zt.plat.framework.common.pojo.CommonResult; +import com.zt.plat.framework.common.util.object.BeanUtils; +import com.zt.plat.module.system.api.iwork.dto.*; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.*; +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; +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; + +/** + * iWork 集成 Feign API 实现类。 + *

+ * 将 system-api 模块中的 DTO 与现有 Controller VO 进行简单转换, + * 再委托给 Service 层完成实际业务逻辑,供其他模块通过 Feign 统一调用。 + */ +@RestController +@Validated +public class IWorkIntegrationApiImpl implements IWorkIntegrationApi { + + @Resource + private IWorkIntegrationService integrationService; + @Resource + private IWorkOrgRestService orgRestService; + @Resource + private IWorkSyncService syncService; + + // ----------------- 认证 / 会话 ----------------- + + @Override + public CommonResult register(IWorkAuthRegisterReqDTO reqDTO) { + IWorkAuthRegisterReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkAuthRegisterReqVO.class); + IWorkAuthRegisterRespVO respVO = integrationService.registerSession(reqVO); + IWorkAuthRegisterRespDTO respDTO = BeanUtils.toBean(respVO, IWorkAuthRegisterRespDTO.class); + return success(respDTO); + } + + @Override + public CommonResult acquireToken(IWorkAuthTokenReqDTO reqDTO) { + IWorkAuthTokenReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkAuthTokenReqVO.class); + IWorkAuthTokenRespVO respVO = integrationService.acquireToken(reqVO); + IWorkAuthTokenRespDTO respDTO = BeanUtils.toBean(respVO, IWorkAuthTokenRespDTO.class); + return success(respDTO); + } + + // ----------------- 流程类能力 ----------------- + + @Override + public CommonResult resolveUser(IWorkUserInfoReqDTO reqDTO) { + IWorkUserInfoReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkUserInfoReqVO.class); + IWorkUserInfoRespVO respVO = integrationService.resolveUserId(reqVO); + IWorkUserInfoRespDTO respDTO = BeanUtils.toBean(respVO, IWorkUserInfoRespDTO.class); + return success(respDTO); + } + + @Override + public CommonResult createWorkflow(IWorkWorkflowCreateReqDTO reqDTO) { + IWorkWorkflowCreateReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkWorkflowCreateReqVO.class); + IWorkOperationRespVO respVO = integrationService.createWorkflow(reqVO); + IWorkOperationRespDTO respDTO = BeanUtils.toBean(respVO, IWorkOperationRespDTO.class); + return success(respDTO); + } + + @Override + public CommonResult voidWorkflow(IWorkWorkflowVoidReqDTO reqDTO) { + IWorkWorkflowVoidReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkWorkflowVoidReqVO.class); + IWorkOperationRespVO respVO = integrationService.voidWorkflow(reqVO); + IWorkOperationRespDTO respDTO = BeanUtils.toBean(respVO, IWorkOperationRespDTO.class); + return success(respDTO); + } + + // ----------------- 人力组织分页接口 ----------------- + + @Override + public CommonResult listSubcompanies(IWorkOrgPageReqDTO reqDTO) { + IWorkSubcompanyQueryReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkSubcompanyQueryReqVO.class); + IWorkHrSubcompanyPageRespVO respVO = orgRestService.listSubcompanies(reqVO); + IWorkHrSubcompanyPageRespDTO respDTO = BeanUtils.toBean(respVO, IWorkHrSubcompanyPageRespDTO.class); + return success(respDTO); + } + + @Override + public CommonResult listDepartments(IWorkOrgPageReqDTO reqDTO) { + IWorkDepartmentQueryReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkDepartmentQueryReqVO.class); + IWorkHrDepartmentPageRespVO respVO = orgRestService.listDepartments(reqVO); + IWorkHrDepartmentPageRespDTO respDTO = BeanUtils.toBean(respVO, IWorkHrDepartmentPageRespDTO.class); + return success(respDTO); + } + + @Override + public CommonResult listJobTitles(IWorkOrgPageReqDTO reqDTO) { + IWorkJobTitleQueryReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkJobTitleQueryReqVO.class); + IWorkHrJobTitlePageRespVO respVO = orgRestService.listJobTitles(reqVO); + IWorkHrJobTitlePageRespDTO respDTO = BeanUtils.toBean(respVO, IWorkHrJobTitlePageRespDTO.class); + return success(respDTO); + } + + @Override + public CommonResult listUsers(IWorkOrgPageReqDTO reqDTO) { + IWorkUserQueryReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkUserQueryReqVO.class); + IWorkHrUserPageRespVO respVO = orgRestService.listUsers(reqVO); + IWorkHrUserPageRespDTO respDTO = BeanUtils.toBean(respVO, IWorkHrUserPageRespDTO.class); + return success(respDTO); + } + +} 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 e968b2c0..a78690aa 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 @@ -113,12 +113,6 @@ public class IWorkIntegrationController { // ----------------- 同步到本地 ----------------- - @PostMapping("/hr/full-sync") - @Operation(summary = "手动触发 iWork 组织/人员同步") - public CommonResult fullSync(@Valid @RequestBody IWorkFullSyncReqVO reqVO) { - return success(syncService.fullSync(reqVO)); - } - @PostMapping("/hr/departments/full-sync") @Operation(summary = "手动触发 iWork 部门同步") public CommonResult fullSyncDepartments(@Valid @RequestBody IWorkFullSyncReqVO reqVO) { @@ -142,10 +136,4 @@ public class IWorkIntegrationController { public CommonResult fullSyncUsers(@Valid @RequestBody IWorkFullSyncReqVO reqVO) { return success(syncService.fullSyncUsers(reqVO)); } - - @PostMapping("/hr/single-sync") - @Operation(summary = "按 iWork ID 同步单条组织/人员") - public CommonResult singleSync(@Valid @RequestBody IWorkSingleSyncReqVO reqVO) { - return success(syncService.syncSingle(reqVO)); - } } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrUserPageRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrUserPageRespVO.java index 03cafc17..22ec3fc6 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrUserPageRespVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrUserPageRespVO.java @@ -171,6 +171,10 @@ public class IWorkHrUserPageRespVO { @JsonProperty("accounttype") private String accounttype; + @Schema(description = "用户密码(MD5 密文)") + @JsonProperty("password") + private String password; + @JsonIgnore private Map attributes; diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOperationRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOperationRespVO.java index d108d3c2..2b01e0a6 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOperationRespVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOperationRespVO.java @@ -14,9 +14,6 @@ public class IWorkOperationRespVO { @Schema(description = "iWork 返回的原始数据") private Map payload; - @Schema(description = "iWork 返回的原始字符串") - private String rawBody; - @Schema(description = "是否判断为成功") private boolean success; diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOrgRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOrgRespVO.java deleted file mode 100644 index 40d6da15..00000000 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOrgRespVO.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.zt.plat.module.system.controller.admin.integration.iwork.vo; - -import io.swagger.v3.oas.annotations.media.Schema; - -/** - * @deprecated 请改用强类型的 IWorkHr*RespVO,避免再引用该占位类。 - */ -@Deprecated(forRemoval = true) -@Schema(description = "已废弃,占位用") -public final class IWorkOrgRespVO { - - private IWorkOrgRespVO() { - throw new UnsupportedOperationException("Use IWorkHr*RespVO instead"); - } -} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSingleSyncReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSingleSyncReqVO.java deleted file mode 100644 index 85d290e4..00000000 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSingleSyncReqVO.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.zt.plat.module.system.controller.admin.integration.iwork.vo; - -import com.zt.plat.module.system.enums.integration.IWorkSyncEntityTypeEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -/** - * iWork 单条同步请求 - */ -@Data -public class IWorkSingleSyncReqVO { - - @Schema(description = "同步的实体类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "user") - @NotNull(message = "实体类型不能为空") - private IWorkSyncEntityTypeEnum entityType; - - @Schema(description = "iWork 提供的实体主键 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "10001") - @NotNull(message = "实体 ID 不能为空") - @Min(1) - private Long entityId; - - @Schema(description = "缺失时是否自动创建", example = "true") - private Boolean createIfMissing = Boolean.TRUE; -} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSingleSyncRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSingleSyncRespVO.java deleted file mode 100644 index 0e799b3e..00000000 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSingleSyncRespVO.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.zt.plat.module.system.controller.admin.integration.iwork.vo; - -import com.zt.plat.module.system.enums.integration.IWorkSyncEntityTypeEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -/** - * iWork 单条同步响应 - */ -@Data -public class IWorkSingleSyncRespVO { - - @Schema(description = "同步的实体类型") - private IWorkSyncEntityTypeEnum entityType; - - @Schema(description = "实体 ID") - private Long entityId; - - @Schema(description = "是否创建了新的记录") - private boolean created; - - @Schema(description = "是否对已有记录进行了更新") - private boolean updated; - - @Schema(description = "提示信息") - private String message; -} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkUserInfoRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkUserInfoRespVO.java index 429c2d8d..e3a70559 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkUserInfoRespVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkUserInfoRespVO.java @@ -14,9 +14,6 @@ public class IWorkUserInfoRespVO { @Schema(description = "iWork 返回的原始数据") private Map payload; - @Schema(description = "iWork 返回的原始字符串") - private String rawBody; - @Schema(description = "是否判断为成功") private boolean success; 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 8d804cde..1b2fcc54 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 @@ -1,15 +1,9 @@ package com.zt.plat.module.system.controller.admin.integration.iwork.vo; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.EqualsAndHashCode; -import java.util.List; -import java.util.Map; - /** * 发起 iWork 流程的请求体。 */ @@ -17,25 +11,36 @@ import java.util.Map; @EqualsAndHashCode(callSuper = true) public class IWorkWorkflowCreateReqVO extends IWorkBaseReqVO { - @Schema(description = "流程标题", example = "测试流程") - @NotBlank(message = "流程标题不能为空") - private String requestName; + @Schema(description = "用印申请人(iWork 人员 ID)", example = "1001") + private String jbr; - @Schema(description = "流程模板编号,可为空使用默认配置", example = "54") - private Long workflowId; + @Schema(description = "用印部门 ID", example = "2001") + private String yybm; - @Schema(description = "主表字段") - @NotEmpty(message = "主表字段不能为空") - @Valid - private List mainFields; + @Schema(description = "用印单位(分部 ID)", example = "3001") + private String fb; - @Schema(description = "明细表数据") - @Valid - private List detailTables; + @Schema(description = "申请时间,格式 yyyy-MM-dd", example = "2025-01-01") + private String sqsj; - @Schema(description = "额外参数") - private Map otherParams; + @Schema(description = "用印去向") + private String yyqx; - @Schema(description = "额外 Form 数据") - private Map formExtras; + @Schema(description = "用印依据附件 URL") + private String yyfkUrl; + + @Schema(description = "用印事由或内容摘要") + private String yysy; + + @Schema(description = "用印材料附件 URL(必填)") + private String xyywjUrl; + + @Schema(description = "用印材料附件文件名(必填)") + private String xyywjFileName; + + @Schema(description = "用印事项") + private String yysx; + + @Schema(description = "业务系统单据编号(用于派生流程标题)", example = "DJ-2025-0001") + private String ywxtdjbh; } 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 92fc5b6b..cadd8b88 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 @@ -34,11 +34,6 @@ public class IWorkProperties { */ private String clientPublicKey; - /** - * 当调用方未指定流程编号时使用的默认流程模板编号。 - */ - private Long workflowId; - /** * 当请求未指定操作人时使用的默认用户编号。 */ @@ -53,6 +48,8 @@ public class IWorkProperties { private final Client client = new Client(); @Valid private final OrgRest org = new OrgRest(); + @Valid + private final Workflow workflow = new Workflow(); @Data public static class Paths { @@ -142,4 +139,13 @@ public class IWorkProperties { private String syncJobTitle; private String syncUser; } + + @Data + public static class Workflow { + /** + * 用印流程对应的 iWork 模板编号。 + */ + @NotBlank(message = "iWork 用印流程模板编号不能为空") + private String sealWorkflowId; + } } 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 6983360b..5038fde4 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 @@ -114,7 +114,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); throw exception(AUTH_LOGIN_BAD_CREDENTIALS); } - if (!userService.isPasswordMatch(password, user.getPassword())) { + if (!userService.isPasswordMatch(user, password)) { createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); throw exception(AUTH_LOGIN_BAD_CREDENTIALS); } @@ -299,7 +299,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { if (length < 4 || length > 16) { throw exception(AUTH_LOGIN_BAD_CREDENTIALS); } - if (!userService.isPasswordMatch(password, user.getPassword())) { + if (!userService.isPasswordMatch(user, password)) { throw exception(AUTH_LOGIN_BAD_CREDENTIALS); } } @@ -436,22 +436,11 @@ public class AdminAuthServiceImpl implements AdminAuthService { } /** - * 判断是否为内部用户 - * 根据UserSourceEnum判断:同步用户为内部用户,外部用户为外部用户 + * 判断是否为内部用户,仅通过 E 办同步(SYNC)来源的账号才视为内部用户 */ private boolean isInternalUser(AdminUserDO user) { - // 根据userSource字段判断用户类型 Integer userSource = user.getUserSource(); - - // 同步用户(SYNC = 2)为内部用户,需要使用E办登录 - if (userSource != null && - (userSource.equals(UserSourceEnum.SYNC.getSource()) || - userSource.equals(UserSourceEnum.IWORK.getSource()))) { - return true; - } - - // 外部用户(EXTERNAL = 1)或其他情况为外部用户,使用账号密码登录 - return false; + return Objects.equals(userSource, UserSourceEnum.SYNC.getSource()); } /** diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkIntegrationErrorCodeConstants.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkIntegrationErrorCodeConstants.java index a7a1f39d..9faf4e1c 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkIntegrationErrorCodeConstants.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkIntegrationErrorCodeConstants.java @@ -18,4 +18,5 @@ public interface IWorkIntegrationErrorCodeConstants { ErrorCode IWORK_WORKFLOW_ID_MISSING = new ErrorCode(1_010_200_008, "缺少 iWork 流程模板编号"); ErrorCode IWORK_ORG_IDENTIFIER_MISSING = new ErrorCode(1_010_200_009, "iWork 人力组织接口缺少认证标识"); ErrorCode IWORK_ORG_REMOTE_FAILED = new ErrorCode(1_010_200_010, "iWork 人力组织接口请求失败{}"); + ErrorCode IWORK_SEAL_REQUIRED_FIELD_MISSING = new ErrorCode(1_010_200_011, "缺少用印必填字段:{}"); } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkSyncService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkSyncService.java index af7ca75a..47c70b70 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkSyncService.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkSyncService.java @@ -2,19 +2,12 @@ package com.zt.plat.module.system.service.integration.iwork; 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.controller.admin.integration.iwork.vo.IWorkSingleSyncReqVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkSingleSyncRespVO; /** * iWork 组织/人员同步服务 */ public interface IWorkSyncService { - /** - * 发起全量分批同步 - */ - IWorkFullSyncRespVO fullSync(IWorkFullSyncReqVO reqVO); - /** * 仅同步部门 */ @@ -35,8 +28,4 @@ public interface IWorkSyncService { */ IWorkFullSyncRespVO fullSyncUsers(IWorkFullSyncReqVO reqVO); - /** - * 根据 iWork ID 进行单条同步 - */ - IWorkSingleSyncRespVO syncSingle(IWorkSingleSyncReqVO reqVO); } 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 82df7f69..9aead1e5 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 @@ -128,7 +128,7 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { IWorkSession session = ensureSession(appId, clientKeyPair, operatorUserId, Boolean.TRUE.equals(reqVO.getForceRefreshToken())); Map payload = buildCreatePayload(reqVO); - String responseBody = executeJsonRequest(properties.getPaths().getCreateWorkflow(), null, appId, session, payload); + String responseBody = executeFormRequest(properties.getPaths().getCreateWorkflow(), null, appId, session, payload); return buildOperationResponse(responseBody); } @@ -324,6 +324,39 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { return executeRequest(request, IWORK_REMOTE_REQUEST_FAILED); } + private String executeFormRequest(String path, + Map queryParams, + String appId, + IWorkSession session, + Map formFields) { + HttpUrl baseUrl = HttpUrl.parse(resolveUrl(path)); + if (baseUrl == null) { + throw ServiceExceptionUtil.exception(IWORK_REMOTE_REQUEST_FAILED, "非法的 URL"); + } + HttpUrl.Builder urlBuilder = baseUrl.newBuilder(); + if (queryParams != null) { + queryParams.forEach((key, value) -> { + if (value != null) { + urlBuilder.addQueryParameter(key, String.valueOf(value)); + } + }); + } + FormBody.Builder bodyBuilder = new FormBody.Builder(); + if (formFields != null) { + formFields.forEach((key, value) -> { + if (StringUtils.hasText(key) && value != null) { + bodyBuilder.add(key, toFormValue(value)); + } + }); + } + Request request = new Request.Builder() + .url(urlBuilder.build()) + .headers(authHeaders(appId, session).build()) + .post(bodyBuilder.build()) + .build(); + return executeRequest(request, IWORK_REMOTE_REQUEST_FAILED); + } + private Headers.Builder authHeaders(String appId, IWorkSession session) { return new Headers.Builder() .set(properties.getHeaders().getAppId(), appId) @@ -331,6 +364,19 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { .set(properties.getHeaders().getUserId(), session.getEncryptedUserId()); } + private String toFormValue(Object value) { + if (value == null) { + return ""; + } + if (value instanceof CharSequence || value instanceof Number || value instanceof Boolean) { + return String.valueOf(value); + } + if (value.getClass().isArray() || value instanceof Collection || value instanceof Map) { + return toJsonString(value); + } + return value.toString(); + } + private Map buildUserPayload(IWorkUserInfoReqVO reqVO) { Map payload = new HashMap<>(); if (reqVO.getPayload() != null) { @@ -342,27 +388,125 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { private Map buildCreatePayload(IWorkWorkflowCreateReqVO reqVO) { Map payload = new LinkedHashMap<>(); - payload.put("requestName", reqVO.getRequestName()); - payload.put("workflowId", resolveWorkflowId(reqVO.getWorkflowId())); - payload.put("mainData", convertFormFields(reqVO.getMainFields())); - if (reqVO.getDetailTables() != null && !reqVO.getDetailTables().isEmpty()) { - payload.put("detailData", convertDetailTables(reqVO.getDetailTables())); - } - if (reqVO.getOtherParams() != null && !reqVO.getOtherParams().isEmpty()) { - payload.put("otherParams", reqVO.getOtherParams()); - } - appendPayloadExtras(payload, reqVO.getFormExtras()); + SealRequestFields fields = resolveSealFields(reqVO); + payload.put("requestName", buildRequestName(fields.ywxtdjbh())); + payload.put("workflowId", parseWorkflowId(fields.workflowId())); + payload.put("mainData", buildSealMainData(fields)); return payload; } - private long resolveWorkflowId(Long requestWorkflowId) { - if (requestWorkflowId != null) { - return requestWorkflowId; + private String buildRequestName(String billNo) { + return "用印-" + billNo; + } + + private long parseWorkflowId(String workflowId) { + try { + return Long.parseLong(workflowId); + } catch (NumberFormatException ex) { + throw ServiceExceptionUtil.exception(IWORK_SEAL_REQUIRED_FIELD_MISSING, "workflowId"); } - if (properties.getWorkflowId() != null) { - return properties.getWorkflowId(); + } + + private List> buildSealMainData(SealRequestFields fields) { + List> main = new ArrayList<>(); + addField(main, "jbr", fields.jbr()); + addField(main, "yybm", fields.yybm()); + addField(main, "fb", fields.fb()); + addField(main, "sqsj", fields.sqsj()); + addField(main, "yyqx", fields.yyqx()); + addField(main, "yyfk", fields.yyfkUrl()); + addField(main, "yysy", fields.yysy()); + // xyywj 需要是一个数组结构 [{fileName,filePath}, ...] + addJsonField(main, "xyywj", buildSealAttachmentValue(fields.xyywjUrl())); + addField(main, "yysx", fields.yysx()); + addField(main, "lclx", SealRequestFields.DEFAULT_FLOW_TYPE); + addField(main, "qsdz", SealRequestFields.DEFAULT_SIGN_ACTION); + addField(main, "ywxtdjbh", fields.ywxtdjbh()); + return main; + } + + private void addField(List> target, String name, String value) { + if (!StringUtils.hasText(value)) { + return; } - throw ServiceExceptionUtil.exception(IWORK_WORKFLOW_ID_MISSING); + Map map = new HashMap<>(2); + map.put("fieldName", name); + map.put("fieldValue", value); + target.add(map); + } + + private void addJsonField(List> target, String name, Object value) { + if (value == null) { + return; + } + Map map = new HashMap<>(2); + map.put("fieldName", name); + map.put("fieldValue", value); + target.add(map); + } + + /** + * 将单个附件 URL 封装成 iWork 需要的数组结构: + * [ {"fileName": "xxx", "filePath": "url"} ] + */ + private List> buildSealAttachmentValue(String xyywjUrl) { + String url = trimToNull(xyywjUrl); + if (url == null) { + return null; + } + Map file = new LinkedHashMap<>(2); + // 这里简单从 URL 截取文件名,调用方也可以直接传入已经带文件名的 URL + String fileName = extractFileNameFromUrl(url); + file.put("fileName", fileName); + file.put("filePath", url); + List> list = new ArrayList<>(1); + list.add(file); + return list; + } + + private String extractFileNameFromUrl(String url) { + String trimmed = trimToNull(url); + if (trimmed == null) { + return null; + } + int queryIndex = trimmed.indexOf('?'); + String pathPart = queryIndex >= 0 ? trimmed.substring(0, queryIndex) : trimmed; + int slashIndex = pathPart.lastIndexOf('/'); + if (slashIndex >= 0 && slashIndex < pathPart.length() - 1) { + return pathPart.substring(slashIndex + 1); + } + return pathPart; + } + + private SealRequestFields resolveSealFields(IWorkWorkflowCreateReqVO reqVO) { + String jbr = requireSealField(reqVO.getJbr(), "jbr"); + String yybm = requireSealField(reqVO.getYybm(), "yybm"); + String fb = requireSealField(reqVO.getFb(), "fb"); + String sqsj = requireSealField(reqVO.getSqsj(), "sqsj"); + String yyqx = requireSealField(reqVO.getYyqx(), "yyqx"); + String xyywjUrl = requireSealField(reqVO.getXyywjUrl(), "xyywjUrl"); + String yysx = requireSealField(reqVO.getYysx(), "yysx"); + String billNo = requireSealField(reqVO.getYwxtdjbh(), "ywxtdjbh"); + String yyfkUrl = trimToNull(reqVO.getYyfkUrl()); + String yysy = trimToNull(reqVO.getYysy()); + String workflowId = requireSealField(properties.getWorkflow().getSealWorkflowId(), "workflowId"); + return new SealRequestFields(jbr, yybm, fb, sqsj, yyqx, yyfkUrl, yysy, xyywjUrl, yysx, billNo, workflowId); + } + + private String requireSealField(String value, String fieldName) { + String trimmed = trimToNull(value); + if (trimmed == null) { + throw ServiceExceptionUtil.exception(IWORK_SEAL_REQUIRED_FIELD_MISSING, fieldName); + } + return trimmed; + } + + private String trimToNull(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; } private Map buildVoidPayload(IWorkWorkflowVoidReqVO reqVO) { @@ -389,35 +533,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { }); } - private List> convertFormFields(List fields) { - return fields.stream().map(field -> { - Map map = new HashMap<>(2); - map.put("fieldName", field.getFieldName()); - map.put("fieldValue", field.getFieldValue()); - return map; - }).toList(); - } - - private List> convertDetailTables(List tables) { - return tables.stream().map(table -> { - Map tableMap = new HashMap<>(2); - tableMap.put("tableDBName", table.getTableDBName()); - List> records = table.getRecords().stream().map(record -> { - Map recordMap = new HashMap<>(2); - if (record.getRecordOrder() != null) { - recordMap.put("recordOrder", record.getRecordOrder()); - } - recordMap.put("workflowRequestTableFields", convertFormFields(record.getFields())); - return recordMap; - }).toList(); - tableMap.put("workflowRequestTableRecords", records); - return tableMap; - }).toList(); - } - private IWorkUserInfoRespVO buildUserInfoResponse(String responseBody) { IWorkUserInfoRespVO respVO = new IWorkUserInfoRespVO(); - respVO.setRawBody(responseBody); if (!StringUtils.hasText(responseBody)) { return respVO; } @@ -432,7 +549,6 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { private IWorkOperationRespVO buildOperationResponse(String responseBody) { IWorkOperationRespVO respVO = new IWorkOperationRespVO(); - respVO.setRawBody(responseBody); if (!StringUtils.hasText(responseBody)) { return respVO; } @@ -666,6 +782,21 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { return value.replace("'", "'\"'\"'"); } + private record SealRequestFields(String jbr, + String yybm, + String fb, + String sqsj, + String yyqx, + String yyfkUrl, + String yysy, + String xyywjUrl, + String yysx, + String ywxtdjbh, + String workflowId) { + private static final String DEFAULT_FLOW_TYPE = "2979600781334966993"; + private static final String DEFAULT_SIGN_ACTION = "CORPORATE"; + } + private record RegistrationState(String secret, String spk, ClientKeyPair clientKeyPair) { } 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 46f7af10..c9922d9f 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 @@ -37,7 +37,6 @@ import java.util.concurrent.ConcurrentHashMap; public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { private static final String JOB_CODE_PREFIX = "IWORK_JOB_"; - private static final String DEFAULT_USER_PASSWORD = "Zgty@9527"; private static final int DEFAULT_SORT = 999; private final DeptService deptService; @@ -234,6 +233,8 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { } Long postId = resolveUserPostId(user); CommonStatusEnum status = inactive ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE; + // 直接沿用 iWork 原始密码,避免重复格式化造成校验偏差 + String externalPassword = trimToNull(user.getPassword()); AdminUserDO existing = adminUserMapper.selectByUsername(username); UserSyncOutcome outcome; if (existing == null) { @@ -242,7 +243,12 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { result.increaseSkipped(); continue; } - outcome = createUser(user, username, deptId, postId, status); + if (StrUtil.isBlank(externalPassword)) { + log.warn("[iWork] 人员缺少密码信息,无法创建:id={} username={}", user.getId(), username); + result.increaseFailed(); + continue; + } + outcome = createUser(user, username, deptId, postId, status, externalPassword); } else { if (!Objects.equals(existing.getUserSource(), UserSourceEnum.IWORK.getSource())) { logSkip("人员", existing.getId(), "非 iWork 来源用户,保持原状"); @@ -254,7 +260,7 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { result.increaseSkipped(); continue; } - outcome = updateUser(existing, user, username, deptId, postId, status); + outcome = updateUser(existing, user, username, deptId, postId, status, externalPassword); } applyUserOutcome(result, outcome, user.getLastname(), username); } catch (Exception ex) { @@ -357,13 +363,14 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { String username, Long deptId, Long postId, - CommonStatusEnum status) { + CommonStatusEnum status, + String externalPassword) { UserSaveReqVO req = buildUserSaveReq(source, username, deptId, postId, status); Long desiredUserId = source.getId() == null ? null : source.getId().longValue(); if (desiredUserId != null) { req.setId(desiredUserId); } - req.setPassword(DEFAULT_USER_PASSWORD); + req.setPassword(externalPassword); req.setUserSource(UserSourceEnum.IWORK.getSource()); Long userId = adminUserService.createUser(req); Long effectiveUserId = desiredUserId != null ? desiredUserId : userId; @@ -375,11 +382,13 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { String username, Long deptId, Long postId, - CommonStatusEnum status) { + CommonStatusEnum status, + String externalPassword) { UserSaveReqVO req = buildUserSaveReq(source, username, deptId, postId, status); req.setId(existing.getId()); boolean disabledChanged = CommonStatusEnum.isDisable(status.getStatus()) && CommonStatusEnum.isEnable(existing.getStatus()); adminUserService.updateUser(req); + syncPassword(existing, externalPassword); return new UserSyncOutcome(SyncAction.UPDATED, disabledChanged, existing.getId()); } @@ -491,6 +500,7 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { return post; } + // 优先匹配部门ID,若缺失则回退使用分部ID private Long resolveUserDeptId(IWorkHrUserPageRespVO.User user) { Long deptId = toLong(user.getDepartmentid()); if (deptId != null) { @@ -503,6 +513,7 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { return null; } + // 通过岗位编码命中缓存,否则才按名称自动建档 private Long resolveUserPostId(IWorkHrUserPageRespVO.User user) { if (user.getJobtitleid() == null) { return null; @@ -552,6 +563,22 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { StrUtil.blankToDefault(displayName, username), describeAction(outcome.action()))); } + /** + * 仅在密码发生变化时才写库,避免多余更新 + */ + private void syncPassword(AdminUserDO existing, String externalPassword) { + if (existing == null || StrUtil.isBlank(externalPassword)) { + return; + } + if (StrUtil.equals(externalPassword, existing.getPassword())) { + return; + } + AdminUserDO updateObj = new AdminUserDO(); + updateObj.setId(existing.getId()); + updateObj.setPassword(externalPassword); + adminUserMapper.updateById(updateObj); + } + private void incrementByAction(BatchResult result, SyncAction action) { if (action == null) { return; @@ -624,23 +651,17 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { return value == null ? null : value.longValue(); } + /** + * 工号优先、登录账号兜底,确保账号体系与 iWork 一致 + */ private String resolveUsername(IWorkHrUserPageRespVO.User user) { - String candidate = sanitizeUsername(user.getWorkcode()); - if (candidate == null) { - candidate = sanitizeUsername(user.getLoginid()); + if (StrUtil.isNotBlank(user.getWorkcode())) { + return user.getWorkcode().trim(); } - return candidate; - } - - private String sanitizeUsername(String raw) { - if (StrUtil.isBlank(raw)) { - return null; + if (StrUtil.isNotBlank(user.getLoginid())) { + return user.getLoginid().trim(); } - String normalized = raw.replaceAll("[^A-Za-z0-9]", ""); - if (StrUtil.isBlank(normalized)) { - return null; - } - return normalized.length() > 30 ? normalized.substring(0, 30) : normalized; + return null; } private Set singletonSet(Long value) { 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 ef237e2d..68996237 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 @@ -12,7 +12,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.util.*; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; import static com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationErrorCodeConstants.IWORK_ORG_REMOTE_FAILED; @@ -27,11 +30,6 @@ public class IWorkSyncServiceImpl implements IWorkSyncService { private final IWorkOrgRestService orgRestService; private final IWorkSyncProcessor syncProcessor; - @Override - public IWorkFullSyncRespVO fullSync(IWorkFullSyncReqVO reqVO) { - return runFullSync(reqVO, reqVO.resolveScopes()); - } - @Override public IWorkFullSyncRespVO fullSyncDepartments(IWorkFullSyncReqVO reqVO) { return runFullSync(reqVO, EnumSet.of(IWorkSyncEntityTypeEnum.DEPARTMENT)); @@ -80,21 +78,6 @@ public class IWorkSyncServiceImpl implements IWorkSyncService { return respVO; } - @Override - public IWorkSingleSyncRespVO syncSingle(IWorkSingleSyncReqVO reqVO) { - IWorkSingleSyncRespVO respVO = new IWorkSingleSyncRespVO(); - respVO.setEntityType(reqVO.getEntityType()); - respVO.setEntityId(reqVO.getEntityId()); - switch (reqVO.getEntityType()) { - case SUBCOMPANY -> processSingleSubcompany(reqVO, respVO); - case DEPARTMENT -> processSingleDepartment(reqVO, respVO); - case JOB_TITLE -> processSingleJob(reqVO, respVO); - case USER -> processSingleUser(reqVO, respVO); - default -> throw new IllegalArgumentException("不支持的实体类型: " + reqVO.getEntityType()); - } - return respVO; - } - private int executeSubcompanyFullSync(IWorkFullSyncReqVO reqVO, IWorkSyncProcessor.SyncOptions options, IWorkSyncEntityStatVO stat, @@ -163,46 +146,6 @@ public class IWorkSyncServiceImpl implements IWorkSyncService { }); } - private void processSingleSubcompany(IWorkSingleSyncReqVO reqVO, IWorkSingleSyncRespVO respVO) { - IWorkHrSubcompanyPageRespVO.Subcompany data = fetchSingleSubcompany(reqVO.getEntityId()); - if (data == null) { - markNotFound(respVO, "分部"); - return; - } - IWorkSyncProcessor.BatchResult result = syncProcessor.syncSubcompany(data, buildSingleOptions(reqVO)); - populateSingleResult(respVO, result); - } - - private void processSingleDepartment(IWorkSingleSyncReqVO reqVO, IWorkSingleSyncRespVO respVO) { - IWorkHrDepartmentPageRespVO.Department data = fetchSingleDepartment(reqVO.getEntityId()); - if (data == null) { - markNotFound(respVO, "部门"); - return; - } - IWorkSyncProcessor.BatchResult result = syncProcessor.syncDepartment(data, buildSingleOptions(reqVO)); - populateSingleResult(respVO, result); - } - - private void processSingleJob(IWorkSingleSyncReqVO reqVO, IWorkSingleSyncRespVO respVO) { - IWorkHrJobTitlePageRespVO.JobTitle data = fetchSingleJob(reqVO.getEntityId()); - if (data == null) { - markNotFound(respVO, "岗位"); - return; - } - IWorkSyncProcessor.BatchResult result = syncProcessor.syncJobTitle(data, buildSingleOptions(reqVO)); - populateSingleResult(respVO, result); - } - - private void processSingleUser(IWorkSingleSyncReqVO reqVO, IWorkSingleSyncRespVO respVO) { - IWorkHrUserPageRespVO.User data = fetchSingleUser(reqVO.getEntityId().toString()); - if (data == null) { - markNotFound(respVO, "人员"); - return; - } - IWorkSyncProcessor.BatchResult result = syncProcessor.syncUser(data, buildSingleOptions(reqVO)); - populateSingleResult(respVO, result); - } - private int executePaged(IWorkFullSyncReqVO reqVO, IWorkSyncEntityTypeEnum type, List batches, @@ -238,66 +181,10 @@ public class IWorkSyncServiceImpl implements IWorkSyncService { stat.incrementFailed(result.getFailed()); } - private void populateSingleResult(IWorkSingleSyncRespVO respVO, IWorkSyncProcessor.BatchResult result) { - respVO.setCreated(result.getCreated() > 0); - respVO.setUpdated(result.getUpdated() > 0); - respVO.setMessage(result.getMessage()); - } - - private void markNotFound(IWorkSingleSyncRespVO respVO, String entityName) { - respVO.setCreated(false); - respVO.setUpdated(false); - respVO.setMessage(StrUtil.format("未在 iWork 中找到{}(ID={})", entityName, respVO.getEntityId())); - } - private IWorkSyncProcessor.SyncOptions buildFullSyncOptions(IWorkFullSyncReqVO reqVO) { return IWorkSyncProcessor.SyncOptions.full(Boolean.TRUE.equals(reqVO.getIncludeCanceled())); } - private IWorkSyncProcessor.SyncOptions buildSingleOptions(IWorkSingleSyncReqVO reqVO) { - return IWorkSyncProcessor.SyncOptions.single(Boolean.TRUE.equals(reqVO.getCreateIfMissing())); - } - - private IWorkHrSubcompanyPageRespVO.Subcompany fetchSingleSubcompany(Long entityId) { - IWorkSubcompanyQueryReqVO query = new IWorkSubcompanyQueryReqVO(); - query.setCurpage(1); - query.setPagesize(1); - query.setParams(Collections.singletonMap("subcompanyid", entityId)); - IWorkHrSubcompanyPageRespVO pageResp = orgRestService.listSubcompanies(query); - ensureIWorkSuccess("获取分部详情", pageResp.isSuccess(), pageResp.getMessage()); - return CollUtil.getFirst(pageResp.getDataList()); - } - - private IWorkHrDepartmentPageRespVO.Department fetchSingleDepartment(Long entityId) { - IWorkDepartmentQueryReqVO query = new IWorkDepartmentQueryReqVO(); - query.setCurpage(1); - query.setPagesize(1); - query.setParams(Collections.singletonMap("departmentid", entityId)); - IWorkHrDepartmentPageRespVO pageResp = orgRestService.listDepartments(query); - ensureIWorkSuccess("获取部门详情", pageResp.isSuccess(), pageResp.getMessage()); - return CollUtil.getFirst(pageResp.getDataList()); - } - - private IWorkHrJobTitlePageRespVO.JobTitle fetchSingleJob(Long entityId) { - IWorkJobTitleQueryReqVO query = new IWorkJobTitleQueryReqVO(); - query.setCurpage(1); - query.setPagesize(1); - query.setParams(Collections.singletonMap("jobtitleid", entityId)); - IWorkHrJobTitlePageRespVO pageResp = orgRestService.listJobTitles(query); - ensureIWorkSuccess("获取岗位详情", pageResp.isSuccess(), pageResp.getMessage()); - return CollUtil.getFirst(pageResp.getDataList()); - } - - private IWorkHrUserPageRespVO.User fetchSingleUser(String entityId) { - IWorkUserQueryReqVO query = new IWorkUserQueryReqVO(); - query.setCurpage(1); - query.setPagesize(1); - query.setParams(Collections.singletonMap("id", entityId)); - IWorkHrUserPageRespVO pageResp = orgRestService.listUsers(query); - ensureIWorkSuccess("获取人员详情", pageResp.isSuccess(), pageResp.getMessage()); - return CollUtil.getFirst(pageResp.getDataList()); - } - private void ensureIWorkSuccess(String action, boolean success, String remoteMessage) { if (success) { return; diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserService.java index 02d21ceb..fb82c63d 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserService.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserService.java @@ -190,10 +190,10 @@ public interface AdminUserService { /** * 判断密码是否匹配 * + * @param user 用户信息(用于决策密码策略) * @param rawPassword 未加密的密码 - * @param encodedPassword 加密后的密码 * @return 是否匹配 */ - boolean isPasswordMatch(String rawPassword, String encodedPassword); + boolean isPasswordMatch(AdminUserDO user, String rawPassword); } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserServiceImpl.java index 03689c12..2680843d 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserServiceImpl.java @@ -27,6 +27,7 @@ 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.UserPostMapper; import com.zt.plat.module.system.dal.mysql.user.AdminUserMapper; +import com.zt.plat.module.system.enums.user.PasswordStrategyEnum; import com.zt.plat.module.system.enums.user.UserSourceEnum; import com.zt.plat.module.system.service.dept.DeptService; import com.zt.plat.module.system.service.dept.PostService; @@ -40,9 +41,12 @@ import org.springframework.context.annotation.Lazy; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.DigestUtils; +import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.*; +import java.util.Locale; import java.util.stream.Collectors; import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -122,8 +126,9 @@ public class AdminUserServiceImpl implements AdminUserService { if (user.getUserSource() == null) { user.setUserSource(UserSourceEnum.EXTERNAL.getSource()); } + PasswordStrategyEnum passwordStrategy = determinePasswordStrategy(user.getUserSource()); user.setAvatar(normalizeAvatarValue(createReqVO.getAvatar())); - user.setPassword(encodePassword(createReqVO.getPassword())); + user.setPassword(encodePassword(createReqVO.getPassword(), passwordStrategy)); userMapper.insert(user); // 2.2 插入关联部门 if (CollectionUtil.isNotEmpty(user.getDeptIds())) { @@ -161,7 +166,7 @@ public class AdminUserServiceImpl implements AdminUserService { AdminUserDO user = BeanUtils.toBean(registerReqVO, AdminUserDO.class); user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启 user.setUserSource(UserSourceEnum.EXTERNAL.getSource()); // 注册用户设为外部用户 - user.setPassword(encodePassword(registerReqVO.getPassword())); // 加密密码 + user.setPassword(encodePassword(registerReqVO.getPassword(), PasswordStrategyEnum.LOCAL_BCRYPT)); // 加密密码 userMapper.insert(user); return user.getId(); } @@ -268,30 +273,23 @@ public class AdminUserServiceImpl implements AdminUserService { @Override public void updateUserPassword(Long id, UserProfileUpdatePasswordReqVO reqVO) { - // 校验旧密码密码 - validateOldPassword(id, reqVO.getOldPassword()); - // 执行更新 - AdminUserDO updateObj = new AdminUserDO().setId(id); - updateObj.setPassword(encodePassword(reqVO.getNewPassword())); // 加密密码 - userMapper.updateById(updateObj); + AdminUserDO user = validateUserExists(id); + ensurePasswordCanBeModified(user); + validateOldPassword(user, reqVO.getOldPassword()); + applyLocalPassword(user, reqVO.getNewPassword()); } @Override @LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_UPDATE_PASSWORD_SUB_TYPE, bizNo = "{{#id}}", success = SYSTEM_USER_UPDATE_PASSWORD_SUCCESS) public void updateUserPassword(Long id, String password) { - // 1. 校验用户存在 AdminUserDO user = validateUserExists(id); - - // 2. 更新密码 - AdminUserDO updateObj = new AdminUserDO(); - updateObj.setId(id); - updateObj.setPassword(encodePassword(password)); // 加密密码 - userMapper.updateById(updateObj); + ensurePasswordCanBeModified(user); + String encoded = applyLocalPassword(user, password); // 3. 记录操作日志上下文 LogRecordContext.putVariable("user", user); - LogRecordContext.putVariable("newPassword", updateObj.getPassword()); + LogRecordContext.putVariable("newPassword", encoded); } @Override @@ -611,15 +609,24 @@ public class AdminUserServiceImpl implements AdminUserService { */ @VisibleForTesting void validateOldPassword(Long id, String oldPassword) { - AdminUserDO user = userMapper.selectById(id); - if (user == null) { - throw exception(USER_NOT_EXISTS); - } - if (!isPasswordMatch(oldPassword, user.getPassword())) { + AdminUserDO user = validateUserExists(id); + validateOldPassword(user, oldPassword); + } + + private void validateOldPassword(AdminUserDO user, String oldPassword) { + if (!isPasswordMatch(user, oldPassword)) { throw exception(USER_PASSWORD_FAILED); } } + private void ensurePasswordCanBeModified(AdminUserDO user) { + UserSourceEnum sourceEnum = UserSourceEnum.of(user.getUserSource()); + if (sourceEnum == null || sourceEnum.isExternal()) { + return; + } + throw exception(USER_PASSWORD_MODIFY_FORBIDDEN); + } + @Override public List getUserListByStatus(Integer status) { List users = userMapper.selectListByStatus(status); @@ -628,18 +635,58 @@ public class AdminUserServiceImpl implements AdminUserService { } @Override - public boolean isPasswordMatch(String rawPassword, String encodedPassword) { - return passwordEncoder.matches(rawPassword, encodedPassword); + public boolean isPasswordMatch(AdminUserDO user, String rawPassword) { + if (user == null) { + return false; + } + PasswordStrategyEnum strategy = determinePasswordStrategy(user.getUserSource()); + if (strategy == PasswordStrategyEnum.IWORK_MD5) { + String stored = user.getPassword(); + if (isBcryptFormat(stored)) { + return passwordEncoder.matches(rawPassword, stored); + } + String hashed = md5Upper(rawPassword); + return StrUtil.isNotBlank(hashed) && hashed.equals(StrUtil.nullToDefault(stored, "")); + } + return passwordEncoder.matches(rawPassword, user.getPassword()); } - /** - * 对密码进行加密 - * - * @param password 密码 - * @return 加密后的密码 - */ - private String encodePassword(String password) { + private String applyLocalPassword(AdminUserDO user, String password) { + AdminUserDO updateObj = new AdminUserDO(); + updateObj.setId(user.getId()); + String encoded = encodePassword(password, PasswordStrategyEnum.LOCAL_BCRYPT); + updateObj.setPassword(encoded); + userMapper.updateById(updateObj); + return encoded; + } + + private PasswordStrategyEnum determinePasswordStrategy(Integer userSource) { + return UserSourceEnum.resolvePasswordStrategy(userSource); + } + + private String encodePassword(String password, PasswordStrategyEnum strategy) { + if (strategy == PasswordStrategyEnum.IWORK_MD5) { + return normalizeMd5(password); + } return passwordEncoder.encode(password); } + private String normalizeMd5(String password) { + if (StrUtil.isBlank(password)) { + return null; + } + return password.trim().toUpperCase(Locale.ROOT); + } + + private String md5Upper(String rawPassword) { + if (StrUtil.isBlank(rawPassword)) { + return null; + } + return DigestUtils.md5DigestAsHex(rawPassword.getBytes(StandardCharsets.UTF_8)).toUpperCase(Locale.ROOT); + } + + private boolean isBcryptFormat(String value) { + return StrUtil.isNotBlank(value) && value.startsWith("$2"); + } + } diff --git a/zt-module-system/zt-module-system-server/src/main/resources/application.yaml b/zt-module-system/zt-module-system-server/src/main/resources/application.yaml index 30293fe0..0240e030 100644 --- a/zt-module-system/zt-module-system-server/src/main/resources/application.yaml +++ b/zt-module-system/zt-module-system-server/src/main/resources/application.yaml @@ -109,9 +109,7 @@ iwork: base-url: http://172.16.36.233:8080 # app-id: f47ac10b-58cc-4372-a567-0e02b2c3d479 app-id: f47ac10b-58cc-4372-a567-0e02b2c3d479 - client-public-key: user-id: 9869 - workflow-id: 1753 paths: register: /api/ec/dev/auth/regist apply-token: /api/ec/dev/auth/applytoken @@ -135,6 +133,8 @@ iwork: sync-department: /api/hrm/resful/synDepartment sync-job-title: /api/hrm/resful/synJobtitle sync-user: /api/hrm/resful/synHrmresource + workflow: + seal-workflow-id: "1753" --- #################### RPC 远程调用相关配置 #################### diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImplTest.java index c9fd33bc..ec8e133c 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImplTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImplTest.java @@ -16,7 +16,11 @@ import com.zt.plat.module.system.enums.logger.LoginLogTypeEnum; import com.zt.plat.module.system.enums.logger.LoginResultEnum; import com.zt.plat.module.system.enums.sms.SmsSceneEnum; import com.zt.plat.module.system.enums.social.SocialTypeEnum; +import com.zt.plat.module.system.enums.user.UserSourceEnum; import com.zt.plat.module.system.service.logger.LoginLogService; +import com.zt.plat.module.system.service.member.MemberService; +import com.zt.plat.module.system.service.oauth2.EbanOAuth2Service; +import com.zt.plat.module.system.service.oauth2.EbanTokenService; import com.zt.plat.module.system.service.oauth2.OAuth2TokenService; import com.zt.plat.module.system.service.social.SocialUserService; import com.zt.plat.module.system.service.user.AdminUserService; @@ -57,6 +61,12 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { private SmsCodeApi smsCodeApi; @MockBean private OAuth2TokenService oauth2TokenService; + @MockBean + private MemberService memberService; + @MockBean + private EbanOAuth2Service ebanOAuth2Service; + @MockBean + private EbanTokenService ebanTokenService; @MockBean private Validator validator; @@ -78,7 +88,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { .setPassword(password).setStatus(CommonStatusEnum.ENABLE.getStatus())); when(userService.getUserByUsername(eq(username))).thenReturn(user); // mock password 匹配 - when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); + when(userService.isPasswordMatch(eq(user), eq(password))).thenReturn(true); // 调用 AdminUserDO loginUser = authService.authenticate(username, password); @@ -132,7 +142,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { .setPassword(password).setStatus(CommonStatusEnum.DISABLE.getStatus())); when(userService.getUserByUsername(eq(username))).thenReturn(user); // mock password 匹配 - when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); + when(userService.isPasswordMatch(eq(user), eq(password))).thenReturn(true); // 调用, 并断言异常 assertServiceException(() -> authService.authenticate(username, password), @@ -158,7 +168,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { .setPassword("test_password").setStatus(CommonStatusEnum.ENABLE.getStatus())); when(userService.getUserByUsername(eq("test_username"))).thenReturn(user); // mock password 匹配 - when(userService.isPasswordMatch(eq("test_password"), eq(user.getPassword()))).thenReturn(true); + when(userService.isPasswordMatch(eq(user), eq("test_password"))).thenReturn(true); // mock 缓存登录用户到 Redis OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) .setUserType(UserTypeEnum.ADMIN.getValue())); @@ -179,6 +189,49 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()))); } + @Test + public void testLogin_internalUserBlocked() { + AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class, o -> { + o.setUsername("sync_user"); + o.setPassword("Pass@123"); + o.setSocialType(null); + }); + authService.setCaptchaEnable(false); + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(10L) + .setUsername("sync_user") + .setPassword("bcrypt") + .setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setUserSource(UserSourceEnum.SYNC.getSource())); + when(userService.getUserByUsername(eq("sync_user"))).thenReturn(user); + when(userService.isPasswordMatch(eq(user), eq("Pass@123"))).thenReturn(true); + + assertServiceException(() -> authService.login(reqVO), AUTH_LOGIN_INTERNAL_USER_PASSWORD_NOT_ALLOWED); + } + + @Test + public void testLogin_iWorkUserAllowed() { + AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class, o -> { + o.setUsername("iwork_user"); + o.setPassword("Password1!"); + o.setSocialType(null); + }); + authService.setCaptchaEnable(false); + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(20L) + .setUsername("iwork_user") + .setPassword("md5") + .setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setUserSource(UserSourceEnum.IWORK.getSource())); + when(userService.getUserByUsername(eq("iwork_user"))).thenReturn(user); + when(userService.isPasswordMatch(eq(user), eq("Password1!"))).thenReturn(true); + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(20L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + when(oauth2TokenService.createAccessToken(eq(20L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull())) + .thenReturn(accessTokenDO); + + AuthLoginRespVO respVO = authService.login(reqVO); + assertPojoEquals(accessTokenDO, respVO); + } + @Test public void testSendSmsCode() { // 准备参数 diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/user/AdminUserServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/user/AdminUserServiceImplTest.java index ba830d9b..06c76d1d 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/user/AdminUserServiceImplTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/user/AdminUserServiceImplTest.java @@ -23,6 +23,7 @@ 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.dal.mysql.userdept.UserDeptMapper; import com.zt.plat.module.system.enums.common.SexEnum; +import com.zt.plat.module.system.enums.user.UserSourceEnum; import com.zt.plat.module.system.service.dept.DeptServiceImpl; import com.zt.plat.module.system.service.dept.PostService; import com.zt.plat.module.system.service.permission.PermissionService; @@ -35,7 +36,9 @@ import org.mockito.stubbing.Answer; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.util.DigestUtils; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.function.Consumer; @@ -267,6 +270,32 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { assertEquals("encode:" + password, user.getPassword()); } + @Test + public void testUpdateUserPassword_forbiddenForIWork() { + AdminUserDO dbUser = randomAdminUserDO(o -> o.setUserSource(UserSourceEnum.IWORK.getSource())); + userMapper.insert(dbUser); + Long userId = dbUser.getId(); + UserProfileUpdatePasswordReqVO reqVO = randomPojo(UserProfileUpdatePasswordReqVO.class, o -> { + o.setOldPassword("oldPwd"); + o.setNewPassword("newPwd"); + }); + + assertServiceException(() -> userService.updateUserPassword(userId, reqVO), USER_PASSWORD_MODIFY_FORBIDDEN); + assertServiceException(() -> userService.updateUserPassword(userId, "anotherPwd"), USER_PASSWORD_MODIFY_FORBIDDEN); + } + + @Test + public void testIsPasswordMatch_iWorkMd5() { + String rawPassword = "Abc12345"; + String md5 = DigestUtils.md5DigestAsHex(rawPassword.getBytes(StandardCharsets.UTF_8)).toUpperCase(Locale.ROOT); + AdminUserDO user = randomAdminUserDO(o -> { + o.setUserSource(UserSourceEnum.IWORK.getSource()); + o.setPassword(md5); + }); + + assertTrue(userService.isPasswordMatch(user, rawPassword)); + } + @Test public void testUpdateUserStatus() { // mock 数据