:feat: 合同审批通过同步推送HCS并回传失败原因

This commit is contained in:
刘泉佳
2026-10-20 21:45:09 +08:00
parent e81eba93d0
commit 8c8a874522
3 changed files with 65 additions and 5 deletions
@@ -40,6 +40,7 @@ import org.springblade.transport.excel.ContractManageExcel;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.vo.ContractManageVO;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.support.hcs.HcsImmediatePushSupport;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -63,6 +64,7 @@ import java.util.Map;
public class ContractManageController extends BladeController {
private final IContractManageService contractManageService;
private final HcsImmediatePushSupport hcsImmediatePushSupport;
@GetMapping("/detail")
@ApiOperationSupport(order = 1)
@@ -110,7 +112,14 @@ public class ContractManageController extends BladeController {
@ApiOperationSupport(order = 7)
@Operation(summary = "审批通过", description = "传入id")
public R approve(@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.status(contractManageService.approve(id));
contractManageService.approve(id);
// 审批事务已提交,再同步推送合规;失败原因以警告文案返回(data=false 供前端区分提示样式),照正式结算单审批接口
hcsImmediatePushSupport.pushContractAndWait(id);
String syncFailure = HcsImmediatePushSupport.pollSyncFailure();
if (syncFailure != null) {
return R.data(Boolean.FALSE, "审批通过,但合规推送失败:" + syncFailure);
}
return R.success("审批通过");
}
@PostMapping("/reject")
@@ -48,7 +48,6 @@ import org.springblade.transport.service.IContractExeStageService;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.support.hcs.HcsCreditCheckSupport;
import org.springblade.transport.support.hcs.HcsImmediatePushSupport;
import org.springblade.transport.wrapper.ContractManageWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -79,7 +78,6 @@ import java.util.regex.Pattern;
public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMapper, ContractManage> implements IContractManageService {
private final HcsContractExeStageMapper hcsContractExeStageMapper;
private final HcsImmediatePushSupport hcsImmediatePushSupport;
private final HcsCreditCheckSupport hcsCreditCheckSupport;
private final IContractExeStageService contractExeStageService;
@@ -241,7 +239,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
contractManage.setContractFileJson(markContractFilesApproved(contractManage.getContractFileJson()));
refreshArchiveStatusOnApprove(contractManage);
updateLatestReviewingChangeRecord(contractManage, contractManage.getApprovalStatus(), "审核通过");
// HCS 推送:审批通过(含变更通过)置待同步状态,事务提交后即时推送,扫描任务保留兜底(ADR-0001 修订,前端接入设计 §6)
// HCS 推送:审批通过(含变更通过)置待同步状态,事务提交后由 Controller 同步推送并回传失败原因,扫描任务保留兜底
contractManage.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
// 合同审批通过:履约规则未同步行统一置待同步标记,随合同链路调 C103-9(ADR-0002)
// 审批通过自动创建默认履约计划行(承运商=付款进度款/客户=收款进度款),已有规则行时不重复创建
@@ -250,7 +248,6 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
.eq(HcsContractExeStage::getContractId, contractManage.getId())
.isNull(HcsContractExeStage::getHcsStageFdId)
.set(HcsContractExeStage::getRowOp, "add"));
hcsImmediatePushSupport.pushContractAfterCommit(id);
return updateById(contractManage);
}
@@ -59,6 +59,12 @@ import java.util.concurrent.atomic.AtomicInteger;
* 自建 2 线程 daemon 池(工程未启用 @EnableAsync),HCS 推送可达 20s+ 不阻塞审批响应;
* Redis 单条锁防与扫描任务并发双推(TTL 120s,持有值比对释放,照 HcsPushTask 惯例),
* 极端双推由 HCS 侧判重/更新幂等吸收。与扫描任务同线程模型(无租户上下文),行为一致。
* <p>
* 例外:TMS 合同/项目立项的"通过"按钮路径由 Controller 在审批事务返回后分别调用
* {@link #pushContractAndWait(Long)}、{@link #pushProjectAndWait(Long)} 同步推送
* (审批响应会等待附件转传 + HCS 调用完成,与正式结算单审批一致),
* 失败原因经 {@link #pollSyncFailure()} 交给 Controller 拼进审批响应(照 KingdeeApprovalSyncListener);
* MK 回调路径仍异步,避免阻塞第三方回调。
*
* @author Chill
*/
@@ -77,6 +83,11 @@ public class HcsImmediatePushSupport {
*/
private static final AtomicInteger THREAD_COUNTER = new AtomicInteger(1);
/**
* 当前审批请求线程的合规推送失败原因,供审批 Controller 取走提示前端
*/
private static final ThreadLocal<String> SYNC_FAILURE = new ThreadLocal<>();
private final HcsProperties hcsProperties;
private final HcsProjectSyncSupport hcsProjectSyncSupport;
private final HcsContractSyncSupport hcsContractSyncSupport;
@@ -111,6 +122,39 @@ public class HcsImmediatePushSupport {
this.submitAfterCommit(HcsSyncConstant.BIZ_TYPE_CONTRACT, id, () -> this.pushContractNow(id));
}
/**
* 合同"通过"按钮路径:在审批事务提交返回后由 Controller 同步调用,失败原因留在当前线程供其取走
*
* @param id 合同单ID
*/
public void pushContractAndWait(Long id) {
if (!hcsProperties.getPush().isImmediateEnabled()) {
return;
}
this.pushWithLock(HcsSyncConstant.BIZ_TYPE_CONTRACT, id, () -> this.pushContractNow(id));
}
/**
* 项目立项"通过"按钮路径:在审批事务提交返回后由 Controller 同步调用,失败原因留在当前线程供其取走
*
* @param id 项目立项单ID
*/
public void pushProjectAndWait(Long id) {
if (!hcsProperties.getPush().isImmediateEnabled()) {
return;
}
this.pushWithLock(HcsSyncConstant.BIZ_TYPE_PROJECT, id, () -> this.pushProjectNow(id));
}
/**
* 取走并清除当前审批请求的合规推送失败原因;null 表示推送成功、未触发或仍异步进行
*/
public static String pollSyncFailure() {
String failure = SYNC_FAILURE.get();
SYNC_FAILURE.remove();
return failure;
}
/**
* 开关校验后注册提交时机:有活动事务注册 afterCommit 回调(回滚不推送),无事务(MK 回调单条更新自动提交)直接异步
*/
@@ -137,6 +181,8 @@ public class HcsImmediatePushSupport {
private void pushWithLock(String bizType, Long id, Runnable pushAction) {
String lockKey = HcsSyncConstant.PUSH_SINGLE_LOCK_KEY_PREFIX + bizType + ":" + id;
String lockHolder = UUID.randomUUID().toString();
// 清理同线程残留的失败原因,避免线程复用串单(同步推送路径会写入)
SYNC_FAILURE.remove();
Boolean locked = bladeRedis.getValueOps()
.setIfAbsent(lockKey, lockHolder, Duration.ofSeconds(HcsSyncConstant.PUSH_SINGLE_LOCK_TTL_SECONDS));
if (!Boolean.TRUE.equals(locked)) {
@@ -165,6 +211,10 @@ public class HcsImmediatePushSupport {
hcsProjectSyncSupport.repushProject(id);
} catch (Exception exception) {
log.error("HCS项目立项即时推送失败(扫描任务将兜底补偿):id={},{}", id, exception.getMessage(), exception);
// 失败原因留在当前线程供审批 Controller 取走;异步路径无人取走,仅影响线程池自身
String reason = exception.getMessage() == null ? "合规接口调用异常" : exception.getMessage();
SYNC_FAILURE.set(reason.startsWith("HCS项目立项推送失败:")
? reason.substring("HCS项目立项推送失败:".length()) : reason);
}
}
@@ -185,6 +235,10 @@ public class HcsImmediatePushSupport {
hcsContractSyncSupport.repushContract(id);
} catch (Exception exception) {
log.error("HCS合同即时推送失败(扫描任务将兜底补偿):id={},{}", id, exception.getMessage(), exception);
// 失败原因留在当前线程供审批 Controller 取走;异步路径无人取走,仅影响线程池自身
String reason = exception.getMessage() == null ? "合规接口调用异常" : exception.getMessage();
SYNC_FAILURE.set(reason.startsWith("HCS合同推送失败:")
? reason.substring("HCS合同推送失败:".length()) : reason);
}
}