@@ -3,6 +3,7 @@ package com.zt.plat.module.system.controller.admin.integration.iwork;
|
||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
|
||||
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.*;
|
||||
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.IWorkOrgRestService;
|
||||
import com.zt.plat.module.system.service.integration.iwork.IWorkSyncService;
|
||||
@@ -19,6 +20,8 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import static com.zt.plat.framework.common.pojo.CommonResult.success;
|
||||
/**
|
||||
* 提供统一 iWork 流程能力的管理端接口。
|
||||
@@ -34,6 +37,7 @@ public class IWorkIntegrationController {
|
||||
private final IWorkIntegrationService integrationService;
|
||||
private final IWorkOrgRestService orgRestService;
|
||||
private final IWorkSyncService syncService;
|
||||
private final IWorkCallbackLogService callbackLogService;
|
||||
|
||||
@PostMapping("/auth/register")
|
||||
@Operation(summary = "注册 iWork 凭证,获取服务端公钥与 secret")
|
||||
@@ -87,6 +91,39 @@ public class IWorkIntegrationController {
|
||||
return success(integrationService.voidWorkflow(reqVO));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermission('system:iwork:log:query')")
|
||||
@PostMapping("/log/page")
|
||||
@Operation(summary = "iWork 回调日志分页查询")
|
||||
public CommonResult<com.zt.plat.framework.common.pojo.PageResult<IWorkCallbackLogRespVO>> pageLogs(@Valid @RequestBody IWorkCallbackLogPageReqVO reqVO) {
|
||||
com.zt.plat.framework.common.pojo.PageResult<com.zt.plat.module.system.dal.dataobject.iwork.IWorkSealLogDO> page = callbackLogService.page(reqVO);
|
||||
java.util.List<IWorkCallbackLogRespVO> mapped = new java.util.ArrayList<>();
|
||||
for (com.zt.plat.module.system.dal.dataobject.iwork.IWorkSealLogDO log : page.getList()) {
|
||||
IWorkCallbackLogRespVO vo = new IWorkCallbackLogRespVO();
|
||||
vo.setId(log.getId());
|
||||
vo.setRequestId(log.getRequestId());
|
||||
vo.setBusinessCode(log.getBusinessCode());
|
||||
vo.setBizCallbackKey(log.getBizCallbackKey());
|
||||
vo.setStatus(log.getStatus());
|
||||
vo.setRetryCount(log.getRetryCount());
|
||||
vo.setMaxRetry(log.getMaxRetry());
|
||||
vo.setLastErrorMessage(log.getLastErrorMessage());
|
||||
vo.setRawCallback(log.getRawCallback());
|
||||
vo.setLastCallbackTime(log.getLastCallbackTime());
|
||||
vo.setCreateTime(log.getCreateTime());
|
||||
vo.setUpdateTime(log.getUpdateTime());
|
||||
mapped.add(vo);
|
||||
}
|
||||
return success(new com.zt.plat.framework.common.pojo.PageResult<>(mapped, page.getTotal(), page.getSummary()));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermission('system:iwork:log:retry')")
|
||||
@PostMapping("/log/retry")
|
||||
@Operation(summary = "iWork 回调手工重试")
|
||||
public CommonResult<Boolean> retry(@Valid @RequestBody IWorkWorkflowVoidReqVO reqVO) {
|
||||
callbackLogService.resetAndDispatch(reqVO.getRequestId());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// ----------------- 人力组织接口 -----------------
|
||||
|
||||
@PostMapping("/hr/subcompany/page")
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
|
||||
|
||||
import com.zt.plat.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static com.zt.plat.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - iWork 用印回调日志分页查询")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class IWorkCallbackLogPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "requestId")
|
||||
private String requestId;
|
||||
|
||||
@Schema(description = "业务单号")
|
||||
private String businessCode;
|
||||
|
||||
@Schema(description = "业务回调标识")
|
||||
private String bizCallbackKey;
|
||||
|
||||
@Schema(description = "状态")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "创建时间范围")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] createTime;
|
||||
|
||||
@Schema(description = "最后回调时间范围")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] lastCallbackTime;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "iWork 用印回调日志响应 VO")
|
||||
@Data
|
||||
public class IWorkCallbackLogRespVO {
|
||||
|
||||
private Long id;
|
||||
private String requestId;
|
||||
private String businessCode;
|
||||
private String bizCallbackKey;
|
||||
private Integer status;
|
||||
private Integer retryCount;
|
||||
private Integer maxRetry;
|
||||
private String lastErrorMessage;
|
||||
private String rawCallback;
|
||||
private LocalDateTime lastCallbackTime;
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -2,12 +2,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 jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(name = "IWorkFileCallbackReqVO", description = "iWork 文件回调请求 VO")
|
||||
@Data
|
||||
public class IWorkFileCallbackReqVO {
|
||||
|
||||
@Schema(description = "iWork requestId,唯一标识", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "requestId 不能为空")
|
||||
private String requestId;
|
||||
|
||||
@Schema(description = "业务回调标识 bizCallbackKey,≤255 字符", example = "seal-flow-callback")
|
||||
@Size(max = 255, message = "bizCallbackKey 长度不能超过 255 字符")
|
||||
private String bizCallbackKey;
|
||||
|
||||
@Schema(description = "文件下载 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://example.com/file.pdf")
|
||||
@NotBlank(message = "文件 URL 不能为空")
|
||||
private String fileUrl;
|
||||
@@ -19,6 +28,9 @@ public class IWorkFileCallbackReqVO {
|
||||
@Schema(description = "文件名称,可选", example = "合同附件.pdf")
|
||||
private String fileName;
|
||||
|
||||
@Schema(description = "OA 单点下载使用的 ssoToken,可选", example = "6102A7C13F09DD6B1AF06CDA0E479AC8...")
|
||||
private String ssoToken;
|
||||
@Schema(description = "业务回调状态/结果码,可选")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "原始回调文本(可选,若不传则使用 payload 或请求体序列化)")
|
||||
private String rawBody;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,6 @@ public class IWorkOaTokenReqVO {
|
||||
@NotBlank(message = "loginId 不能为空")
|
||||
private String loginId;
|
||||
|
||||
@Schema(description = "应用 appid,未填则使用配置默认值", example = "a17ca6ca-88b0-463e-bffa-7995086bf225")
|
||||
@Schema(description = "应用 appid,已固定使用配置值,无需传递", example = "")
|
||||
private String appId;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
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(description = "iWork 流程回调请求")
|
||||
@Data
|
||||
public class IWorkWorkflowCallbackReqVO {
|
||||
|
||||
@Schema(description = "iWork requestId,唯一标识", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "requestId 不能为空")
|
||||
private String requestId;
|
||||
|
||||
@Schema(description = "业务单号 (ywxtdjbh)")
|
||||
private String businessCode;
|
||||
|
||||
@Schema(description = "业务回调标识 bizCallbackKey")
|
||||
private String bizCallbackKey;
|
||||
|
||||
@Schema(description = "回调状态/结果码")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "原始回调文本(可截断存储)")
|
||||
private String rawBody;
|
||||
}
|
||||
@@ -34,9 +34,9 @@ public class IWorkWorkflowCreateReqVO extends IWorkBaseReqVO {
|
||||
|
||||
@Schema(description = "用印材料附件 URL(必填)")
|
||||
private String xyywjUrl;
|
||||
|
||||
@Schema(description = "用印材料附件文件名(必填)")
|
||||
private String xyywjFileName;
|
||||
|
||||
@Schema(description = "业务回调标识(回调分发使用,≤255 字符)")
|
||||
private String bizCallbackKey;
|
||||
|
||||
@Schema(description = "用印事项")
|
||||
private String yysx;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
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;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* iWork 用印流程回调日志。
|
||||
*/
|
||||
@TableName("system_iwork_seal_log")
|
||||
@KeySequence("system_iwork_seal_log_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class IWorkSealLogDO extends BaseDO {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* iWork 返回的请求编号,唯一业务标识。
|
||||
*/
|
||||
private String requestId;
|
||||
|
||||
/**
|
||||
* 业务单号(ywxtdjbh)。
|
||||
*/
|
||||
private String businessCode;
|
||||
|
||||
/**
|
||||
* 业务回调标识。
|
||||
*/
|
||||
private String bizCallbackKey;
|
||||
|
||||
/**
|
||||
* 状态枚举,参考 IWorkCallbackStatusEnum。
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 已执行的自动/手工重试次数。
|
||||
*/
|
||||
private Integer retryCount;
|
||||
|
||||
/**
|
||||
* 最大重试次数(快照)。
|
||||
*/
|
||||
private Integer maxRetry;
|
||||
|
||||
/**
|
||||
* 最后一次错误信息。
|
||||
*/
|
||||
private String lastErrorMessage;
|
||||
|
||||
/**
|
||||
* 回调原始负载(截断)。
|
||||
*/
|
||||
private String rawCallback;
|
||||
|
||||
/**
|
||||
* 最近一次回调时间。
|
||||
*/
|
||||
private LocalDateTime lastCallbackTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.zt.plat.module.system.dal.mysql.iwork;
|
||||
|
||||
import com.zt.plat.framework.common.pojo.PageResult;
|
||||
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkCallbackLogPageReqVO;
|
||||
import com.zt.plat.module.system.dal.dataobject.iwork.IWorkSealLogDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface IWorkSealLogMapper extends BaseMapperX<IWorkSealLogDO> {
|
||||
|
||||
default IWorkSealLogDO selectByRequestId(String requestId) {
|
||||
return selectOne(IWorkSealLogDO::getRequestId, requestId);
|
||||
}
|
||||
|
||||
default PageResult<IWorkSealLogDO> selectPage(IWorkCallbackLogPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<IWorkSealLogDO>()
|
||||
.eqIfPresent(IWorkSealLogDO::getRequestId, reqVO.getRequestId())
|
||||
.eqIfPresent(IWorkSealLogDO::getBusinessCode, reqVO.getBusinessCode())
|
||||
.eqIfPresent(IWorkSealLogDO::getBizCallbackKey, reqVO.getBizCallbackKey())
|
||||
.eqIfPresent(IWorkSealLogDO::getStatus, reqVO.getStatus())
|
||||
.betweenIfPresent(IWorkSealLogDO::getCreateTime, reqVO.getCreateTime())
|
||||
.betweenIfPresent(IWorkSealLogDO::getLastCallbackTime, reqVO.getLastCallbackTime())
|
||||
.orderByDesc(IWorkSealLogDO::getId));
|
||||
}
|
||||
}
|
||||
@@ -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 Callback callback = new Callback();
|
||||
private final Oa oa = new Oa();
|
||||
|
||||
@Data
|
||||
@@ -127,6 +128,26 @@ public class IWorkProperties {
|
||||
private String sealWorkflowId;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Callback {
|
||||
/**
|
||||
* 业务回调重试配置。
|
||||
*/
|
||||
private final Retry retry = new Retry();
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Retry {
|
||||
/**
|
||||
* 最大重试次数,默认 3。
|
||||
*/
|
||||
private int maxAttempts = 3;
|
||||
/**
|
||||
* 重试延迟(秒),默认 5。
|
||||
*/
|
||||
private int delaySeconds = 5;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Oa {
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.zt.plat.module.system.mq.iwork;
|
||||
|
||||
import com.zt.plat.module.system.framework.integration.iwork.config.IWorkProperties;
|
||||
import com.zt.plat.module.system.service.integration.iwork.IWorkCallbackLogService;
|
||||
import com.zt.plat.module.system.mq.iwork.IWorkBizCallbackMessage;
|
||||
import com.zt.plat.module.system.mq.iwork.IWorkBizCallbackResultMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 监听业务模块回传的处理结果,并在失败时由 system 模块负责重试投递原始回调消息。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@RocketMQMessageListener(topic = IWorkBizCallbackResultMessage.TOPIC, consumerGroup = IWorkBizCallbackResultMessage.TOPIC + "_CONSUMER")
|
||||
public class IWorkBizCallbackListener implements RocketMQListener<IWorkBizCallbackResultMessage>, InitializingBean {
|
||||
|
||||
private final IWorkCallbackLogService logService;
|
||||
private final IWorkProperties properties;
|
||||
private final IWorkBizCallbackProducer producer;
|
||||
private ScheduledExecutorService scheduler;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
scheduler = Executors.newScheduledThreadPool(1, r -> new Thread(r, "iwork-callback-retry"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(IWorkBizCallbackResultMessage message) {
|
||||
String key = message.getBizCallbackKey();
|
||||
if (message.isSuccess()) {
|
||||
logService.markSuccess(message.getRequestId());
|
||||
return;
|
||||
}
|
||||
|
||||
int attempt = message.getAttempt() + 1;
|
||||
logService.incrementRetry(message.getRequestId());
|
||||
int maxAttempts = message.getMaxAttempts() > 0 ? message.getMaxAttempts() : properties.getCallback().getRetry().getMaxAttempts();
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
logService.markFailure(message.getRequestId(), message.getErrorMessage(), true, maxAttempts);
|
||||
|
||||
IWorkBizCallbackMessage next = IWorkBizCallbackMessage.builder()
|
||||
.requestId(message.getRequestId())
|
||||
.bizCallbackKey(key)
|
||||
.payload(message.getPayload())
|
||||
.attempt(attempt)
|
||||
.maxAttempts(maxAttempts)
|
||||
.build();
|
||||
|
||||
int delay = properties.getCallback().getRetry().getDelaySeconds();
|
||||
scheduler.schedule(() -> producer.send(next), delay, TimeUnit.SECONDS);
|
||||
} else {
|
||||
logService.markFailure(message.getRequestId(), message.getErrorMessage(), false, maxAttempts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.zt.plat.module.system.mq.iwork;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class IWorkBizCallbackProducer {
|
||||
|
||||
private final RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
public void send(IWorkBizCallbackMessage message) {
|
||||
// 使用 tag=bizCallbackKey,方便业务侧按 key 订阅
|
||||
String destination = IWorkBizCallbackMessage.TOPIC + ":" + message.getBizCallbackKey();
|
||||
rocketMQTemplate.syncSend(destination, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.zt.plat.module.system.service.integration.iwork;
|
||||
|
||||
import com.zt.plat.framework.common.pojo.PageResult;
|
||||
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkCallbackLogPageReqVO;
|
||||
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkWorkflowCallbackReqVO;
|
||||
import com.zt.plat.module.system.dal.dataobject.iwork.IWorkSealLogDO;
|
||||
|
||||
public interface IWorkCallbackLogService {
|
||||
|
||||
IWorkSealLogDO upsertOnCallback(IWorkWorkflowCallbackReqVO reqVO, int maxRetry, String rawBody);
|
||||
|
||||
void markSuccess(String requestId);
|
||||
|
||||
void markFailure(String requestId, String error, boolean retrying, int maxRetry);
|
||||
|
||||
void incrementRetry(String requestId);
|
||||
|
||||
PageResult<IWorkSealLogDO> page(IWorkCallbackLogPageReqVO reqVO);
|
||||
|
||||
void resetAndDispatch(String requestId);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.zt.plat.module.system.service.integration.iwork.callback;
|
||||
|
||||
public interface IWorkBizCallbackHandler {
|
||||
|
||||
/**
|
||||
* 返回 handler 能处理的 bizCallbackKey。
|
||||
*/
|
||||
String getBizCallbackKey();
|
||||
|
||||
/**
|
||||
* 处理业务回调。
|
||||
* @param requestId iWork requestId
|
||||
* @param payload 回调负载
|
||||
*/
|
||||
void handle(String requestId, Object payload) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.zt.plat.module.system.service.integration.iwork.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum IWorkCallbackStatusEnum {
|
||||
|
||||
CREATE_PENDING(0),
|
||||
CREATE_SUCCESS(1),
|
||||
CREATE_FAILED(2),
|
||||
CALLBACK_PENDING(3),
|
||||
CALLBACK_SUCCESS(4),
|
||||
CALLBACK_FAILED(5),
|
||||
CALLBACK_RETRYING(6),
|
||||
CALLBACK_RETRY_FAILED(7);
|
||||
|
||||
private final int status;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.zt.plat.module.system.service.integration.iwork.impl;
|
||||
|
||||
import com.zt.plat.framework.common.pojo.PageResult;
|
||||
import com.zt.plat.framework.common.util.json.JsonUtils;
|
||||
import com.zt.plat.framework.common.util.object.ObjectUtils;
|
||||
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkCallbackLogPageReqVO;
|
||||
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkWorkflowCallbackReqVO;
|
||||
import com.zt.plat.module.system.dal.dataobject.iwork.IWorkSealLogDO;
|
||||
import com.zt.plat.module.system.dal.mysql.iwork.IWorkSealLogMapper;
|
||||
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.enums.IWorkCallbackStatusEnum;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class IWorkCallbackLogServiceImpl implements IWorkCallbackLogService {
|
||||
|
||||
private static final int RAW_MAX = 2000;
|
||||
|
||||
private final IWorkSealLogMapper logMapper;
|
||||
private final IWorkBizCallbackProducer producer;
|
||||
private final IWorkProperties properties;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public IWorkSealLogDO upsertOnCallback(IWorkWorkflowCallbackReqVO reqVO, int maxRetry, String rawBody) {
|
||||
IWorkSealLogDO existing = logMapper.selectByRequestId(reqVO.getRequestId());
|
||||
IWorkSealLogDO log = Optional.ofNullable(existing).orElseGet(IWorkSealLogDO::new);
|
||||
log.setRequestId(reqVO.getRequestId());
|
||||
log.setBusinessCode(reqVO.getBusinessCode());
|
||||
log.setBizCallbackKey(reqVO.getBizCallbackKey());
|
||||
log.setStatus(IWorkCallbackStatusEnum.CALLBACK_PENDING.getStatus());
|
||||
log.setRetryCount(ObjectUtils.defaultIfNull(log.getRetryCount(), 0));
|
||||
log.setMaxRetry(maxRetry);
|
||||
log.setRawCallback(truncate(rawBody));
|
||||
log.setLastCallbackTime(LocalDateTime.now());
|
||||
if (log.getId() == null) {
|
||||
logMapper.insert(log);
|
||||
} else {
|
||||
logMapper.updateById(log);
|
||||
}
|
||||
return log;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markSuccess(String requestId) {
|
||||
IWorkSealLogDO log = new IWorkSealLogDO();
|
||||
log.setRequestId(requestId);
|
||||
log.setStatus(IWorkCallbackStatusEnum.CALLBACK_SUCCESS.getStatus());
|
||||
log.setLastErrorMessage(null);
|
||||
log.setLastCallbackTime(LocalDateTime.now());
|
||||
logMapper.update(log, new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<IWorkSealLogDO>()
|
||||
.eq(IWorkSealLogDO::getRequestId, requestId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markFailure(String requestId, String error, boolean retrying, int maxRetry) {
|
||||
IWorkSealLogDO log = new IWorkSealLogDO();
|
||||
log.setRequestId(requestId);
|
||||
log.setStatus(retrying ? IWorkCallbackStatusEnum.CALLBACK_RETRYING.getStatus() : IWorkCallbackStatusEnum.CALLBACK_FAILED.getStatus());
|
||||
log.setLastErrorMessage(error);
|
||||
log.setLastCallbackTime(LocalDateTime.now());
|
||||
log.setMaxRetry(maxRetry);
|
||||
logMapper.update(log, new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<IWorkSealLogDO>()
|
||||
.eq(IWorkSealLogDO::getRequestId, requestId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void incrementRetry(String requestId) {
|
||||
IWorkSealLogDO db = logMapper.selectByRequestId(requestId);
|
||||
if (db == null) {
|
||||
return;
|
||||
}
|
||||
IWorkSealLogDO log = new IWorkSealLogDO();
|
||||
log.setId(db.getId());
|
||||
log.setRetryCount(ObjectUtils.defaultIfNull(db.getRetryCount(), 0) + 1);
|
||||
log.setLastCallbackTime(LocalDateTime.now());
|
||||
logMapper.updateById(log);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<IWorkSealLogDO> page(IWorkCallbackLogPageReqVO reqVO) {
|
||||
return logMapper.selectPage(reqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetAndDispatch(String requestId) {
|
||||
IWorkSealLogDO db = logMapper.selectByRequestId(requestId);
|
||||
if (db == null) {
|
||||
return;
|
||||
}
|
||||
IWorkSealLogDO log = new IWorkSealLogDO();
|
||||
log.setId(db.getId());
|
||||
log.setRetryCount(0);
|
||||
log.setStatus(IWorkCallbackStatusEnum.CALLBACK_RETRYING.getStatus());
|
||||
log.setLastCallbackTime(LocalDateTime.now());
|
||||
logMapper.updateById(log);
|
||||
|
||||
int maxAttempts = properties.getCallback().getRetry().getMaxAttempts();
|
||||
Object payload;
|
||||
try {
|
||||
payload = JsonUtils.parseObject(db.getRawCallback(), Object.class);
|
||||
} catch (Exception ex) {
|
||||
payload = db.getRawCallback();
|
||||
}
|
||||
producer.send(IWorkBizCallbackMessage.builder()
|
||||
.requestId(db.getRequestId())
|
||||
.bizCallbackKey(db.getBizCallbackKey())
|
||||
.payload(payload)
|
||||
.attempt(0)
|
||||
.maxAttempts(maxAttempts)
|
||||
.build());
|
||||
}
|
||||
|
||||
private String truncate(String raw) {
|
||||
if (!StringUtils.hasText(raw)) {
|
||||
return raw;
|
||||
}
|
||||
return raw.length() > RAW_MAX ? raw.substring(0, RAW_MAX) : raw;
|
||||
}
|
||||
}
|
||||
@@ -182,7 +182,7 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
|
||||
}
|
||||
|
||||
AtomicReference<Long> attachmentIdRef = new AtomicReference<>();
|
||||
TenantUtils.execute(tenantId, () -> attachmentIdRef.set(saveCallbackAttachment(fileUrl, reqVO.getFileName(), referenceBusinessFile, reqVO.getSsoToken())));
|
||||
TenantUtils.execute(tenantId, () -> attachmentIdRef.set(saveCallbackAttachment(fileUrl, reqVO.getFileName(), referenceBusinessFile)));
|
||||
return attachmentIdRef.get();
|
||||
}
|
||||
|
||||
@@ -251,14 +251,14 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
|
||||
return executeOaRequest(request);
|
||||
}
|
||||
|
||||
private Long saveCallbackAttachment(String fileUrl, String overrideFileName, BusinessFileRespDTO referenceBusinessFile, String ssoToken) {
|
||||
private Long saveCallbackAttachment(String fileUrl, String overrideFileName, BusinessFileRespDTO referenceBusinessFile) {
|
||||
Long businessId = referenceBusinessFile.getBusinessId();
|
||||
|
||||
FileCreateReqDTO fileCreateReqDTO = new FileCreateReqDTO();
|
||||
fileCreateReqDTO.setName(resolveFileName(overrideFileName, fileUrl));
|
||||
fileCreateReqDTO.setDirectory(null);
|
||||
fileCreateReqDTO.setType(null);
|
||||
fileCreateReqDTO.setContent(downloadFileBytes(fileUrl, ssoToken));
|
||||
fileCreateReqDTO.setContent(downloadFileBytes(fileUrl));
|
||||
|
||||
CommonResult<FileRespDTO> fileResult = fileApi.createFileWithReturn(fileCreateReqDTO);
|
||||
if (fileResult == null || !fileResult.isSuccess() || fileResult.getData() == null) {
|
||||
@@ -297,10 +297,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
|
||||
return businessFile;
|
||||
}
|
||||
|
||||
private byte[] downloadFileBytes(String fileUrl, String ssoToken) {
|
||||
// 如果回调已提供 ssoToken,按需拼接后下载 OA 附件
|
||||
String finalUrl = appendSsoTokenIfNeeded(fileUrl, ssoToken);
|
||||
|
||||
private byte[] downloadFileBytes(String fileUrl) {
|
||||
String finalUrl = fileUrl;
|
||||
OkHttpClient client = okHttpClient();
|
||||
Request request = new Request.Builder().url(finalUrl).get().build();
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
@@ -317,22 +315,6 @@ 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;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.zt.plat.module.system.service.integration.iwork.impl;
|
||||
|
||||
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkWorkflowCallbackReqVO;
|
||||
import com.zt.plat.module.system.dal.dataobject.iwork.IWorkSealLogDO;
|
||||
import com.zt.plat.module.system.dal.mysql.iwork.IWorkSealLogMapper;
|
||||
import com.zt.plat.module.system.framework.integration.iwork.config.IWorkProperties;
|
||||
import com.zt.plat.module.system.mq.iwork.IWorkBizCallbackProducer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class IWorkCallbackLogServiceImplTest {
|
||||
|
||||
private IWorkSealLogMapper mapper;
|
||||
private IWorkBizCallbackProducer producer;
|
||||
private IWorkProperties properties;
|
||||
private IWorkCallbackLogServiceImpl service;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
mapper = mock(IWorkSealLogMapper.class);
|
||||
producer = mock(IWorkBizCallbackProducer.class);
|
||||
properties = new IWorkProperties();
|
||||
service = new IWorkCallbackLogServiceImpl(mapper, producer, properties);
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertOnCallback_shouldTruncateRaw() {
|
||||
String longRaw = "x".repeat(2100);
|
||||
IWorkWorkflowCallbackReqVO req = new IWorkWorkflowCallbackReqVO();
|
||||
req.setRequestId("REQ-1");
|
||||
req.setBizCallbackKey("key");
|
||||
|
||||
ArgumentCaptor<IWorkSealLogDO> captor = ArgumentCaptor.forClass(IWorkSealLogDO.class);
|
||||
when(mapper.selectByRequestId("REQ-1")).thenReturn(null);
|
||||
|
||||
service.upsertOnCallback(req, 3, longRaw);
|
||||
|
||||
verify(mapper).insert(captor.capture());
|
||||
IWorkSealLogDO saved = captor.getValue();
|
||||
assertThat(saved.getRawCallback()).hasSize(2000);
|
||||
assertThat(saved.getMaxRetry()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void incrementRetry_shouldIncreaseCount() {
|
||||
IWorkSealLogDO existing = new IWorkSealLogDO();
|
||||
existing.setId(1L);
|
||||
existing.setRequestId("REQ-2");
|
||||
existing.setRetryCount(1);
|
||||
when(mapper.selectByRequestId("REQ-2")).thenReturn(existing);
|
||||
|
||||
service.incrementRetry("REQ-2");
|
||||
|
||||
ArgumentCaptor<IWorkSealLogDO> captor = ArgumentCaptor.forClass(IWorkSealLogDO.class);
|
||||
verify(mapper).updateById(captor.capture());
|
||||
assertThat(captor.getValue().getRetryCount()).isEqualTo(2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user