:feat: 项目立项、合同审批通过后即时推送HCS合规系统(复用单条重推链路,扫描任务降级为兜底补偿,附前端接入设计§6增补)
This commit is contained in:
+10
@@ -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() {
|
||||
}
|
||||
}
|
||||
|
||||
+15
-4
@@ -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
|
||||
|
||||
+4
-1
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+199
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user