:feat: 项目立项、合同审批通过后即时推送HCS合规系统(复用单条重推链路,扫描任务降级为兜底补偿,附前端接入设计§6增补)

This commit is contained in:
刘泉佳
2026-10-10 18:24:05 +08:00
parent 7855254a4f
commit 2ea5901238
6 changed files with 293 additions and 11 deletions
@@ -180,6 +180,16 @@ public final class HcsSyncConstant {
*/
public static final String PUSH_TASK_LOCK_KEY_PREFIX = "blade:third:hcs:push:task:lock:";
/**
* 即时推送单条锁 key 前缀(审批通过即时推送防与扫描任务并发双推,完整 key = 前缀 + 业务类型 + ":" + 单据ID)
*/
public static final String PUSH_SINGLE_LOCK_KEY_PREFIX = "blade:third:hcs:push:lock:";
/**
* 即时推送单条锁 TTL(秒):大于单条推送预期时长,异常残留锁自动过期后由扫描兜底
*/
public static final long PUSH_SINGLE_LOCK_TTL_SECONDS = 120L;
private HcsSyncConstant() {
}
}
@@ -17,6 +17,7 @@ import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.hcs.HcsImmediatePushSupport;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@@ -66,6 +67,7 @@ public final class MkProcessBizHandlers {
@RequiredArgsConstructor
public static class ProjectHandler implements IMkProcessBizHandler {
private final IProjectApplyService service;
private final HcsImmediatePushSupport hcsImmediatePushSupport;
@Override
public String bizType() {
@@ -113,9 +115,13 @@ public final class MkProcessBizHandlers {
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
entity.setApprovedTime(LocalDateTime.now());
// HCS 推送零埋点:MK 审批回调仅置待同步状态,与 /approve 入口收敛于同一权威状态(ADR-0001)
// HCS 推送:审批通过置待同步状态,更新成功后即时推送,扫描任务保留兜底(ADR-0001 修订,前端接入设计 §6)
entity.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
return service.updateById(entity);
boolean updated = service.updateById(entity);
if (updated) {
hcsImmediatePushSupport.pushProjectAfterCommit(id);
}
return updated;
}
@Override
@@ -138,6 +144,7 @@ public final class MkProcessBizHandlers {
@RequiredArgsConstructor
public static class ContractHandler implements IMkProcessBizHandler {
private final IContractManageService service;
private final HcsImmediatePushSupport hcsImmediatePushSupport;
@Override
public String bizType() {
@@ -183,9 +190,13 @@ public final class MkProcessBizHandlers {
entity.setApprovalStatus(change ? "change_approved" : "approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
// HCS 推送零埋点:MK 审批回调仅置待同步状态,与 /approve 入口收敛于同一权威状态(ADR-0001)
// HCS 推送:审批通过置待同步状态,更新成功后即时推送,扫描任务保留兜底(ADR-0001 修订,前端接入设计 §6)
entity.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
return service.updateById(entity);
boolean updated = service.updateById(entity);
if (updated) {
hcsImmediatePushSupport.pushContractAfterCommit(id);
}
return updated;
}
@Override
@@ -44,6 +44,7 @@ import org.springblade.transport.pojo.entity.HcsContractExeStage;
import org.springblade.transport.pojo.vo.ContractManageVO;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.support.hcs.HcsImmediatePushSupport;
import org.springblade.transport.wrapper.ContractManageWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -72,6 +73,7 @@ import java.util.Set;
public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMapper, ContractManage> implements IContractManageService {
private final HcsContractExeStageMapper hcsContractExeStageMapper;
private final HcsImmediatePushSupport hcsImmediatePushSupport;
private static final String STAGE_DRAFT = "draft";
private static final String STAGE_TEMPORARY = "temporary";
@@ -223,13 +225,14 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
contractManage.setContractFileJson(markContractFilesApproved(contractManage.getContractFileJson()));
refreshArchiveStatusOnApprove(contractManage);
updateLatestReviewingChangeRecord(contractManage, contractManage.getApprovalStatus(), "审核通过");
// HCS 推送零埋点:审批通过(含变更通过)仅置待同步状态,由定时扫描任务准实时首推(ADR-0001)
// HCS 推送:审批通过(含变更通过)置待同步状态,事务提交后即时推送,扫描任务保留兜底(ADR-0001 修订,前端接入设计 §6)
contractManage.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
// 合同审批通过:履约规则未同步行统一置待同步标记,随合同链路调 C103-9(ADR-0002)
hcsContractExeStageMapper.update(null, Wrappers.<HcsContractExeStage>lambdaUpdate()
.eq(HcsContractExeStage::getContractId, contractManage.getId())
.isNull(HcsContractExeStage::getHcsStageFdId)
.set(HcsContractExeStage::getRowOp, "add"));
hcsImmediatePushSupport.pushContractAfterCommit(id);
return updateById(contractManage);
}
@@ -49,6 +49,7 @@ import org.springblade.transport.pojo.entity.TemporaryCreditLimit;
import org.springblade.transport.pojo.vo.ProjectApplyVO;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.support.hcs.HcsImmediatePushSupport;
import org.springblade.transport.wrapper.ProjectApplyWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -95,6 +96,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
private final TemporaryCreditLimitMapper temporaryCreditLimitMapper;
private final FormalSettlementMapper formalSettlementMapper;
private final ReceiptClaimSettlementMapper receiptClaimSettlementMapper;
private final HcsImmediatePushSupport hcsImmediatePushSupport;
@Override
public IPage<ProjectApplyVO> selectProjectApplyPage(IPage<ProjectApply> page, ProjectApplyVO projectApply) {
@@ -212,8 +214,9 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
projectApply.setCurrentNode("审批通过");
projectApply.setCurrentProcessor(AuthUtil.getUserName());
projectApply.setApprovedTime(LocalDateTime.now());
// HCS 推送零埋点:审批通过仅置待同步状态,由定时扫描任务准实时首推(ADR-0001)
// HCS 推送:审批通过置待同步状态,事务提交后即时推送,扫描任务保留兜底(ADR-0001 修订,前端接入设计 §6)
projectApply.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
hcsImmediatePushSupport.pushProjectAfterCommit(id);
return updateById(projectApply);
}
@@ -0,0 +1,199 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support.hcs;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.mapper.ContractManageMapper;
import org.springblade.transport.mapper.ProjectApplyMapper;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
/**
* HCS 审批通过即时推送(前端接入设计 §6,2026-10-10 增补)
* <p>
* 项目立项(C102)/合同(C103)审批通过(含变更通过)事务提交后立即异步触发单条推送,不再等待扫描周期;
* 执行体复用 {@link HcsProjectSyncSupport#repushProject(Long)} / {@link HcsContractSyncSupport#repushContract(Long)}
* (已含审批状态校验、失败 markFailed、高风险特批挂起判断),失败仅留痕,由 HcsPushTask 扫描兜底补偿
* (ADR-0001 修订:审批代码埋点为一行触发调用,推送逻辑仍收敛在 hcs support 包)。
* <p>
* 触发模型:审批事务内调用则注册 afterCommit 回调(事务回滚不推送),无活动事务(MK 回调单条更新自动提交)直接提交;
* 自建 2 线程 daemon 池(工程未启用 @EnableAsync),HCS 推送可达 20s+ 不阻塞审批响应;
* Redis 单条锁防与扫描任务并发双推(TTL 120s,持有值比对释放,照 HcsPushTask 惯例),
* 极端双推由 HCS 侧判重/更新幂等吸收。与扫描任务同线程模型(无租户上下文),行为一致。
*
* @author Chill
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class HcsImmediatePushSupport {
/**
* 审批通过状态集合(与 HcsPushTask.APPROVED_STATUSES 一致,含变更通过)
*/
private static final List<String> APPROVED_STATUSES = List.of("approved", "change_approved");
/**
* 推送线程编号(线程名 hcs-immediate-push-N 便于日志定位)
*/
private static final AtomicInteger THREAD_COUNTER = new AtomicInteger(1);
private final HcsProjectSyncSupport hcsProjectSyncSupport;
private final HcsContractSyncSupport hcsContractSyncSupport;
private final ProjectApplyMapper projectApplyMapper;
private final ContractManageMapper contractManageMapper;
private final BladeRedis bladeRedis;
/**
* 即时推送总开关(thirdParty.hcs.push.immediate-enabled,默认关闭灰度打开;关闭时完全回落扫描链路)
*/
@Value("${thirdParty.hcs.push.immediate-enabled:false}")
private boolean immediateEnabled;
/**
* 推送线程池:2 线程 daemon,即时推送与定时扫描互不影响
*/
private final ExecutorService pushExecutor = Executors.newFixedThreadPool(2, task -> {
Thread thread = new Thread(task, "hcs-immediate-push-" + THREAD_COUNTER.getAndIncrement());
thread.setDaemon(true);
return thread;
});
/**
* 项目立项(C102)审批通过后即时推送:须在审批事务内调用(afterCommit 触发)
*
* @param id 项目立项单ID
*/
public void pushProjectAfterCommit(Long id) {
this.submitAfterCommit(HcsSyncConstant.BIZ_TYPE_PROJECT, id, () -> this.pushProjectNow(id));
}
/**
* 合同(C103)审批通过后即时推送:须在审批事务内调用(afterCommit 触发)
*
* @param id 合同单ID
*/
public void pushContractAfterCommit(Long id) {
this.submitAfterCommit(HcsSyncConstant.BIZ_TYPE_CONTRACT, id, () -> this.pushContractNow(id));
}
/**
* 开关校验后注册提交时机:有活动事务注册 afterCommit 回调(回滚不推送),无事务(MK 回调单条更新自动提交)直接异步
*/
private void submitAfterCommit(String bizType, Long id, Runnable pushAction) {
if (!immediateEnabled) {
return;
}
Runnable pushTask = () -> this.pushWithLock(bizType, id, pushAction);
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
pushExecutor.execute(pushTask);
}
});
} else {
pushExecutor.execute(pushTask);
}
}
/**
* 单条锁防并发双推:抢不到即放弃(扫描兜底),持有值比对后释放
*/
private void pushWithLock(String bizType, Long id, Runnable pushAction) {
String lockKey = HcsSyncConstant.PUSH_SINGLE_LOCK_KEY_PREFIX + bizType + ":" + id;
String lockHolder = UUID.randomUUID().toString();
Boolean locked = bladeRedis.getValueOps()
.setIfAbsent(lockKey, lockHolder, Duration.ofSeconds(HcsSyncConstant.PUSH_SINGLE_LOCK_TTL_SECONDS));
if (!Boolean.TRUE.equals(locked)) {
log.info("HCS即时推送跳过(其他线程推送中):bizType={},id={}", bizType, id);
return;
}
try {
pushAction.run();
} finally {
if (lockHolder.equals(bladeRedis.get(lockKey))) {
bladeRedis.del(lockKey);
}
}
}
/**
* 项目立项推送执行体:非审批通过/已同步短路跳过,失败已由 Support markFailed,仅留痕交扫描补偿
*/
private void pushProjectNow(Long id) {
try {
ProjectApply projectApply = projectApplyMapper.selectById(id);
if (projectApply == null || !APPROVED_STATUSES.contains(projectApply.getApprovalStatus())
|| HcsSyncConstant.HCS_STATUS_SYNCED.equals(projectApply.getHcsSyncStatus())) {
return;
}
hcsProjectSyncSupport.repushProject(id);
} catch (Exception exception) {
log.error("HCS项目立项即时推送失败(扫描任务将兜底补偿):id={},{}", id, exception.getMessage(), exception);
}
}
/**
* 合同推送执行体:非审批通过/已同步/特批挂起中短路跳过,失败已由 Support markFailed,仅留痕交扫描补偿
*/
private void pushContractNow(Long id) {
try {
ContractManage contractManage = contractManageMapper.selectById(id);
if (contractManage == null || !APPROVED_STATUSES.contains(contractManage.getApprovalStatus())
|| HcsSyncConstant.HCS_STATUS_SYNCED.equals(contractManage.getHcsSyncStatus())) {
return;
}
if (List.of(HcsSyncConstant.SPEC_STATUS_NEED, HcsSyncConstant.SPEC_STATUS_PROCESSING).contains(contractManage.getHcsSpecStatus())) {
log.info("HCS合同即时推送跳过(特批挂起中):id={},hcsSpecStatus={}", id, contractManage.getHcsSpecStatus());
return;
}
hcsContractSyncSupport.repushContract(id);
} catch (Exception exception) {
log.error("HCS合同即时推送失败(扫描任务将兜底补偿):id={},{}", id, exception.getMessage(), exception);
}
}
@PreDestroy
public void shutdown() {
pushExecutor.shutdown();
}
}