Compare commits

..

59 Commits

Author SHA1 Message Date
chenbowen
2dac28d3b3 Merge branch 'dev' into test 2025-11-26 13:46:23 +08:00
chenbowen
dbb1d1905e Merge remote-tracking branch 'base-version/main' into dev 2025-11-26 13:46:02 +08:00
chenbowen
08232eb3cb iwork 人员组织同步相关 2025-11-26 13:45:06 +08:00
chenbowen
5de2801fc9 Merge branch 'dev' into test 2025-11-26 12:40:05 +08:00
chenbowen
e9994a24c2 Merge remote-tracking branch 'base-version/main' into dev 2025-11-26 12:39:44 +08:00
chenbowen
a10732119b iwork 人员组织同步相关 2025-11-26 12:38:38 +08:00
qianshijiang
e7efddf976 配置mybais-plus打印sql 2025-11-26 11:57:17 +08:00
chenbowen
13ec805c20 Merge branch 'dev' into test 2025-11-26 11:34:59 +08:00
chenbowen
61e61d08b6 Merge remote-tracking branch 'base-version/main' into dev 2025-11-26 11:34:41 +08:00
chenbowen
5698c34185 iwork 人员组织同步相关 2025-11-26 11:34:04 +08:00
qianshijiang
96058e29c2 Merge remote-tracking branch 'origin/dev' into dev 2025-11-26 10:44:28 +08:00
qianshijiang
27d22de4e0 日志配置修改 2025-11-26 10:44:13 +08:00
chenbowen
f1242e74fc Merge branch 'dev' into test 2025-11-26 10:43:25 +08:00
chenbowen
0c0d82f465 Merge remote-tracking branch 'base-version/main' into dev 2025-11-26 10:43:04 +08:00
chenbowen
12ba2cf756 iwork 人员组织同步相关 2025-11-26 10:42:24 +08:00
qianshijiang
b1bd193f50 nacos配置。 2025-11-26 08:57:00 +08:00
chenbowen
0b4b87845c Merge branch 'dev' into test 2025-11-26 01:48:49 +08:00
chenbowen
a2f2325119 Merge remote-tracking branch 'base-version/main' into dev 2025-11-26 01:48:31 +08:00
chenbowen
4c79ac8a6d iwork 人员组织同步相关 2025-11-26 01:48:10 +08:00
chenbowen
0c0cb27c15 Merge branch 'dev' into test 2025-11-26 01:35:09 +08:00
chenbowen
a263632e49 Merge remote-tracking branch 'base-version/main' into dev 2025-11-26 01:34:50 +08:00
chenbowen
2e2b7ac6fa iwork 人员组织同步相关 2025-11-26 01:34:08 +08:00
chenbowen
9730573546 Merge branch 'dev' into test 2025-11-26 01:06:13 +08:00
chenbowen
299132943c Merge remote-tracking branch 'base-version/main' into dev 2025-11-26 01:05:55 +08:00
chenbowen
76ba994b50 iwork 人员组织同步相关 2025-11-26 01:04:35 +08:00
chenbowen
dd284728b4 Merge branch 'dev' into test 2025-11-25 23:27:22 +08:00
chenbowen
685ed6b504 Merge remote-tracking branch 'base-version/main' into dev 2025-11-25 23:27:01 +08:00
chenbowen
f754b1c694 iwork 人员组织同步相关 2025-11-25 23:26:26 +08:00
chenbowen
dc1db47d07 iwork 人员组织同步相关 2025-11-25 20:31:56 +08:00
chenbowen
dd38e65972 Merge branch 'dev' into test 2025-11-25 20:09:31 +08:00
chenbowen
02e0c81446 Merge remote-tracking branch 'base-version/main' into dev 2025-11-25 20:09:06 +08:00
chenbowen
6c8c479984 同步 nacos 配置到基础系统 2025-11-25 20:08:31 +08:00
chenbowen
829229a355 Merge branch 'dev' into test 2025-11-25 19:15:29 +08:00
chenbowen
067f7226f4 Merge remote-tracking branch 'base-version/main' into dev 2025-11-25 19:14:59 +08:00
chenbowen
b35df8493c 同步 nacos 配置到基础系统 2025-11-25 19:13:27 +08:00
hewencai
518aa2a773 Merge remote-tracking branch 'origin/dev' into dev 2025-11-25 19:09:13 +08:00
hewencai
4003388740 feat:集成移动云mas短信平台 2025-11-25 19:08:55 +08:00
chenbowen
f16509c107 Merge branch 'dev' into test 2025-11-25 18:58:07 +08:00
chenbowen
565a625df7 Merge remote-tracking branch 'base-version/main' into dev 2025-11-25 18:57:47 +08:00
chenbowen
adcea87bbf 同步 nacos 配置到基础系统 2025-11-25 18:56:12 +08:00
chenbowen
a79806690d Merge branch 'dev' into test 2025-11-25 17:43:28 +08:00
chenbowen
8689c5e844 Merge remote-tracking branch 'base-version/main' into dev 2025-11-25 17:42:26 +08:00
chenbowen
5be1b75be8 iwork 人员组织同步相关,兼容 iwork 返回 2025-11-25 17:41:39 +08:00
chenbowen
06d9ae2688 Merge branch 'dev' into test 2025-11-25 17:22:53 +08:00
chenbowen
547b1d9afb Merge remote-tracking branch 'base-version/main' into dev 2025-11-25 17:22:36 +08:00
chenbowen
2f9c28f166 iwork 人员组织同步相关,兼容 iwork 返回 2025-11-25 17:22:11 +08:00
chenbowen
9a0e60ad84 Merge branch 'dev' into test 2025-11-25 17:19:06 +08:00
chenbowen
c24ae5bad8 Merge remote-tracking branch 'base-version/main' into dev 2025-11-25 17:18:43 +08:00
chenbowen
64eb031486 iwork 人员组织同步相关,兼容 iwork 返回 2025-11-25 17:18:04 +08:00
chenbowen
30b22698e8 Merge branch 'dev' into test 2025-11-25 16:50:11 +08:00
chenbowen
95fab27556 Merge remote-tracking branch 'base-version/main' into dev 2025-11-25 16:49:43 +08:00
chenbowen
2efb815d59 iwork 人员组织同步相关,兼容 iwork 返回 2025-11-25 16:41:29 +08:00
chenbowen
3d5f07b7a5 Merge branch 'dev' into test 2025-11-25 16:08:41 +08:00
chenbowen
77b4e62def Merge remote-tracking branch 'base-version/main' into dev 2025-11-25 16:06:41 +08:00
chenbowen
e2dbaf12a4 iwork 人员组织同步相关 2025-11-25 16:05:52 +08:00
chenbowen
f8d95218f5 Merge branch 'dev' into test 2025-11-25 15:55:44 +08:00
chenbowen
e0d5c0221e Merge remote-tracking branch 'base-version/main' into dev 2025-11-25 15:55:13 +08:00
chenbowen
59afa893b0 修复编译错误 2025-11-25 15:54:50 +08:00
chenbowen
d4d80ce86a iwork 人员组织同步相关 2025-11-25 15:48:47 +08:00
43 changed files with 3018 additions and 221 deletions

21
pom.xml
View File

@@ -237,8 +237,8 @@
<config.server-addr>172.16.46.63:30848</config.server-addr>
<config.namespace>dev</config.namespace>
<config.group>DEFAULT_GROUP</config.group>
<config.username/>
<config.password/>
<config.username>nacos</config.username>
<config.password>P@ssword25</config.password>
<config.version>1.0.0</config.version>
</properties>
</profile>
@@ -250,8 +250,8 @@
<config.server-addr>172.16.46.63:30848</config.server-addr>
<config.namespace>prod</config.namespace>
<config.group>DEFAULT_GROUP</config.group>
<config.username/>
<config.password/>
<config.username>nacos</config.username>
<config.password>P@ssword25</config.password>
<config.version>1.0.0</config.version>
</properties>
</profile>
@@ -263,8 +263,8 @@
<config.server-addr>172.16.46.63:30848</config.server-addr>
<config.namespace>local</config.namespace>
<config.group>DEFAULT_GROUP</config.group>
<config.username/>
<config.password/>
<config.username>nacos</config.username>
<config.password>P@ssword25</config.password>
<config.version>1.0.0</config.version>
</properties>
</profile>
@@ -277,7 +277,14 @@
<profile>
<id>qsj</id>
<properties>
<config.namespace>qsj</config.namespace>
<env.name>dev</env.name>
<!--Nacos 配置-->
<config.server-addr>172.16.46.63:30848</config.server-addr>
<config.namespace>qsj</config.namespace>
<config.group>DEFAULT_GROUP</config.group>
<config.username>nacos</config.username>
<config.password>P@ssword25</config.password>
<config.version>1.0.0</config.version>
</properties>
</profile>
</profiles>

View File

@@ -87,6 +87,7 @@
<netty.version>4.1.116.Final</netty.version>
<mqtt.version>1.2.5</mqtt.version>
<pf4j-spring.version>0.9.0</pf4j-spring.version>
<okhttp3.version>4.12.0</okhttp3.version>
<!-- 规则引擎 -->
<liteflow.version>2.15.1</liteflow.version>
<vertx.version>4.5.13</vertx.version>

View File

@@ -4,8 +4,8 @@ spring:
cloud:
nacos:
server-addr: 172.16.46.63:30848 # Nacos 服务器地址
username: # Nacos 账号
password: # Nacos 密码
username: ${config.username} # Nacos 账号
password: ${config.password} # Nacos 密码
discovery: # 【配置中心】配置项
namespace: ${config.namespace} # 命名空间。这里使用 maven Profile 资源过滤进行动态替换
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP

View File

@@ -31,7 +31,7 @@
<!-- 启动服务时,是否清理历史日志,一般不建议清理 -->
<cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
<!-- 日志文件,到达多少容量,进行滚动 -->
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}</maxFileSize>
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-50MB}</maxFileSize>
<!-- 日志文件的总大小0 表示不限制 -->
<totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
<!-- 日志文件的保留天数 -->
@@ -56,14 +56,16 @@
</encoder>
</appender>
<!--logback的日志级别 FATAL > ERROR > WARN > INFO > DEBUG-->
<!-- 本地环境 -->
<springProfile name="local">
<root level="INFO">
<springProfile name="local,dev">
<root level="WARN">
<appender-ref ref="STDOUT"/>
<appender-ref ref="GRPC"/> <!-- 本地环境下,如果不想接入 SkyWalking 日志服务,可以注释掉本行 -->
<appender-ref ref="ASYNC"/> <!-- 本地环境下,如果不想打印日志,可以注释掉本行 -->
</root>
</springProfile>
<!-- 其它环境 -->
<springProfile name="dev,test,stage,prod,default">
<root level="INFO">

View File

@@ -31,7 +31,7 @@
<!-- 启动服务时,是否清理历史日志,一般不建议清理 -->
<cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
<!-- 日志文件,到达多少容量,进行滚动 -->
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}</maxFileSize>
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-50MB}</maxFileSize>
<!-- 日志文件的总大小0 表示不限制 -->
<totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
<!-- 日志文件的保留天数 -->
@@ -56,14 +56,21 @@
</encoder>
</appender>
<!--logback的日志级别 FATAL > ERROR > WARN > INFO > DEBUG-->
<!-- 本地环境 -->
<springProfile name="local">
<root level="INFO">
<springProfile name="local,dev">
<root level="WARN">
<appender-ref ref="STDOUT"/>
<appender-ref ref="GRPC"/> <!-- 本地环境下,如果不想接入 SkyWalking 日志服务,可以注释掉本行 -->
<appender-ref ref="ASYNC"/> <!-- 本地环境下,如果不想打印日志,可以注释掉本行 -->
</root>
<!--针对不同的业务路径,配置dao层的sql打印日志级别为DEBUG-->
<logger name="com.zt.plat.module.infra.dal.mysql" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
</springProfile>
<!-- 其它环境 -->
<springProfile name="dev,test,stage,prod,default">
<root level="INFO">

View File

@@ -6,6 +6,7 @@ import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.infra.api.file.FileApi;
import com.zt.plat.module.infra.api.file.dto.FileCreateReqDTO;
import com.zt.plat.module.mp.controller.admin.material.vo.MpMaterialPageReqVO;
import com.zt.plat.module.mp.controller.admin.material.vo.MpMaterialUploadNewsImageReqVO;
import com.zt.plat.module.mp.controller.admin.material.vo.MpMaterialUploadPermanentReqVO;
@@ -218,7 +219,8 @@ public class MpMaterialServiceImpl implements MpMaterialService {
private String uploadFile(String mediaId, File file) {
String path = mediaId + "." + FileTypeUtil.getType(file);
return fileApi.createFile(FileUtil.readBytes(file), path);
FileCreateReqDTO createReqDTO = new FileCreateReqDTO().setName(file.getName()).setDirectory(path).setType(FileTypeUtil.getType(file)).setContent(FileUtil.readBytes(file));
return fileApi.createFile(createReqDTO).getData();
}
}

View File

@@ -31,7 +31,7 @@
<!-- 启动服务时,是否清理历史日志,一般不建议清理 -->
<cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
<!-- 日志文件,到达多少容量,进行滚动 -->
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}</maxFileSize>
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-50MB}</maxFileSize>
<!-- 日志文件的总大小0 表示不限制 -->
<totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
<!-- 日志文件的保留天数 -->
@@ -56,14 +56,21 @@
</encoder>
</appender>
<!--logback的日志级别 FATAL > ERROR > WARN > INFO > DEBUG-->
<!-- 本地环境 -->
<springProfile name="local">
<root level="INFO">
<springProfile name="local,dev">
<root level="WARN">
<appender-ref ref="STDOUT"/>
<appender-ref ref="GRPC"/> <!-- 本地环境下,如果不想接入 SkyWalking 日志服务,可以注释掉本行 -->
<appender-ref ref="ASYNC"/> <!-- 本地环境下,如果不想打印日志,可以注释掉本行 -->
</root>
<!--针对不同的业务路径,配置dao层的sql打印日志级别为DEBUG-->
<logger name="com.zt.plat.module.mp.dal.mysql" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
</springProfile>
<!-- 其它环境 -->
<springProfile name="dev,test,stage,prod,default">
<root level="INFO">

View File

@@ -31,7 +31,7 @@
<!-- 启动服务时,是否清理历史日志,一般不建议清理 -->
<cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
<!-- 日志文件,到达多少容量,进行滚动 -->
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}</maxFileSize>
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-50MB}</maxFileSize>
<!-- 日志文件的总大小0 表示不限制 -->
<totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
<!-- 日志文件的保留天数 -->
@@ -56,14 +56,21 @@
</encoder>
</appender>
<!--logback的日志级别 FATAL > ERROR > WARN > INFO > DEBUG-->
<!-- 本地环境 -->
<springProfile name="local">
<root level="INFO">
<springProfile name="local,dev">
<root level="WARN">
<appender-ref ref="STDOUT"/>
<appender-ref ref="GRPC"/> <!-- 本地环境下,如果不想接入 SkyWalking 日志服务,可以注释掉本行 -->
<appender-ref ref="ASYNC"/> <!-- 本地环境下,如果不想打印日志,可以注释掉本行 -->
</root>
<!--针对不同的业务路径,配置dao层的sql打印日志级别为DEBUG-->
<logger name="com.zt.plat.module.report.dal.mysql" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
</springProfile>
<!-- 其它环境 -->
<springProfile name="dev,test,stage,prod,default">
<root level="INFO">

View File

@@ -14,6 +14,9 @@ public class AdminUserRespDTO implements VO {
@Schema(description = "用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "zhangsan")
private String username;
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小王")
private String nickname;

View File

@@ -13,7 +13,8 @@ import lombok.Getter;
public enum DeptSourceEnum {
EXTERNAL(1, "外部部门"), // 系统创建的部门
SYNC(2, "同步部门"); // 通过 OrgSyncService 同步的部门
SYNC(2, "同步部门"), // 通过 OrgSyncService 同步的部门
IWORK(3, "iWork 同步"); // 通过 iWork 同步的部门
/**
* 类型

View File

@@ -13,7 +13,8 @@ import lombok.Getter;
public enum UserSourceEnum {
EXTERNAL(1, "外部用户"), // 系统创建、注册等方式产生的用户
SYNC(2, "同步用户"); // 通过 UserSyncService 同步的用户
SYNC(2, "同步用户"), // 通过 UserSyncService 同步的用户
IWORK(3, "iWork 用户"); // 通过 iWork 全量/单条同步产生的用户
/**
* 类型

View File

@@ -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,87 @@ public class IWorkIntegrationController {
@PostMapping("/hr/subcompany/page")
@Operation(summary = "获取 iWork 分部列表")
public CommonResult<IWorkOrgRespVO> listSubcompanies(@Valid @RequestBody IWorkSubcompanyQueryReqVO reqVO) {
public CommonResult<IWorkHrSubcompanyPageRespVO> listSubcompanies(@Valid @RequestBody IWorkSubcompanyQueryReqVO reqVO) {
return success(orgRestService.listSubcompanies(reqVO));
}
@PostMapping("/hr/department/page")
@Operation(summary = "获取 iWork 部门列表")
public CommonResult<IWorkOrgRespVO> listDepartments(@Valid @RequestBody IWorkDepartmentQueryReqVO reqVO) {
public CommonResult<IWorkHrDepartmentPageRespVO> listDepartments(@Valid @RequestBody IWorkDepartmentQueryReqVO reqVO) {
return success(orgRestService.listDepartments(reqVO));
}
@PostMapping("/hr/job-title/page")
@Operation(summary = "获取 iWork 岗位列表")
public CommonResult<IWorkOrgRespVO> listJobTitles(@Valid @RequestBody IWorkJobTitleQueryReqVO reqVO) {
public CommonResult<IWorkHrJobTitlePageRespVO> listJobTitles(@Valid @RequestBody IWorkJobTitleQueryReqVO reqVO) {
return success(orgRestService.listJobTitles(reqVO));
}
@PostMapping("/hr/user/page")
@Operation(summary = "获取 iWork 人员列表")
public CommonResult<IWorkOrgRespVO> listUsers(@Valid @RequestBody IWorkUserQueryReqVO reqVO) {
public CommonResult<IWorkHrUserPageRespVO> listUsers(@Valid @RequestBody IWorkUserQueryReqVO reqVO) {
return success(orgRestService.listUsers(reqVO));
}
@PostMapping("/hr/subcompany/sync")
@Operation(summary = "同步分部信息至 iWork")
public CommonResult<IWorkOrgRespVO> syncSubcompanies(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) {
return success(orgRestService.syncSubcompanies(reqVO));
// @PostMapping("/hr/subcompany/sync")
// @Operation(summary = "同步分部信息至 iWork")
// public CommonResult<IWorkHrSyncRespVO> syncSubcompanies(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) {
// return success(orgRestService.syncSubcompanies(reqVO));
// }
//
// @PostMapping("/hr/department/sync")
// @Operation(summary = "同步部门信息至 iWork")
// public CommonResult<IWorkHrSyncRespVO> syncDepartments(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) {
// return success(orgRestService.syncDepartments(reqVO));
// }
//
// @PostMapping("/hr/job-title/sync")
// @Operation(summary = "同步岗位信息至 iWork")
// public CommonResult<IWorkHrSyncRespVO> syncJobTitles(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) {
// return success(orgRestService.syncJobTitles(reqVO));
// }
//
// @PostMapping("/hr/user/sync")
// @Operation(summary = "同步人员信息至 iWork")
// public CommonResult<IWorkHrSyncRespVO> syncUsers(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) {
// return success(orgRestService.syncUsers(reqVO));
// }
// ----------------- 同步到本地 -----------------
@PostMapping("/hr/full-sync")
@Operation(summary = "手动触发 iWork 组织/人员同步")
public CommonResult<IWorkFullSyncRespVO> fullSync(@Valid @RequestBody IWorkFullSyncReqVO reqVO) {
return success(syncService.fullSync(reqVO));
}
@PostMapping("/hr/department/sync")
@Operation(summary = "同步部门信息至 iWork")
public CommonResult<IWorkOrgRespVO> syncDepartments(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) {
return success(orgRestService.syncDepartments(reqVO));
@PostMapping("/hr/departments/full-sync")
@Operation(summary = "手动触发 iWork 部门同步")
public CommonResult<IWorkFullSyncRespVO> fullSyncDepartments(@Valid @RequestBody IWorkFullSyncReqVO reqVO) {
return success(syncService.fullSyncDepartments(reqVO));
}
@PostMapping("/hr/job-title/sync")
@Operation(summary = "同步岗位信息至 iWork")
public CommonResult<IWorkOrgRespVO> syncJobTitles(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) {
return success(orgRestService.syncJobTitles(reqVO));
@PostMapping("/hr/subcompanies/full-sync")
@Operation(summary = "手动触发 iWork 分部同步")
public CommonResult<IWorkFullSyncRespVO> fullSyncSubcompanies(@Valid @RequestBody IWorkFullSyncReqVO reqVO) {
return success(syncService.fullSyncSubcompanies(reqVO));
}
@PostMapping("/hr/user/sync")
@Operation(summary = "同步人员信息至 iWork")
public CommonResult<IWorkOrgRespVO> syncUsers(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) {
return success(orgRestService.syncUsers(reqVO));
@PostMapping("/hr/job-titles/full-sync")
@Operation(summary = "手动触发 iWork 岗位全量同步")
public CommonResult<IWorkFullSyncRespVO> fullSyncJobTitles(@Valid @RequestBody IWorkFullSyncReqVO reqVO) {
return success(syncService.fullSyncJobTitles(reqVO));
}
@PostMapping("/hr/users/full-sync")
@Operation(summary = "手动触发 iWork 人员全量同步")
public CommonResult<IWorkFullSyncRespVO> fullSyncUsers(@Valid @RequestBody IWorkFullSyncReqVO reqVO) {
return success(syncService.fullSyncUsers(reqVO));
}
@PostMapping("/hr/single-sync")
@Operation(summary = "按 iWork ID 同步单条组织/人员")
public CommonResult<IWorkSingleSyncRespVO> singleSync(@Valid @RequestBody IWorkSingleSyncReqVO reqVO) {
return success(syncService.syncSingle(reqVO));
}
}

View File

@@ -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<String> scopes;
@Schema(description = "是否包含已失效canceled=1的记录", example = "false")
private Boolean includeCanceled = Boolean.FALSE;
public Set<IWorkSyncEntityTypeEnum> resolveScopes() {
EnumSet<IWorkSyncEntityTypeEnum> defaults = EnumSet.allOf(IWorkSyncEntityTypeEnum.class);
if (scopes == null || scopes.isEmpty()) {
return defaults;
}
Set<IWorkSyncEntityTypeEnum> 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;
}
}

View File

@@ -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<IWorkSyncBatchStatVO> batches;
}

View File

@@ -0,0 +1,212 @@
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 com.zt.plat.module.system.service.integration.iwork.jackson.LenientIntegerDeserializer;
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 IWorkHrDepartmentPageRespVO {
@Schema(description = "响应码")
private String code;
@Schema(description = "提示信息")
private String message;
@Schema(description = "是否成功")
private boolean success;
@Schema(description = "iWork 返回的数据体")
@JsonProperty("data")
private PageData data;
@JsonIgnore
public Integer getTotalSize() {
return data == null ? null : data.getTotalSize();
}
@JsonIgnore
public void setTotalSize(Integer totalSize) {
ensureData().setTotalSize(totalSize);
}
@JsonIgnore
public Integer getTotalPage() {
return data == null ? null : data.getTotalPage();
}
@JsonIgnore
public void setTotalPage(Integer totalPage) {
ensureData().setTotalPage(totalPage);
}
@JsonIgnore
public Integer getPageSize() {
return data == null ? null : data.getPageSize();
}
@JsonIgnore
public void setPageSize(Integer pageSize) {
ensureData().setPageSize(pageSize);
}
@JsonIgnore
public Integer getPageNumber() {
return data == null ? null : data.getPageNumber();
}
@JsonIgnore
public void setPageNumber(Integer pageNumber) {
ensureData().setPageNumber(pageNumber);
}
@JsonIgnore
public List<Department> getDataList() {
return data == null ? null : data.getDataList();
}
@JsonIgnore
public void setDataList(List<Department> dataList) {
ensureData().setDataList(dataList);
}
@JsonIgnore
private PageData ensureData() {
if (data == null) {
data = new PageData();
}
return data;
}
@Data
@Schema(description = "iWork 部门分页数据体")
public static class PageData {
@Schema(description = "总条数")
private Integer totalSize;
@Schema(description = "总页数")
private Integer totalPage;
@Schema(description = "每页条数")
private Integer pageSize;
@Schema(description = "当前页码")
@JsonProperty("page")
private Integer pageNumber;
@Schema(description = "部门数据列表")
@JsonProperty("dataList")
private List<Department> dataList;
}
@Data
@Schema(description = "部门信息")
public static class Department {
@Schema(description = "部门 ID")
@JsonProperty("departmentid")
@JsonDeserialize(using = LenientIntegerDeserializer.class)
private Integer departmentid;
@Schema(description = "部门 IDiWork 主键)")
@JsonProperty("id")
@JsonDeserialize(using = LenientIntegerDeserializer.class)
private Integer id;
@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")
@JsonDeserialize(using = LenientIntegerDeserializer.class)
private Integer subcompanyid1;
@Schema(description = "所属分部名称")
@JsonProperty("subcompanyname")
private String subcompanyname;
@Schema(description = "上级分部 ID")
@JsonProperty("supsubcomid")
@JsonDeserialize(using = LenientIntegerDeserializer.class)
private Integer supsubcomid;
@Schema(description = "上级分部名称")
@JsonProperty("supsubcomname")
private String supsubcomname;
@Schema(description = "父部门 ID")
@JsonProperty("supdepid")
@JsonDeserialize(using = LenientIntegerDeserializer.class)
private Integer supdepid;
@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")
@JsonDeserialize(using = LenientIntegerDeserializer.class)
private Integer managerid;
@Schema(description = "负责人名称")
@JsonProperty("manager")
private String manager;
@JsonIgnore
private Map<String, Object> attributes;
@JsonAnySetter
public void putAttribute(String key, Object value) {
if (attributes == null) {
attributes = new LinkedHashMap<>();
}
attributes.put(key, value);
}
@JsonAnyGetter
public Map<String, Object> any() {
return attributes == null ? Collections.emptyMap() : attributes;
}
}
}

View File

@@ -0,0 +1,121 @@
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 com.zt.plat.module.system.service.integration.iwork.jackson.LenientIntegerDeserializer;
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 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<JobTitle> dataList;
@Data
@Schema(description = "岗位信息")
public static class JobTitle {
@Schema(description = "岗位 ID")
@JsonProperty("id")
private Integer id;
@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<String, Object> attributes;
@JsonAnySetter
public void putAttribute(String key, Object value) {
if (attributes == null) {
attributes = new LinkedHashMap<>();
}
attributes.put(key, value);
}
@JsonAnyGetter
public Map<String, Object> any() {
return attributes == null ? Collections.emptyMap() : attributes;
}
}
}

View File

@@ -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 com.zt.plat.module.system.service.integration.iwork.jackson.LenientIntegerDeserializer;
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 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<Subcompany> dataList;
@Data
@Schema(description = "分部信息")
public static class Subcompany {
@Schema(description = "部门 IDiWork 主键)")
@JsonProperty("id")
@JsonDeserialize(using = LenientIntegerDeserializer.class)
private Integer id;
@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<String, Object> attributes;
@JsonAnySetter
public void putAttribute(String key, Object value) {
if (attributes == null) {
attributes = new LinkedHashMap<>();
}
attributes.put(key, value);
}
@JsonAnyGetter
public Map<String, Object> any() {
return attributes == null ? Collections.emptyMap() : attributes;
}
}
}

View File

@@ -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<SyncResult> 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<String, Object> attributes;
@JsonAnySetter
public void putAttribute(String key, Object value) {
if (attributes == null) {
attributes = new LinkedHashMap<>();
}
attributes.put(key, value);
}
@JsonAnyGetter
public Map<String, Object> any() {
return attributes == null ? Collections.emptyMap() : attributes;
}
}
}

View File

@@ -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<User> 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<String, Object> attributes;
@JsonAnySetter
public void putAttribute(String key, Object value) {
if (attributes == null) {
attributes = new LinkedHashMap<>();
}
attributes.put(key, value);
}
@JsonAnyGetter
public Map<String, Object> any() {
return attributes == null ? Collections.emptyMap() : attributes;
}
}
}

View File

@@ -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<String, Object> 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");
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -84,6 +84,18 @@ public class UserSaveReqVO {
@Schema(description = "用户来源类型", example = "1")
private Integer userSource;
@Schema(hidden = true)
@JsonIgnore
private boolean skipAssociationValidation;
@Schema(hidden = true)
@JsonIgnore
private boolean skipMobileValidation;
@Schema(hidden = true)
@JsonIgnore
private boolean skipEmailValidation;
// ========== 仅【创建】时,需要传递的字段 ==========
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")

View File

@@ -0,0 +1,51 @@
package com.zt.plat.module.system.enums.integration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import java.util.Locale;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* iWork 同步支持的实体类型。
*/
@AllArgsConstructor
@Getter
public enum IWorkSyncEntityTypeEnum {
SUBCOMPANY("subcompany", "分部 / 公司"),
DEPARTMENT("department", "部门"),
JOB_TITLE("jobTitle", "岗位"),
USER("user", "人员");
@JsonValue
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;
}
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
public static IWorkSyncEntityTypeEnum fromJson(String code) {
IWorkSyncEntityTypeEnum value = fromCode(code);
if (value != null || code == null) {
return value;
}
try {
return IWorkSyncEntityTypeEnum.valueOf(code.trim().toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException ex) {
return null;
}
}
}

View File

@@ -0,0 +1,222 @@
package com.zt.plat.module.system.framework.sms.core.client.impl;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.zt.plat.framework.common.core.KeyValue;
import com.zt.plat.framework.common.util.http.HttpUtils;
import com.zt.plat.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.zt.plat.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
import com.zt.plat.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.zt.plat.module.system.framework.sms.core.property.SmsChannelProperties;
import lombok.extern.slf4j.Slf4j;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 中国移动云MAS短信客户端实现类
*
* @author zt-team
* @since 2025-01-19
*/
@Slf4j
public class CmccMasSmsClient extends AbstractSmsClient {
private static final String URL = "https://112.35.10.201:28888/sms/submit";
private static final String RESPONSE_SUCCESS = "success";
public CmccMasSmsClient(SmsChannelProperties properties) {
super(properties);
Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
validateCmccMasConfig(properties);
}
/**
* 参数校验中国移动云MAS的配置
*
* 原因是中国移动云MAS需要三个参数ecName、apId、secretKey
*
* 解决方案:考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 ecName 和 apId 拼接到 apiKey 字段中,格式为 "ecName apId"。
* secretKey 存储在 apiSecret 字段中。
*
* @param properties 配置
*/
private static void validateCmccMasConfig(SmsChannelProperties properties) {
String combineKey = properties.getApiKey();
Assert.notEmpty(combineKey, "apiKey 不能为空");
String[] keys = combineKey.trim().split(" ");
Assert.isTrue(keys.length == 2, "中国移动云MAS apiKey 配置格式错误,请配置为 [ecName apId]");
}
/**
* 获取 ecName企业名称
*/
private String getEcName() {
return StrUtil.subBefore(properties.getApiKey(), " ", true);
}
/**
* 获取 apId应用ID
*/
private String getApId() {
return StrUtil.subAfter(properties.getApiKey(), " ", true);
}
/**
* 获取 secretKey密钥
*/
private String getSecretKey() {
return properties.getApiSecret();
}
/**
* 发送短信
*
* @param logId 日志ID
* @param mobile 手机号
* @param apiTemplateId 模板ID本平台不使用模板传入内容
* @param templateParams 模板参数
* @return 发送结果
*/
@Override
public SmsSendRespDTO sendSms(Long logId, String mobile, String apiTemplateId,
List<KeyValue<String, Object>> templateParams) throws Throwable {
// 1. 构建短信内容
String content = buildContent(apiTemplateId, templateParams);
// 2. 计算MAC校验值
String mac = calculateMac(mobile, content);
// 3. 构建请求参数
JSONObject requestBody = new JSONObject();
requestBody.set("ecName", getEcName()); // 企业名称
requestBody.set("apId", getApId()); // 应用ID
requestBody.set("secretKey", getSecretKey()); // 密钥
requestBody.set("sign", properties.getSignature()); // 签名编码
requestBody.set("mobiles", mobile);
requestBody.set("content", content);
requestBody.set("addSerial", "");
requestBody.set("mac", mac);
log.info("[sendSms][发送短信 {}]", JSONUtil.toJsonStr(requestBody));
// 4. Base64编码请求体
String encodedBody = Base64.encode(requestBody.toString());
log.info("[sendSms][Base64编码后: {}]", encodedBody);
// 5. 构建请求头需要JWT Token
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + getJwtToken());
headers.put("Content-Type", "text/plain");
// 6. 发起请求
String responseBody = HttpUtils.post(URL, headers, encodedBody);
JSONObject response = JSONUtil.parseObj(responseBody);
log.info("[sendSms][收到响应 - {}]", response);
// 7. 解析响应
return new SmsSendRespDTO()
.setSuccess(response.getBool("success", false))
.setSerialNo(response.getStr("msgGroup"))
.setApiCode(response.getStr("rspcod"))
.setApiMsg(response.getStr("message", "未知错误"));
}
/**
* 解析短信接收状态回调
*
* @param text 回调文本
* @return 接收状态列表
*/
@Override
public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) throws Throwable {
// TODO: 根据移动云MAS回调格式实现
log.warn("[parseSmsReceiveStatus][暂未实现短信状态回调解析]");
return Collections.emptyList();
}
/**
* 查询短信模板
*
* @param apiTemplateId 模板ID
* @return 模板信息
*/
@Override
public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
// 移动云MAS不使用模板机制直接发送内容
log.debug("[getSmsTemplate][中国移动云MAS不支持模板查询]");
return null;
}
/**
* 计算MAC校验值
* 算法MD5(ecName + apId + secretKey + mobiles + content + sign + addSerial)
*
* @param mobile 手机号
* @param content 短信内容
* @return MAC校验值
*/
private String calculateMac(String mobile, String content) {
String rawString = getEcName() // ecName
+ getApId() // apId
+ getSecretKey() // secretKey
+ mobile // mobiles
+ content // content
+ properties.getSignature() // sign
+ ""; // addSerial
String mac = DigestUtil.md5Hex(rawString).toLowerCase();
log.debug("[calculateMac][原始字符串长度: {}, MAC: {}]", rawString.length(), mac);
return mac;
}
/**
* 构建短信内容
*
* @param apiTemplateId 模板ID
* @param templateParams 模板参数
* @return 短信内容
*/
private String buildContent(String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
// 简单实现直接返回模板ID作为内容
// 实际使用时需要根据业务需求构建短信内容
if (templateParams == null || templateParams.isEmpty()) {
return apiTemplateId;
}
// 替换模板参数,支持 {{key}} 格式
String content = apiTemplateId;
for (KeyValue<String, Object> param : templateParams) {
String placeholder = "{{" + param.getKey() + "}}";
String value = String.valueOf(param.getValue());
content = content.replace(placeholder, value);
}
return content;
}
/**
* 获取JWT Token
* TODO: 实现Token获取逻辑可能需要
* 1. 调用认证接口获取Token
* 2. 缓存Token并在过期前自动刷新
* 3. 处理Token失效情况
*
* @return JWT Token
*/
private String getJwtToken() {
// 临时实现:从配置中读取或调用认证接口获取
// 实际生产环境需要实现完整的Token管理机制
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.example.token";
log.warn("[getJwtToken][使用临时Token生产环境需实现完整的Token获取机制]");
return token;
}
}

View File

@@ -81,6 +81,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
case TENCENT: return new TencentSmsClient(properties);
case HUAWEI: return new HuaweiSmsClient(properties);
case QINIU: return new QiniuSmsClient(properties);
case CMCC_MAS: return new CmccMasSmsClient(properties);
}
// 创建失败,错误日志 + 抛出异常
log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties);

View File

@@ -19,6 +19,7 @@ public enum SmsChannelEnum {
TENCENT("TENCENT", "腾讯云"),
HUAWEI("HUAWEI", "华为云"),
QINIU("QINIU", "七牛云"),
CMCC_MAS("CMCC_MAS", "中国移动云MAS"),
;
/**

View File

@@ -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;
}

View File

@@ -74,16 +74,23 @@ public class DeptServiceImpl implements DeptService {
// 校验部门名的唯一性
validateDeptNameUnique(null, createReqVO.getParentId(), createReqVO.getName());
// 生成并校验部门编码
Long effectiveParentId = normalizeParentId(createReqVO.getParentId());
boolean isTopLevel = Objects.equals(effectiveParentId, DeptDO.PARENT_ID_ROOT);
String resolvedCode;
if (isTopLevel) {
resolvedCode = resolveTopLevelCode(null, createReqVO.getCode());
boolean isIWorkSource = Objects.equals(createReqVO.getDeptSource(), DeptSourceEnum.IWORK.getSource());
if (isIWorkSource) {
// iWork 来源直接使用提供的编码,不再生成
String providedCode = StrUtil.blankToDefault(createReqVO.getCode(), null);
createReqVO.setCode(providedCode);
} else {
resolvedCode = generateDeptCode(effectiveParentId);
validateDeptCodeUnique(null, resolvedCode);
Long effectiveParentId = normalizeParentId(createReqVO.getParentId());
boolean isTopLevel = Objects.equals(effectiveParentId, DeptDO.PARENT_ID_ROOT);
String resolvedCode;
if (isTopLevel) {
resolvedCode = resolveTopLevelCode(null, createReqVO.getCode());
} else {
resolvedCode = generateDeptCode(effectiveParentId);
validateDeptCodeUnique(null, resolvedCode);
}
createReqVO.setCode(resolvedCode);
}
createReqVO.setCode(resolvedCode);
// 插入部门
DeptDO dept = BeanUtils.toBean(createReqVO, DeptDO.class);
@@ -110,28 +117,35 @@ public class DeptServiceImpl implements DeptService {
// 校验部门名的唯一性
validateDeptNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName());
// 如果上级发生变化,需要重新生成编码并同步子级
boolean isIWorkSource = Objects.equals(originalDept.getDeptSource(), DeptSourceEnum.IWORK.getSource());
Long newParentId = normalizeParentId(updateReqVO.getParentId());
Long oldParentId = normalizeParentId(originalDept.getParentId());
boolean parentChanged = !Objects.equals(newParentId, oldParentId);
if (parentChanged) {
String newCode;
if (Objects.equals(newParentId, DeptDO.PARENT_ID_ROOT)) {
newCode = resolveTopLevelCode(updateReqVO.getId(), updateReqVO.getCode());
} else {
newCode = generateDeptCode(updateReqVO.getParentId());
validateDeptCodeUnique(updateReqVO.getId(), newCode);
}
updateReqVO.setCode(newCode);
if (isIWorkSource) {
// iWork 来源直接使用提供的编码,不再生成
String providedCode = StrUtil.blankToDefault(updateReqVO.getCode(), null);
updateReqVO.setCode(providedCode);
} else {
if (Objects.equals(newParentId, DeptDO.PARENT_ID_ROOT)) {
String requestedCode = updateReqVO.getCode();
if (StrUtil.isNotBlank(requestedCode) && !StrUtil.equals(requestedCode.trim(), originalDept.getCode())) {
updateReqVO.setCode(resolveTopLevelCode(updateReqVO.getId(), requestedCode));
if (parentChanged) {
String newCode;
if (Objects.equals(newParentId, DeptDO.PARENT_ID_ROOT)) {
newCode = resolveTopLevelCode(updateReqVO.getId(), updateReqVO.getCode());
} else {
newCode = generateDeptCode(updateReqVO.getParentId());
validateDeptCodeUnique(updateReqVO.getId(), newCode);
}
updateReqVO.setCode(newCode);
} else {
if (Objects.equals(newParentId, DeptDO.PARENT_ID_ROOT)) {
String requestedCode = updateReqVO.getCode();
if (StrUtil.isNotBlank(requestedCode) && !StrUtil.equals(requestedCode.trim(), originalDept.getCode())) {
updateReqVO.setCode(resolveTopLevelCode(updateReqVO.getId(), requestedCode));
} else {
updateReqVO.setCode(originalDept.getCode());
}
} else {
updateReqVO.setCode(originalDept.getCode());
}
} else {
updateReqVO.setCode(originalDept.getCode());
}
}
@@ -189,7 +203,7 @@ public class DeptServiceImpl implements DeptService {
// 2. 父部门不存在
DeptDO parentDept = deptMapper.selectById(parentId);
if (parentDept == null) {
throw exception(DEPT_PARENT_NOT_EXITS);
return;
}
// 3. 递归校验父部门,如果父部门是自己的子部门,则报错,避免形成环路
if (id == null) { // id 为空,说明新增,不需要考虑环路
@@ -251,19 +265,18 @@ public class DeptServiceImpl implements DeptService {
private String generateDeptCode(Long parentId) {
Long effectiveParentId = normalizeParentId(parentId);
Long codeParentId = effectiveParentId;
String prefix = ROOT_CODE_PREFIX;
if (!DeptDO.PARENT_ID_ROOT.equals(effectiveParentId)) {
DeptDO parentDept = deptMapper.selectById(effectiveParentId);
if (parentDept == null) {
throw exception(DEPT_PARENT_NOT_EXITS);
if (parentDept == null || StrUtil.isBlank(parentDept.getCode())) {
codeParentId = DeptDO.PARENT_ID_ROOT;
} else {
prefix = parentDept.getCode();
}
if (StrUtil.isBlank(parentDept.getCode())) {
throw exception(DEPT_PARENT_CODE_NOT_INITIALIZED);
}
prefix = parentDept.getCode();
}
int nextSequence = determineNextSequence(effectiveParentId, prefix);
int nextSequence = determineNextSequence(codeParentId, prefix);
assertSequenceRange(nextSequence);
return prefix + formatSequence(nextSequence);
}

View File

@@ -17,5 +17,5 @@ public interface IWorkIntegrationErrorCodeConstants {
ErrorCode IWORK_OPERATOR_USER_MISSING = new ErrorCode(1_010_200_007, "缺少 iWork 操作人用户编号");
ErrorCode IWORK_WORKFLOW_ID_MISSING = new ErrorCode(1_010_200_008, "缺少 iWork 流程模板编号");
ErrorCode IWORK_ORG_IDENTIFIER_MISSING = new ErrorCode(1_010_200_009, "iWork 人力组织接口缺少认证标识");
ErrorCode IWORK_ORG_REMOTE_FAILED = new ErrorCode(1_010_200_010, "iWork 人力组织接口请求失败");
ErrorCode IWORK_ORG_REMOTE_FAILED = new ErrorCode(1_010_200_010, "iWork 人力组织接口请求失败{}");
}

View File

@@ -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);
}

View File

@@ -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<IWorkHrSubcompanyPageRespVO.Subcompany> data, SyncOptions options);
BatchResult syncDepartments(List<IWorkHrDepartmentPageRespVO.Department> data, SyncOptions options);
BatchResult syncJobTitles(List<IWorkHrJobTitlePageRespVO.JobTitle> data, SyncOptions options);
BatchResult syncUsers(List<IWorkHrUserPageRespVO.User> 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);
}
}

View File

@@ -0,0 +1,42 @@
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);
/**
* 仅同步部门
*/
IWorkFullSyncRespVO fullSyncDepartments(IWorkFullSyncReqVO reqVO);
/**
* 仅同步分部
*/
IWorkFullSyncRespVO fullSyncSubcompanies(IWorkFullSyncReqVO reqVO);
/**
* 仅同步岗位
*/
IWorkFullSyncRespVO fullSyncJobTitles(IWorkFullSyncReqVO reqVO);
/**
* 仅同步人员(会自动包含依赖的分部、部门)
*/
IWorkFullSyncRespVO fullSyncUsers(IWorkFullSyncReqVO reqVO);
/**
* 根据 iWork ID 进行单条同步
*/
IWorkSingleSyncRespVO syncSingle(IWorkSingleSyncReqVO reqVO);
}

View File

@@ -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<String, Object>> MAP_TYPE = new TypeReference<>() {
};
private static final TypeReference<List<IWorkHrSubcompanyPageRespVO.Subcompany>> SUBCOMPANY_LIST_TYPE =
new TypeReference<>() {
};
private static final TypeReference<List<IWorkHrDepartmentPageRespVO.Department>> DEPARTMENT_LIST_TYPE =
new TypeReference<>() {
};
private static final TypeReference<List<IWorkHrJobTitlePageRespVO.JobTitle>> JOB_TITLE_LIST_TYPE =
new TypeReference<>() {
};
private static final TypeReference<List<IWorkHrUserPageRespVO.User>> USER_LIST_TYPE =
new TypeReference<>() {
};
private static final TypeReference<List<IWorkHrSyncRespVO.SyncResult>> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> params) {
private JsonNode invokeParamsEndpoint(String path, Map<String, Object> params) {
Objects.requireNonNull(params, "查询参数不能为空");
Map<String, Object> 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<String, Object> 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<String, Object> payload) {
private JsonNode executeJson(String path, Map<String, Object> payload) {
// 统一封装请求体并发送 POST 调用
assertOrgConfigured(path);
Map<String, Object> body = new HashMap<>();
if (payload != null && !payload.isEmpty()) {
body.putAll(payload);
}
Map<String, Object> 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<String, String> 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 <T> List<T> readList(JsonNode parent, String field, TypeReference<List<T>> 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());
}

View File

@@ -0,0 +1,727 @@
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.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.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.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.*;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Service
@RequiredArgsConstructor
public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
private static final String JOB_CODE_PREFIX = "IWORK_JOB_";
private static final String DEFAULT_USER_PASSWORD = "Zgty@9527";
private static final int DEFAULT_SORT = 999;
private final DeptService deptService;
private final PostService postService;
private final PostMapper postMapper;
private final AdminUserService adminUserService;
private final AdminUserMapper adminUserMapper;
private final Map<String, PostDO> postCache = new ConcurrentHashMap<>();
@Override
public BatchResult syncSubcompanies(List<IWorkHrSubcompanyPageRespVO.Subcompany> data, SyncOptions options) {
List<IWorkHrSubcompanyPageRespVO.Subcompany> records = CollUtil.emptyIfNull(data);
BatchResult result = BatchResult.empty();
if (records.isEmpty()) {
return result;
}
result.increasePulled(records.size());
List<IWorkHrSubcompanyPageRespVO.Subcompany> queue = new ArrayList<>(records);
int guard = 0;
int maxPasses = Math.max(1, queue.size() * 2);
while (!queue.isEmpty() && guard++ < maxPasses) {
int processed = 0;
Iterator<IWorkHrSubcompanyPageRespVO.Subcompany> iterator = queue.iterator();
while (iterator.hasNext()) {
IWorkHrSubcompanyPageRespVO.Subcompany sub = iterator.next();
if (shouldSkipByCanceled(sub.getCanceled(), options)) {
logSkip("分部", sub.getId(), "iWork 标记为失效且当前不同步失效记录");
result.increaseSkipped();
iterator.remove();
continue;
}
Integer externalId = sub.getId();
if (externalId == null) {
log.warn("[iWork] 分部缺少标识,跳过:{}", sub.getSubcompanyname());
result.increaseFailed();
iterator.remove();
continue;
}
Long deptId = externalId.longValue();
ParentHolder parentHolder = resolveSubcompanyParent(sub.getSupsubcomid());
boolean canceled = isCanceledFlag(sub.getCanceled());
DeptSaveReqVO saveReq = buildSubcompanySaveReq(sub, deptId, parentHolder.parentId(), canceled);
try {
DeptSyncOutcome outcome = upsertDept(deptId,
saveReq,
canceled,
options);
applyDeptOutcome(result, outcome, "分部", sub.getSubcompanyname());
} catch (Exception ex) {
log.error("[iWork] 同步分部失败: id={} name={}", sub.getId(), 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.getId(), remaining.getSubcompanyname());
result.increaseFailed();
}
}
return result;
}
@Override
public BatchResult syncDepartments(List<IWorkHrDepartmentPageRespVO.Department> data, SyncOptions options) {
List<IWorkHrDepartmentPageRespVO.Department> records = CollUtil.emptyIfNull(data);
BatchResult result = BatchResult.empty();
if (records.isEmpty()) {
return result;
}
result.increasePulled(records.size());
List<IWorkHrDepartmentPageRespVO.Department> queue = new ArrayList<>(records);
int guard = 0;
int maxPasses = Math.max(1, queue.size() * 2);
while (!queue.isEmpty() && guard++ < maxPasses) {
int processed = 0;
Iterator<IWorkHrDepartmentPageRespVO.Department> iterator = queue.iterator();
while (iterator.hasNext()) {
IWorkHrDepartmentPageRespVO.Department dept = iterator.next();
if (shouldSkipByCanceled(dept.getCanceled(), options)) {
logSkip("部门", dept.getId(), "iWork 标记为失效且当前不同步失效记录");
result.increaseSkipped();
iterator.remove();
continue;
}
Integer externalId = dept.getId();
if (externalId == null) {
log.warn("[iWork] 部门缺少标识,跳过:{}", dept.getDepartmentname());
result.increaseFailed();
iterator.remove();
continue;
}
Long deptId = externalId.longValue();
ParentHolder parentHolder = resolveDepartmentParent(dept);
boolean canceled = isCanceledFlag(dept.getCanceled());
DeptSaveReqVO saveReq = buildDepartmentSaveReq(dept, deptId, parentHolder.parentId(), canceled);
try {
DeptSyncOutcome outcome = upsertDept(deptId,
saveReq,
canceled,
options);
applyDeptOutcome(result, outcome, "部门", dept.getDepartmentname());
} catch (Exception ex) {
log.error("[iWork] 同步部门失败: id={} name={}", dept.getId(), 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.getId(), remaining.getDepartmentname());
result.increaseFailed();
}
}
return result;
}
@Override
public BatchResult syncJobTitles(List<IWorkHrJobTitlePageRespVO.JobTitle> data, SyncOptions options) {
List<IWorkHrJobTitlePageRespVO.JobTitle> 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)) {
logSkip("岗位", job.getId(), "iWork 标记为失效且当前不同步失效记录");
result.increaseSkipped();
continue;
}
if (job.getId() == null) {
log.warn("[iWork] 岗位缺少标识,跳过:{}", job.getJobtitlename());
result.increaseFailed();
continue;
}
boolean canceled = isCanceledFlag(job.getCanceled());
Integer status = toStatus(canceled);
String code = buildJobCode(job.getId());
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.getId(), job.getJobtitlename(), ex);
result.increaseFailed();
result.withMessage("同步岗位失败: " + ex.getMessage());
}
}
return result;
}
@Override
public BatchResult syncUsers(List<IWorkHrUserPageRespVO.User> data, SyncOptions options) {
List<IWorkHrUserPageRespVO.User> 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());
String username = resolveUsername(user);
if (StrUtil.isBlank(username)) {
log.warn("[iWork] 人员缺少可用账号(工号={}, 登录账号={})跳过id={} name={}",
user.getWorkcode(), user.getLoginid(), 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());
}
Long postId = resolveUserPostId(user);
CommonStatusEnum status = inactive ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE;
AdminUserDO existing = adminUserMapper.selectByUsername(username);
UserSyncOutcome outcome;
if (existing == null) {
if (!options.isCreateIfMissing()) {
logSkip("人员", username, "系统未找到该账号且不同步缺失用户");
result.increaseSkipped();
continue;
}
outcome = createUser(user, username, deptId, postId, status);
} else {
if (!Objects.equals(existing.getUserSource(), UserSourceEnum.IWORK.getSource())) {
logSkip("人员", existing.getId(), "非 iWork 来源用户,保持原状");
result.increaseSkipped();
continue;
}
if (!options.isAllowUpdate()) {
logSkip("人员", existing.getId(), "当前策略禁止更新已存在用户");
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(Long deptId,
DeptSaveReqVO desired,
boolean disabled,
SyncOptions options) {
if (deptId == null) {
return new DeptSyncOutcome(SyncAction.SKIPPED, false, null);
}
DeptDO existing = deptService.getDept(deptId);
if (existing == null) {
if (!options.isCreateIfMissing()) {
logSkip("部门", deptId, "当前策略禁止创建缺失部门");
return new DeptSyncOutcome(SyncAction.SKIPPED, false, null);
}
desired.setId(deptId);
Long createdId = deptService.createDept(desired);
return new DeptSyncOutcome(SyncAction.CREATED, CommonStatusEnum.isDisable(desired.getStatus()), createdId);
}
if (!Objects.equals(existing.getDeptSource(), DeptSourceEnum.IWORK.getSource())) {
logSkip("部门", existing.getId(), "来源非 iWork保持原状");
return new DeptSyncOutcome(SyncAction.SKIPPED, false, existing.getId());
}
if (!options.isAllowUpdate()) {
logSkip("部门", existing.getId(), "当前策略禁止更新已存在部门");
return new DeptSyncOutcome(SyncAction.SKIPPED, false, existing.getId());
}
desired.setId(existing.getId());
mergeDeptDefaults(desired, existing);
boolean disabledChanged = disabled && CommonStatusEnum.isEnable(existing.getStatus());
deptService.updateDept(desired);
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()) {
logSkip("岗位", job.getId(), "当前策略禁止创建缺失岗位");
return new JobSyncOutcome(SyncAction.SKIPPED, false, null);
}
PostSaveReqVO createReq = new PostSaveReqVO();
Long desiredId = job.getId() == null ? null : job.getId().longValue();
if (desiredId != null) {
createReq.setId(desiredId);
}
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);
Long effectivePostId = desiredId != null ? desiredId : postId;
PostDO created = new PostDO();
created.setId(effectivePostId);
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, effectivePostId);
}
if (!options.isAllowUpdate()) {
logSkip("岗位", existing.getId(), "当前策略禁止更新已存在岗位");
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);
Long desiredUserId = source.getId() == null ? null : source.getId().longValue();
if (desiredUserId != null) {
req.setId(desiredUserId);
}
req.setPassword(DEFAULT_USER_PASSWORD);
req.setUserSource(UserSourceEnum.IWORK.getSource());
Long userId = adminUserService.createUser(req);
Long effectiveUserId = desiredUserId != null ? desiredUserId : userId;
return new UserSyncOutcome(SyncAction.CREATED, CommonStatusEnum.isDisable(req.getStatus()), effectiveUserId);
}
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 deptId,
Long parentId,
boolean canceled) {
DeptSaveReqVO req = new DeptSaveReqVO();
req.setId(deptId);
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 deptId,
Long parentId,
boolean canceled) {
DeptSaveReqVO req = new DeptSaveReqVO();
req.setId(deptId);
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));
if (deptId != null) {
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());
req.setSkipAssociationValidation(true);
req.setSkipMobileValidation(true);
req.setSkipEmailValidation(true);
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);
}
return new ParentHolder(parentExternalId.longValue());
}
private ParentHolder resolveDepartmentParent(IWorkHrDepartmentPageRespVO.Department dept) {
Long parentDeptId = toLong(dept.getSupdepid());
if (parentDeptId != null && parentDeptId > 0) {
return new ParentHolder(parentDeptId);
}
Long subcompanyId = toLong(dept.getSubcompanyid1());
if (subcompanyId != null) {
return new ParentHolder(subcompanyId);
}
return new ParentHolder(DeptDO.PARENT_ID_ROOT);
}
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) {
Long deptId = toLong(user.getDepartmentid());
if (deptId != null) {
return deptId;
}
Long subcompanyId = toLong(user.getSubcompanyid1());
if (subcompanyId != null) {
return subcompanyId;
}
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) {
if (StrUtil.isBlank(sexFlag)) {
return SexEnum.UNKNOWN.getSex();
}
Integer external = parseInteger(sexFlag);
if (external != null) {
Integer converted = SyncVerifyUtil.convertExternalToInternal(external);
return converted != null ? converted : SexEnum.UNKNOWN.getSex();
}
String normalized = sexFlag.trim();
if (isMaleFlag(normalized)) {
return SexEnum.MALE.getSex();
}
if (isFemaleFlag(normalized)) {
return SexEnum.FEMALE.getSex();
}
return SexEnum.UNKNOWN.getSex();
}
private boolean isMaleFlag(String value) {
return "".equals(value) || "M".equalsIgnoreCase(value) || "MALE".equalsIgnoreCase(value);
}
private boolean isFemaleFlag(String value) {
return "".equals(value) || "F".equalsIgnoreCase(value) || "FEMALE".equalsIgnoreCase(value);
}
private Integer parseInteger(String raw) {
if (StrUtil.isBlank(raw)) {
return null;
}
try {
return Integer.parseInt(raw.trim());
} catch (NumberFormatException ex) {
return null;
}
}
private Long toLong(Integer value) {
return value == null ? null : value.longValue();
}
private String resolveUsername(IWorkHrUserPageRespVO.User user) {
String candidate = sanitizeUsername(user.getWorkcode());
if (candidate == null) {
candidate = sanitizeUsername(user.getLoginid());
}
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<Long> singletonSet(Long value) {
Set<Long> 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 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 void logSkip(String entityLabel, Object identifier, String reason) {
if (log.isInfoEnabled()) {
log.info("[iWork] {}[id={}] 跳过:{}", entityLabel, identifier, reason);
}
}
private record ParentHolder(Long parentId) {
}
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) {
}
}

View File

@@ -0,0 +1,316 @@
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.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.*;
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.*;
import static com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationErrorCodeConstants.IWORK_ORG_REMOTE_FAILED;
/**
* iWork 同步服务实现
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class IWorkSyncServiceImpl implements IWorkSyncService {
private final IWorkOrgRestService orgRestService;
private final IWorkSyncProcessor syncProcessor;
@Override
public IWorkFullSyncRespVO fullSync(IWorkFullSyncReqVO reqVO) {
return runFullSync(reqVO, reqVO.resolveScopes());
}
@Override
public IWorkFullSyncRespVO fullSyncDepartments(IWorkFullSyncReqVO reqVO) {
return runFullSync(reqVO, EnumSet.of(IWorkSyncEntityTypeEnum.DEPARTMENT));
}
@Override
public IWorkFullSyncRespVO fullSyncSubcompanies(IWorkFullSyncReqVO reqVO) {
return runFullSync(reqVO, EnumSet.of(IWorkSyncEntityTypeEnum.SUBCOMPANY));
}
@Override
public IWorkFullSyncRespVO fullSyncJobTitles(IWorkFullSyncReqVO reqVO) {
return runFullSync(reqVO, EnumSet.of(IWorkSyncEntityTypeEnum.JOB_TITLE));
}
@Override
public IWorkFullSyncRespVO fullSyncUsers(IWorkFullSyncReqVO reqVO) {
return runFullSync(reqVO, EnumSet.of(IWorkSyncEntityTypeEnum.USER));
}
private IWorkFullSyncRespVO runFullSync(IWorkFullSyncReqVO reqVO, Set<IWorkSyncEntityTypeEnum> scopes) {
IWorkFullSyncRespVO respVO = new IWorkFullSyncRespVO();
respVO.setPageSize(reqVO.getPageSize());
List<IWorkSyncBatchStatVO> batchStats = new ArrayList<>();
respVO.setBatches(batchStats);
boolean syncUsers = scopes.contains(IWorkSyncEntityTypeEnum.USER);
boolean syncDepartments = scopes.contains(IWorkSyncEntityTypeEnum.DEPARTMENT);
boolean syncSubcompanies = scopes.contains(IWorkSyncEntityTypeEnum.SUBCOMPANY);
boolean syncJobTitle = scopes.contains(IWorkSyncEntityTypeEnum.JOB_TITLE);
int processedPages = 0;
IWorkSyncProcessor.SyncOptions options = buildFullSyncOptions(reqVO);
if (syncSubcompanies) {
processedPages += executeSubcompanyFullSync(reqVO, options, respVO.getSubcompanyStat(), batchStats);
}
if (syncDepartments) {
processedPages += executeDepartmentFullSync(reqVO, options, respVO.getDepartmentStat(), batchStats);
}
if (syncJobTitle) {
processedPages += executeJobTitleFullSync(reqVO, options, respVO.getJobTitleStat(), batchStats);
}
if (syncUsers) {
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<IWorkSyncBatchStatVO> batches) {
return executePaged(reqVO, IWorkSyncEntityTypeEnum.SUBCOMPANY, batches, (page, pageSize) -> {
IWorkSubcompanyQueryReqVO query = new IWorkSubcompanyQueryReqVO();
query.setCurpage(page);
query.setPagesize(pageSize);
IWorkHrSubcompanyPageRespVO pageResp = orgRestService.listSubcompanies(query);
ensureIWorkSuccess("拉取分部", pageResp.isSuccess(), pageResp.getMessage());
List<IWorkHrSubcompanyPageRespVO.Subcompany> 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<IWorkSyncBatchStatVO> batches) {
return executePaged(reqVO, IWorkSyncEntityTypeEnum.DEPARTMENT, batches, (page, pageSize) -> {
IWorkDepartmentQueryReqVO query = new IWorkDepartmentQueryReqVO();
query.setCurpage(page);
query.setPagesize(pageSize);
IWorkHrDepartmentPageRespVO pageResp = orgRestService.listDepartments(query);
ensureIWorkSuccess("拉取部门", pageResp.isSuccess(), pageResp.getMessage());
List<IWorkHrDepartmentPageRespVO.Department> 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<IWorkSyncBatchStatVO> 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);
ensureIWorkSuccess("拉取岗位", pageResp.isSuccess(), pageResp.getMessage());
List<IWorkHrJobTitlePageRespVO.JobTitle> 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<IWorkSyncBatchStatVO> batches) {
return executePaged(reqVO, IWorkSyncEntityTypeEnum.USER, batches, (page, pageSize) -> {
IWorkUserQueryReqVO query = new IWorkUserQueryReqVO();
query.setCurpage(page);
query.setPagesize(pageSize);
IWorkHrUserPageRespVO pageResp = orgRestService.listUsers(query);
ensureIWorkSuccess("拉取人员", pageResp.isSuccess(), pageResp.getMessage());
List<IWorkHrUserPageRespVO.User> 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().toString());
if (data == null) {
markNotFound(respVO, "人员");
return;
}
IWorkSyncProcessor.BatchResult result = syncProcessor.syncUser(data, buildSingleOptions(reqVO));
populateSingleResult(respVO, result);
}
private int executePaged(IWorkFullSyncReqVO reqVO,
IWorkSyncEntityTypeEnum type,
List<IWorkSyncBatchStatVO> 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);
ensureIWorkSuccess("获取分部详情", pageResp.isSuccess(), pageResp.getMessage());
return CollUtil.getFirst(pageResp.getDataList());
}
private IWorkHrDepartmentPageRespVO.Department fetchSingleDepartment(Long entityId) {
IWorkDepartmentQueryReqVO query = new IWorkDepartmentQueryReqVO();
query.setCurpage(1);
query.setPagesize(1);
query.setParams(Collections.singletonMap("departmentid", entityId));
IWorkHrDepartmentPageRespVO pageResp = orgRestService.listDepartments(query);
ensureIWorkSuccess("获取部门详情", pageResp.isSuccess(), pageResp.getMessage());
return CollUtil.getFirst(pageResp.getDataList());
}
private IWorkHrJobTitlePageRespVO.JobTitle fetchSingleJob(Long entityId) {
IWorkJobTitleQueryReqVO query = new IWorkJobTitleQueryReqVO();
query.setCurpage(1);
query.setPagesize(1);
query.setParams(Collections.singletonMap("jobtitleid", entityId));
IWorkHrJobTitlePageRespVO pageResp = orgRestService.listJobTitles(query);
ensureIWorkSuccess("获取岗位详情", pageResp.isSuccess(), pageResp.getMessage());
return CollUtil.getFirst(pageResp.getDataList());
}
private IWorkHrUserPageRespVO.User fetchSingleUser(String entityId) {
IWorkUserQueryReqVO query = new IWorkUserQueryReqVO();
query.setCurpage(1);
query.setPagesize(1);
query.setParams(Collections.singletonMap("id", entityId));
IWorkHrUserPageRespVO pageResp = orgRestService.listUsers(query);
ensureIWorkSuccess("获取人员详情", pageResp.isSuccess(), pageResp.getMessage());
return CollUtil.getFirst(pageResp.getDataList());
}
private void ensureIWorkSuccess(String action, boolean success, String remoteMessage) {
if (success) {
return;
}
String message = StrUtil.blankToDefault(remoteMessage, StrUtil.format("{}iWork 返回失败", action));
throw ServiceExceptionUtil.exception(IWORK_ORG_REMOTE_FAILED, message);
}
@FunctionalInterface
private interface PageExecutor {
BatchExecution execute(int pageNumber, int pageSize);
}
private record BatchExecution(IWorkSyncProcessor.BatchResult result, int totalPulled) {
}
}

View File

@@ -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<Integer> {
@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);
}
}

View File

@@ -94,6 +94,7 @@ public class SmsSendServiceImpl implements SmsSendService {
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams);
// 发送 MQ 消息,异步执行发送短信
@@ -183,7 +184,6 @@ public class SmsSendServiceImpl implements SmsSendService {
if (CollUtil.isEmpty(receiveResults)) {
return;
}
// 更新短信日志的接收结果. 因为量一般不大,所以先使用 for 循环更新
receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(result.getLogId(),
result.getSuccess(), result.getReceiveTime(), result.getErrorCode(), result.getErrorMsg()));
}

View File

@@ -108,7 +108,8 @@ public class AdminUserServiceImpl implements AdminUserService {
});
// 1.2 校验正确性
validateUserForCreateOrUpdate(null, createReqVO.getUsername(),
createReqVO.getMobile(), createReqVO.getEmail(), createReqVO.getDeptIds(), createReqVO.getPostIds());
createReqVO.getMobile(), createReqVO.getEmail(), createReqVO.getDeptIds(), createReqVO.getPostIds(),
createReqVO.isSkipAssociationValidation(), createReqVO.isSkipMobileValidation(), createReqVO.isSkipEmailValidation());
// 2.1 插入用户
AdminUserDO user = BeanUtils.toBean(createReqVO, AdminUserDO.class);
user.setStatus(CommonStatusEnum.ENABLE.getStatus());
@@ -154,7 +155,7 @@ public class AdminUserServiceImpl implements AdminUserService {
}
});
// 1.3 校验正确性
validateUserForCreateOrUpdate(null, registerReqVO.getUsername(), null, null, null, null);
validateUserForCreateOrUpdate(null, registerReqVO.getUsername(), null, null, null, null, false, false, false);
// 2. 插入用户
AdminUserDO user = BeanUtils.toBean(registerReqVO, AdminUserDO.class);
@@ -173,7 +174,8 @@ public class AdminUserServiceImpl implements AdminUserService {
updateReqVO.setPassword(null); // 特殊:此处不更新密码
// 1. 校验正确性
AdminUserDO oldUser = validateUserForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getUsername(),
updateReqVO.getMobile(), updateReqVO.getEmail(), updateReqVO.getDeptIds(), updateReqVO.getPostIds());
updateReqVO.getMobile(), updateReqVO.getEmail(), updateReqVO.getDeptIds(),
updateReqVO.getPostIds(), updateReqVO.isSkipAssociationValidation(), updateReqVO.isSkipMobileValidation(), updateReqVO.isSkipEmailValidation());
// 2.1 只更新非空字段
AdminUserDO updateObj = new AdminUserDO();
@@ -512,7 +514,8 @@ public class AdminUserServiceImpl implements AdminUserService {
}
private AdminUserDO validateUserForCreateOrUpdate(Long id, String username, String mobile, String email,
Set<Long> deptIds, Set<Long> postIds) {
Set<Long> deptIds, Set<Long> postIds, boolean skipAssociationValidation,
boolean skipMobileValidation, boolean skipEmailValidation) {
// 关闭数据权限,避免因为没有数据权限,查询不到数据,进而导致唯一校验不正确
return DataPermissionUtils.executeIgnore(() -> {
// 校验用户存在
@@ -520,13 +523,17 @@ public class AdminUserServiceImpl implements AdminUserService {
// 校验用户名唯一 - 注释掉,允许用户名重复
// validateUsernameUnique(id, username);
// 校验手机号唯一
validateMobileUnique(id, mobile);
if (!skipMobileValidation) {
validateMobileUnique(id, mobile);
}
// 校验邮箱唯一
validateEmailUnique(id, email);
// 校验部门处于开启状态
deptService.validateDeptList(deptIds);
if (!skipEmailValidation) {
validateEmailUnique(id, email);
}
// 校验岗位处于开启状态
postService.validatePostList(postIds);
if (!skipAssociationValidation) {
postService.validatePostList(postIds);
}
return user;
});
}

View File

@@ -31,7 +31,7 @@
<!-- 启动服务时,是否清理历史日志,一般不建议清理 -->
<cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
<!-- 日志文件,到达多少容量,进行滚动 -->
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}</maxFileSize>
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-50MB}</maxFileSize>
<!-- 日志文件的总大小0 表示不限制 -->
<totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
<!-- 日志文件的保留天数 -->
@@ -56,14 +56,21 @@
</encoder>
</appender>
<!--logback的日志级别 FATAL > ERROR > WARN > INFO > DEBUG-->
<!-- 本地环境 -->
<springProfile name="local">
<root level="INFO">
<springProfile name="local,dev">
<root level="WARN">
<appender-ref ref="STDOUT"/>
<appender-ref ref="GRPC"/> <!-- 本地环境下,如果不想接入 SkyWalking 日志服务,可以注释掉本行 -->
<appender-ref ref="ASYNC"/> <!-- 本地环境下,如果不想打印日志,可以注释掉本行 -->
</root>
<!--针对不同的业务路径,配置dao层的sql打印日志级别为DEBUG-->
<logger name="com.zt.plat.module.system.dal.mysql" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
</springProfile>
<!-- 其它环境 -->
<springProfile name="dev,test,stage,prod,default">
<root level="INFO">

View File

@@ -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());