新增 iwork 附件回调接口
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user