Merge remote-tracking branch 'ztcloud/test' into dev

This commit is contained in:
yangchaojin
2026-01-29 15:09:44 +08:00
18 changed files with 492 additions and 5 deletions

View File

@@ -1,10 +1,14 @@
package com.zt.plat.module.system.mq.iwork;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class IWorkBizCallbackMessage {
/** 统一回调主题 */

View File

@@ -229,6 +229,7 @@ zt:
- system_seq_dtl
- system_seq_rcd
- system_sync_log
- system_iwork_seal_log
ignore-caches:
- user_role_ids
- permission_menu_ids

View File

@@ -77,10 +77,16 @@ public class IWorkIntegrationController {
return success(integrationService.createWorkflow(reqVO));
}
@PostMapping("/workflow/create-generic")
@Operation(summary = "通用流程创建:支持透传任意业务参数和调用不同的 workflowId")
public CommonResult<IWorkOperationRespVO> createGenericWorkflow(@Valid @RequestBody IWorkGenericWorkflowCreateReqVO reqVO) {
return success(integrationService.createGenericWorkflow(reqVO));
}
@PermitAll
@TenantIgnore
@PostMapping("/callback/file")
@Operation(summary = "iWork 文件回调:根据文件 URL 保存为附件并创建业务附件关联")
@Operation(summary = "iWork 文件回调:根据文件 URL 保存为附件并创建业务附件关联,记录日志并发送 MQ 通知业务系统")
public CommonResult<Long> callbackFile(@Valid @RequestBody IWorkFileCallbackReqVO reqVO) {
return success(integrationService.handleFileCallback(reqVO));
}

View File

@@ -0,0 +1,34 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Map;
/**
* 通用 iWork 流程创建请求 VO
* <p>
* 支持透传任意业务参数,直接将整个 payload 转发给 iWork
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "通用 iWork 流程创建请求")
public class IWorkGenericWorkflowCreateReqVO extends IWorkBaseReqVO {
@Schema(description = "流程模板 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "54")
@NotNull(message = "流程模板 ID 不能为空")
private Long workflowId;
@Schema(description = "透传给 iWork 的业务参数,直接作为请求体发送", requiredMode = Schema.RequiredMode.REQUIRED,
example = "{\"requestName\":\"用印-DJ-2025-0001\",\"mainData\":[{\"fieldName\":\"jbr\",\"fieldValue\":\"1001\"}]}")
@NotNull(message = "业务参数不能为空")
private Map<String, Object> payload;
@Schema(description = "业务编码,用于回调时关联业务数据", example = "DJ-2025-0001")
private String ywxtdjbh;
@Schema(description = "业务回调标识,用于 MQ 消息路由到对应业务消费者", example = "seal-callback")
private String bizCallbackKey;
}

View File

@@ -0,0 +1,55 @@
package com.zt.plat.module.system.dal.dataobject.iwork;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.zt.plat.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* iWork 流程创建日志。
* 用于记录流程创建时的关键信息,供回调时查询使用。
*/
@TableName("system_iwork_workflow_log")
@KeySequence("system_iwork_workflow_log_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class IWorkWorkflowLogDO extends BaseDO {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* iWork 返回的请求编号,唯一业务标识。
*/
private String requestId;
/**
* 流程模板 ID。
*/
private Long workflowId;
/**
* 业务编码(用于关联业务数据)。
*/
private String businessCode;
/**
* 业务回调标识(用于 MQ 消息路由)。
*/
private String bizCallbackKey;
/**
* 创建请求的原始参数JSON 格式,截断存储)。
*/
private String rawRequest;
/**
* 流程状态CREATED-已创建, CALLBACK_RECEIVED-已收到回调, COMPLETED-已完成
*/
private String status;
}

View File

@@ -0,0 +1,13 @@
package com.zt.plat.module.system.dal.mysql.iwork;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.module.system.dal.dataobject.iwork.IWorkWorkflowLogDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface IWorkWorkflowLogMapper extends BaseMapperX<IWorkWorkflowLogDO> {
default IWorkWorkflowLogDO selectByRequestId(String requestId) {
return selectOne(IWorkWorkflowLogDO::getRequestId, requestId);
}
}

View File

@@ -0,0 +1,67 @@
package com.zt.plat.module.system.mq.iwork;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.stereotype.Component;
/**
* iWork 业务回调消息测试消费者
* <p>
* 用于测试 IWorkBizCallbackMessage 消息的消费,
* 消费成功后会发送 IWorkBizCallbackResultMessage 通知 system 模块。
* <p>
* 注意:这是一个测试消费者,生产环境应由具体业务模块实现自己的消费者。
*/
@Slf4j
@Component
@RequiredArgsConstructor
@RocketMQMessageListener(
topic = IWorkBizCallbackMessage.TOPIC,
consumerGroup = IWorkBizCallbackMessage.TOPIC + "_ceshi_bz_01",
selectorExpression = "ceshi_bz_01" // 只订阅 tag=ceshi_bz_01 的消息
)
public class IWorkBizCallbackTestConsumer implements RocketMQListener<IWorkBizCallbackMessage> {
private final RocketMQTemplate rocketMQTemplate;
@Override
public void onMessage(IWorkBizCallbackMessage message) {
log.info("[IWorkBizCallbackTestConsumer] 收到回调消息: requestId={}, bizCallbackKey={}, attempt={}/{}",
message.getRequestId(),
message.getBizCallbackKey(),
message.getAttempt(),
message.getMaxAttempts());
log.info("[IWorkBizCallbackTestConsumer] payload={}", message.getPayload());
// 模拟业务处理
boolean success = processCallback(message);
// 发送处理结果给 system 模块
IWorkBizCallbackResultMessage result = IWorkBizCallbackResultMessage.builder()
.requestId(message.getRequestId())
.bizCallbackKey(message.getBizCallbackKey())
.payload(message.getPayload())
.attempt(message.getAttempt())
.maxAttempts(message.getMaxAttempts())
.success(success)
.errorMessage(success ? null : "测试模拟失败")
.build();
log.info("[IWorkBizCallbackTestConsumer] 准备发送结果到 topic={}", IWorkBizCallbackResultMessage.TOPIC);
rocketMQTemplate.syncSend(IWorkBizCallbackResultMessage.TOPIC+":123", result);
log.info("[IWorkBizCallbackTestConsumer] 已发送处理结果: success={}, requestId={}", success, message.getRequestId());
}
/**
* 模拟业务处理逻辑
*/
private boolean processCallback(IWorkBizCallbackMessage message) {
// 随机返回成功或失败,用于测试重试逻辑
boolean success = Math.random() > 0.5;
log.info("[IWorkBizCallbackTestConsumer] 模拟处理结果: {}", success ? "成功" : "失败");
return success;
}
}

View File

@@ -5,6 +5,7 @@ import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkAuth
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.IWorkFileCallbackReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkGenericWorkflowCreateReqVO;
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;
@@ -39,6 +40,11 @@ public interface IWorkIntegrationService {
*/
IWorkOperationRespVO createWorkflow(IWorkWorkflowCreateReqVO reqVO);
/**
* 通用流程创建:支持透传任意业务参数和调用不同的 workflowId
*/
IWorkOperationRespVO createGenericWorkflow(IWorkGenericWorkflowCreateReqVO reqVO);
/**
* 在 iWork 中对已有流程执行作废或干预。
*/

View File

@@ -0,0 +1,24 @@
package com.zt.plat.module.system.service.integration.iwork;
import com.zt.plat.module.system.dal.dataobject.iwork.IWorkWorkflowLogDO;
/**
* iWork 流程创建日志 Service
*/
public interface IWorkWorkflowLogService {
/**
* 保存流程创建日志
*/
void saveWorkflowLog(IWorkWorkflowLogDO logDO);
/**
* 根据 requestId 查询流程创建日志
*/
IWorkWorkflowLogDO getByRequestId(String requestId);
/**
* 更新流程状态
*/
void updateStatus(String requestId, String status);
}

View File

@@ -18,7 +18,12 @@ 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.framework.integration.iwork.config.IWorkProperties;
import com.zt.plat.module.system.mq.iwork.IWorkBizCallbackMessage;
import com.zt.plat.module.system.mq.iwork.IWorkBizCallbackProducer;
import com.zt.plat.module.system.service.integration.iwork.IWorkCallbackLogService;
import com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationService;
import com.zt.plat.module.system.service.integration.iwork.IWorkWorkflowLogService;
import com.zt.plat.module.system.dal.dataobject.iwork.IWorkWorkflowLogDO;
import com.zt.plat.framework.tenant.core.util.TenantUtils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -60,6 +65,9 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
private final FileApi fileApi;
private final BusinessFileApi businessFileApi;
private final IWorkCallbackLogService callbackLogService;
private final IWorkWorkflowLogService workflowLogService;
private final IWorkBizCallbackProducer bizCallbackProducer;
private final Cache<String, RegistrationState> registrationCache = Caffeine.newBuilder()
.maximumSize(32)
@@ -141,7 +149,45 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
Map<String, Object> payload = buildCreatePayload(reqVO);
String createWorkflowPath = requireConfiguredPath(properties.getPaths().getCreateWorkflow(), "iwork.paths.create-workflow");
String responseBody = executeFormRequest(createWorkflowPath, null, appId, session, payload);
return buildOperationResponse(responseBody);
IWorkOperationRespVO respVO = buildOperationResponse(responseBody);
// 创建成功后记录日志
if (respVO.isSuccess() && respVO.getPayload() != null
&& respVO.getPayload().getData() != null
&& respVO.getPayload().getData().getRequestId() != null) {
saveWorkflowLog(respVO, reqVO);
}
return respVO;
}
@Override
public IWorkOperationRespVO createGenericWorkflow(IWorkGenericWorkflowCreateReqVO reqVO) {
assertConfigured();
String appId = resolveAppId();
ClientKeyPair clientKeyPair = resolveClientKeyPair(appId, Boolean.TRUE.equals(reqVO.getForceRefreshToken()));
String operatorUserId = resolveOperatorUserId(reqVO.getOperatorUserId());
IWorkSession session = createSession(appId, clientKeyPair, operatorUserId, Boolean.TRUE.equals(reqVO.getForceRefreshToken()));
// 构建透传参数,将 workflowId 加入 payload
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("workflowId", reqVO.getWorkflowId());
if (reqVO.getPayload() != null) {
payload.putAll(reqVO.getPayload());
}
String createWorkflowPath = requireConfiguredPath(properties.getPaths().getCreateWorkflow(), "iwork.paths.create-workflow");
String responseBody = executeFormRequest(createWorkflowPath, null, appId, session, payload);
IWorkOperationRespVO respVO = buildOperationResponse(responseBody);
// 创建成功后记录日志,保存 requestId、businessCode、bizCallbackKey
if (respVO.isSuccess() && respVO.getPayload() != null
&& respVO.getPayload().getData() != null
&& respVO.getPayload().getData().getRequestId() != null) {
saveWorkflowCreateLog(respVO, reqVO);
}
return respVO;
}
@Override
@@ -175,6 +221,14 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "业务编码不能为空");
}
// 1. 通过 requestId 查询流程创建日志,获取 bizCallbackKeyiWork 回调不会回传此字段)
String bizCallbackKey = null;
IWorkWorkflowLogDO workflowLog = workflowLogService.getByRequestId(reqVO.getRequestId());
if (workflowLog != null && StringUtils.hasText(workflowLog.getBizCallbackKey())) {
bizCallbackKey = workflowLog.getBizCallbackKey();
}
// 2. 保存文件附件
BusinessFileRespDTO referenceBusinessFile = loadBusinessFileByBusinessCode(businessCode);
Long tenantId = referenceBusinessFile.getTenantId();
if (tenantId == null) {
@@ -183,7 +237,113 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
AtomicReference<Long> attachmentIdRef = new AtomicReference<>();
TenantUtils.execute(tenantId, () -> attachmentIdRef.set(saveCallbackAttachment(fileUrl, reqVO.getFileName(), referenceBusinessFile)));
return attachmentIdRef.get();
Long attachmentId = attachmentIdRef.get();
// 3. 更新回调日志
int maxRetry = properties.getCallback().getRetry().getMaxAttempts();
String rawBody = buildCallbackRawBody(reqVO);
IWorkWorkflowCallbackReqVO logReqVO = buildCallbackLogReqVO(reqVO);
if (StringUtils.hasText(bizCallbackKey)) {
logReqVO.setBizCallbackKey(bizCallbackKey);
}
callbackLogService.upsertOnCallback(logReqVO, maxRetry, rawBody);
// 4. 更新流程创建日志状态
if (workflowLog != null) {
String status = StringUtils.hasText(reqVO.getStatus()) ? reqVO.getStatus() : "CALLBACK_RECEIVED";
workflowLogService.updateStatus(reqVO.getRequestId(), status);
log.info("[handleFileCallback] 已更新流程状态: requestId={}, status={}", reqVO.getRequestId(), status);
}
// 5. 发送 MQ 通知业务系统(仅当 bizCallbackKey 存在时发送)
if (StringUtils.hasText(bizCallbackKey)) {
IWorkBizCallbackMessage message = IWorkBizCallbackMessage.builder()
.requestId(reqVO.getRequestId())
.bizCallbackKey(bizCallbackKey)
.payload(reqVO)
.attempt(0)
.maxAttempts(maxRetry)
.build();
bizCallbackProducer.send(message);
log.info("[handleFileCallback] 已发送 MQ 通知requestId={}, bizCallbackKey={}",
reqVO.getRequestId(), bizCallbackKey);
} else {
log.warn("[handleFileCallback] 未找到 bizCallbackKey跳过 MQ 通知requestId={}",
reqVO.getRequestId());
}
return attachmentId;
}
private IWorkWorkflowCallbackReqVO buildCallbackLogReqVO(IWorkFileCallbackReqVO reqVO) {
IWorkWorkflowCallbackReqVO logReqVO = new IWorkWorkflowCallbackReqVO();
logReqVO.setRequestId(reqVO.getRequestId());
logReqVO.setBusinessCode(reqVO.getBusinessCode());
logReqVO.setBizCallbackKey(reqVO.getBizCallbackKey());
logReqVO.setStatus(reqVO.getStatus());
logReqVO.setRawBody(reqVO.getRawBody());
return logReqVO;
}
private String buildCallbackRawBody(IWorkFileCallbackReqVO reqVO) {
if (StringUtils.hasText(reqVO.getRawBody())) {
return reqVO.getRawBody();
}
try {
return objectMapper.writeValueAsString(reqVO);
} catch (JsonProcessingException e) {
log.warn("[buildCallbackRawBody] 序列化回调请求失败", e);
return null;
}
}
private void saveWorkflowCreateLog(IWorkOperationRespVO respVO, IWorkGenericWorkflowCreateReqVO reqVO) {
try {
String requestId = String.valueOf(respVO.getPayload().getData().getRequestId());
String rawRequest = objectMapper.writeValueAsString(reqVO);
IWorkWorkflowLogDO logDO = new IWorkWorkflowLogDO();
logDO.setRequestId(requestId);
logDO.setWorkflowId(reqVO.getWorkflowId());
logDO.setBusinessCode(reqVO.getYwxtdjbh());
logDO.setBizCallbackKey(reqVO.getBizCallbackKey());
logDO.setRawRequest(truncateRawRequest(rawRequest));
logDO.setStatus("CREATED");
workflowLogService.saveWorkflowLog(logDO);
log.info("[createGenericWorkflow] 已记录流程创建日志requestId={}, businessCode={}, bizCallbackKey={}",
requestId, reqVO.getYwxtdjbh(), reqVO.getBizCallbackKey());
} catch (Exception e) {
log.warn("[createGenericWorkflow] 记录流程创建日志失败", e);
}
}
private void saveWorkflowLog(IWorkOperationRespVO respVO, IWorkWorkflowCreateReqVO reqVO) {
try {
String requestId = String.valueOf(respVO.getPayload().getData().getRequestId());
String rawRequest = objectMapper.writeValueAsString(reqVO);
IWorkWorkflowLogDO logDO = new IWorkWorkflowLogDO();
logDO.setRequestId(requestId);
logDO.setWorkflowId(Long.parseLong(properties.getWorkflow().getSealWorkflowId()));
logDO.setBusinessCode(reqVO.getYwxtdjbh());
logDO.setBizCallbackKey(reqVO.getBizCallbackKey());
logDO.setRawRequest(truncateRawRequest(rawRequest));
logDO.setStatus("CREATED");
workflowLogService.saveWorkflowLog(logDO);
log.info("[createWorkflow] 已记录流程创建日志requestId={}, workflowId={}, businessCode={}, bizCallbackKey={}",
requestId, properties.getWorkflow().getSealWorkflowId(), reqVO.getYwxtdjbh(), reqVO.getBizCallbackKey());
} catch (Exception e) {
log.warn("[createWorkflow] 记录流程创建日志失败", e);
}
}
private String truncateRawRequest(String raw) {
if (!StringUtils.hasText(raw)) {
return raw;
}
return raw.length() > 2000 ? raw.substring(0, 2000) : raw;
}
@Override
@@ -760,7 +920,7 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
return value.booleanValue();
}
String text = value.asText();
return Objects.equals("0", text) || Objects.equals("1", text) || Objects.equals("success", text);
return Objects.equals("0", text) || Objects.equals("1", text) || Objects.equals("success", text) || Objects.equals("SUCCESS", text);
}
private String resolveMessage(JsonNode node) {

View File

@@ -0,0 +1,35 @@
package com.zt.plat.module.system.service.integration.iwork.impl;
import com.zt.plat.module.system.dal.dataobject.iwork.IWorkWorkflowLogDO;
import com.zt.plat.module.system.dal.mysql.iwork.IWorkWorkflowLogMapper;
import com.zt.plat.module.system.service.integration.iwork.IWorkWorkflowLogService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class IWorkWorkflowLogServiceImpl implements IWorkWorkflowLogService {
private final IWorkWorkflowLogMapper workflowLogMapper;
@Override
public void saveWorkflowLog(IWorkWorkflowLogDO logDO) {
workflowLogMapper.insert(logDO);
}
@Override
public IWorkWorkflowLogDO getByRequestId(String requestId) {
return workflowLogMapper.selectByRequestId(requestId);
}
@Override
public void updateStatus(String requestId, String status) {
IWorkWorkflowLogDO existing = workflowLogMapper.selectByRequestId(requestId);
if (existing != null) {
IWorkWorkflowLogDO update = new IWorkWorkflowLogDO();
update.setId(existing.getId());
update.setStatus(status);
workflowLogMapper.updateById(update);
}
}
}