Merge remote-tracking branch 'base-version/main' into dev

# Conflicts:
#	zt-dependencies/pom.xml
This commit is contained in:
chenbowen
2025-11-20 18:35:03 +08:00
27 changed files with 2040 additions and 180 deletions

View File

@@ -67,10 +67,10 @@
<artifactId>zt-spring-boot-starter-redis</artifactId>
</dependency>
<!-- Reactive HTTP client for iWork integration -->
<!-- HTTP client for iWork integration -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
<dependency>

View File

@@ -1,12 +1,19 @@
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.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.service.integration.iwork.IWorkIntegrationService;
import com.zt.plat.module.system.service.integration.iwork.IWorkOrgRestService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
@@ -30,6 +37,7 @@ import static com.zt.plat.framework.common.pojo.CommonResult.success;
public class IWorkIntegrationController {
private final IWorkIntegrationService integrationService;
private final IWorkOrgRestService orgRestService;
@PostMapping("/user/resolve")
@Operation(summary = "根据外部标识获取 iWork 用户编号")
@@ -48,4 +56,54 @@ public class IWorkIntegrationController {
public CommonResult<IWorkOperationRespVO> voidWorkflow(@Valid @RequestBody IWorkWorkflowVoidReqVO reqVO) {
return success(integrationService.voidWorkflow(reqVO));
}
// ----------------- 人力组织接口 -----------------
@PostMapping("/hr/subcompany/page")
@Operation(summary = "获取 iWork 分部列表")
public CommonResult<IWorkOrgRespVO> 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) {
return success(orgRestService.listDepartments(reqVO));
}
@PostMapping("/hr/job-title/page")
@Operation(summary = "获取 iWork 岗位列表")
public CommonResult<IWorkOrgRespVO> 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) {
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/department/sync")
@Operation(summary = "同步部门信息至 iWork")
public CommonResult<IWorkOrgRespVO> syncDepartments(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) {
return success(orgRestService.syncDepartments(reqVO));
}
@PostMapping("/hr/job-title/sync")
@Operation(summary = "同步岗位信息至 iWork")
public CommonResult<IWorkOrgRespVO> syncJobTitles(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) {
return success(orgRestService.syncJobTitles(reqVO));
}
@PostMapping("/hr/user/sync")
@Operation(summary = "同步人员信息至 iWork")
public CommonResult<IWorkOrgRespVO> syncUsers(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) {
return success(orgRestService.syncUsers(reqVO));
}
}

View File

@@ -0,0 +1,24 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* iWork 部门查询参数。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class IWorkDepartmentQueryReqVO extends IWorkOrgBaseQueryReqVO {
@Schema(description = "部门编码")
private String departmentCode;
@Schema(description = "部门名称")
private String departmentName;
@Schema(description = "所属分部ID")
private String subcompanyId;
}

View File

@@ -0,0 +1,21 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* iWork 岗位查询参数。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class IWorkJobTitleQueryReqVO extends IWorkOrgBaseQueryReqVO {
@Schema(description = "岗位编码")
private String jobTitleCode;
@Schema(description = "岗位名称")
private String jobTitleName;
}

View File

@@ -0,0 +1,22 @@
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 组织查询基础参数。
*/
@Data
public class IWorkOrgBaseQueryReqVO {
@Schema(description = "当前页码", example = "1")
private Integer curpage;
@Schema(description = "每页条数", example = "10")
private Integer pagesize;
@Schema(description = "查询参数(扩展用),将被序列化为 params 传给 iWork")
private Map<String, Object> params;
}

View File

@@ -0,0 +1,73 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.Map;
/**
* 查询 iWork 人力组织信息所需的参数。
*/
@Data
public class IWorkOrgQueryReqVO {
@Schema(description = "当前页码", example = "1")
private Integer curpage;
@Schema(description = "每页条数", example = "10")
private Integer pagesize;
// ================= 分部查询 =================
@Schema(description = "分部编码")
private String subcompanyCode;
@Schema(description = "分部名称")
private String subcompanyName;
// ================= 部门查询 =================
@Schema(description = "部门编码")
private String departmentCode;
@Schema(description = "部门名称")
private String departmentName;
@Schema(description = "所属分部ID")
private String subcompanyId;
// ================= 岗位查询 =================
@Schema(description = "岗位编码")
private String jobTitleCode;
@Schema(description = "岗位名称")
private String jobTitleName;
// ================= 人员查询 =================
@Schema(description = "人员工号")
private String workCode;
@Schema(description = "人员姓名")
private String lastName;
@Schema(description = "所属部门ID")
private String departmentId;
@Schema(description = "所属岗位ID")
private String jobTitleId;
@Schema(description = "人员状态 (0:试用, 1:正式, 2:临时, 3:试用延期, 4:解聘, 5:离职, 6:退休, 7:无效)")
private String status;
@Schema(description = "手机号")
private String mobile;
@Schema(description = "邮箱")
private String email;
@Schema(description = "查询参数(扩展用),将被序列化为 params 传给 iWork")
private Map<String, Object> params;
}

View File

@@ -0,0 +1,28 @@
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 请求的响应封装。
*/
@Data
public 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;
}

View File

@@ -0,0 +1,19 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* 同步 iWork 人力组织信息的请求。
*/
@Data
public class IWorkOrgSyncReqVO {
@Schema(description = "同步数据集合,将被序列化为 data 传给 iWork", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "同步数据不能为空")
private List<Map<String, Object>> data;
}

View File

@@ -0,0 +1,21 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* iWork 分部查询参数。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class IWorkSubcompanyQueryReqVO extends IWorkOrgBaseQueryReqVO {
@Schema(description = "分部编码")
private String subcompanyCode;
@Schema(description = "分部名称")
private String subcompanyName;
}

View File

@@ -0,0 +1,39 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* iWork 人员查询参数。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class IWorkUserQueryReqVO extends IWorkOrgBaseQueryReqVO {
@Schema(description = "人员工号")
private String workCode;
@Schema(description = "人员姓名")
private String lastName;
@Schema(description = "所属部门ID")
private String departmentId;
@Schema(description = "所属分部ID")
private String subcompanyId;
@Schema(description = "所属岗位ID")
private String jobTitleId;
@Schema(description = "人员状态 (0:试用, 1:正式, 2:临时, 3:试用延期, 4:解聘, 5:离职, 6:退休, 7:无效)")
private String status;
@Schema(description = "手机号")
private String mobile;
@Schema(description = "邮箱")
private String email;
}

View File

@@ -19,11 +19,6 @@ import java.time.Duration;
@ConfigurationProperties(prefix = "iwork")
public class IWorkProperties {
/**
* 是否开启 iWork 集成能力。
*/
private boolean enabled = false;
/**
* iWork 网关的基础地址。
*/
@@ -56,6 +51,8 @@ public class IWorkProperties {
private final Token token = new Token();
@Valid
private final Client client = new Client();
@Valid
private final OrgRest org = new OrgRest();
@Data
public static class Paths {
@@ -123,4 +120,26 @@ public class IWorkProperties {
@NotNull(message = "iWork 客户端响应超时时间不能为空")
private Duration responseTimeout;
}
@Data
public static class OrgRest {
/**
* 认证所需的标识(与 iWork 约定)。
*/
private String tokenSeed;
@Valid
private final OrgPaths paths = new OrgPaths();
}
@Data
public static class OrgPaths {
private String subcompanyPage;
private String departmentPage;
private String jobTitlePage;
private String userPage;
private String syncSubcompany;
private String syncDepartment;
private String syncJobTitle;
private String syncUser;
}
}

View File

@@ -7,15 +7,15 @@ import com.zt.plat.framework.common.exception.ErrorCode;
*/
public interface IWorkIntegrationErrorCodeConstants {
ErrorCode IWORK_DISABLED = new ErrorCode(1_010_200_001, "iWork 集成未启用,请先完成配置");
ErrorCode IWORK_BASE_URL_MISSING = new ErrorCode(1_010_200_002, "iWork 集成未配置网关地址");
ErrorCode IWORK_CONFIGURATION_INVALID = new ErrorCode(1_010_200_003,
ErrorCode IWORK_BASE_URL_MISSING = new ErrorCode(1_010_200_001, "iWork 集成未配置网关地址");
ErrorCode IWORK_CONFIGURATION_INVALID = new ErrorCode(1_010_200_002,
"iWork 集成缺少必填配置appId/clientPublicKey/userId/workflowId");
ErrorCode IWORK_REGISTER_FAILED = new ErrorCode(1_010_200_004, "iWork 注册授权失败");
ErrorCode IWORK_APPLY_TOKEN_FAILED = new ErrorCode(1_010_200_005, "iWork 令牌申请失败");
ErrorCode IWORK_REMOTE_REQUEST_FAILED = new ErrorCode(1_010_200_006, "iWork 接口请求失败");
ErrorCode IWORK_USER_IDENTIFIER_MISSING = new ErrorCode(1_010_200_007, "缺少用户识别信息,无法调用 iWork 接口");
ErrorCode IWORK_OPERATOR_USER_MISSING = new ErrorCode(1_010_200_008, "缺少 iWork 操作人用户编号");
ErrorCode IWORK_WORKFLOW_ID_MISSING = new ErrorCode(1_010_200_009, "缺少 iWork 流程模板编号");
ErrorCode IWORK_REGISTER_FAILED = new ErrorCode(1_010_200_003, "iWork 注册授权失败");
ErrorCode IWORK_APPLY_TOKEN_FAILED = new ErrorCode(1_010_200_004, "iWork 令牌申请失败");
ErrorCode IWORK_REMOTE_REQUEST_FAILED = new ErrorCode(1_010_200_005, "iWork 接口请求失败");
ErrorCode IWORK_USER_IDENTIFIER_MISSING = new ErrorCode(1_010_200_006, "缺少用户识别信息,无法调用 iWork 接口");
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 人力组织接口请求失败");
}

View File

@@ -0,0 +1,30 @@
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.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;
/**
* iWork 人力组织 REST 接口门面。
*/
public interface IWorkOrgRestService {
IWorkOrgRespVO listSubcompanies(IWorkSubcompanyQueryReqVO reqVO);
IWorkOrgRespVO listDepartments(IWorkDepartmentQueryReqVO reqVO);
IWorkOrgRespVO listJobTitles(IWorkJobTitleQueryReqVO reqVO);
IWorkOrgRespVO listUsers(IWorkUserQueryReqVO reqVO);
IWorkOrgRespVO syncSubcompanies(IWorkOrgSyncReqVO reqVO);
IWorkOrgRespVO syncDepartments(IWorkOrgSyncReqVO reqVO);
IWorkOrgRespVO syncJobTitles(IWorkOrgSyncReqVO reqVO);
IWorkOrgRespVO syncUsers(IWorkOrgSyncReqVO reqVO);
}

View File

@@ -23,21 +23,25 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import javax.crypto.Cipher;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.Collections;
@@ -58,24 +62,24 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {
};
private static final okhttp3.MediaType JSON_MEDIA_TYPE = okhttp3.MediaType.get("application/json; charset=UTF-8");
private final IWorkProperties properties;
private final ObjectMapper objectMapper;
private final WebClient.Builder webClientBuilder;
private final Cache<SessionKey, IWorkSession> sessionCache = Caffeine.newBuilder()
.maximumSize(256)
.build();
private final Cache<String, PublicKey> publicKeyCache = Caffeine.newBuilder()
private final Cache<String, PublicKey> publicKeyCache = Caffeine.newBuilder()
.maximumSize(64)
.build();
private volatile WebClient cachedWebClient;
private volatile OkHttpClient cachedHttpClient;
@Override
public IWorkUserInfoRespVO resolveUserId(IWorkUserInfoReqVO reqVO) {
assertEnabled();
assertConfigured();
String appId = resolveAppId();
String clientPublicKey = resolveClientPublicKey();
String operatorUserId = resolveOperatorUserId(reqVO.getOperatorUserId());
@@ -90,7 +94,7 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
@Override
public IWorkOperationRespVO createWorkflow(IWorkWorkflowCreateReqVO reqVO) {
assertEnabled();
assertConfigured();
String appId = resolveAppId();
String clientPublicKey = resolveClientPublicKey();
String operatorUserId = resolveOperatorUserId(reqVO.getOperatorUserId());
@@ -104,7 +108,7 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
@Override
public IWorkOperationRespVO voidWorkflow(IWorkWorkflowVoidReqVO reqVO) {
assertEnabled();
assertConfigured();
String appId = resolveAppId();
String clientPublicKey = resolveClientPublicKey();
String operatorUserId = resolveOperatorUserId(reqVO.getOperatorUserId());
@@ -119,10 +123,7 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
return buildOperationResponse(responseBody);
}
private void assertEnabled() {
if (!properties.isEnabled()) {
throw ServiceExceptionUtil.exception(IWORK_DISABLED);
}
private void assertConfigured() {
if (!StringUtils.hasText(properties.getBaseUrl())) {
throw ServiceExceptionUtil.exception(IWORK_BASE_URL_MISSING);
}
@@ -193,25 +194,13 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
private RegistrationResult register(String appId, String clientPublicKey) {
String responseBody;
try {
responseBody = webClient()
.post()
.uri(properties.getPaths().getRegister())
.headers(headers -> {
headers.set(properties.getHeaders().getAppId(), appId);
headers.set(properties.getHeaders().getClientPublicKey(), clientPublicKey);
})
.retrieve()
.bodyToMono(String.class)
.block();
} catch (WebClientResponseException ex) {
log.error("[iWork] register failed. status={}, body={}", ex.getStatusCode(), ex.getResponseBodyAsString(), ex);
throw ServiceExceptionUtil.exception(IWORK_REGISTER_FAILED, ex.getStatusCode().value(), ex.getResponseBodyAsString());
} catch (Exception ex) {
log.error("[iWork] register failed", ex);
throw ServiceExceptionUtil.exception(IWORK_REGISTER_FAILED, ex.getMessage());
}
Request request = new Request.Builder()
.url(resolveUrl(properties.getPaths().getRegister()))
.header(properties.getHeaders().getAppId(), appId)
.header(properties.getHeaders().getClientPublicKey(), clientPublicKey)
.post(RequestBody.create(null, new byte[0]))
.build();
String responseBody = executeRequest(request, IWORK_REGISTER_FAILED);
JsonNode node = parseJson(responseBody, IWORK_REGISTER_FAILED);
String secret = textValue(node, "secret");
String spk = textValue(node, "spk");
@@ -222,26 +211,14 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
private String applyToken(String appId, String encryptedSecret) {
String responseBody;
try {
responseBody = webClient()
.post()
.uri(properties.getPaths().getApplyToken())
.headers(headers -> {
headers.set(properties.getHeaders().getAppId(), appId);
headers.set(properties.getHeaders().getSecret(), encryptedSecret);
headers.set(properties.getHeaders().getTime(), String.valueOf(properties.getToken().getTtlSeconds()));
})
.retrieve()
.bodyToMono(String.class)
.block();
} catch (WebClientResponseException ex) {
log.error("[iWork] apply token failed. status={}, body={}", ex.getStatusCode(), ex.getResponseBodyAsString(), ex);
throw ServiceExceptionUtil.exception(IWORK_APPLY_TOKEN_FAILED, ex.getStatusCode().value(), ex.getResponseBodyAsString());
} catch (Exception ex) {
log.error("[iWork] apply token failed", ex);
throw ServiceExceptionUtil.exception(IWORK_APPLY_TOKEN_FAILED, ex.getMessage());
}
Request request = new Request.Builder()
.url(resolveUrl(properties.getPaths().getApplyToken()))
.header(properties.getHeaders().getAppId(), appId)
.header(properties.getHeaders().getSecret(), encryptedSecret)
.header(properties.getHeaders().getTime(), String.valueOf(properties.getToken().getTtlSeconds()))
.post(RequestBody.create(null, new byte[0]))
.build();
String responseBody = executeRequest(request, IWORK_APPLY_TOKEN_FAILED);
JsonNode node = parseJson(responseBody, IWORK_APPLY_TOKEN_FAILED);
String token = textValue(node, "token");
if (!StringUtils.hasText(token)) {
@@ -255,64 +232,54 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
String appId,
IWorkSession session,
Map<String, Object> payload) {
try {
return webClient()
.post()
.uri(uriBuilder -> {
uriBuilder.path(path);
if (queryParams != null) {
queryParams.forEach((key, value) -> {
if (value != null) {
uriBuilder.queryParam(key, value);
}
});
}
return uriBuilder.build();
})
.headers(headers -> setAuthHeaders(headers, appId, session))
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(payload == null ? Collections.emptyMap() : payload)
.retrieve()
.bodyToMono(String.class)
.block();
} catch (WebClientResponseException ex) {
log.error("[iWork] request {} failed. status={}, body={}", path, ex.getStatusCode(), ex.getResponseBodyAsString(), ex);
throw ServiceExceptionUtil.exception(IWORK_REMOTE_REQUEST_FAILED, ex.getStatusCode().value(), ex.getResponseBodyAsString());
} catch (Exception ex) {
log.error("[iWork] request {} failed", path, ex);
throw ServiceExceptionUtil.exception(IWORK_REMOTE_REQUEST_FAILED, ex.getMessage());
HttpUrl baseUrl = HttpUrl.parse(resolveUrl(path));
if (baseUrl == null) {
throw ServiceExceptionUtil.exception(IWORK_REMOTE_REQUEST_FAILED, "非法的 URL");
}
HttpUrl.Builder urlBuilder = baseUrl.newBuilder();
if (queryParams != null) {
queryParams.forEach((key, value) -> {
if (value != null) {
urlBuilder.addQueryParameter(key, String.valueOf(value));
}
});
}
String jsonPayload = toJsonString(payload == null ? Collections.emptyMap() : payload);
RequestBody body = RequestBody.create(JSON_MEDIA_TYPE, jsonPayload);
Headers headers = authHeaders(appId, session)
.set("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.build();
Request request = new Request.Builder()
.url(urlBuilder.build())
.headers(headers)
.post(body)
.build();
return executeRequest(request, IWORK_REMOTE_REQUEST_FAILED);
}
private String executeFormRequest(String path,
String appId,
IWorkSession session,
MultiValueMap<String, String> formData) {
try {
return webClient()
.post()
.uri(path)
.headers(headers -> setAuthHeaders(headers, appId, session))
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData(formData))
.retrieve()
.bodyToMono(String.class)
.block();
} catch (WebClientResponseException ex) {
log.error("[iWork] form request {} failed. status={}, body={}", path, ex.getStatusCode(), ex.getResponseBodyAsString(), ex);
throw ServiceExceptionUtil.exception(IWORK_REMOTE_REQUEST_FAILED, ex.getStatusCode().value(), ex.getResponseBodyAsString());
} catch (Exception ex) {
log.error("[iWork] form request {} failed", path, ex);
throw ServiceExceptionUtil.exception(IWORK_REMOTE_REQUEST_FAILED, ex.getMessage());
}
FormBody.Builder builder = new FormBody.Builder();
formData.forEach((key, values) -> {
if (values != null) {
values.forEach(value -> builder.add(key, value));
}
});
Request request = new Request.Builder()
.url(resolveUrl(path))
.headers(authHeaders(appId, session).build())
.post(builder.build())
.build();
return executeRequest(request, IWORK_REMOTE_REQUEST_FAILED);
}
private void setAuthHeaders(org.springframework.http.HttpHeaders headers,
String appId,
IWorkSession session) {
headers.set(properties.getHeaders().getAppId(), appId);
headers.set(properties.getHeaders().getToken(), session.getToken());
headers.set(properties.getHeaders().getUserId(), session.getEncryptedUserId());
private Headers.Builder authHeaders(String appId, IWorkSession session) {
return new Headers.Builder()
.set(properties.getHeaders().getAppId(), appId)
.set(properties.getHeaders().getToken(), session.getToken())
.set(properties.getHeaders().getUserId(), session.getEncryptedUserId());
}
private Map<String, Object> buildUserPayload(IWorkUserInfoReqVO reqVO) {
@@ -545,38 +512,66 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
}
private WebClient webClient() {
WebClient client = cachedWebClient;
private OkHttpClient okHttpClient() {
OkHttpClient client = cachedHttpClient;
if (client != null) {
return client;
}
synchronized (this) {
if (cachedWebClient == null) {
cachedWebClient = buildWebClient();
if (cachedHttpClient == null) {
cachedHttpClient = buildHttpClient();
}
return cachedWebClient;
return cachedHttpClient;
}
}
private WebClient buildWebClient() {
WebClient.Builder builder = cloneBuilder();
builder.baseUrl(properties.getBaseUrl());
private OkHttpClient buildHttpClient() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
IWorkProperties.Client clientProps = properties.getClient();
if (clientProps != null) {
Duration responseTimeout = clientProps.getResponseTimeout();
if (responseTimeout != null) {
builder.filter((request, next) -> next.exchange(request).timeout(responseTimeout));
if (clientProps.getConnectTimeout() != null) {
builder.connectTimeout(clientProps.getConnectTimeout());
}
if (clientProps.getResponseTimeout() != null) {
builder.callTimeout(clientProps.getResponseTimeout());
builder.readTimeout(clientProps.getResponseTimeout());
builder.writeTimeout(clientProps.getResponseTimeout());
}
// 连接超时时间由全局的 HttpClient 自定义器统一配置(若存在)。
}
builder.retryOnConnectionFailure(true);
return builder.build();
}
private WebClient.Builder cloneBuilder() {
try {
return webClientBuilder.clone();
} catch (UnsupportedOperationException ex) {
return WebClient.builder();
private String resolveUrl(String path) {
if (StringUtils.hasText(path) && (path.startsWith("http://") || path.startsWith("https://"))) {
return path;
}
String baseUrl = properties.getBaseUrl();
if (!StringUtils.hasText(baseUrl)) {
throw ServiceExceptionUtil.exception(IWORK_BASE_URL_MISSING);
}
boolean baseEndsWithSlash = baseUrl.endsWith("/");
boolean pathStartsWithSlash = StringUtils.hasText(path) && path.startsWith("/");
if (baseEndsWithSlash && pathStartsWithSlash) {
return baseUrl + path.substring(1);
}
if (!baseEndsWithSlash && !pathStartsWithSlash) {
return baseUrl + "/" + path;
}
return baseUrl + path;
}
private String executeRequest(Request request, ErrorCode errorCode) {
try (Response response = okHttpClient().newCall(request).execute()) {
String responseBody = response.body() != null ? response.body().string() : null;
if (!response.isSuccessful()) {
log.error("[iWork] request {} failed. status={} body={}", request.url(), response.code(), responseBody);
throw ServiceExceptionUtil.exception(errorCode, response.code(), responseBody);
}
return responseBody;
} catch (IOException ex) {
log.error("[iWork] request {} failed", request.url(), ex);
throw ServiceExceptionUtil.exception(errorCode, ex.getMessage());
}
}

View File

@@ -0,0 +1,371 @@
package com.zt.plat.module.system.service.integration.iwork.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
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.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;
import com.zt.plat.module.system.framework.integration.iwork.config.IWorkProperties;
import com.zt.plat.module.system.service.integration.iwork.IWorkOrgRestService;
import lombok.extern.slf4j.Slf4j;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
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;
import static com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationErrorCodeConstants.IWORK_ORG_REMOTE_FAILED;
/**
* 默认的人力组织 REST 代理实现。
*/
@Slf4j
@Service
public class IWorkOrgRestServiceImpl implements IWorkOrgRestService {
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {
};
private final IWorkProperties properties;
private final ObjectMapper objectMapper;
private final Clock clock;
private volatile OkHttpClient cachedHttpClient;
@Autowired
public IWorkOrgRestServiceImpl(IWorkProperties properties,
ObjectMapper objectMapper) {
this(properties, objectMapper, Clock.systemUTC());
}
IWorkOrgRestServiceImpl(IWorkProperties properties,
ObjectMapper objectMapper,
Clock clock) {
this.properties = properties;
this.objectMapper = objectMapper;
this.clock = clock;
}
@Override
public IWorkOrgRespVO listSubcompanies(IWorkSubcompanyQueryReqVO reqVO) {
String path = orgPaths().getSubcompanyPage();
Map<String, Object> params = buildBaseParams(reqVO);
if (StringUtils.hasText(reqVO.getSubcompanyCode())) {
params.put("subcompanycode", reqVO.getSubcompanyCode());
}
if (StringUtils.hasText(reqVO.getSubcompanyName())) {
params.put("subcompanyname", reqVO.getSubcompanyName());
}
return invokeParamsEndpoint(path, params);
}
@Override
public IWorkOrgRespVO listDepartments(IWorkDepartmentQueryReqVO reqVO) {
String path = orgPaths().getDepartmentPage();
Map<String, Object> params = buildBaseParams(reqVO);
if (StringUtils.hasText(reqVO.getDepartmentCode())) {
params.put("departmentcode", reqVO.getDepartmentCode());
}
if (StringUtils.hasText(reqVO.getDepartmentName())) {
params.put("departmentname", reqVO.getDepartmentName());
}
if (StringUtils.hasText(reqVO.getSubcompanyId())) {
params.put("subcompanyid", reqVO.getSubcompanyId());
}
return invokeParamsEndpoint(path, params);
}
@Override
public IWorkOrgRespVO listJobTitles(IWorkJobTitleQueryReqVO reqVO) {
String path = orgPaths().getJobTitlePage();
Map<String, Object> params = buildBaseParams(reqVO);
if (StringUtils.hasText(reqVO.getJobTitleCode())) {
params.put("jobtitlecode", reqVO.getJobTitleCode());
}
if (StringUtils.hasText(reqVO.getJobTitleName())) {
params.put("jobtitlename", reqVO.getJobTitleName());
}
return invokeParamsEndpoint(path, params);
}
@Override
public IWorkOrgRespVO listUsers(IWorkUserQueryReqVO reqVO) {
String path = orgPaths().getUserPage();
Map<String, Object> params = buildBaseParams(reqVO);
if (StringUtils.hasText(reqVO.getWorkCode())) {
params.put("workcode", reqVO.getWorkCode());
}
if (StringUtils.hasText(reqVO.getLastName())) {
params.put("lastname", reqVO.getLastName());
}
if (StringUtils.hasText(reqVO.getSubcompanyId())) {
params.put("subcompanyid", reqVO.getSubcompanyId());
}
if (StringUtils.hasText(reqVO.getDepartmentId())) {
params.put("departmentid", reqVO.getDepartmentId());
}
if (StringUtils.hasText(reqVO.getJobTitleId())) {
params.put("jobtitleid", reqVO.getJobTitleId());
}
if (StringUtils.hasText(reqVO.getStatus())) {
params.put("status", reqVO.getStatus());
}
if (StringUtils.hasText(reqVO.getMobile())) {
params.put("mobile", reqVO.getMobile());
}
if (StringUtils.hasText(reqVO.getEmail())) {
params.put("email", reqVO.getEmail());
}
return invokeParamsEndpoint(path, params);
}
private Map<String, Object> buildBaseParams(IWorkOrgBaseQueryReqVO reqVO) {
Map<String, Object> params = new HashMap<>();
if (reqVO.getParams() != null) {
params.putAll(reqVO.getParams());
}
if (reqVO.getCurpage() != null) {
params.put("curpage", reqVO.getCurpage());
}
if (reqVO.getPagesize() != null) {
params.put("pagesize", reqVO.getPagesize());
}
return params;
}
@Override
public IWorkOrgRespVO syncSubcompanies(IWorkOrgSyncReqVO reqVO) {
String path = orgPaths().getSyncSubcompany();
return invokeDataEndpoint(path, reqVO.getData());
}
@Override
public IWorkOrgRespVO syncDepartments(IWorkOrgSyncReqVO reqVO) {
String path = orgPaths().getSyncDepartment();
return invokeDataEndpoint(path, reqVO.getData());
}
@Override
public IWorkOrgRespVO syncJobTitles(IWorkOrgSyncReqVO reqVO) {
String path = orgPaths().getSyncJobTitle();
return invokeDataEndpoint(path, reqVO.getData());
}
@Override
public IWorkOrgRespVO syncUsers(IWorkOrgSyncReqVO reqVO) {
String path = orgPaths().getSyncUser();
return invokeDataEndpoint(path, reqVO.getData());
}
private IWorkOrgRespVO invokeParamsEndpoint(String path, Map<String, Object> params) {
String payload = toJson(params);
return executeForm(path, "params", payload);
}
private IWorkOrgRespVO invokeDataEndpoint(String path, Object data) {
String payload = toJson(data);
return executeForm(path, "data", payload);
}
private IWorkOrgRespVO executeForm(String path, String fieldName, String jsonPayload) {
assertOrgConfigured(path);
FormBody.Builder formBuilder = new FormBody.Builder(StandardCharsets.UTF_8);
if (StringUtils.hasText(fieldName) && StringUtils.hasText(jsonPayload)) {
formBuilder.add(fieldName, jsonPayload);
}
formBuilder.add("token", buildTokenJson());
RequestBody requestBody = formBuilder.build();
Request request = new Request.Builder()
.url(resolveUrl(path))
.header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.post(requestBody)
.build();
try (Response response = okHttpClient().newCall(request).execute()) {
String responseBody = response.body() != null ? response.body().string() : null;
if (!response.isSuccessful()) {
log.error("[iWork-Org] 调用 {} 失败status={} body={}", path, response.code(), responseBody);
throw ServiceExceptionUtil.exception(IWORK_ORG_REMOTE_FAILED, response.code(), responseBody);
}
return buildResponse(responseBody);
} catch (IOException ex) {
log.error("[iWork-Org] 调用 {} 失败", path, ex);
throw ServiceExceptionUtil.exception(IWORK_ORG_REMOTE_FAILED, ex.getMessage());
}
}
private String buildTokenJson() {
String tokenSeed = StringUtils.trimWhitespace(orgConfig().getTokenSeed());
if (!StringUtils.hasText(tokenSeed)) {
throw ServiceExceptionUtil.exception(IWORK_ORG_IDENTIFIER_MISSING);
}
long ts = clock.millis();
String raw = tokenSeed + ts;
// 通过 MD5(tokenSeed + ts) 计算 key并转为大写以符合 PDF 约定
String hashed = md5Upper(raw);
Map<String, String> token = Map.of(
"key", hashed,
"ts", String.valueOf(ts)
);
return toJson(token);
}
private String md5Upper(String raw) {
byte[] bytes = raw.getBytes(StandardCharsets.UTF_8);
String hex = org.springframework.util.DigestUtils.md5DigestAsHex(bytes);
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());
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));
return respVO;
}
private JsonNode parseJson(String body) {
try {
return objectMapper.readTree(body);
} catch (JsonProcessingException ex) {
log.error("[iWork-Org] 无法解析 JSON 响应: {}", body, ex);
throw ServiceExceptionUtil.exception(IWORK_ORG_REMOTE_FAILED, "响应不是合法 JSON");
}
}
private String resolveMessage(JsonNode node) {
if (node == null) {
return null;
}
if (node.has("msg")) {
return node.get("msg").asText();
}
if (node.has("message")) {
return node.get("message").asText();
}
return null;
}
private boolean isSuccess(JsonNode node) {
if (node == null) {
return false;
}
if ("1".equals(textValue(node, "code"))) {
return true;
}
if ("1".equals(textValue(node, "status"))) {
return true;
}
return "1".equals(textValue(node, "success"));
}
private String textValue(JsonNode node, String field) {
return node != null && node.has(field) ? node.get(field).asText() : null;
}
private void assertOrgConfigured(String path) {
IWorkProperties.OrgRest org = orgConfig();
if (!StringUtils.hasText(properties.getBaseUrl())) {
throw ServiceExceptionUtil.exception(IWORK_BASE_URL_MISSING);
}
if (!StringUtils.hasText(path)) {
throw ServiceExceptionUtil.exception(IWORK_ORG_REMOTE_FAILED, "缺少接口路径配置");
}
}
private IWorkProperties.OrgRest orgConfig() {
return properties.getOrg();
}
private IWorkProperties.OrgPaths orgPaths() {
return orgConfig().getPaths();
}
private String toJson(Object payload) {
try {
return objectMapper.writeValueAsString(payload == null ? Collections.emptyMap() : payload);
} catch (JsonProcessingException ex) {
throw ServiceExceptionUtil.exception(IWORK_ORG_REMOTE_FAILED, "序列化 JSON 失败: " + ex.getMessage());
}
}
private OkHttpClient okHttpClient() {
OkHttpClient client = cachedHttpClient;
if (client != null) {
return client;
}
synchronized (this) {
if (cachedHttpClient == null) {
cachedHttpClient = buildHttpClient();
}
return cachedHttpClient;
}
}
private OkHttpClient buildHttpClient() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
IWorkProperties.Client clientProps = properties.getClient();
if (clientProps != null) {
if (clientProps.getConnectTimeout() != null) {
builder.connectTimeout(clientProps.getConnectTimeout());
}
if (clientProps.getResponseTimeout() != null) {
builder.callTimeout(clientProps.getResponseTimeout());
builder.readTimeout(clientProps.getResponseTimeout());
builder.writeTimeout(clientProps.getResponseTimeout());
}
}
builder.retryOnConnectionFailure(true);
return builder.build();
}
private String resolveUrl(String path) {
if (StringUtils.hasText(path) && (path.startsWith("http://") || path.startsWith("https://"))) {
return path;
}
String baseUrl = properties.getBaseUrl();
if (!StringUtils.hasText(baseUrl)) {
throw ServiceExceptionUtil.exception(IWORK_BASE_URL_MISSING);
}
boolean baseEndsWithSlash = baseUrl.endsWith("/");
boolean pathStartsWithSlash = StringUtils.hasText(path) && path.startsWith("/");
if (baseEndsWithSlash && pathStartsWithSlash) {
return baseUrl + path.substring(1);
}
if (!baseEndsWithSlash && !pathStartsWithSlash) {
return baseUrl + "/" + path;
}
return baseUrl + path;
}
}

View File

@@ -50,8 +50,8 @@ spring:
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
data:
redis:
host: nacos-redis # 地址
port: 6379 # 端口
host: 172.16.46.63 # 地址
port: 30379 # 端口
database: 1 # 数据库索引
# password: 123456 # 密码,建议生产环境开启
@@ -61,6 +61,16 @@ spring:
rocketmq:
name-server: 172.16.46.63:30876 # RocketMQ Namesrv
spring:
# RabbitMQ 配置项,对应 RabbitProperties 配置类
rabbitmq:
host: 127.0.0.1 # RabbitMQ 服务的地址
port: 5672 # RabbitMQ 服务的端口
username: guest # RabbitMQ 服务的账号
password: guest # RabbitMQ 服务的密码
# Kafka 配置项,对应 KafkaProperties 配置类
kafka:
bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔
--- #################### 定时任务相关配置 ####################
xxl:
@@ -153,3 +163,28 @@ justauth:
type: REDIS
prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE::
timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟
seata:
enabled: true
application-id: system-server
tx-service-group: dev_tx_group
enable-auto-data-source-proxy: true
data-source-proxy-mode: AT
registry:
type: file
config:
type: file
service:
vgroupMapping:
default_tx_group: default
dev_tx_group: default
test_tx_group: default
prod_tx_group: default
default:
grouplist: 172.16.46.63:30088
client:
tm:
defaultGlobalTransactionTimeout: 60000
undo:
logTable: undo_log
dataValidation: true
logSerialization: jackson

View File

@@ -106,7 +106,6 @@ easy-trans:
--- #################### iWork 集成配置 ####################
iwork:
enabled: true
base-url: http://172.16.36.233:8080
app-id:
client-public-key:
@@ -124,6 +123,17 @@ iwork:
client:
connect-timeout: 5s
response-timeout: 30s
org:
token-seed: 456465
paths:
subcompany-page: /api/hrm/resful/getHrmsubcompanyWithPage
department-page: /api/hrm/resful/getHrmdepartmentWithPage
job-title-page: /api/hrm/resful/getJobtitleInfoWithPage
user-page: /api/hrm/resful/getHrmUserInfoWithPage
sync-subcompany: /api/hrm/resful/synSubcompany
sync-department: /api/hrm/resful/synDepartment
sync-job-title: /api/hrm/resful/synJobtitle
sync-user: /api/hrm/resful/synHrmresource
--- #################### RPC 远程调用相关配置 ####################

View File

@@ -0,0 +1,121 @@
package com.zt.plat.module.system.service.integration.iwork.impl;
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.IWorkOrgSyncReqVO;
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;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.util.DigestUtils;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
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;
import static org.assertj.core.api.Assertions.assertThat;
class IWorkOrgRestServiceImplTest {
private MockWebServer mockWebServer;
private IWorkOrgRestService service;
private IWorkProperties properties;
private Clock fixedClock;
@BeforeEach
void setUp() throws Exception {
mockWebServer = new MockWebServer();
mockWebServer.start();
properties = buildProperties();
fixedClock = Clock.fixed(Instant.ofEpochMilli(1_672_531_200_000L), ZoneOffset.UTC);
service = new IWorkOrgRestServiceImpl(properties, new ObjectMapper(), fixedClock);
}
@AfterEach
void tearDown() throws Exception {
mockWebServer.shutdown();
}
@Test
void shouldListSubcompanies() throws Exception {
mockWebServer.enqueue(jsonResponse("{\"code\":\"1\",\"data\":{\"page\":1}}"));
IWorkOrgQueryReqVO reqVO = new IWorkOrgQueryReqVO();
reqVO.setParams(Map.of("curpage", 1));
IWorkOrgRespVO respVO = service.listSubcompanies(reqVO);
assertThat(respVO.isSuccess()).isTrue();
assertThat(respVO.getPayload()).containsEntry("page", 1);
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 + "\"");
}
@Test
void shouldSyncDepartments() throws Exception {
mockWebServer.enqueue(jsonResponse("{\"code\":\"1\",\"result\":{}}"));
IWorkOrgSyncReqVO reqVO = new IWorkOrgSyncReqVO();
reqVO.setData(List.of(Map.of("@action", "add", "code", "demo")));
IWorkOrgRespVO respVO = service.syncDepartments(reqVO);
assertThat(respVO.isSuccess()).isTrue();
assertThat(respVO.getPayload()).containsKey("result");
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\"}]");
}
private MockResponse jsonResponse(String body) {
return new MockResponse()
.setHeader("Content-Type", "application/json")
.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());
properties.getClient().setConnectTimeout(Duration.ofSeconds(5));
properties.getClient().setResponseTimeout(Duration.ofSeconds(5));
properties.getOrg().setTokenSeed("test-seed");
IWorkProperties.OrgPaths paths = properties.getOrg().getPaths();
paths.setSubcompanyPage("/api/hrm/resful/getHrmsubcompanyWithPage");
paths.setDepartmentPage("/api/hrm/resful/getHrmdepartmentWithPage");
paths.setJobTitlePage("/api/hrm/resful/getJobtitleInfoWithPage");
paths.setUserPage("/api/hrm/resful/getHrmUserInfoWithPage");
paths.setSyncSubcompany("/api/hrm/resful/synSubcompany");
paths.setSyncDepartment("/api/hrm/resful/synDepartment");
paths.setSyncJobTitle("/api/hrm/resful/synJobtitle");
paths.setSyncUser("/api/hrm/resful/synHrmresource");
return properties;
}
}

View File

@@ -0,0 +1,85 @@
package com.zt.plat.module.system.service.sso;
import com.zt.plat.module.system.framework.sso.config.ExternalSsoProperties;
import com.zt.plat.module.system.service.logger.LoginLogService;
import com.zt.plat.module.system.service.logger.OperateLogService;
import com.zt.plat.module.system.service.oauth2.OAuth2TokenService;
import com.zt.plat.module.system.service.sso.strategy.ExternalSsoStrategy;
import com.zt.plat.module.system.service.user.AdminUserService;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class ExternalSsoServiceImplTest {
@Test
void selectStrategy_returnsExactMatch_whenStrategySupportsSource() {
ExternalSsoStrategy exactStrategy = mock(ExternalSsoStrategy.class);
ExternalSsoStrategy fallbackStrategy = mock(ExternalSsoStrategy.class);
when(exactStrategy.supports("BRMS")).thenReturn(true);
ExternalSsoServiceImpl service = newService(List.of(exactStrategy, fallbackStrategy));
ExternalSsoStrategy selected = invokeSelectStrategy(service, "BRMS");
assertThat(selected).isSameAs(exactStrategy);
}
@Test
void selectStrategy_returnsWildcardStrategy_whenNoExactMatch() {
ExternalSsoStrategy strictStrategy = mock(ExternalSsoStrategy.class);
ExternalSsoStrategy wildcardStrategy = mock(ExternalSsoStrategy.class);
when(strictStrategy.supports("ERP")).thenReturn(false);
when(strictStrategy.supports(null)).thenReturn(false);
when(wildcardStrategy.supports("ERP")).thenReturn(false);
when(wildcardStrategy.supports(null)).thenReturn(true);
ExternalSsoServiceImpl service = newService(List.of(strictStrategy, wildcardStrategy));
ExternalSsoStrategy selected = invokeSelectStrategy(service, "ERP");
assertThat(selected).isSameAs(wildcardStrategy);
}
@Test
void selectStrategy_returnsNull_whenNoStrategyAvailable() {
ExternalSsoServiceImpl service = newService(Collections.emptyList());
ExternalSsoStrategy selected = invokeSelectStrategy(service, "ANY");
assertThat(selected).isNull();
}
@Test
void selectStrategy_skipsStrategy_whenSupportsThrowsException() {
ExternalSsoStrategy unstableStrategy = mock(ExternalSsoStrategy.class);
ExternalSsoStrategy fallbackStrategy = mock(ExternalSsoStrategy.class);
when(unstableStrategy.supports("CRM")).thenThrow(new IllegalStateException("boom"));
when(unstableStrategy.supports(null)).thenReturn(false);
when(fallbackStrategy.supports("CRM")).thenReturn(false);
when(fallbackStrategy.supports(null)).thenReturn(true);
ExternalSsoServiceImpl service = newService(List.of(unstableStrategy, fallbackStrategy));
ExternalSsoStrategy selected = invokeSelectStrategy(service, "CRM");
assertThat(selected).isSameAs(fallbackStrategy);
}
@SuppressWarnings("unchecked")
private ExternalSsoStrategy invokeSelectStrategy(ExternalSsoServiceImpl service, String sourceSystem) {
return ReflectionTestUtils.invokeMethod(service, "selectStrategy", sourceSystem);
}
private ExternalSsoServiceImpl newService(List<ExternalSsoStrategy> strategies) {
ExternalSsoProperties properties = new ExternalSsoProperties();
AdminUserService adminUserService = mock(AdminUserService.class);
LoginLogService loginLogService = mock(LoginLogService.class);
OAuth2TokenService oauth2TokenService = mock(OAuth2TokenService.class);
OperateLogService operateLogService = mock(OperateLogService.class);
return new ExternalSsoServiceImpl(properties, strategies, adminUserService,
loginLogService, oauth2TokenService, operateLogService);
}
}