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

This commit is contained in:
chenbowen
2025-12-05 18:18:06 +08:00
8 changed files with 114 additions and 205 deletions

View File

@@ -97,30 +97,6 @@ public class IWorkIntegrationController {
return success(orgRestService.listUsers(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/departments/full-sync")

View File

@@ -1,74 +0,0 @@
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

@@ -1,19 +0,0 @@
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

@@ -1,12 +1,35 @@
package com.zt.plat.module.system.framework.integration.iwork.config;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* 负责加载 {@link IWorkProperties} 的自动配置类。
*/
@Slf4j
@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(IWorkProperties.class)
public class IWorkIntegrationConfiguration {
private final IWorkProperties properties;
@PostConstruct
void reportConfigurationStatus() {
if (!properties.hasAnyConfiguredValue()) {
log.info("[iWork] 未检测到集成配置,默认关闭 iWork 功能。需要时请在配置文件中补充 iwork.* 项。");
return;
}
List<String> issues = properties.collectCriticalIssues();
if (!issues.isEmpty()) {
log.warn("[iWork] 配置不完整:{}。系统会继续启动,但 iWork 能力将不可用。", String.join("", issues));
return;
}
log.info("[iWork] 已检测到完整配置iWork 集成能力已启用。");
}
}

View File

@@ -1,21 +1,18 @@
package com.zt.plat.module.system.framework.integration.iwork.config;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
/**
* iWork 集成所需的配置项。
*/
@Data
@Validated
@ConfigurationProperties(prefix = "iwork")
public class IWorkProperties {
@@ -39,16 +36,11 @@ public class IWorkProperties {
*/
private String userId;
@Valid
private final Paths paths = new Paths();
private final Headers headers = new Headers();
@Valid
private final Token token = new Token();
@Valid
private final Client client = new Client();
@Valid
private final OrgRest org = new OrgRest();
@Valid
private final Workflow workflow = new Workflow();
@Data
@@ -56,27 +48,22 @@ public class IWorkProperties {
/**
* 负责交换公钥和密钥的注册接口路径。
*/
@NotBlank(message = "iWork 注册接口路径不能为空")
private String register;
/**
* 申请访问令牌的接口路径。
*/
@NotBlank(message = "iWork 申请 Token 接口路径不能为空")
private String applyToken;
/**
* 查询用户信息的接口路径。
*/
@NotBlank(message = "iWork 用户信息接口路径不能为空")
private String userInfo;
/**
* 发起流程的接口路径。
*/
@NotBlank(message = "iWork 发起流程接口路径不能为空")
private String createWorkflow;
/**
* 干预或作废流程的接口路径。
*/
@NotBlank(message = "iWork 作废流程接口路径不能为空")
private String voidWorkflow;
}
@@ -95,7 +82,6 @@ public class IWorkProperties {
/**
* 向 iWork 申请的 Token 有效期(单位秒)。
*/
@Min(value = 1, message = "iWork Token 有效期必须大于 0")
private long ttlSeconds;
}
@@ -104,12 +90,10 @@ public class IWorkProperties {
/**
* Reactor Netty 连接超时时间。
*/
@NotNull(message = "iWork 客户端连接超时时间不能为空")
private Duration connectTimeout;
/**
* Reactor Netty 响应超时时间。
*/
@NotNull(message = "iWork 客户端响应超时时间不能为空")
private Duration responseTimeout;
}
@@ -119,7 +103,6 @@ public class IWorkProperties {
* 认证所需的标识(与 iWork 约定)。
*/
private String tokenSeed;
@Valid
private final OrgPaths paths = new OrgPaths();
}
@@ -140,7 +123,69 @@ public class IWorkProperties {
/**
* 用印流程对应的 iWork 模板编号。
*/
@NotBlank(message = "iWork 用印流程模板编号不能为空")
private String sealWorkflowId;
}
/**
* 是否显式配置了关键参数,用于判断是否需要提示用户。
*/
public boolean hasAnyConfiguredValue() {
return hasText(baseUrl)
|| hasText(appId)
|| hasText(clientPublicKey)
|| hasText(userId)
|| hasAnyPathConfigured()
|| token.getTtlSeconds() > 0
|| client.getConnectTimeout() != null
|| client.getResponseTimeout() != null
|| hasText(org.getTokenSeed())
|| hasText(workflow.getSealWorkflowId());
}
/**
* 收集关键配置缺失信息,用于提示或日志告警。
*/
public List<String> collectCriticalIssues() {
List<String> issues = new ArrayList<>();
if (!hasText(baseUrl)) {
issues.add("iwork.base-url 未配置");
}
if (!hasText(appId)) {
issues.add("iwork.app-id 未配置");
}
if (!hasText(paths.getRegister())) {
issues.add("iwork.paths.register 未配置");
}
if (!hasText(paths.getApplyToken())) {
issues.add("iwork.paths.apply-token 未配置");
}
if (!hasText(paths.getUserInfo())) {
issues.add("iwork.paths.user-info 未配置");
}
if (!hasText(paths.getCreateWorkflow())) {
issues.add("iwork.paths.create-workflow 未配置");
}
if (!hasText(paths.getVoidWorkflow())) {
issues.add("iwork.paths.void-workflow 未配置");
}
if (token.getTtlSeconds() <= 0) {
issues.add("iwork.token.ttl-seconds 需要大于 0");
}
if (!hasText(workflow.getSealWorkflowId())) {
issues.add("iwork.workflow.seal-workflow-id 未配置");
}
return issues;
}
private boolean hasAnyPathConfigured() {
return hasText(paths.getRegister())
|| hasText(paths.getApplyToken())
|| hasText(paths.getUserInfo())
|| hasText(paths.getCreateWorkflow())
|| hasText(paths.getVoidWorkflow());
}
private boolean hasText(String value) {
return StringUtils.hasText(value);
}
}

View File

@@ -4,10 +4,8 @@ import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkDepa
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.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;
@@ -24,11 +22,4 @@ public interface IWorkOrgRestService {
IWorkHrUserPageRespVO listUsers(IWorkUserQueryReqVO reqVO);
IWorkHrSyncRespVO syncSubcompanies(IWorkOrgSyncReqVO reqVO);
IWorkHrSyncRespVO syncDepartments(IWorkOrgSyncReqVO reqVO);
IWorkHrSyncRespVO syncJobTitles(IWorkOrgSyncReqVO reqVO);
IWorkHrSyncRespVO syncUsers(IWorkOrgSyncReqVO reqVO);
}

View File

@@ -124,7 +124,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
IWorkSession session = createSession(appId, clientKeyPair, operatorUserId, Boolean.TRUE.equals(reqVO.getForceRefreshToken()));
Map<String, Object> payload = buildUserPayload(reqVO);
String responseBody = executeJsonRequest(properties.getPaths().getUserInfo(), reqVO.getQueryParams(), appId, session, payload);
String userInfoPath = requireConfiguredPath(properties.getPaths().getUserInfo(), "iwork.paths.user-info");
String responseBody = executeJsonRequest(userInfoPath, reqVO.getQueryParams(), appId, session, payload);
return buildUserInfoResponse(responseBody);
}
@@ -138,7 +139,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
IWorkSession session = createSession(appId, clientKeyPair, operatorUserId, Boolean.TRUE.equals(reqVO.getForceRefreshToken()));
Map<String, Object> payload = buildCreatePayload(reqVO);
String responseBody = executeFormRequest(properties.getPaths().getCreateWorkflow(), null, appId, session, payload);
String createWorkflowPath = requireConfiguredPath(properties.getPaths().getCreateWorkflow(), "iwork.paths.create-workflow");
String responseBody = executeFormRequest(createWorkflowPath, null, appId, session, payload);
return buildOperationResponse(responseBody);
}
@@ -154,7 +156,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
IWorkSession session = createSession(appId, clientKeyPair, operatorUserId, Boolean.TRUE.equals(reqVO.getForceRefreshToken()));
Map<String, Object> payload = buildVoidPayload(reqVO);
String responseBody = executeJsonRequest(properties.getPaths().getVoidWorkflow(), null, appId, session, payload);
String voidWorkflowPath = requireConfiguredPath(properties.getPaths().getVoidWorkflow(), "iwork.paths.void-workflow");
String responseBody = executeJsonRequest(voidWorkflowPath, null, appId, session, payload);
return buildOperationResponse(responseBody);
}
@@ -293,6 +296,17 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
return StringUtils.trimWhitespace(value);
}
private String requireConfiguredPath(String value, String propertyPath) {
if (!StringUtils.hasText(value)) {
throw ServiceExceptionUtil.exception(IWORK_CONFIGURATION_INVALID, propertyPath + " 未配置");
}
return StringUtils.trimWhitespace(value);
}
private long resolveTokenTtlSeconds() {
return Math.max(1L, properties.getToken().getTtlSeconds());
}
private ClientKeyPair resolveClientKeyPair(String appId, boolean forceRefresh) {
String configured = properties.getClientPublicKey();
if (StringUtils.hasText(configured)) {
@@ -333,7 +347,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
String encryptedSecret = encryptWithPublicKey(registration.secret(), registration.spk());
String encryptedUserId = encryptWithPublicKey(operatorUserId, registration.spk());
String token = applyToken(appId, encryptedSecret);
Instant expiresAt = Instant.now().plusSeconds(Math.max(1L, properties.getToken().getTtlSeconds()));
long ttlSeconds = resolveTokenTtlSeconds();
Instant expiresAt = Instant.now().plusSeconds(ttlSeconds);
return new IWorkSession(token, encryptedUserId, expiresAt, registration.spk());
}
@@ -359,8 +374,9 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
private RegistrationState register(String appId, ClientKeyPair clientKeyPair) {
String registerPath = requireConfiguredPath(properties.getPaths().getRegister(), "iwork.paths.register");
Request request = new Request.Builder()
.url(resolveUrl(properties.getPaths().getRegister()))
.url(resolveUrl(registerPath))
.header(properties.getHeaders().getAppId(), appId)
.header(properties.getHeaders().getClientPublicKey(), clientKeyPair.publicKey())
.post(RequestBody.create(null, new byte[0]))
@@ -376,11 +392,13 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
private String applyToken(String appId, String encryptedSecret) {
String applyTokenPath = requireConfiguredPath(properties.getPaths().getApplyToken(), "iwork.paths.apply-token");
long ttlSeconds = resolveTokenTtlSeconds();
Request request = new Request.Builder()
.url(resolveUrl(properties.getPaths().getApplyToken()))
.url(resolveUrl(applyTokenPath))
.header(properties.getHeaders().getAppId(), appId)
.header(properties.getHeaders().getSecret(), encryptedSecret)
.header(properties.getHeaders().getTime(), String.valueOf(properties.getToken().getTtlSeconds()))
.header(properties.getHeaders().getTime(), String.valueOf(ttlSeconds))
.post(RequestBody.create(null, new byte[0]))
.build();
String responseBody = executeRequest(request, IWORK_APPLY_TOKEN_FAILED);

View File

@@ -9,11 +9,9 @@ import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkDepa
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.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;
@@ -62,9 +60,6 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService {
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;
@@ -178,34 +173,6 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService {
return params;
}
@Override
public IWorkHrSyncRespVO syncSubcompanies(IWorkOrgSyncReqVO reqVO) {
String path = orgPaths().getSyncSubcompany();
JsonNode node = invokeDataEndpoint(path, reqVO.getData());
return buildSyncResp(node);
}
@Override
public IWorkHrSyncRespVO syncDepartments(IWorkOrgSyncReqVO reqVO) {
String path = orgPaths().getSyncDepartment();
JsonNode node = invokeDataEndpoint(path, reqVO.getData());
return buildSyncResp(node);
}
@Override
public IWorkHrSyncRespVO syncJobTitles(IWorkOrgSyncReqVO reqVO) {
String path = orgPaths().getSyncJobTitle();
JsonNode node = invokeDataEndpoint(path, reqVO.getData());
return buildSyncResp(node);
}
@Override
public IWorkHrSyncRespVO syncUsers(IWorkOrgSyncReqVO reqVO) {
String path = orgPaths().getSyncUser();
JsonNode node = invokeDataEndpoint(path, reqVO.getData());
return buildSyncResp(node);
}
private JsonNode invokeParamsEndpoint(String path, Map<String, Object> params) {
Objects.requireNonNull(params, "查询参数不能为空");
Map<String, Object> payload = new HashMap<>();
@@ -213,13 +180,6 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService {
return executeJson(path, payload);
}
private JsonNode invokeDataEndpoint(String path, Object data) {
Objects.requireNonNull(data, "同步数据不能为空");
Map<String, Object> payload = new HashMap<>();
payload.put("data", data);
return executeJson(path, payload);
}
private JsonNode executeJson(String path, Map<String, Object> payload) {
// 统一封装请求体并发送 POST 调用
assertOrgConfigured(path);
@@ -369,17 +329,6 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService {
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;
}
private JsonNode parseJson(String body) {
try {
return objectMapper.readTree(body);