新增 iwork 附件回调接口

This commit is contained in:
chenbowen
2025-11-28 18:47:21 +08:00
parent 0c22975df0
commit 6ab387cba0
5 changed files with 137 additions and 1 deletions

View File

@@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.security.PermitAll;
import static com.zt.plat.framework.common.pojo.CommonResult.success; import static com.zt.plat.framework.common.pojo.CommonResult.success;
@@ -55,6 +56,13 @@ public class IWorkIntegrationController {
return success(integrationService.createWorkflow(reqVO)); return success(integrationService.createWorkflow(reqVO));
} }
@PermitAll
@PostMapping("/callback/file")
@Operation(summary = "iWork 文件回调:根据文件 URL 保存为附件并创建业务附件关联")
public CommonResult<Long> callbackFile(@Valid @RequestBody IWorkFileCallbackReqVO reqVO) {
return success(integrationService.handleFileCallback(reqVO));
}
@PostMapping("/workflow/void") @PostMapping("/workflow/void")
@Operation(summary = "作废 / 干预 iWork 流程") @Operation(summary = "作废 / 干预 iWork 流程")
public CommonResult<IWorkOperationRespVO> voidWorkflow(@Valid @RequestBody IWorkWorkflowVoidReqVO reqVO) { public CommonResult<IWorkOperationRespVO> voidWorkflow(@Valid @RequestBody IWorkWorkflowVoidReqVO reqVO) {

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 jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Schema(name = "IWorkFileCallbackReqVO", description = "iWork 文件回调请求 VO")
@Data
public class IWorkFileCallbackReqVO {
@Schema(description = "文件下载 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://example.com/file.pdf")
@NotBlank(message = "文件 URL 不能为空")
private String fileUrl;
@Schema(description = "业务 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotBlank(message = "业务 ID 不能为空")
private String businessId;
@Schema(description = "文件名称,可选", example = "合同附件.pdf")
private String fileName;
}

View File

@@ -1,5 +1,6 @@
package com.zt.plat.module.system.framework.rpc.config; package com.zt.plat.module.system.framework.rpc.config;
import com.zt.plat.module.infra.api.businessfile.BusinessFileApi;
import com.zt.plat.module.infra.api.config.ConfigApi; import com.zt.plat.module.infra.api.config.ConfigApi;
import com.zt.plat.module.infra.api.file.FileApi; import com.zt.plat.module.infra.api.file.FileApi;
import com.zt.plat.module.infra.api.websocket.WebSocketSenderApi; import com.zt.plat.module.infra.api.websocket.WebSocketSenderApi;
@@ -7,6 +8,6 @@ import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@Configuration(value = "systemRpcConfiguration", proxyBeanMethods = false) @Configuration(value = "systemRpcConfiguration", proxyBeanMethods = false)
@EnableFeignClients(clients = {FileApi.class, WebSocketSenderApi.class, ConfigApi.class}) @EnableFeignClients(clients = {FileApi.class, WebSocketSenderApi.class, ConfigApi.class, BusinessFileApi.class})
public class RpcConfiguration { public class RpcConfiguration {
} }

View File

@@ -4,6 +4,7 @@ import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkAuth
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkAuthRegisterRespVO; 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.IWorkAuthTokenReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkAuthTokenRespVO; 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.IWorkOperationRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkUserInfoReqVO; 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.IWorkUserInfoRespVO;
@@ -39,4 +40,12 @@ public interface IWorkIntegrationService {
* 在 iWork 中对已有流程执行作废或干预。 * 在 iWork 中对已有流程执行作废或干预。
*/ */
IWorkOperationRespVO voidWorkflow(IWorkWorkflowVoidReqVO reqVO); IWorkOperationRespVO voidWorkflow(IWorkWorkflowVoidReqVO reqVO);
/**
* 处理来自 iWork 的文件回调:根据文件 URL 下载并保存为附件,同时创建业务附件关联记录。
*
* @param reqVO 回调请求
* @return 创建的业务附件关联记录 ID 或附件 ID视实现而定
*/
Long handleFileCallback(IWorkFileCallbackReqVO reqVO);
} }

View File

@@ -9,6 +9,12 @@ import com.github.benmanes.caffeine.cache.Caffeine;
import com.zt.plat.framework.common.exception.ErrorCode; import com.zt.plat.framework.common.exception.ErrorCode;
import com.zt.plat.framework.common.exception.ServiceException; import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.module.infra.api.businessfile.BusinessFileApi;
import com.zt.plat.module.infra.api.businessfile.dto.BusinessFileSaveReqDTO;
import com.zt.plat.module.infra.api.file.FileApi;
import com.zt.plat.module.infra.api.file.dto.FileCreateReqDTO;
import com.zt.plat.module.infra.api.file.dto.FileRespDTO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.*; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.*;
import com.zt.plat.module.system.framework.integration.iwork.config.IWorkProperties; import com.zt.plat.module.system.framework.integration.iwork.config.IWorkProperties;
import com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationService; import com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationService;
@@ -24,6 +30,8 @@ import org.springframework.util.StringUtils;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.*; import java.security.*;
import java.security.spec.X509EncodedKeySpec; import java.security.spec.X509EncodedKeySpec;
@@ -48,6 +56,9 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
private final IWorkProperties properties; private final IWorkProperties properties;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final FileApi fileApi;
private final BusinessFileApi businessFileApi;
private final Cache<SessionKey, IWorkSession> sessionCache = Caffeine.newBuilder() private final Cache<SessionKey, IWorkSession> sessionCache = Caffeine.newBuilder()
.maximumSize(256) .maximumSize(256)
.build(); .build();
@@ -149,6 +160,92 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
return buildOperationResponse(responseBody); return buildOperationResponse(responseBody);
} }
@Override
public Long handleFileCallback(IWorkFileCallbackReqVO reqVO) {
if (reqVO == null) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "回调请求不能为空");
}
String fileUrl = Optional.ofNullable(reqVO.getFileUrl()).map(String::trim).orElse("");
String businessIdStr = Optional.ofNullable(reqVO.getBusinessId()).map(String::trim).orElse("");
if (!StringUtils.hasText(fileUrl)) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "文件 URL 不能为空");
}
if (!StringUtils.hasText(businessIdStr)) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "业务 ID 不能为空");
}
Long businessId;
try {
businessId = Long.valueOf(businessIdStr);
} catch (NumberFormatException ex) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "业务 ID 必须是数字: " + businessIdStr);
}
// 通过文件 API 创建文件
FileCreateReqDTO fileCreateReqDTO = new FileCreateReqDTO();
fileCreateReqDTO.setName(resolveFileName(reqVO.getFileName(), fileUrl));
fileCreateReqDTO.setDirectory(null);
fileCreateReqDTO.setType(null);
fileCreateReqDTO.setContent(downloadFileBytes(fileUrl));
CommonResult<FileRespDTO> fileResult = fileApi.createFileWithReturn(fileCreateReqDTO);
if (fileResult == null || !fileResult.isSuccess() || fileResult.getData() == null) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "通过文件服务创建附件失败: " + Optional.ofNullable(fileResult).map(CommonResult::getMsg).orElse("未知错误"));
}
Long fileId = fileResult.getData().getId();
BusinessFileSaveReqDTO businessReq = BusinessFileSaveReqDTO.builder()
.businessId(businessId)
.fileId(fileId)
.fileName(fileResult.getData().getName())
.source("iwork")
.build();
CommonResult<Long> bizResult = businessFileApi.createBusinessFile(businessReq);
if (bizResult == null || !bizResult.isSuccess() || bizResult.getData() == null) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "创建业务附件关联失败: " + Optional.ofNullable(bizResult).map(CommonResult::getMsg).orElse("未知错误"));
}
return bizResult.getData();
}
private byte[] downloadFileBytes(String fileUrl) {
OkHttpClient client = okHttpClient();
Request request = new Request.Builder().url(fileUrl).get().build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "下载文件失败HTTP 状态码: " + response.code());
}
ResponseBody body = response.body();
if (body == null) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "下载文件失败,响应体为空");
}
return body.bytes();
} catch (IOException ex) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "下载文件异常: " + ex.getMessage());
}
}
private String resolveFileName(String overrideName, String fileUrl) {
if (StringUtils.hasText(overrideName)) {
return overrideName;
}
try {
URI uri = new URI(fileUrl);
String path = uri.getPath();
if (!StringUtils.hasText(path)) {
return "iwork-file";
}
int idx = path.lastIndexOf('/');
if (idx >= 0 && idx + 1 < path.length()) {
return path.substring(idx + 1);
}
return path;
} catch (URISyntaxException ex) {
return "iwork-file";
}
}
private void assertConfigured() { private void assertConfigured() {
if (!StringUtils.hasText(properties.getBaseUrl())) { if (!StringUtils.hasText(properties.getBaseUrl())) {
throw ServiceExceptionUtil.exception(IWORK_BASE_URL_MISSING); throw ServiceExceptionUtil.exception(IWORK_BASE_URL_MISSING);