diff --git a/pom.xml b/pom.xml index 07531eeb..9c0cd2b3 100644 --- a/pom.xml +++ b/pom.xml @@ -237,8 +237,8 @@ 172.16.46.63:30848 dev DEFAULT_GROUP - - + nacos + P@ssword25 1.0.0 @@ -250,8 +250,8 @@ 172.16.46.63:30848 prod DEFAULT_GROUP - - + nacos + P@ssword25 1.0.0 @@ -263,8 +263,8 @@ 172.16.46.63:30848 local DEFAULT_GROUP - - + nacos + P@ssword25 1.0.0 diff --git a/zt-dependencies/pom.xml b/zt-dependencies/pom.xml index 21599245..db55ba9d 100644 --- a/zt-dependencies/pom.xml +++ b/zt-dependencies/pom.xml @@ -87,6 +87,7 @@ 4.1.116.Final 1.2.5 0.9.0 + 4.12.0 2.15.1 4.5.13 diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/dept/DeptSourceEnum.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/dept/DeptSourceEnum.java index 9933f0b3..805d56d9 100644 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/dept/DeptSourceEnum.java +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/dept/DeptSourceEnum.java @@ -13,7 +13,8 @@ import lombok.Getter; public enum DeptSourceEnum { EXTERNAL(1, "外部部门"), // 系统创建的部门 - SYNC(2, "同步部门"); // 通过 OrgSyncService 同步的部门 + SYNC(2, "同步部门"), // 通过 OrgSyncService 同步的部门 + IWORK(3, "iWork 同步"); // 通过 iWork 同步的部门 /** * 类型 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 20c8b9f0..2299cd0d 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 @@ -13,7 +13,8 @@ import lombok.Getter; public enum UserSourceEnum { EXTERNAL(1, "外部用户"), // 系统创建、注册等方式产生的用户 - SYNC(2, "同步用户"); // 通过 UserSyncService 同步的用户 + SYNC(2, "同步用户"), // 通过 UserSyncService 同步的用户 + IWORK(3, "iWork 用户"); // 通过 iWork 全量/单条同步产生的用户 /** * 类型 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 f66a0f2a..97637868 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 @@ -1,23 +1,10 @@ package com.zt.plat.module.system.controller.admin.integration.iwork; import com.zt.plat.framework.common.pojo.CommonResult; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkAuthRegisterReqVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkAuthRegisterRespVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkAuthTokenReqVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkAuthTokenRespVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkDepartmentQueryReqVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkJobTitleQueryReqVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOperationRespVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOrgRespVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOrgSyncReqVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkSubcompanyQueryReqVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkUserInfoReqVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkUserInfoRespVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkUserQueryReqVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkWorkflowCreateReqVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkWorkflowVoidReqVO; +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 io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -42,6 +29,7 @@ public class IWorkIntegrationController { private final IWorkIntegrationService integrationService; private final IWorkOrgRestService orgRestService; + private final IWorkSyncService syncService; @PostMapping("/auth/register") @Operation(summary = "注册 iWork 凭证,获取服务端公钥与 secret") @@ -77,49 +65,63 @@ public class IWorkIntegrationController { @PostMapping("/hr/subcompany/page") @Operation(summary = "获取 iWork 分部列表") - public CommonResult listSubcompanies(@Valid @RequestBody IWorkSubcompanyQueryReqVO reqVO) { + public CommonResult listSubcompanies(@Valid @RequestBody IWorkSubcompanyQueryReqVO reqVO) { return success(orgRestService.listSubcompanies(reqVO)); } @PostMapping("/hr/department/page") @Operation(summary = "获取 iWork 部门列表") - public CommonResult listDepartments(@Valid @RequestBody IWorkDepartmentQueryReqVO reqVO) { + public CommonResult listDepartments(@Valid @RequestBody IWorkDepartmentQueryReqVO reqVO) { return success(orgRestService.listDepartments(reqVO)); } @PostMapping("/hr/job-title/page") @Operation(summary = "获取 iWork 岗位列表") - public CommonResult listJobTitles(@Valid @RequestBody IWorkJobTitleQueryReqVO reqVO) { + public CommonResult listJobTitles(@Valid @RequestBody IWorkJobTitleQueryReqVO reqVO) { return success(orgRestService.listJobTitles(reqVO)); } @PostMapping("/hr/user/page") @Operation(summary = "获取 iWork 人员列表") - public CommonResult listUsers(@Valid @RequestBody IWorkUserQueryReqVO reqVO) { + public CommonResult listUsers(@Valid @RequestBody IWorkUserQueryReqVO reqVO) { return success(orgRestService.listUsers(reqVO)); } - @PostMapping("/hr/subcompany/sync") - @Operation(summary = "同步分部信息至 iWork") - public CommonResult syncSubcompanies(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) { - return success(orgRestService.syncSubcompanies(reqVO)); +// @PostMapping("/hr/subcompany/sync") +// @Operation(summary = "同步分部信息至 iWork") +// public CommonResult syncSubcompanies(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) { +// return success(orgRestService.syncSubcompanies(reqVO)); +// } +// +// @PostMapping("/hr/department/sync") +// @Operation(summary = "同步部门信息至 iWork") +// public CommonResult syncDepartments(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) { +// return success(orgRestService.syncDepartments(reqVO)); +// } +// +// @PostMapping("/hr/job-title/sync") +// @Operation(summary = "同步岗位信息至 iWork") +// public CommonResult syncJobTitles(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) { +// return success(orgRestService.syncJobTitles(reqVO)); +// } +// +// @PostMapping("/hr/user/sync") +// @Operation(summary = "同步人员信息至 iWork") +// public CommonResult syncUsers(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) { +// return success(orgRestService.syncUsers(reqVO)); +// } + + // ----------------- 同步到本地 ----------------- + + @PostMapping("/hr/full-sync") + @Operation(summary = "手动触发 iWork 组织/人员全量同步") + public CommonResult fullSync(@Valid @RequestBody IWorkFullSyncReqVO reqVO) { + return success(syncService.fullSync(reqVO)); } - @PostMapping("/hr/department/sync") - @Operation(summary = "同步部门信息至 iWork") - public CommonResult syncDepartments(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) { - return success(orgRestService.syncDepartments(reqVO)); - } - - @PostMapping("/hr/job-title/sync") - @Operation(summary = "同步岗位信息至 iWork") - public CommonResult syncJobTitles(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) { - return success(orgRestService.syncJobTitles(reqVO)); - } - - @PostMapping("/hr/user/sync") - @Operation(summary = "同步人员信息至 iWork") - public CommonResult syncUsers(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) { - return success(orgRestService.syncUsers(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/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 new file mode 100644 index 00000000..5fa63d05 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFullSyncReqVO.java @@ -0,0 +1,53 @@ +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.Max; +import jakarta.validation.constraints.Min; +import lombok.Data; + +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * iWork 全量同步请求 + */ +@Data +public class IWorkFullSyncReqVO { + + @Schema(description = "起始页码,从 1 开始", example = "1") + @Min(1) + private Integer startPage = 1; + + @Schema(description = "最大处理页数,null 表示处理至 iWork 返回的末页", example = "10") + @Min(1) + private Integer maxPages; + + @Schema(description = "每次分页从 iWork 拉取的记录数", example = "100") + @Min(1) + @Max(500) + private Integer pageSize = 100; + + @Schema(description = "同步范围列表,默认同步全部。可选:subcompany、department、jobTitle、user") + private List scopes; + + @Schema(description = "是否包含已失效(canceled=1)的记录", example = "false") + private Boolean includeCanceled = Boolean.FALSE; + + public Set resolveScopes() { + EnumSet defaults = EnumSet.allOf(IWorkSyncEntityTypeEnum.class); + if (scopes == null || scopes.isEmpty()) { + return defaults; + } + Set resolved = scopes.stream() + .map(IWorkSyncEntityTypeEnum::fromCode) + .filter(java.util.Objects::nonNull) + .collect(Collectors.toCollection(() -> EnumSet.noneOf(IWorkSyncEntityTypeEnum.class))); + if (resolved.isEmpty()) { + return defaults; + } + return resolved; + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFullSyncRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFullSyncRespVO.java new file mode 100644 index 00000000..e88e45c1 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFullSyncRespVO.java @@ -0,0 +1,34 @@ +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.List; + +/** + * iWork 全量同步响应 + */ +@Data +public class IWorkFullSyncRespVO { + + @Schema(description = "本次处理的总页数") + private Integer processedPages; + + @Schema(description = "每次分页请求的条数") + private Integer pageSize; + + @Schema(description = "分部统计信息") + private IWorkSyncEntityStatVO subcompanyStat = new IWorkSyncEntityStatVO(); + + @Schema(description = "部门统计信息") + private IWorkSyncEntityStatVO departmentStat = new IWorkSyncEntityStatVO(); + + @Schema(description = "岗位统计信息") + private IWorkSyncEntityStatVO jobTitleStat = new IWorkSyncEntityStatVO(); + + @Schema(description = "人员统计信息") + private IWorkSyncEntityStatVO userStat = new IWorkSyncEntityStatVO(); + + @Schema(description = "每个批次的详细统计") + private List batches; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrDepartmentPageRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrDepartmentPageRespVO.java new file mode 100644 index 00000000..ecdf6045 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrDepartmentPageRespVO.java @@ -0,0 +1,138 @@ +package com.zt.plat.module.system.controller.admin.integration.iwork.vo; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import com.zt.plat.module.system.service.integration.iwork.jackson.LenientIntegerDeserializer; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * iWork 部门分页响应。 + */ +@Data +@Schema(description = "iWork 部门分页响应") +public class IWorkHrDepartmentPageRespVO { + + @Schema(description = "响应码") + private String code; + + @Schema(description = "提示信息") + private String message; + + @Schema(description = "是否成功") + private boolean success; + + @Schema(description = "总条数") + private Integer totalSize; + + @Schema(description = "总页数") + private Integer totalPage; + + @Schema(description = "每页条数") + private Integer pageSize; + + @Schema(description = "当前页码") + private Integer pageNumber; + + @Schema(description = "部门数据列表") + private List dataList; + + @Data + @Schema(description = "部门信息") + public static class Department { + + @Schema(description = "部门 ID") + @JsonProperty("departmentid") + private Integer departmentid; + + @Schema(description = "部门编码") + @JsonProperty("departmentcode") + private String departmentcode; + + @Schema(description = "部门名称") + @JsonProperty("departmentname") + private String departmentname; + + @Schema(description = "部门标识") + @JsonProperty("departmentmark") + private String departmentmark; + + @Schema(description = "所属分部 ID") + @JsonProperty("subcompanyid1") + private Integer subcompanyid1; + + @Schema(description = "所属分部名称") + @JsonProperty("subcompanyname") + private String subcompanyname; + + @Schema(description = "上级分部 ID") + @JsonProperty("supsubcomid") + private Integer supsubcomid; + + @Schema(description = "上级分部名称") + @JsonProperty("supsubcomname") + private String supsubcomname; + + @Schema(description = "父部门 ID") + @JsonProperty("parentdeptid") + private Integer parentdeptid; + + @Schema(description = "父部门名称") + @JsonProperty("parentdeptname") + private String parentdeptname; + + @Schema(description = "层级路径") + @JsonProperty("alllevel") + private String alllevel; + + @Schema(description = "显示顺序") + @JsonProperty("showorder") + @JsonDeserialize(using = LenientIntegerDeserializer.class) + private Integer showorder; + + @Schema(description = "是否有子部门 (0/1)") + @JsonProperty("haschild") + private String haschild; + + @Schema(description = "是否已失效 (0/1)") + @JsonProperty("canceled") + private String canceled; + + @Schema(description = "部门类型") + @JsonProperty("departmenttype") + private String departmenttype; + + @Schema(description = "负责人 ID") + @JsonProperty("managerid") + private Integer managerid; + + @Schema(description = "负责人名称") + @JsonProperty("manager") + private String manager; + + @JsonIgnore + private Map attributes; + + @JsonAnySetter + public void putAttribute(String key, Object value) { + if (attributes == null) { + attributes = new LinkedHashMap<>(); + } + attributes.put(key, value); + } + + @JsonAnyGetter + public Map any() { + return attributes == null ? Collections.emptyMap() : attributes; + } + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrJobTitlePageRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrJobTitlePageRespVO.java new file mode 100644 index 00000000..2bb7089a --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrJobTitlePageRespVO.java @@ -0,0 +1,122 @@ +package com.zt.plat.module.system.controller.admin.integration.iwork.vo; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import com.zt.plat.module.system.service.integration.iwork.jackson.LenientIntegerDeserializer; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * iWork 岗位分页响应。 + */ +@Data +@Schema(description = "iWork 岗位分页响应") +public class IWorkHrJobTitlePageRespVO { + + @Schema(description = "响应码") + private String code; + + @Schema(description = "提示信息") + private String message; + + @Schema(description = "是否成功") + private boolean success; + + @Schema(description = "总条数") + private Integer totalSize; + + @Schema(description = "总页数") + private Integer totalPage; + + @Schema(description = "每页条数") + private Integer pageSize; + + @Schema(description = "当前页码") + private Integer pageNumber; + + @Schema(description = "岗位数据列表") + private List dataList; + + @Data + @Schema(description = "岗位信息") + public static class JobTitle { + + @Schema(description = "岗位 ID") + @JsonProperty("jobtitleid") + private Integer jobtitleid; + + @Schema(description = "岗位编码") + @JsonProperty("jobtitlecode") + private String jobtitlecode; + + @Schema(description = "岗位名称") + @JsonProperty("jobtitlename") + private String jobtitlename; + + @Schema(description = "岗位类型") + @JsonProperty("jobtitletype") + private String jobtitletype; + + @Schema(description = "所属岗位组 ID") + @JsonProperty("jobgroupid") + private Integer jobgroupid; + + @Schema(description = "所属岗位组名称") + @JsonProperty("jobgroupname") + private String jobgroupname; + + @Schema(description = "岗位层级") + @JsonProperty("joblevel") + private String joblevel; + + @Schema(description = "岗位职责") + @JsonProperty("jobfunction") + private String jobfunction; + + @Schema(description = "岗位描述") + @JsonProperty("description") + private String description; + + @Schema(description = "上级岗位 ID") + @JsonProperty("supjobtitleid") + private Integer supjobtitleid; + + @Schema(description = "上级岗位名称") + @JsonProperty("supjobtitlename") + private String supjobtitlename; + + @Schema(description = "显示顺序") + @JsonProperty("showorder") + @JsonDeserialize(using = LenientIntegerDeserializer.class) + private Integer showorder; + + @Schema(description = "是否已失效 (0/1)") + @JsonProperty("canceled") + private String canceled; + + @JsonIgnore + private Map attributes; + + @JsonAnySetter + public void putAttribute(String key, Object value) { + if (attributes == null) { + attributes = new LinkedHashMap<>(); + } + attributes.put(key, value); + } + + @JsonAnyGetter + public Map any() { + return attributes == null ? Collections.emptyMap() : attributes; + } + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrSubcompanyPageRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrSubcompanyPageRespVO.java new file mode 100644 index 00000000..fdbda82a --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrSubcompanyPageRespVO.java @@ -0,0 +1,114 @@ +package com.zt.plat.module.system.controller.admin.integration.iwork.vo; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import com.zt.plat.module.system.service.integration.iwork.jackson.LenientIntegerDeserializer; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * iWork 分部分页响应。 + */ +@Data +@Schema(description = "iWork 分部分页响应") +public class IWorkHrSubcompanyPageRespVO { + + @Schema(description = "响应码") + private String code; + + @Schema(description = "提示信息") + private String message; + + @Schema(description = "是否成功") + private boolean success; + + @Schema(description = "总条数") + private Integer totalSize; + + @Schema(description = "总页数") + private Integer totalPage; + + @Schema(description = "每页条数") + private Integer pageSize; + + @Schema(description = "当前页码") + private Integer pageNumber; + + @Schema(description = "分部数据列表") + private List dataList; + + @Data + @Schema(description = "分部信息") + public static class Subcompany { + + @Schema(description = "分部唯一 ID") + @JsonProperty("subcompanyid1") + private Integer subcompanyid1; + + @Schema(description = "分部编码") + @JsonProperty("subcompanycode") + private String subcompanycode; + + @Schema(description = "分部名称") + @JsonProperty("subcompanyname") + private String subcompanyname; + + @Schema(description = "所属总部 ID") + @JsonProperty("companyid") + private Integer companyid; + + @Schema(description = "所属总部名称") + @JsonProperty("companyname") + private String companyname; + + @Schema(description = "上级分部 ID") + @JsonProperty("supsubcomid") + private Integer supsubcomid; + + @Schema(description = "上级分部名称") + @JsonProperty("supsubcomname") + private String supsubcomname; + + @Schema(description = "显示顺序") + @JsonProperty("showorder") + @JsonDeserialize(using = LenientIntegerDeserializer.class) + private Integer showorder; + + @Schema(description = "分部描述") + @JsonProperty("description") + private String description; + + @Schema(description = "是否已失效(0/1)") + @JsonProperty("canceled") + private String canceled; + + @Schema(description = "层级路径") + @JsonProperty("alllevel") + private String alllevel; + + @JsonIgnore + private Map attributes; + + @JsonAnySetter + public void putAttribute(String key, Object value) { + if (attributes == null) { + attributes = new LinkedHashMap<>(); + } + attributes.put(key, value); + } + + @JsonAnyGetter + public Map any() { + return attributes == null ? Collections.emptyMap() : attributes; + } + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrSyncRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrSyncRespVO.java new file mode 100644 index 00000000..c0e17393 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrSyncRespVO.java @@ -0,0 +1,74 @@ +package com.zt.plat.module.system.controller.admin.integration.iwork.vo; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * iWork 人力同步响应。 + */ +@Data +@Schema(description = "iWork 人力同步响应") +public class IWorkHrSyncRespVO { + + @Schema(description = "响应码") + private String code; + + @Schema(description = "提示信息") + private String message; + + @Schema(description = "是否成功") + private boolean success; + + @Schema(description = "同步结果明细") + private List result; + + @Data + @Schema(description = "同步结果项") + public static class SyncResult { + + @Schema(description = "操作动作 add/update/delete") + @JsonProperty("@action") + private String action; + + @Schema(description = "外部编码") + @JsonProperty("code") + private String code; + + @Schema(description = "执行结果 success/fail") + @JsonProperty("result") + private String result; + + @Schema(description = "是否成功") + @JsonProperty("success") + private Boolean success; + + @Schema(description = "失败描述") + @JsonProperty("message") + private String message; + + @JsonIgnore + private Map attributes; + + @JsonAnySetter + public void putAttribute(String key, Object value) { + if (attributes == null) { + attributes = new LinkedHashMap<>(); + } + attributes.put(key, value); + } + + @JsonAnyGetter + public Map any() { + return attributes == null ? Collections.emptyMap() : attributes; + } + } +} 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 new file mode 100644 index 00000000..03cafc17 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrUserPageRespVO.java @@ -0,0 +1,190 @@ +package com.zt.plat.module.system.controller.admin.integration.iwork.vo; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import com.zt.plat.module.system.service.integration.iwork.jackson.LenientIntegerDeserializer; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * iWork 人员分页响应。 + */ +@Data +@Schema(description = "iWork 人员分页响应") +public class IWorkHrUserPageRespVO { + + @Schema(description = "响应码") + private String code; + + @Schema(description = "提示信息") + private String message; + + @Schema(description = "是否成功") + private boolean success; + + @Schema(description = "总条数") + private Integer totalSize; + + @Schema(description = "总页数") + private Integer totalPage; + + @Schema(description = "每页条数") + private Integer pageSize; + + @Schema(description = "当前页码") + private Integer pageNumber; + + @Schema(description = "人员数据列表") + private List dataList; + + @Data + @Schema(description = "人员信息") + public static class User { + + @Schema(description = "人员 ID") + @JsonProperty("id") + private Integer id; + + @Schema(description = "人员姓名") + @JsonProperty("lastname") + private String lastname; + + @Schema(description = "登录账号") + @JsonProperty("loginid") + private String loginid; + + @Schema(description = "工号") + @JsonProperty("workcode") + private String workcode; + + @Schema(description = "性别") + @JsonProperty("sex") + private String sex; + + @Schema(description = "所属分部 ID") + @JsonProperty("subcompanyid1") + private Integer subcompanyid1; + + @Schema(description = "所属分部名称") + @JsonProperty("subcompanyname") + private String subcompanyname; + + @Schema(description = "所属部门 ID") + @JsonProperty("departmentid") + private Integer departmentid; + + @Schema(description = "所属部门名称") + @JsonProperty("departmentname") + private String departmentname; + + @Schema(description = "所属岗位 ID") + @JsonProperty("jobtitleid") + private Integer jobtitleid; + + @Schema(description = "所属岗位名称") + @JsonProperty("jobtitlename") + private String jobtitlename; + + @Schema(description = "手机号码") + @JsonProperty("mobile") + private String mobile; + + @Schema(description = "办公电话") + @JsonProperty("telephone") + private String telephone; + + @Schema(description = "邮箱") + @JsonProperty("email") + private String email; + + @Schema(description = "直属上级 ID") + @JsonProperty("managerid") + private Integer managerid; + + @Schema(description = "助理 ID") + @JsonProperty("assistantid") + private Integer assistantid; + + @Schema(description = "安全级别") + @JsonProperty("seclevel") + private Integer seclevel; + + @Schema(description = "当前状态") + @JsonProperty("status") + private String status; + + @Schema(description = "入职日期") + @JsonProperty("hiredate") + private String hiredate; + + @Schema(description = "离职日期") + @JsonProperty("leavedate") + private String leavedate; + + @Schema(description = "出生日期") + @JsonProperty("birthday") + private String birthday; + + @Schema(description = "民族") + @JsonProperty("folk") + private String folk; + + @Schema(description = "婚姻状况") + @JsonProperty("maritalstatus") + private String maritalstatus; + + @Schema(description = "文化程度") + @JsonProperty("educationlevel") + private String educationlevel; + + @Schema(description = "籍贯") + @JsonProperty("nativeplace") + private String nativeplace; + + @Schema(description = "户口所在地") + @JsonProperty("nationality") + private String nationality; + + @Schema(description = "证件号码") + @JsonProperty("certificatenum") + private String certificatenum; + + @Schema(description = "显示顺序") + @JsonProperty("dsporder") + @JsonDeserialize(using = LenientIntegerDeserializer.class) + private Integer dsporder; + + @Schema(description = "系统语言") + @JsonProperty("systemlanguage") + private String systemlanguage; + + @Schema(description = "账号类型") + @JsonProperty("accounttype") + private String accounttype; + + @JsonIgnore + private Map attributes; + + @JsonAnySetter + public void putAttribute(String key, Object value) { + if (attributes == null) { + attributes = new LinkedHashMap<>(); + } + attributes.put(key, value); + } + + @JsonAnyGetter + public Map any() { + return attributes == null ? Collections.emptyMap() : attributes; + } + } +} 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 index 63bf2845..40d6da15 100644 --- 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 @@ -1,28 +1,15 @@ 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 人力组织 REST 请求的响应封装。 + * @deprecated 请改用强类型的 IWorkHr*RespVO,避免再引用该占位类。 */ -@Data -public class IWorkOrgRespVO { +@Deprecated(forRemoval = true) +@Schema(description = "已废弃,占位用") +public final class IWorkOrgRespVO { - @Schema(description = "响应中的业务数据(data 字段或整体映射)") - private Map payload; - - @Schema(description = "原始响应字符串") - private String rawBody; - - @Schema(description = "是否判断为成功") - private boolean success; - - @Schema(description = "提示信息") - private String message; - - @Schema(description = "响应码") - private String code; + 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 new file mode 100644 index 00000000..85d290e4 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSingleSyncReqVO.java @@ -0,0 +1,26 @@ +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 new file mode 100644 index 00000000..0e799b3e --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSingleSyncRespVO.java @@ -0,0 +1,27 @@ +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/IWorkSyncBatchStatVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSyncBatchStatVO.java new file mode 100644 index 00000000..87d284a4 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSyncBatchStatVO.java @@ -0,0 +1,34 @@ +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; + +/** + * 记录一次分页批次执行的统计信息。 + */ +@Data +public class IWorkSyncBatchStatVO { + + @Schema(description = "同步的实体类型") + private IWorkSyncEntityTypeEnum entityType; + + @Schema(description = "当前批次处理的页码,从 1 开始") + private Integer pageNumber; + + @Schema(description = "本批次从 iWork 拉取的记录数量") + private Integer pulled; + + @Schema(description = "本批次创建的记录数量") + private Integer created; + + @Schema(description = "本批次因已存在而跳过的记录数量") + private Integer skippedExisting; + + @Schema(description = "本批次禁用的记录数量") + private Integer disabled; + + @Schema(description = "本批次失败的记录数量") + private Integer failed; + +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSyncEntityStatVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSyncEntityStatVO.java new file mode 100644 index 00000000..98741ddb --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSyncEntityStatVO.java @@ -0,0 +1,46 @@ +package com.zt.plat.module.system.controller.admin.integration.iwork.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * iWork 同步实体统计信息 + */ +@Data +public class IWorkSyncEntityStatVO { + + @Schema(description = "从 iWork 拉取的记录数量") + private int pulled; + + @Schema(description = "在本系统中新创建的记录数量") + private int created; + + @Schema(description = "因已存在而跳过的记录数量") + private int skippedExisting; + + @Schema(description = "在本系统中被禁用的记录数量") + private int disabled; + + @Schema(description = "同步失败的记录数量") + private int failed; + + public void incrementPulled(int delta) { + this.pulled += delta; + } + + public void incrementCreated(int delta) { + this.created += delta; + } + + public void incrementSkipped(int delta) { + this.skippedExisting += delta; + } + + public void incrementDisabled(int delta) { + this.disabled += delta; + } + + public void incrementFailed(int delta) { + this.failed += delta; + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/enums/integration/IWorkSyncEntityTypeEnum.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/enums/integration/IWorkSyncEntityTypeEnum.java new file mode 100644 index 00000000..c9e9e00a --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/enums/integration/IWorkSyncEntityTypeEnum.java @@ -0,0 +1,32 @@ +package com.zt.plat.module.system.enums.integration; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * iWork 同步支持的实体类型。 + */ +@AllArgsConstructor +@Getter +public enum IWorkSyncEntityTypeEnum { + + SUBCOMPANY("subcompany", "分部 / 公司"), + DEPARTMENT("department", "部门"), + JOB_TITLE("jobTitle", "岗位"), + USER("user", "人员"); + + private final String code; + private final String label; + + public static IWorkSyncEntityTypeEnum fromCode(String code) { + if (code == null) { + return null; + } + for (IWorkSyncEntityTypeEnum value : values()) { + if (value.code.equalsIgnoreCase(code)) { + return value; + } + } + return null; + } +} 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 6b6246fb..6983360b 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 @@ -444,7 +444,9 @@ public class AdminAuthServiceImpl implements AdminAuthService { Integer userSource = user.getUserSource(); // 同步用户(SYNC = 2)为内部用户,需要使用E办登录 - if (userSource != null && userSource.equals(UserSourceEnum.SYNC.getSource())) { + if (userSource != null && + (userSource.equals(UserSourceEnum.SYNC.getSource()) || + userSource.equals(UserSourceEnum.IWORK.getSource()))) { return true; } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkOrgRestService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkOrgRestService.java index 18fbe3e4..2e291fe3 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkOrgRestService.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkOrgRestService.java @@ -1,8 +1,12 @@ package com.zt.plat.module.system.service.integration.iwork; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkDepartmentQueryReqVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrDepartmentPageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrJobTitlePageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSubcompanyPageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSyncRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrUserPageRespVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkJobTitleQueryReqVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOrgRespVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOrgSyncReqVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkSubcompanyQueryReqVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkUserQueryReqVO; @@ -12,19 +16,19 @@ import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkUser */ public interface IWorkOrgRestService { - IWorkOrgRespVO listSubcompanies(IWorkSubcompanyQueryReqVO reqVO); + IWorkHrSubcompanyPageRespVO listSubcompanies(IWorkSubcompanyQueryReqVO reqVO); - IWorkOrgRespVO listDepartments(IWorkDepartmentQueryReqVO reqVO); + IWorkHrDepartmentPageRespVO listDepartments(IWorkDepartmentQueryReqVO reqVO); - IWorkOrgRespVO listJobTitles(IWorkJobTitleQueryReqVO reqVO); + IWorkHrJobTitlePageRespVO listJobTitles(IWorkJobTitleQueryReqVO reqVO); - IWorkOrgRespVO listUsers(IWorkUserQueryReqVO reqVO); + IWorkHrUserPageRespVO listUsers(IWorkUserQueryReqVO reqVO); - IWorkOrgRespVO syncSubcompanies(IWorkOrgSyncReqVO reqVO); + IWorkHrSyncRespVO syncSubcompanies(IWorkOrgSyncReqVO reqVO); - IWorkOrgRespVO syncDepartments(IWorkOrgSyncReqVO reqVO); + IWorkHrSyncRespVO syncDepartments(IWorkOrgSyncReqVO reqVO); - IWorkOrgRespVO syncJobTitles(IWorkOrgSyncReqVO reqVO); + IWorkHrSyncRespVO syncJobTitles(IWorkOrgSyncReqVO reqVO); - IWorkOrgRespVO syncUsers(IWorkOrgSyncReqVO reqVO); + IWorkHrSyncRespVO syncUsers(IWorkOrgSyncReqVO reqVO); } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkSyncProcessor.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkSyncProcessor.java new file mode 100644 index 00000000..c93c6208 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkSyncProcessor.java @@ -0,0 +1,195 @@ +package com.zt.plat.module.system.service.integration.iwork; + +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrDepartmentPageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrJobTitlePageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSubcompanyPageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrUserPageRespVO; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Abstraction for applying iWork entities into local persistence. + */ +public interface IWorkSyncProcessor { + + BatchResult syncSubcompanies(List data, SyncOptions options); + + BatchResult syncDepartments(List data, SyncOptions options); + + BatchResult syncJobTitles(List data, SyncOptions options); + + BatchResult syncUsers(List data, SyncOptions options); + + /** + * Execution options shared by batch and single sync flows. + */ + final class SyncOptions { + private final boolean includeCanceled; + private final boolean allowUpdate; + private final boolean createIfMissing; + + private SyncOptions(boolean includeCanceled, boolean allowUpdate, boolean createIfMissing) { + this.includeCanceled = includeCanceled; + this.allowUpdate = allowUpdate; + this.createIfMissing = createIfMissing; + } + + public static SyncOptions full(boolean includeCanceled) { + return new SyncOptions(includeCanceled, false, true); + } + + public static SyncOptions single(boolean createIfMissing) { + return new SyncOptions(true, true, Boolean.TRUE.equals(createIfMissing)); + } + + public static SyncOptions custom(boolean includeCanceled, boolean allowUpdate, boolean createIfMissing) { + return new SyncOptions(includeCanceled, allowUpdate, createIfMissing); + } + + public boolean isIncludeCanceled() { + return includeCanceled; + } + + public boolean isAllowUpdate() { + return allowUpdate; + } + + public boolean isCreateIfMissing() { + return createIfMissing; + } + } + + /** + * Aggregated result for a sync batch. + */ + final class BatchResult { + private int pulled; + private int created; + private int skipped; + private int disabled; + private int failed; + private int updated; + private String message; + + public static BatchResult empty() { + return new BatchResult(); + } + + public BatchResult withMessage(String message) { + this.message = message; + return this; + } + + public BatchResult merge(BatchResult other) { + if (other == null) { + return this; + } + this.pulled += other.pulled; + this.created += other.created; + this.skipped += other.skipped; + this.disabled += other.disabled; + this.failed += other.failed; + this.updated += other.updated; + if (Objects.nonNull(other.message)) { + this.message = other.message; + } + return this; + } + + public static BatchResult fromSingle(BatchResult single) { + return empty().merge(single); + } + + public static BatchResult singleCreated(String message) { + BatchResult result = empty(); + result.created = 1; + result.message = message; + return result; + } + + public static BatchResult singleSkipped(String message) { + BatchResult result = empty(); + result.skipped = 1; + result.message = message; + return result; + } + + public static BatchResult singleFailed(String message) { + BatchResult result = empty(); + result.failed = 1; + result.message = message; + return result; + } + + public BatchResult increasePulled(int delta) { + this.pulled += delta; + return this; + } + + public void increaseCreated() { + this.created++; + } + + public void increaseSkipped() { + this.skipped++; + } + + public void increaseDisabled() { + this.disabled++; + } + + public void increaseFailed() { + this.failed++; + } + + public void increaseUpdated() { + this.updated++; + } + + public int getPulled() { + return pulled; + } + + public int getCreated() { + return created; + } + + public int getSkipped() { + return skipped; + } + + public int getDisabled() { + return disabled; + } + + public int getFailed() { + return failed; + } + + public int getUpdated() { + return updated; + } + + public String getMessage() { + return message; + } + } + + default BatchResult syncSubcompany(IWorkHrSubcompanyPageRespVO.Subcompany data, SyncOptions options) { + return syncSubcompanies(Collections.singletonList(data), options); + } + + default BatchResult syncDepartment(IWorkHrDepartmentPageRespVO.Department data, SyncOptions options) { + return syncDepartments(Collections.singletonList(data), options); + } + + default BatchResult syncJobTitle(IWorkHrJobTitlePageRespVO.JobTitle data, SyncOptions options) { + return syncJobTitles(Collections.singletonList(data), options); + } + + default BatchResult syncUser(IWorkHrUserPageRespVO.User data, SyncOptions options) { + return syncUsers(Collections.singletonList(data), options); + } +} 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 new file mode 100644 index 00000000..2c84ffa0 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkSyncService.java @@ -0,0 +1,22 @@ +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); + + /** + * 根据 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/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 dfd962e2..89133941 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 @@ -6,9 +6,13 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkDepartmentQueryReqVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrDepartmentPageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrJobTitlePageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSubcompanyPageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSyncRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrUserPageRespVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkJobTitleQueryReqVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOrgBaseQueryReqVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOrgRespVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOrgSyncReqVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkSubcompanyQueryReqVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkUserQueryReqVO; @@ -30,8 +34,10 @@ import java.nio.charset.StandardCharsets; import java.time.Clock; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import static com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationErrorCodeConstants.IWORK_BASE_URL_MISSING; import static com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationErrorCodeConstants.IWORK_ORG_IDENTIFIER_MISSING; @@ -44,8 +50,21 @@ import static com.zt.plat.module.system.service.integration.iwork.IWorkIntegrati @Service public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { - private static final TypeReference> MAP_TYPE = new TypeReference<>() { - }; + private static final TypeReference> SUBCOMPANY_LIST_TYPE = + new TypeReference<>() { + }; + private static final TypeReference> DEPARTMENT_LIST_TYPE = + new TypeReference<>() { + }; + private static final TypeReference> JOB_TITLE_LIST_TYPE = + new TypeReference<>() { + }; + private static final TypeReference> USER_LIST_TYPE = + new TypeReference<>() { + }; + private static final TypeReference> SYNC_RESULT_LIST_TYPE = + new TypeReference<>() { + }; private static final okhttp3.MediaType JSON_MEDIA_TYPE = okhttp3.MediaType.get("application/json; charset=UTF-8"); private final IWorkProperties properties; @@ -69,7 +88,7 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { } @Override - public IWorkOrgRespVO listSubcompanies(IWorkSubcompanyQueryReqVO reqVO) { + public IWorkHrSubcompanyPageRespVO listSubcompanies(IWorkSubcompanyQueryReqVO reqVO) { String path = orgPaths().getSubcompanyPage(); Map params = buildBaseParams(reqVO); if (StringUtils.hasText(reqVO.getSubcompanyCode())) { @@ -78,11 +97,12 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { if (StringUtils.hasText(reqVO.getSubcompanyName())) { params.put("subcompanyname", reqVO.getSubcompanyName()); } - return invokeParamsEndpoint(path, params); + JsonNode node = invokeParamsEndpoint(path, params); + return buildSubcompanyPageResp(node); } @Override - public IWorkOrgRespVO listDepartments(IWorkDepartmentQueryReqVO reqVO) { + public IWorkHrDepartmentPageRespVO listDepartments(IWorkDepartmentQueryReqVO reqVO) { String path = orgPaths().getDepartmentPage(); Map params = buildBaseParams(reqVO); if (StringUtils.hasText(reqVO.getDepartmentCode())) { @@ -94,11 +114,12 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { if (StringUtils.hasText(reqVO.getSubcompanyId())) { params.put("subcompanyid", reqVO.getSubcompanyId()); } - return invokeParamsEndpoint(path, params); + JsonNode node = invokeParamsEndpoint(path, params); + return buildDepartmentPageResp(node); } @Override - public IWorkOrgRespVO listJobTitles(IWorkJobTitleQueryReqVO reqVO) { + public IWorkHrJobTitlePageRespVO listJobTitles(IWorkJobTitleQueryReqVO reqVO) { String path = orgPaths().getJobTitlePage(); Map params = buildBaseParams(reqVO); if (StringUtils.hasText(reqVO.getJobTitleCode())) { @@ -107,11 +128,12 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { if (StringUtils.hasText(reqVO.getJobTitleName())) { params.put("jobtitlename", reqVO.getJobTitleName()); } - return invokeParamsEndpoint(path, params); + JsonNode node = invokeParamsEndpoint(path, params); + return buildJobTitlePageResp(node); } @Override - public IWorkOrgRespVO listUsers(IWorkUserQueryReqVO reqVO) { + public IWorkHrUserPageRespVO listUsers(IWorkUserQueryReqVO reqVO) { String path = orgPaths().getUserPage(); Map params = buildBaseParams(reqVO); if (StringUtils.hasText(reqVO.getWorkCode())) { @@ -138,7 +160,8 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { if (StringUtils.hasText(reqVO.getEmail())) { params.put("email", reqVO.getEmail()); } - return invokeParamsEndpoint(path, params); + JsonNode node = invokeParamsEndpoint(path, params); + return buildUserPageResp(node); } private Map buildBaseParams(IWorkOrgBaseQueryReqVO reqVO) { @@ -156,47 +179,51 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { } @Override - public IWorkOrgRespVO syncSubcompanies(IWorkOrgSyncReqVO reqVO) { + public IWorkHrSyncRespVO syncSubcompanies(IWorkOrgSyncReqVO reqVO) { String path = orgPaths().getSyncSubcompany(); - return invokeDataEndpoint(path, reqVO.getData()); + JsonNode node = invokeDataEndpoint(path, reqVO.getData()); + return buildSyncResp(node); } @Override - public IWorkOrgRespVO syncDepartments(IWorkOrgSyncReqVO reqVO) { + public IWorkHrSyncRespVO syncDepartments(IWorkOrgSyncReqVO reqVO) { String path = orgPaths().getSyncDepartment(); - return invokeDataEndpoint(path, reqVO.getData()); + JsonNode node = invokeDataEndpoint(path, reqVO.getData()); + return buildSyncResp(node); } @Override - public IWorkOrgRespVO syncJobTitles(IWorkOrgSyncReqVO reqVO) { + public IWorkHrSyncRespVO syncJobTitles(IWorkOrgSyncReqVO reqVO) { String path = orgPaths().getSyncJobTitle(); - return invokeDataEndpoint(path, reqVO.getData()); + JsonNode node = invokeDataEndpoint(path, reqVO.getData()); + return buildSyncResp(node); } @Override - public IWorkOrgRespVO syncUsers(IWorkOrgSyncReqVO reqVO) { + public IWorkHrSyncRespVO syncUsers(IWorkOrgSyncReqVO reqVO) { String path = orgPaths().getSyncUser(); - return invokeDataEndpoint(path, reqVO.getData()); + JsonNode node = invokeDataEndpoint(path, reqVO.getData()); + return buildSyncResp(node); } - private IWorkOrgRespVO invokeParamsEndpoint(String path, Map params) { + private JsonNode invokeParamsEndpoint(String path, Map params) { + Objects.requireNonNull(params, "查询参数不能为空"); Map payload = new HashMap<>(); - payload.put("params", params == null ? Collections.emptyMap() : params); + payload.put("params", params); return executeJson(path, payload); } - private IWorkOrgRespVO invokeDataEndpoint(String path, Object data) { + private JsonNode invokeDataEndpoint(String path, Object data) { + Objects.requireNonNull(data, "同步数据不能为空"); Map payload = new HashMap<>(); - payload.put("data", data == null ? Collections.emptyMap() : data); + payload.put("data", data); return executeJson(path, payload); } - private IWorkOrgRespVO executeJson(String path, Map payload) { + private JsonNode executeJson(String path, Map payload) { + // 统一封装请求体并发送 POST 调用 assertOrgConfigured(path); - Map body = new HashMap<>(); - if (payload != null && !payload.isEmpty()) { - body.putAll(payload); - } + Map body = new HashMap<>(payload); body.put("token", buildTokenPayload()); String jsonBody = toJson(body); @@ -214,7 +241,10 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { log.error("[iWork-Org] 调用 {} 失败,status={} body={}", path, response.code(), responseBody); throw ServiceExceptionUtil.exception(IWORK_ORG_REMOTE_FAILED, response.code(), responseBody); } - return buildResponse(responseBody); + if (!StringUtils.hasText(responseBody)) { + throw ServiceExceptionUtil.exception(IWORK_ORG_REMOTE_FAILED, "iWork 响应为空"); + } + return parseJson(responseBody); } catch (IOException ex) { log.error("[iWork-Org] 调用 {} 失败", path, ex); throw ServiceExceptionUtil.exception(IWORK_ORG_REMOTE_FAILED, ex.getMessage()); @@ -222,6 +252,7 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { } private Map buildTokenPayload() { + // 按 iWork 约定生成 key + ts 结构的鉴权信息 String tokenSeed = StringUtils.trimWhitespace(orgConfig().getTokenSeed()); if (!StringUtils.hasText(tokenSeed)) { throw ServiceExceptionUtil.exception(IWORK_ORG_IDENTIFIER_MISSING); @@ -242,20 +273,110 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { return hex.toUpperCase(Locale.ROOT); } - private IWorkOrgRespVO buildResponse(String responseBody) { - // 统一解析 iWork 响应,兼容 data 节点和扁平结构 - IWorkOrgRespVO respVO = new IWorkOrgRespVO(); - respVO.setRawBody(responseBody); - if (!StringUtils.hasText(responseBody)) { - respVO.setPayload(Collections.emptyMap()); + // 解析并封装分部分页响应 + private IWorkHrSubcompanyPageRespVO buildSubcompanyPageResp(JsonNode node) { + ParsedEnvelope envelope = parseEnvelope(node); + IWorkHrSubcompanyPageRespVO respVO = new IWorkHrSubcompanyPageRespVO(); + respVO.setCode(envelope.code()); + respVO.setMessage(envelope.message()); + respVO.setSuccess(envelope.success()); + JsonNode dataNode = envelope.success() ? readNode(envelope.root(), "data") : null; + if (dataNode == null) { + respVO.setTotalSize(0); + respVO.setTotalPage(0); + respVO.setPageSize(0); + respVO.setPageNumber(0); + respVO.setDataList(Collections.emptyList()); return respVO; } - JsonNode node = parseJson(responseBody); - respVO.setCode(textValue(node, "code")); - respVO.setMessage(resolveMessage(node)); - respVO.setSuccess(isSuccess(node)); - JsonNode payloadNode = node.has("data") ? node.get("data") : node; - respVO.setPayload(objectMapper.convertValue(payloadNode, MAP_TYPE)); + respVO.setTotalSize(readInt(dataNode, 0, "totalSize")); + respVO.setTotalPage(readInt(dataNode, 0, "totalPage", "totalPageCount")); + respVO.setPageSize(readInt(dataNode, 0, "pageSize", "pagesize")); + respVO.setPageNumber(readInt(dataNode, 0, "pageNumber", "page", "curpage")); + respVO.setDataList(readList(dataNode, "dataList", SUBCOMPANY_LIST_TYPE)); + return respVO; + } + + // 解析并封装部门分页响应 + private IWorkHrDepartmentPageRespVO buildDepartmentPageResp(JsonNode node) { + ParsedEnvelope envelope = parseEnvelope(node); + IWorkHrDepartmentPageRespVO respVO = new IWorkHrDepartmentPageRespVO(); + respVO.setCode(envelope.code()); + respVO.setMessage(envelope.message()); + respVO.setSuccess(envelope.success()); + JsonNode dataNode = envelope.success() ? readNode(envelope.root(), "data") : null; + if (dataNode == null) { + respVO.setTotalSize(0); + respVO.setTotalPage(0); + respVO.setPageSize(0); + respVO.setPageNumber(0); + respVO.setDataList(Collections.emptyList()); + return respVO; + } + respVO.setTotalSize(readInt(dataNode, 0, "totalSize")); + respVO.setTotalPage(readInt(dataNode, 0, "totalPage", "totalPageCount")); + respVO.setPageSize(readInt(dataNode, 0, "pageSize", "pagesize")); + respVO.setPageNumber(readInt(dataNode, 0, "pageNumber", "page", "curpage")); + respVO.setDataList(readList(dataNode, "dataList", DEPARTMENT_LIST_TYPE)); + return respVO; + } + + // 解析并封装岗位分页响应 + private IWorkHrJobTitlePageRespVO buildJobTitlePageResp(JsonNode node) { + ParsedEnvelope envelope = parseEnvelope(node); + IWorkHrJobTitlePageRespVO respVO = new IWorkHrJobTitlePageRespVO(); + respVO.setCode(envelope.code()); + respVO.setMessage(envelope.message()); + respVO.setSuccess(envelope.success()); + JsonNode dataNode = envelope.success() ? readNode(envelope.root(), "data") : null; + if (dataNode == null) { + respVO.setTotalSize(0); + respVO.setTotalPage(0); + respVO.setPageSize(0); + respVO.setPageNumber(0); + respVO.setDataList(Collections.emptyList()); + return respVO; + } + respVO.setTotalSize(readInt(dataNode, 0, "totalSize")); + respVO.setTotalPage(readInt(dataNode, 0, "totalPage", "totalPageCount")); + respVO.setPageSize(readInt(dataNode, 0, "pageSize", "pagesize")); + respVO.setPageNumber(readInt(dataNode, 0, "pageNumber", "page", "curpage")); + respVO.setDataList(readList(dataNode, "dataList", JOB_TITLE_LIST_TYPE)); + return respVO; + } + + // 解析并封装人员分页响应 + private IWorkHrUserPageRespVO buildUserPageResp(JsonNode node) { + ParsedEnvelope envelope = parseEnvelope(node); + IWorkHrUserPageRespVO respVO = new IWorkHrUserPageRespVO(); + respVO.setCode(envelope.code()); + respVO.setMessage(envelope.message()); + respVO.setSuccess(envelope.success()); + JsonNode dataNode = envelope.success() ? readNode(envelope.root(), "data") : null; + if (dataNode == null) { + respVO.setTotalSize(0); + respVO.setTotalPage(0); + respVO.setPageSize(0); + respVO.setPageNumber(0); + respVO.setDataList(Collections.emptyList()); + return respVO; + } + respVO.setTotalSize(readInt(dataNode, 0, "totalSize")); + respVO.setTotalPage(readInt(dataNode, 0, "totalPage", "totalPageCount")); + respVO.setPageSize(readInt(dataNode, 0, "pageSize", "pagesize")); + respVO.setPageNumber(readInt(dataNode, 0, "pageNumber", "page", "curpage")); + respVO.setDataList(readList(dataNode, "dataList", USER_LIST_TYPE)); + return respVO; + } + + // 解析并封装同步结果 + private IWorkHrSyncRespVO buildSyncResp(JsonNode node) { + ParsedEnvelope envelope = parseEnvelope(node); + IWorkHrSyncRespVO respVO = new IWorkHrSyncRespVO(); + respVO.setCode(envelope.code()); + respVO.setMessage(envelope.message()); + respVO.setSuccess(envelope.success()); + respVO.setResult(readList(envelope.root(), "result", SYNC_RESULT_LIST_TYPE)); return respVO; } @@ -268,34 +389,74 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { } } - private String resolveMessage(JsonNode node) { + private ParsedEnvelope parseEnvelope(JsonNode node) { + // 校验 iWork 顶层响应并抽取公共字段 if (node == null) { + throw ServiceExceptionUtil.exception(IWORK_ORG_REMOTE_FAILED, "iWork 响应缺失"); + } + String code = node.has("code") ? node.get("code").asText() : null; + if (!StringUtils.hasText(code)) { + throw ServiceExceptionUtil.exception(IWORK_ORG_REMOTE_FAILED, "iWork 响应缺少 code 字段"); + } + String message = null; + if (node.has("msg")) { + message = node.get("msg").asText(); + } else if (node.has("message")) { + message = node.get("message").asText(); + } + boolean success = "1".equals(code); + return new ParsedEnvelope(code, message, success, node); + } + + private JsonNode readNode(JsonNode parent, String field) { + if (parent == null || !parent.has(field)) { return null; } - if (node.has("msg")) { - return node.get("msg").asText(); - } - if (node.has("message")) { - return node.get("message").asText(); - } - return null; + JsonNode value = parent.get(field); + return value == null || value.isNull() ? null : value; } - private boolean isSuccess(JsonNode node) { - if (node == null) { - return false; + private int readInt(JsonNode parent, int defaultValue, String... fieldNames) { + if (parent == null || fieldNames == null) { + return defaultValue; } - if ("1".equals(textValue(node, "code"))) { - return true; + for (String field : fieldNames) { + if (!StringUtils.hasText(field)) { + continue; + } + JsonNode valueNode = readNode(parent, field); + if (valueNode == null) { + continue; + } + if (valueNode.isNumber()) { + return valueNode.intValue(); + } + if (valueNode.isTextual()) { + try { + return Integer.parseInt(valueNode.asText()); + } catch (NumberFormatException ex) { + log.warn("[iWork-Org] 字段格式非数值,使用默认值: {}", field); + return defaultValue; + } + } + log.warn("[iWork-Org] 字段类型非整数,使用默认值: {}", field); + return defaultValue; } - if ("1".equals(textValue(node, "status"))) { - return true; - } - return "1".equals(textValue(node, "success")); + return defaultValue; } - private String textValue(JsonNode node, String field) { - return node != null && node.has(field) ? node.get(field).asText() : null; + private List readList(JsonNode parent, String field, TypeReference> typeReference) { + JsonNode arrayNode = readNode(parent, field); + if (arrayNode == null) { + return Collections.emptyList(); + } + if (!arrayNode.isArray()) { + log.warn("[iWork-Org] 字段应为数组但实际不是,尝试转换: {}", field); + } + return objectMapper.convertValue(arrayNode, typeReference); + } + + private record ParsedEnvelope(String code, String message, boolean success, JsonNode root) { } private void assertOrgConfigured(String path) { @@ -318,7 +479,7 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { private String toJson(Object payload) { try { - return objectMapper.writeValueAsString(payload == null ? Collections.emptyMap() : payload); + return objectMapper.writeValueAsString(payload); } catch (JsonProcessingException ex) { throw ServiceExceptionUtil.exception(IWORK_ORG_REMOTE_FAILED, "序列化 JSON 失败: " + ex.getMessage()); } 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 new file mode 100644 index 00000000..1e75bf2f --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncProcessorImpl.java @@ -0,0 +1,775 @@ +package com.zt.plat.module.system.service.integration.iwork.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.zt.plat.framework.common.enums.CommonStatusEnum; +import com.zt.plat.framework.tenant.core.context.TenantContextHolder; +import com.zt.plat.module.system.controller.admin.dept.vo.depexternalcode.DeptExternalCodeSaveReqVO; +import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; +import com.zt.plat.module.system.controller.admin.dept.vo.post.PostSaveReqVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrDepartmentPageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrJobTitlePageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSubcompanyPageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrUserPageRespVO; +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.DeptExternalCodeDO; +import com.zt.plat.module.system.dal.dataobject.dept.PostDO; +import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO; +import com.zt.plat.module.system.dal.mysql.dept.PostMapper; +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; +import com.zt.plat.module.system.enums.user.UserSourceEnum; +import com.zt.plat.module.system.service.dept.DeptExternalCodeService; +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.util.sync.SyncVerifyUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Service +@RequiredArgsConstructor +public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { + + private static final String SYSTEM_CODE_SUBCOMPANY = "IWORK_SUBCOMPANY"; + private static final String SYSTEM_CODE_DEPARTMENT = "IWORK_DEPARTMENT"; + 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; + private final DeptExternalCodeService deptExternalCodeService; + private final PostService postService; + private final PostMapper postMapper; + private final AdminUserService adminUserService; + private final AdminUserMapper adminUserMapper; + + private final Map deptExternalCache = new ConcurrentHashMap<>(); + private final Map postCache = new ConcurrentHashMap<>(); + + @Override + public BatchResult syncSubcompanies(List data, SyncOptions options) { + List records = CollUtil.emptyIfNull(data); + BatchResult result = BatchResult.empty(); + if (records.isEmpty()) { + return result; + } + result.increasePulled(records.size()); + List queue = new ArrayList<>(records); + int guard = 0; + int maxPasses = Math.max(1, queue.size() * 2); + while (!queue.isEmpty() && guard++ < maxPasses) { + int processed = 0; + Iterator iterator = queue.iterator(); + while (iterator.hasNext()) { + IWorkHrSubcompanyPageRespVO.Subcompany sub = iterator.next(); + if (shouldSkipByCanceled(sub.getCanceled(), options)) { + result.increaseSkipped(); + iterator.remove(); + continue; + } + if (sub.getSubcompanyid1() == null) { + log.warn("[iWork] 分部缺少标识,跳过:{}", sub.getSubcompanyname()); + result.increaseFailed(); + iterator.remove(); + continue; + } + ParentHolder parentHolder = resolveSubcompanyParent(sub.getSupsubcomid()); + if (parentHolder.required() && parentHolder.parentId() == null) { + continue; + } + boolean canceled = isCanceledFlag(sub.getCanceled()); + DeptSaveReqVO saveReq = buildSubcompanySaveReq(sub, parentHolder.parentId(), canceled); + try { + DeptSyncOutcome outcome = upsertDept(saveReq, + SYSTEM_CODE_SUBCOMPANY, + sub.getSubcompanyid1().toString(), + sub.getSubcompanyname(), + canceled, + options); + applyDeptOutcome(result, outcome, "分部", sub.getSubcompanyname()); + } catch (Exception ex) { + log.error("[iWork] 同步分部失败: id={} name={}", sub.getSubcompanyid1(), sub.getSubcompanyname(), ex); + result.increaseFailed(); + result.withMessage("同步分部失败: " + ex.getMessage()); + } + iterator.remove(); + processed++; + } + if (processed == 0) { + break; + } + } + if (!queue.isEmpty()) { + for (IWorkHrSubcompanyPageRespVO.Subcompany remaining : queue) { + log.warn("[iWork] 分部因父级缺失未同步: id={} name={}", remaining.getSubcompanyid1(), remaining.getSubcompanyname()); + result.increaseFailed(); + } + } + return result; + } + + @Override + public BatchResult syncDepartments(List data, SyncOptions options) { + List records = CollUtil.emptyIfNull(data); + BatchResult result = BatchResult.empty(); + if (records.isEmpty()) { + return result; + } + result.increasePulled(records.size()); + List queue = new ArrayList<>(records); + int guard = 0; + int maxPasses = Math.max(1, queue.size() * 2); + while (!queue.isEmpty() && guard++ < maxPasses) { + int processed = 0; + Iterator iterator = queue.iterator(); + while (iterator.hasNext()) { + IWorkHrDepartmentPageRespVO.Department dept = iterator.next(); + if (shouldSkipByCanceled(dept.getCanceled(), options)) { + result.increaseSkipped(); + iterator.remove(); + continue; + } + if (dept.getDepartmentid() == null) { + log.warn("[iWork] 部门缺少标识,跳过:{}", dept.getDepartmentname()); + result.increaseFailed(); + iterator.remove(); + continue; + } + ParentHolder parentHolder = resolveDepartmentParent(dept); + if (parentHolder.required() && parentHolder.parentId() == null) { + continue; + } + boolean canceled = isCanceledFlag(dept.getCanceled()); + DeptSaveReqVO saveReq = buildDepartmentSaveReq(dept, parentHolder.parentId(), canceled); + try { + DeptSyncOutcome outcome = upsertDept(saveReq, + SYSTEM_CODE_DEPARTMENT, + dept.getDepartmentid().toString(), + dept.getDepartmentname(), + canceled, + options); + applyDeptOutcome(result, outcome, "部门", dept.getDepartmentname()); + } catch (Exception ex) { + log.error("[iWork] 同步部门失败: id={} name={}", dept.getDepartmentid(), dept.getDepartmentname(), ex); + result.increaseFailed(); + result.withMessage("同步部门失败: " + ex.getMessage()); + } + iterator.remove(); + processed++; + } + if (processed == 0) { + break; + } + } + if (!queue.isEmpty()) { + for (IWorkHrDepartmentPageRespVO.Department remaining : queue) { + log.warn("[iWork] 部门因父级缺失未同步: id={} name={}", remaining.getDepartmentid(), remaining.getDepartmentname()); + result.increaseFailed(); + } + } + return result; + } + + @Override + public BatchResult syncJobTitles(List data, SyncOptions options) { + List records = CollUtil.emptyIfNull(data); + BatchResult result = BatchResult.empty(); + if (records.isEmpty()) { + return result; + } + result.increasePulled(records.size()); + for (IWorkHrJobTitlePageRespVO.JobTitle job : records) { + if (job == null) { + continue; + } + if (shouldSkipByCanceled(job.getCanceled(), options)) { + result.increaseSkipped(); + continue; + } + if (job.getJobtitleid() == null) { + log.warn("[iWork] 岗位缺少标识,跳过:{}", job.getJobtitlename()); + result.increaseFailed(); + continue; + } + boolean canceled = isCanceledFlag(job.getCanceled()); + Integer status = toStatus(canceled); + String code = buildJobCode(job.getJobtitleid()); + String name = limitLength(StrUtil.blankToDefault(job.getJobtitlename(), code), 50); + try { + JobSyncOutcome outcome = upsertJobTitle(job, code, name, status, options); + applyJobOutcome(result, outcome, name); + } catch (Exception ex) { + log.error("[iWork] 同步岗位失败: id={} name={}", job.getJobtitleid(), job.getJobtitlename(), ex); + result.increaseFailed(); + result.withMessage("同步岗位失败: " + ex.getMessage()); + } + } + return result; + } + + @Override + public BatchResult syncUsers(List data, SyncOptions options) { + List records = CollUtil.emptyIfNull(data); + BatchResult result = BatchResult.empty(); + if (records.isEmpty()) { + return result; + } + result.increasePulled(records.size()); + for (IWorkHrUserPageRespVO.User user : records) { + if (user == null) { + continue; + } + boolean inactive = isInactiveUser(user.getStatus()); + if (inactive && !options.isIncludeCanceled()) { + result.increaseSkipped(); + continue; + } + String username = resolveUsername(user); + if (StrUtil.isBlank(username)) { + log.warn("[iWork] 人员缺少可用账号,跳过:id={} name={}", user.getId(), user.getLastname()); + result.increaseFailed(); + continue; + } + try { + Long deptId = resolveUserDeptId(user); + if (deptId == null) { + log.warn("[iWork] 人员未找到匹配部门,跳过:id={} name={} deptId={} subcompany={}", + user.getId(), user.getLastname(), user.getDepartmentid(), user.getSubcompanyid1()); + result.increaseFailed(); + continue; + } + Long postId = resolveUserPostId(user); + CommonStatusEnum status = inactive ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE; + AdminUserDO existing = adminUserMapper.selectByUsername(username); + UserSyncOutcome outcome; + if (existing == null) { + if (!options.isCreateIfMissing()) { + result.increaseSkipped(); + continue; + } + outcome = createUser(user, username, deptId, postId, status); + } else { + if (!Objects.equals(existing.getUserSource(), UserSourceEnum.IWORK.getSource())) { + result.increaseSkipped(); + continue; + } + if (!options.isAllowUpdate()) { + result.increaseSkipped(); + continue; + } + outcome = updateUser(existing, user, username, deptId, postId, status); + } + applyUserOutcome(result, outcome, user.getLastname(), username); + } catch (Exception ex) { + log.error("[iWork] 同步人员失败: id={} name={}", user.getId(), user.getLastname(), ex); + result.increaseFailed(); + result.withMessage("同步人员失败: " + ex.getMessage()); + } + } + return result; + } + + private DeptSyncOutcome upsertDept(DeptSaveReqVO desired, + String systemCode, + String externalCode, + String externalName, + boolean disabled, + SyncOptions options) { + DeptExternalCodeDO mapping = getDeptMapping(systemCode, externalCode); + if (mapping == null) { + if (!options.isCreateIfMissing()) { + return new DeptSyncOutcome(SyncAction.SKIPPED, false, null); + } + Long deptId = deptService.createDept(desired); + persistDeptExternalCode(null, deptId, systemCode, externalCode, externalName, desired.getStatus()); + return new DeptSyncOutcome(SyncAction.CREATED, CommonStatusEnum.isDisable(desired.getStatus()), deptId); + } + Long deptId = mapping.getDeptId(); + DeptDO existing = deptService.getDept(deptId); + if (existing == null) { + deptExternalCache.remove(buildDeptCacheKey(systemCode, externalCode)); + if (!options.isCreateIfMissing()) { + return new DeptSyncOutcome(SyncAction.SKIPPED, false, null); + } + Long recreatedId = deptService.createDept(desired); + persistDeptExternalCode(mapping.getId(), recreatedId, systemCode, externalCode, externalName, desired.getStatus()); + return new DeptSyncOutcome(SyncAction.CREATED, CommonStatusEnum.isDisable(desired.getStatus()), recreatedId); + } + if (!Objects.equals(existing.getDeptSource(), DeptSourceEnum.IWORK.getSource())) { + return new DeptSyncOutcome(SyncAction.SKIPPED, false, existing.getId()); + } + if (!options.isAllowUpdate()) { + return new DeptSyncOutcome(SyncAction.SKIPPED, false, existing.getId()); + } + desired.setId(existing.getId()); + mergeDeptDefaults(desired, existing); + boolean disabledChanged = CommonStatusEnum.isDisable(desired.getStatus()) && CommonStatusEnum.isEnable(existing.getStatus()); + deptService.updateDept(desired); + persistDeptExternalCode(mapping.getId(), existing.getId(), systemCode, externalCode, externalName, desired.getStatus()); + return new DeptSyncOutcome(SyncAction.UPDATED, disabledChanged, existing.getId()); + } + + private JobSyncOutcome upsertJobTitle(IWorkHrJobTitlePageRespVO.JobTitle job, + String code, + String name, + Integer status, + SyncOptions options) { + PostDO existing = resolvePostByCode(code); + boolean disabled = CommonStatusEnum.isDisable(status); + if (existing == null) { + if (!options.isCreateIfMissing()) { + return new JobSyncOutcome(SyncAction.SKIPPED, false, null); + } + PostSaveReqVO createReq = new PostSaveReqVO(); + createReq.setCode(code); + createReq.setName(name); + createReq.setSort(defaultSort(job.getShoworder())); + createReq.setStatus(status); + createReq.setRemark(StrUtil.blankToDefault(job.getJobfunction(), job.getDescription())); + Long postId = postService.createPost(createReq); + PostDO created = new PostDO(); + created.setId(postId); + created.setCode(code); + created.setName(createReq.getName()); + created.setSort(createReq.getSort()); + created.setStatus(status); + created.setRemark(createReq.getRemark()); + postCache.put(buildPostCacheKey(code), created); + return new JobSyncOutcome(SyncAction.CREATED, disabled, postId); + } + if (!options.isAllowUpdate()) { + return new JobSyncOutcome(SyncAction.SKIPPED, false, existing.getId()); + } + PostSaveReqVO updateReq = new PostSaveReqVO(); + updateReq.setId(existing.getId()); + updateReq.setCode(code); + updateReq.setName(name); + updateReq.setSort(defaultSort(job.getShoworder(), existing.getSort())); + updateReq.setStatus(status); + updateReq.setRemark(StrUtil.blankToDefault(job.getJobfunction(), job.getDescription())); + boolean disabledChanged = disabled && CommonStatusEnum.isEnable(existing.getStatus()); + postService.updatePost(updateReq); + existing.setName(updateReq.getName()); + existing.setSort(updateReq.getSort()); + existing.setStatus(status); + existing.setRemark(updateReq.getRemark()); + postCache.put(buildPostCacheKey(code), existing); + return new JobSyncOutcome(SyncAction.UPDATED, disabledChanged, existing.getId()); + } + + private UserSyncOutcome createUser(IWorkHrUserPageRespVO.User source, + String username, + Long deptId, + Long postId, + CommonStatusEnum status) { + UserSaveReqVO req = buildUserSaveReq(source, username, deptId, postId, status); + req.setPassword(DEFAULT_USER_PASSWORD); + req.setUserSource(UserSourceEnum.IWORK.getSource()); + Long userId = adminUserService.createUser(req); + return new UserSyncOutcome(SyncAction.CREATED, CommonStatusEnum.isDisable(req.getStatus()), userId); + } + + private UserSyncOutcome updateUser(AdminUserDO existing, + IWorkHrUserPageRespVO.User source, + String username, + Long deptId, + Long postId, + CommonStatusEnum status) { + 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); + return new UserSyncOutcome(SyncAction.UPDATED, disabledChanged, existing.getId()); + } + + private DeptSaveReqVO buildSubcompanySaveReq(IWorkHrSubcompanyPageRespVO.Subcompany data, + Long parentId, + boolean canceled) { + DeptSaveReqVO req = new DeptSaveReqVO(); + req.setName(limitLength(StrUtil.blankToDefault(data.getSubcompanyname(), "未命名分部"), 30)); + req.setShortName(limitLength(data.getSubcompanyname(), 20)); + req.setCode(trimToNull(data.getSubcompanycode())); + req.setParentId(parentId == null ? DeptDO.PARENT_ID_ROOT : parentId); + req.setSort(defaultSort(data.getShoworder())); + req.setStatus(toStatus(canceled)); + req.setIsCompany(Boolean.TRUE); + req.setIsGroup(Boolean.FALSE); + req.setDeptSource(DeptSourceEnum.IWORK.getSource()); + return req; + } + + private DeptSaveReqVO buildDepartmentSaveReq(IWorkHrDepartmentPageRespVO.Department data, + Long parentId, + boolean canceled) { + DeptSaveReqVO req = new DeptSaveReqVO(); + req.setName(limitLength(StrUtil.blankToDefault(data.getDepartmentname(), "未命名部门"), 30)); + req.setShortName(limitLength(StrUtil.blankToDefault(data.getDepartmentmark(), data.getDepartmentname()), 20)); + req.setCode(trimToNull(data.getDepartmentcode())); + req.setParentId(parentId == null ? DeptDO.PARENT_ID_ROOT : parentId); + req.setSort(defaultSort(data.getShoworder())); + req.setStatus(toStatus(canceled)); + req.setIsCompany(Boolean.FALSE); + req.setIsGroup(Boolean.FALSE); + req.setDeptSource(DeptSourceEnum.IWORK.getSource()); + return req; + } + + private UserSaveReqVO buildUserSaveReq(IWorkHrUserPageRespVO.User source, + String username, + Long deptId, + Long postId, + CommonStatusEnum status) { + UserSaveReqVO req = new UserSaveReqVO(); + req.setUsername(username); + req.setNickname(limitLength(StrUtil.blankToDefault(source.getLastname(), username), 30)); + req.setRemark(buildUserRemark(source)); + req.setDeptIds(singletonSet(deptId)); + if (postId != null) { + req.setPostIds(singletonSet(postId)); + } + req.setEmail(trimToNull(source.getEmail())); + req.setMobile(trimToNull(source.getMobile())); + req.setSex(resolveSex(source.getSex())); + req.setStatus(status.getStatus()); + return req; + } + + private void mergeDeptDefaults(DeptSaveReqVO target, DeptDO existing) { + target.setCode(StrUtil.blankToDefault(target.getCode(), existing.getCode())); + target.setShortName(StrUtil.blankToDefault(target.getShortName(), existing.getShortName())); + target.setParentId(target.getParentId() == null ? existing.getParentId() : target.getParentId()); + target.setSort(target.getSort() == null ? existing.getSort() : target.getSort()); + target.setStatus(target.getStatus() == null ? existing.getStatus() : target.getStatus()); + target.setLeaderUserId(existing.getLeaderUserId()); + target.setPhone(existing.getPhone()); + target.setEmail(existing.getEmail()); + target.setTenantId(existing.getTenantId()); + target.setIsCompany(target.getIsCompany() == null ? existing.getIsCompany() : target.getIsCompany()); + target.setIsGroup(target.getIsGroup() == null ? existing.getIsGroup() : target.getIsGroup()); + target.setDeptSource(DeptSourceEnum.IWORK.getSource()); + } + + private ParentHolder resolveSubcompanyParent(Integer parentExternalId) { + if (parentExternalId == null || parentExternalId <= 0) { + return new ParentHolder(DeptDO.PARENT_ID_ROOT, false); + } + Long parentId = resolveDeptId(SYSTEM_CODE_SUBCOMPANY, parentExternalId.toString()); + return new ParentHolder(parentId, true); + } + + private ParentHolder resolveDepartmentParent(IWorkHrDepartmentPageRespVO.Department dept) { + Integer parentDeptId = dept.getParentdeptid(); + if (parentDeptId != null && parentDeptId > 0) { + Long parentId = resolveDeptId(SYSTEM_CODE_DEPARTMENT, parentDeptId.toString()); + return new ParentHolder(parentId, true); + } + Integer subcompanyId = dept.getSubcompanyid1(); + if (subcompanyId != null && subcompanyId > 0) { + Long parentId = resolveDeptId(SYSTEM_CODE_SUBCOMPANY, subcompanyId.toString()); + return new ParentHolder(parentId, true); + } + return new ParentHolder(DeptDO.PARENT_ID_ROOT, false); + } + + private DeptExternalCodeDO getDeptMapping(String systemCode, String externalCode) { + if (StrUtil.isBlank(externalCode)) { + return null; + } + String key = buildDeptCacheKey(systemCode, externalCode); + DeptExternalCodeDO cached = deptExternalCache.get(key); + if (cached != null) { + return cached; + } + DeptExternalCodeDO mapping = deptExternalCodeService.getBySystemCodeAndExternalCode(systemCode, externalCode); + if (mapping != null) { + deptExternalCache.put(key, mapping); + } + return mapping; + } + + private void persistDeptExternalCode(Long mappingId, + Long deptId, + String systemCode, + String externalCode, + String externalName, + Integer status) { + DeptExternalCodeSaveReqVO req = new DeptExternalCodeSaveReqVO(); + req.setDeptId(deptId); + req.setSystemCode(systemCode); + req.setExternalDeptCode(externalCode); + req.setExternalDeptName(StrUtil.blankToDefault(externalName, externalCode)); + req.setStatus(status); + req.setRemark("iWork 同步"); + DeptExternalCodeDO mapping; + if (mappingId == null) { + Long id = deptExternalCodeService.createDeptExternalCode(req); + mapping = buildMapping(id, deptId, req); + } else { + req.setId(mappingId); + deptExternalCodeService.updateDeptExternalCode(req); + mapping = buildMapping(mappingId, deptId, req); + } + deptExternalCache.put(buildDeptCacheKey(systemCode, externalCode), mapping); + } + + private DeptExternalCodeDO buildMapping(Long id, Long deptId, DeptExternalCodeSaveReqVO req) { + DeptExternalCodeDO mapping = new DeptExternalCodeDO(); + mapping.setId(id); + mapping.setDeptId(deptId); + mapping.setSystemCode(req.getSystemCode()); + mapping.setExternalDeptCode(req.getExternalDeptCode()); + mapping.setExternalDeptName(req.getExternalDeptName()); + mapping.setStatus(req.getStatus()); + mapping.setRemark(req.getRemark()); + return mapping; + } + + private Long resolveDeptId(String systemCode, String externalCode) { + DeptExternalCodeDO mapping = getDeptMapping(systemCode, externalCode); + return mapping == null ? null : mapping.getDeptId(); + } + + private PostDO resolvePostByCode(String code) { + String key = buildPostCacheKey(code); + PostDO cached = postCache.get(key); + if (cached != null) { + return cached; + } + PostDO post = postMapper.selectByCode(code); + if (post != null) { + postCache.put(key, post); + } + return post; + } + + private Long resolveUserDeptId(IWorkHrUserPageRespVO.User user) { + if (user.getDepartmentid() != null) { + Long deptId = resolveDeptId(SYSTEM_CODE_DEPARTMENT, user.getDepartmentid().toString()); + if (deptId != null) { + return deptId; + } + } + if (user.getSubcompanyid1() != null) { + Long deptId = resolveDeptId(SYSTEM_CODE_SUBCOMPANY, user.getSubcompanyid1().toString()); + if (deptId != null) { + return deptId; + } + } + return null; + } + + private Long resolveUserPostId(IWorkHrUserPageRespVO.User user) { + if (user.getJobtitleid() == null) { + return null; + } + String code = buildJobCode(user.getJobtitleid()); + PostDO post = resolvePostByCode(code); + if (post != null) { + return post.getId(); + } + if (StrUtil.isNotBlank(user.getJobtitlename())) { + return postService.getOrCreatePostByName(user.getJobtitlename().trim()); + } + return null; + } + + private void applyDeptOutcome(BatchResult result, DeptSyncOutcome outcome, String entityLabel, String name) { + if (outcome == null) { + return; + } + incrementByAction(result, outcome.action()); + if (outcome.disabled()) { + result.increaseDisabled(); + } + result.withMessage(StrUtil.format("{}[{}]{}", entityLabel, StrUtil.blankToDefault(name, "-"), describeAction(outcome.action()))); + } + + private void applyJobOutcome(BatchResult result, JobSyncOutcome outcome, String name) { + if (outcome == null) { + return; + } + incrementByAction(result, outcome.action()); + if (outcome.disabled()) { + result.increaseDisabled(); + } + result.withMessage(StrUtil.format("岗位[{}]{}", StrUtil.blankToDefault(name, "-"), describeAction(outcome.action()))); + } + + private void applyUserOutcome(BatchResult result, UserSyncOutcome outcome, String displayName, String username) { + if (outcome == null) { + return; + } + incrementByAction(result, outcome.action()); + if (outcome.disabled()) { + result.increaseDisabled(); + } + result.withMessage(StrUtil.format("人员[{}]{}", + StrUtil.blankToDefault(displayName, username), describeAction(outcome.action()))); + } + + private void incrementByAction(BatchResult result, SyncAction action) { + if (action == null) { + return; + } + switch (action) { + case CREATED -> result.increaseCreated(); + case UPDATED -> result.increaseUpdated(); + case SKIPPED -> result.increaseSkipped(); + } + } + + private boolean shouldSkipByCanceled(String canceled, SyncOptions options) { + return isCanceledFlag(canceled) && !options.isIncludeCanceled(); + } + + private boolean isCanceledFlag(String flag) { + if (StrUtil.isBlank(flag)) { + return false; + } + String normalized = flag.trim(); + return "1".equals(normalized) || "true".equalsIgnoreCase(normalized) || "yes".equalsIgnoreCase(normalized); + } + + private boolean isInactiveUser(String statusFlag) { + if (StrUtil.isBlank(statusFlag)) { + return false; + } + return !"0".equals(statusFlag.trim()); + } + + private Integer resolveSex(String sexFlag) { + Integer external = parseInteger(sexFlag); + Integer converted = SyncVerifyUtil.convertExternalToInternal(external); + return converted != null ? converted : SexEnum.UNKNOWN.getSex(); + } + + private Integer parseInteger(String raw) { + if (StrUtil.isBlank(raw)) { + return null; + } + try { + return Integer.parseInt(raw.trim()); + } catch (NumberFormatException ex) { + return null; + } + } + + private String resolveUsername(IWorkHrUserPageRespVO.User user) { + String candidate = sanitizeUsername(user.getLoginid()); + if (candidate == null) { + candidate = sanitizeUsername(user.getWorkcode()); + } + if (candidate == null && user.getId() != null) { + candidate = sanitizeUsername("IWORK" + user.getId()); + } + return candidate; + } + + private String sanitizeUsername(String raw) { + if (StrUtil.isBlank(raw)) { + return null; + } + String normalized = raw.replaceAll("[^A-Za-z0-9]", ""); + if (StrUtil.isBlank(normalized)) { + return null; + } + return normalized.length() > 30 ? normalized.substring(0, 30) : normalized; + } + + private Set singletonSet(Long value) { + Set set = new HashSet<>(1); + set.add(value); + return set; + } + + private Integer defaultSort(Integer value) { + return value == null ? DEFAULT_SORT : value; + } + + private Integer defaultSort(Integer value, Integer fallback) { + return value == null ? (fallback == null ? DEFAULT_SORT : fallback) : value; + } + + private Integer toStatus(boolean disabled) { + return disabled ? CommonStatusEnum.DISABLE.getStatus() : CommonStatusEnum.ENABLE.getStatus(); + } + + private String limitLength(String value, int maxLength) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.length() > maxLength ? trimmed.substring(0, maxLength) : trimmed; + } + + private String trimToNull(String value) { + if (StrUtil.isBlank(value)) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + private String buildUserRemark(IWorkHrUserPageRespVO.User user) { + if (user.getId() == null) { + return "iWork 同步"; + } + return StrUtil.format("iWork 同步,源ID={}", user.getId()); + } + + private String buildJobCode(Integer jobTitleId) { + return JOB_CODE_PREFIX + jobTitleId; + } + + private String buildDeptCacheKey(String systemCode, String externalCode) { + Long tenantId = TenantContextHolder.getTenantId(); + return (tenantId == null ? "_" : tenantId.toString()) + "::" + systemCode + "::" + externalCode; + } + + private String buildPostCacheKey(String code) { + Long tenantId = TenantContextHolder.getTenantId(); + return (tenantId == null ? "_" : tenantId.toString()) + "::POST::" + code; + } + + private String describeAction(SyncAction action) { + return switch (action) { + case CREATED -> "已创建"; + case UPDATED -> "已更新"; + case SKIPPED -> "已跳过"; + }; + } + + private record ParentHolder(Long parentId, boolean required) { + } + + private enum SyncAction { + CREATED, + UPDATED, + SKIPPED + } + + private record DeptSyncOutcome(SyncAction action, boolean disabled, Long deptId) { + } + + private record JobSyncOutcome(SyncAction action, boolean disabled, Long postId) { + } + + private record UserSyncOutcome(SyncAction action, boolean disabled, Long userId) { + } +} 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 new file mode 100644 index 00000000..fbc21151 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncServiceImpl.java @@ -0,0 +1,286 @@ +package com.zt.plat.module.system.service.integration.iwork.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkDepartmentQueryReqVO; +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.IWorkHrDepartmentPageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrJobTitlePageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSubcompanyPageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrUserPageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkJobTitleQueryReqVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkSingleSyncReqVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkSingleSyncRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkSubcompanyQueryReqVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkSyncBatchStatVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkSyncEntityStatVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkUserQueryReqVO; +import com.zt.plat.module.system.enums.integration.IWorkSyncEntityTypeEnum; +import com.zt.plat.module.system.service.integration.iwork.IWorkOrgRestService; +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.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * iWork 同步服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class IWorkSyncServiceImpl implements IWorkSyncService { + + private final IWorkOrgRestService orgRestService; + private final IWorkSyncProcessor syncProcessor; + + @Override + public IWorkFullSyncRespVO fullSync(IWorkFullSyncReqVO reqVO) { + IWorkFullSyncRespVO respVO = new IWorkFullSyncRespVO(); + respVO.setPageSize(reqVO.getPageSize()); + List batchStats = new ArrayList<>(); + respVO.setBatches(batchStats); + + Set scopes = reqVO.resolveScopes(); + int processedPages = 0; + IWorkSyncProcessor.SyncOptions options = buildFullSyncOptions(reqVO); + if (scopes.contains(IWorkSyncEntityTypeEnum.SUBCOMPANY)) { + processedPages += executeSubcompanyFullSync(reqVO, options, respVO.getSubcompanyStat(), batchStats); + } + if (scopes.contains(IWorkSyncEntityTypeEnum.DEPARTMENT)) { + processedPages += executeDepartmentFullSync(reqVO, options, respVO.getDepartmentStat(), batchStats); + } + if (scopes.contains(IWorkSyncEntityTypeEnum.JOB_TITLE)) { + processedPages += executeJobTitleFullSync(reqVO, options, respVO.getJobTitleStat(), batchStats); + } + if (scopes.contains(IWorkSyncEntityTypeEnum.USER)) { + processedPages += executeUserFullSync(reqVO, options, respVO.getUserStat(), batchStats); + } + respVO.setProcessedPages(processedPages); + 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, + List batches) { + return executePaged(reqVO, IWorkSyncEntityTypeEnum.SUBCOMPANY, batches, (page, pageSize) -> { + IWorkSubcompanyQueryReqVO query = new IWorkSubcompanyQueryReqVO(); + query.setCurpage(page); + query.setPagesize(pageSize); + IWorkHrSubcompanyPageRespVO pageResp = orgRestService.listSubcompanies(query); + List dataList = CollUtil.emptyIfNull(pageResp.getDataList()); + IWorkSyncProcessor.BatchResult result = syncProcessor.syncSubcompanies(dataList, options); + updateStat(stat, result, dataList.size()); + return new BatchExecution(result, dataList.size()); + }); + } + + private int executeDepartmentFullSync(IWorkFullSyncReqVO reqVO, + IWorkSyncProcessor.SyncOptions options, + IWorkSyncEntityStatVO stat, + List batches) { + return executePaged(reqVO, IWorkSyncEntityTypeEnum.DEPARTMENT, batches, (page, pageSize) -> { + IWorkDepartmentQueryReqVO query = new IWorkDepartmentQueryReqVO(); + query.setCurpage(page); + query.setPagesize(pageSize); + IWorkHrDepartmentPageRespVO pageResp = orgRestService.listDepartments(query); + List dataList = CollUtil.emptyIfNull(pageResp.getDataList()); + IWorkSyncProcessor.BatchResult result = syncProcessor.syncDepartments(dataList, options); + updateStat(stat, result, dataList.size()); + return new BatchExecution(result, dataList.size()); + }); + } + + private int executeJobTitleFullSync(IWorkFullSyncReqVO reqVO, + IWorkSyncProcessor.SyncOptions options, + IWorkSyncEntityStatVO stat, + List batches) { + return executePaged(reqVO, IWorkSyncEntityTypeEnum.JOB_TITLE, batches, (page, pageSize) -> { + IWorkJobTitleQueryReqVO query = new IWorkJobTitleQueryReqVO(); + query.setCurpage(page); + query.setPagesize(pageSize); + IWorkHrJobTitlePageRespVO pageResp = orgRestService.listJobTitles(query); + List dataList = CollUtil.emptyIfNull(pageResp.getDataList()); + IWorkSyncProcessor.BatchResult result = syncProcessor.syncJobTitles(dataList, options); + updateStat(stat, result, dataList.size()); + return new BatchExecution(result, dataList.size()); + }); + } + + private int executeUserFullSync(IWorkFullSyncReqVO reqVO, + IWorkSyncProcessor.SyncOptions options, + IWorkSyncEntityStatVO stat, + List batches) { + return executePaged(reqVO, IWorkSyncEntityTypeEnum.USER, batches, (page, pageSize) -> { + IWorkUserQueryReqVO query = new IWorkUserQueryReqVO(); + query.setCurpage(page); + query.setPagesize(pageSize); + IWorkHrUserPageRespVO pageResp = orgRestService.listUsers(query); + List dataList = CollUtil.emptyIfNull(pageResp.getDataList()); + IWorkSyncProcessor.BatchResult result = syncProcessor.syncUsers(dataList, options); + updateStat(stat, result, dataList.size()); + return new BatchExecution(result, dataList.size()); + }); + } + + 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()); + 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, + PageExecutor executor) { + int startPage = reqVO.getStartPage() == null ? 1 : reqVO.getStartPage(); + int pageSize = reqVO.getPageSize() == null ? 100 : reqVO.getPageSize(); + int pagesLimit = reqVO.getMaxPages() == null ? Integer.MAX_VALUE : reqVO.getMaxPages(); + int processedPages = 0; + for (int page = startPage; processedPages < pagesLimit; page++) { + BatchExecution execution = executor.execute(page, pageSize); + if (execution == null || execution.totalPulled == 0) { + break; + } + processedPages++; + IWorkSyncBatchStatVO batchStat = new IWorkSyncBatchStatVO(); + batchStat.setEntityType(type); + batchStat.setPageNumber(page); + batchStat.setPulled(execution.totalPulled); + batchStat.setCreated(execution.result.getCreated()); + batchStat.setSkippedExisting(execution.result.getSkipped()); + batchStat.setDisabled(execution.result.getDisabled()); + batchStat.setFailed(execution.result.getFailed()); + batches.add(batchStat); + } + return processedPages; + } + + private void updateStat(IWorkSyncEntityStatVO stat, IWorkSyncProcessor.BatchResult result, int pulled) { + stat.incrementPulled(pulled); + stat.incrementCreated(result.getCreated()); + stat.incrementSkipped(result.getSkipped()); + stat.incrementDisabled(result.getDisabled()); + 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); + 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); + 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); + return CollUtil.getFirst(pageResp.getDataList()); + } + + private IWorkHrUserPageRespVO.User fetchSingleUser(Long entityId) { + IWorkUserQueryReqVO query = new IWorkUserQueryReqVO(); + query.setCurpage(1); + query.setPagesize(1); + query.setParams(Collections.singletonMap("id", entityId)); + IWorkHrUserPageRespVO pageResp = orgRestService.listUsers(query); + return CollUtil.getFirst(pageResp.getDataList()); + } + + @FunctionalInterface + private interface PageExecutor { + BatchExecution execute(int pageNumber, int pageSize); + } + + private record BatchExecution(IWorkSyncProcessor.BatchResult result, int totalPulled) { + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/jackson/LenientIntegerDeserializer.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/jackson/LenientIntegerDeserializer.java new file mode 100644 index 00000000..8258e4d9 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/jackson/LenientIntegerDeserializer.java @@ -0,0 +1,54 @@ +package com.zt.plat.module.system.service.integration.iwork.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.math.BigDecimal; + +/** + * Jackson 反序列化器,允许将诸如 "0.0"、"5" 等字符串形式的数字解析为整数, + * 用于兼容 iWork 返回的非标准整型字段。 + */ +public class LenientIntegerDeserializer extends JsonDeserializer { + + @Override + public Integer deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException { + JsonToken token = parser.currentToken(); + if (token == null) { + token = parser.nextToken(); + } + if (token == null) { + return null; + } + if (token.isNumeric()) { + return parser.getNumberValue().intValue(); + } + if (token == JsonToken.VALUE_STRING) { + String text = parser.getText(); + if (text == null) { + return null; + } + String trimmed = text.trim(); + if (trimmed.isEmpty()) { + return null; + } + try { + if (trimmed.contains(".")) { + BigDecimal decimal = new BigDecimal(trimmed); + return decimal.intValue(); + } + return Integer.parseInt(trimmed); + } catch (NumberFormatException ex) { + return (Integer) ctxt.handleWeirdStringValue(Integer.class, trimmed, + "无法将文本转换为整数"); + } + } + if (token == JsonToken.VALUE_NULL) { + return null; + } + return (Integer) ctxt.handleUnexpectedToken(Integer.class, parser); + } +} diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkOrgRestServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkOrgRestServiceImplTest.java index e857117f..f0c96f6d 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkOrgRestServiceImplTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkOrgRestServiceImplTest.java @@ -1,9 +1,11 @@ package com.zt.plat.module.system.service.integration.iwork.impl; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOrgQueryReqVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOrgRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSubcompanyPageRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSyncRespVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOrgSyncReqVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkSubcompanyQueryReqVO; import com.zt.plat.module.system.framework.integration.iwork.config.IWorkProperties; import com.zt.plat.module.system.service.integration.iwork.IWorkOrgRestService; import okhttp3.mockwebserver.MockResponse; @@ -20,7 +22,6 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.time.ZoneOffset; -import java.util.Arrays; import java.util.List; import java.util.Map; @@ -32,6 +33,7 @@ class IWorkOrgRestServiceImplTest { private IWorkOrgRestService service; private IWorkProperties properties; private Clock fixedClock; + private ObjectMapper objectMapper; @BeforeEach void setUp() throws Exception { @@ -40,7 +42,8 @@ class IWorkOrgRestServiceImplTest { properties = buildProperties(); fixedClock = Clock.fixed(Instant.ofEpochMilli(1_672_531_200_000L), ZoneOffset.UTC); - service = new IWorkOrgRestServiceImpl(properties, new ObjectMapper(), fixedClock); + objectMapper = new ObjectMapper(); + service = new IWorkOrgRestServiceImpl(properties, objectMapper, fixedClock); } @AfterEach @@ -50,41 +53,46 @@ class IWorkOrgRestServiceImplTest { @Test void shouldListSubcompanies() throws Exception { - mockWebServer.enqueue(jsonResponse("{\"code\":\"1\",\"data\":{\"page\":1}}")); + mockWebServer.enqueue(jsonResponse("{\"code\":\"1\",\"data\":{\"totalSize\":1,\"totalPage\":1,\"pageSize\":10,\"pageNumber\":1,\"dataList\":[{\"subcompanyid1\":4,\"subcompanyname\":\"总部\"}]}}")); - IWorkOrgQueryReqVO reqVO = new IWorkOrgQueryReqVO(); + IWorkSubcompanyQueryReqVO reqVO = new IWorkSubcompanyQueryReqVO(); reqVO.setParams(Map.of("curpage", 1)); - IWorkOrgRespVO respVO = service.listSubcompanies(reqVO); + IWorkHrSubcompanyPageRespVO respVO = service.listSubcompanies(reqVO); assertThat(respVO.isSuccess()).isTrue(); - assertThat(respVO.getPayload()).containsEntry("page", 1); + assertThat(respVO.getTotalSize()).isEqualTo(1); + assertThat(respVO.getDataList()).hasSize(1); + assertThat(respVO.getDataList().get(0).getSubcompanyname()).isEqualTo("总部"); RecordedRequest request = mockWebServer.takeRequest(); assertThat(request.getPath()).isEqualTo(properties.getOrg().getPaths().getSubcompanyPage()); String decoded = URLDecoder.decode(request.getBody().readUtf8(), StandardCharsets.UTF_8); - assertThat(decoded).contains("params={\"curpage\":1}"); - String tokenJson = extractField(decoded, "token"); - assertThat(tokenJson).isNotBlank(); - assertThat(tokenJson).contains("\"ts\":\"1672531200000\""); - String expectedKey = DigestUtils.md5DigestAsHex("test-seed1672531200000".getBytes(StandardCharsets.UTF_8)).toUpperCase(); - assertThat(tokenJson).contains("\"key\":\"" + expectedKey + "\""); + JsonNode bodyNode = objectMapper.readTree(decoded); + assertThat(bodyNode.path("params").path("curpage").asInt()).isEqualTo(1); + JsonNode tokenNode = bodyNode.path("token"); + assertThat(tokenNode.path("ts").asText()).isEqualTo("1672531200000"); + String expectedKey = DigestUtils.md5DigestAsHex("test-seed1672531200000".getBytes(StandardCharsets.UTF_8)).toUpperCase(); + assertThat(tokenNode.path("key").asText()).isEqualTo(expectedKey); } @Test void shouldSyncDepartments() throws Exception { - mockWebServer.enqueue(jsonResponse("{\"code\":\"1\",\"result\":{}}")); + mockWebServer.enqueue(jsonResponse("{\"code\":\"1\",\"result\":[{\"@action\":\"add\",\"code\":\"demo\",\"result\":\"success\"}]}")); IWorkOrgSyncReqVO reqVO = new IWorkOrgSyncReqVO(); reqVO.setData(List.of(Map.of("@action", "add", "code", "demo"))); - IWorkOrgRespVO respVO = service.syncDepartments(reqVO); + IWorkHrSyncRespVO respVO = service.syncDepartments(reqVO); assertThat(respVO.isSuccess()).isTrue(); - assertThat(respVO.getPayload()).containsKey("result"); + assertThat(respVO.getResult()).hasSize(1); + assertThat(respVO.getResult().get(0).getCode()).isEqualTo("demo"); RecordedRequest request = mockWebServer.takeRequest(); assertThat(request.getPath()).isEqualTo(properties.getOrg().getPaths().getSyncDepartment()); String decoded = URLDecoder.decode(request.getBody().readUtf8(), StandardCharsets.UTF_8); - assertThat(decoded).contains("data=[{\"@action\":\"add\",\"code\":\"demo\"}]"); + JsonNode bodyNode = objectMapper.readTree(decoded); + assertThat(bodyNode.path("data").isArray()).isTrue(); + assertThat(bodyNode.path("data").get(0).path("code").asText()).isEqualTo("demo"); } private MockResponse jsonResponse(String body) { @@ -93,14 +101,6 @@ class IWorkOrgRestServiceImplTest { .setBody(body); } - private String extractField(String decoded, String key) { - return Arrays.stream(decoded.split("&")) - .filter(part -> part.startsWith(key + "=")) - .map(part -> part.substring(key.length() + 1)) - .findFirst() - .orElse(""); - } - private IWorkProperties buildProperties() { IWorkProperties properties = new IWorkProperties(); properties.setBaseUrl(mockWebServer.url("/").toString());