1. 新增 OA Token 获取与校验接口,更新相关配置

2. 设置组织可以设置为顶层组织
This commit is contained in:
chenbowen
2025-12-08 18:56:06 +08:00
parent 8ea3757105
commit 91c0cbc5d7
11 changed files with 333 additions and 13 deletions

View File

@@ -12,6 +12,8 @@ import jakarta.annotation.security.PermitAll;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -45,6 +47,20 @@ public class IWorkIntegrationController {
return success(integrationService.acquireToken(reqVO));
}
@PostMapping("/oa/token")
@Operation(summary = "透传获取 OA Token")
public ResponseEntity<String> acquireOaToken(@Valid @RequestBody IWorkOaTokenReqVO reqVO) {
IWorkOaRawResponse resp = integrationService.getOaToken(reqVO);
return buildOaResponse(resp);
}
@PostMapping("/oa/check")
@Operation(summary = "透传校验 OA Token")
public ResponseEntity<String> checkOaToken(@Valid @RequestBody IWorkOaCheckTokenReqVO reqVO) {
IWorkOaRawResponse resp = integrationService.checkOaToken(reqVO);
return buildOaResponse(resp);
}
@PostMapping("/user/resolve")
@Operation(summary = "根据外部标识获取 iWork 用户编号")
public CommonResult<IWorkUserInfoRespVO> resolveUser(@Valid @RequestBody IWorkUserInfoReqVO reqVO) {
@@ -122,4 +138,20 @@ public class IWorkIntegrationController {
public CommonResult<IWorkFullSyncRespVO> fullSyncUsers(@Valid @RequestBody IWorkFullSyncReqVO reqVO) {
return success(syncService.fullSyncUsers(reqVO));
}
private ResponseEntity<String> buildOaResponse(IWorkOaRawResponse resp) {
if (resp == null) {
return ResponseEntity.internalServerError().body("OA 响应为空");
}
HttpHeaders headers = new HttpHeaders();
if (resp.getHeaders() != null) {
resp.getHeaders().forEach(headers::add);
}
if (resp.getContentType() != null) {
headers.setContentType(resp.getContentType());
}
return ResponseEntity.status(resp.getStatusCode())
.headers(headers)
.body(resp.getBody());
}
}

View File

@@ -18,4 +18,7 @@ public class IWorkFileCallbackReqVO {
@Schema(description = "文件名称,可选", example = "合同附件.pdf")
private String fileName;
@Schema(description = "OA 单点下载使用的 ssoToken可选", example = "6102A7C13F09DD6B1AF06CDA0E479AC8...")
private String ssoToken;
}

View File

@@ -0,0 +1,16 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 校验 OA Token 的请求参数。
*/
@Data
public class IWorkOaCheckTokenReqVO {
@Schema(description = "需要校验的 OA token")
@NotBlank(message = "token 不能为空")
private String token;
}

View File

@@ -0,0 +1,27 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.http.MediaType;
import java.util.Map;
/**
* 封装 OA 接口的原始返回,用于透传 HTTP 状态与 body。
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class IWorkOaRawResponse {
private int statusCode;
private String body;
private MediaType contentType;
private Map<String, String> headers;
}

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.NotBlank;
import lombok.Data;
/**
* 获取 OA Token 的请求参数。
*/
@Data
public class IWorkOaTokenReqVO {
@Schema(description = "OA 登录账号 loginid", example = "zixun004")
@NotBlank(message = "loginId 不能为空")
private String loginId;
@Schema(description = "应用 appid未填则使用配置默认值", example = "a17ca6ca-88b0-463e-bffa-7995086bf225")
private String appId;
}

View File

@@ -42,6 +42,7 @@ public class IWorkProperties {
private final Client client = new Client();
private final OrgRest org = new OrgRest();
private final Workflow workflow = new Workflow();
private final Oa oa = new Oa();
@Data
public static class Paths {
@@ -126,6 +127,34 @@ public class IWorkProperties {
private String sealWorkflowId;
}
@Data
public static class Oa {
/**
* OA 网关基础地址例如http://172.16.36.233:8080
*/
private String baseUrl;
/**
* 默认 appid调用方未传时使用。
*/
private String appId;
private final OaPaths paths = new OaPaths();
}
@Data
public static class OaPaths {
/**
* 获取 token 的接口路径,例如:/ssologin/getToken
*/
private String getToken;
/**
* 校验 token 的接口路径,例如:/ssologin/checkToken
*/
private String checkToken;
}
/**
* 是否显式配置了关键参数,用于判断是否需要提示用户。
*/
@@ -139,7 +168,8 @@ public class IWorkProperties {
|| client.getConnectTimeout() != null
|| client.getResponseTimeout() != null
|| hasText(org.getTokenSeed())
|| hasText(workflow.getSealWorkflowId());
|| hasText(workflow.getSealWorkflowId())
|| hasAnyOaConfigured();
}
/**
@@ -174,6 +204,20 @@ public class IWorkProperties {
if (!hasText(workflow.getSealWorkflowId())) {
issues.add("iwork.workflow.seal-workflow-id 未配置");
}
if (oa != null) {
if (!hasText(oa.getBaseUrl())) {
issues.add("iwork.oa.base-url 未配置");
}
if (!hasText(oa.getAppId())) {
issues.add("iwork.oa.app-id 未配置");
}
if (oa.getPaths() == null || !hasText(oa.getPaths().getGetToken())) {
issues.add("iwork.oa.paths.get-token 未配置");
}
if (oa.getPaths() == null || !hasText(oa.getPaths().getCheckToken())) {
issues.add("iwork.oa.paths.check-token 未配置");
}
}
return issues;
}
@@ -185,6 +229,13 @@ public class IWorkProperties {
|| hasText(paths.getVoidWorkflow());
}
private boolean hasAnyOaConfigured() {
return oa != null && (hasText(oa.getBaseUrl())
|| hasText(oa.getAppId())
|| (oa.getPaths() != null && (hasText(oa.getPaths().getGetToken())
|| hasText(oa.getPaths().getCheckToken()))));
}
private boolean hasText(String value) {
return StringUtils.hasText(value);
}

View File

@@ -66,9 +66,8 @@ public class DeptServiceImpl implements DeptService {
@CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST,
allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存
public Long createDept(DeptSaveReqVO createReqVO) {
if (createReqVO.getParentId() == null) {
createReqVO.setParentId(DeptDO.PARENT_ID_ROOT);
}
// 允许上级组织为空,视为顶级组织
createReqVO.setParentId(normalizeParentId(createReqVO.getParentId()));
// 创建时默认有效
createReqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
// 校验父部门的有效性
@@ -113,9 +112,8 @@ public class DeptServiceImpl implements DeptService {
allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存
@DataPermission(enable = false) // 禁用数据权限,避免确实上级部门导致无法保存
public void updateDept(DeptSaveReqVO updateReqVO) {
if (updateReqVO.getParentId() == null) {
updateReqVO.setParentId(DeptDO.PARENT_ID_ROOT);
}
// 允许上级组织为空,视为顶级组织
updateReqVO.setParentId(normalizeParentId(updateReqVO.getParentId()));
// 校验自己存在
DeptDO originalDept = getRequiredDept(updateReqVO.getId());
// 校验父部门的有效性

View File

@@ -6,6 +6,9 @@ import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkAuth
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkAuthTokenRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkFileCallbackReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOperationRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOaCheckTokenReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOaRawResponse;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOaTokenReqVO;
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.IWorkWorkflowCreateReqVO;
@@ -48,4 +51,14 @@ public interface IWorkIntegrationService {
* @return 创建的业务附件关联记录 ID 或附件 ID视实现而定
*/
Long handleFileCallback(IWorkFileCallbackReqVO reqVO);
/**
* 透传调用 OA 获取 token。
*/
IWorkOaRawResponse getOaToken(IWorkOaTokenReqVO reqVO);
/**
* 透传调用 OA 校验 token。
*/
IWorkOaRawResponse checkOaToken(IWorkOaCheckTokenReqVO reqVO);
}

View File

@@ -182,18 +182,83 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
AtomicReference<Long> attachmentIdRef = new AtomicReference<>();
TenantUtils.execute(tenantId, () -> attachmentIdRef.set(saveCallbackAttachment(fileUrl, reqVO.getFileName(), referenceBusinessFile)));
TenantUtils.execute(tenantId, () -> attachmentIdRef.set(saveCallbackAttachment(fileUrl, reqVO.getFileName(), referenceBusinessFile, reqVO.getSsoToken())));
return attachmentIdRef.get();
}
private Long saveCallbackAttachment(String fileUrl, String overrideFileName, BusinessFileRespDTO referenceBusinessFile) {
@Override
public IWorkOaRawResponse getOaToken(IWorkOaTokenReqVO reqVO) {
IWorkProperties.Oa oa = properties.getOa();
if (oa == null) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "OA 配置未初始化");
}
String loginId = Optional.ofNullable(reqVO)
.map(IWorkOaTokenReqVO::getLoginId)
.map(String::trim)
.orElse("");
if (!StringUtils.hasText(loginId)) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "loginId 不能为空");
}
String appId = Optional.ofNullable(reqVO)
.map(IWorkOaTokenReqVO::getAppId)
.filter(StringUtils::hasText)
.map(StringUtils::trimWhitespace)
.orElseGet(() -> StringUtils.trimWhitespace(oa.getAppId()));
if (!StringUtils.hasText(appId)) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "OA appid 未配置");
}
String path = requireOaPath(oa.getPaths().getGetToken(), "iwork.oa.paths.get-token");
FormBody formBody = new FormBody.Builder()
.add("loginid", loginId)
.add("appid", appId)
.build();
Request request = new Request.Builder()
.url(resolveOaUrl(path))
.post(formBody)
.build();
return executeOaRequest(request);
}
@Override
public IWorkOaRawResponse checkOaToken(IWorkOaCheckTokenReqVO reqVO) {
IWorkProperties.Oa oa = properties.getOa();
if (oa == null) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "OA 配置未初始化");
}
String token = Optional.ofNullable(reqVO)
.map(IWorkOaCheckTokenReqVO::getToken)
.map(String::trim)
.orElse("");
if (!StringUtils.hasText(token)) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "token 不能为空");
}
String path = requireOaPath(oa.getPaths().getCheckToken(), "iwork.oa.paths.check-token");
FormBody formBody = new FormBody.Builder()
.add("token", token)
.build();
Request request = new Request.Builder()
.url(resolveOaUrl(path))
.post(formBody)
.build();
return executeOaRequest(request);
}
private Long saveCallbackAttachment(String fileUrl, String overrideFileName, BusinessFileRespDTO referenceBusinessFile, String ssoToken) {
Long businessId = referenceBusinessFile.getBusinessId();
FileCreateReqDTO fileCreateReqDTO = new FileCreateReqDTO();
fileCreateReqDTO.setName(resolveFileName(overrideFileName, fileUrl));
fileCreateReqDTO.setDirectory(null);
fileCreateReqDTO.setType(null);
fileCreateReqDTO.setContent(downloadFileBytes(fileUrl));
fileCreateReqDTO.setContent(downloadFileBytes(fileUrl, ssoToken));
CommonResult<FileRespDTO> fileResult = fileApi.createFileWithReturn(fileCreateReqDTO);
if (fileResult == null || !fileResult.isSuccess() || fileResult.getData() == null) {
@@ -232,9 +297,12 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
return businessFile;
}
private byte[] downloadFileBytes(String fileUrl) {
private byte[] downloadFileBytes(String fileUrl, String ssoToken) {
// 如果回调已提供 ssoToken按需拼接后下载 OA 附件
String finalUrl = appendSsoTokenIfNeeded(fileUrl, ssoToken);
OkHttpClient client = okHttpClient();
Request request = new Request.Builder().url(fileUrl).get().build();
Request request = new Request.Builder().url(finalUrl).get().build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "下载文件失败HTTP 状态码: " + response.code());
@@ -249,6 +317,22 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
}
private String appendSsoTokenIfNeeded(String fileUrl, String ssoToken) {
// 未提供 token 或 URL 为空,直接返回原链接
if (!StringUtils.hasText(ssoToken) || !StringUtils.hasText(fileUrl)) {
return fileUrl;
}
// 已包含 ssoToken不区分大小写则不重复添加
String lower = fileUrl.toLowerCase();
if (lower.contains("ssotoken=")) {
return fileUrl;
}
// 简单拼接查询参数
return fileUrl.contains("?")
? fileUrl + "&ssoToken=" + ssoToken
: fileUrl + "?ssoToken=" + ssoToken;
}
private String resolveFileName(String overrideName, String fileUrl) {
if (StringUtils.hasText(overrideName)) {
return overrideName;
@@ -852,6 +936,30 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
return baseUrl + path;
}
private String resolveOaUrl(String path) {
IWorkProperties.Oa oa = properties.getOa();
if (oa == null || !StringUtils.hasText(oa.getBaseUrl())) {
throw ServiceExceptionUtil.exception(IWORK_BASE_URL_MISSING, "iwork.oa.base-url 未配置");
}
String baseUrl = oa.getBaseUrl();
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 requireOaPath(String path, String propertyPath) {
if (!StringUtils.hasText(path)) {
throw ServiceExceptionUtil.exception(IWORK_CONFIGURATION_INVALID, propertyPath + " 未配置");
}
return StringUtils.trimWhitespace(path);
}
private String executeRequest(Request request, ErrorCode errorCode) {
logCurlCommand(request);
try (Response response = okHttpClient().newCall(request).execute()) {
@@ -867,6 +975,47 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
}
private IWorkOaRawResponse executeOaRequest(Request request) {
long start = System.currentTimeMillis();
try (Response response = okHttpClient().newCall(request).execute()) {
ResponseBody responseBody = response.body();
okhttp3.MediaType okhttpMediaType = responseBody != null ? responseBody.contentType() : null;
String bodyString = responseBody != null ? responseBody.string() : null;
MediaType contentType = null;
if (okhttpMediaType != null) {
try {
contentType = MediaType.parseMediaType(okhttpMediaType.toString());
} catch (Exception ignored) {
// ignore parse error
}
}
Map<String, String> headers = new LinkedHashMap<>();
Headers respHeaders = response.headers();
for (String name : respHeaders.names()) {
headers.put(name, respHeaders.get(name));
}
long duration = System.currentTimeMillis() - start;
String briefBody = abbreviate(bodyString, 200);
if (response.isSuccessful()) {
log.info("[OA] {} {} -> {} ({}ms)", request.method(), request.url(), response.code(), duration);
} else {
log.warn("[OA] {} {} -> {} ({}ms) body={}", request.method(), request.url(), response.code(), duration, briefBody);
}
return IWorkOaRawResponse.builder()
.statusCode(response.code())
.body(bodyString)
.contentType(contentType)
.headers(headers)
.build();
} catch (IOException ex) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "调用 OA 接口失败: " + ex.getMessage());
}
}
private ServiceException buildRemoteException(ErrorCode errorCode, int statusCode, String responseBody) {
String detail = buildRemoteErrorDetail(statusCode, responseBody);
if (!StringUtils.hasText(detail)) {

View File

@@ -134,6 +134,12 @@ iwork:
sync-user: /api/hrm/resful/synHrmresource
workflow:
seal-workflow-id: "1753"
oa:
base-url: http://172.16.36.233:8080
app-id: a17ca6ca-88b0-463e-bffa-7995086bf225
paths:
get-token: /ssologin/getToken
check-token: /ssologin/checkToken
--- #################### RPC 远程调用相关配置 ####################