: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();
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
| # | 原则 |
|
||||
|---|---|
|
||||
| P1 | **前端不直连 HCS**。所有 C 系列接口由 tms-api 代理:`Authorization`/`Key-Auth` 静态双头只存在于服务端,浏览器不可见,也无跨域问题 |
|
||||
| P2 | **自动推送不动**。C102-1/2、C103-1/11 仍走"审批通过 → 置 unsynced → 扫描任务推送"链路(零埋点,ADR-0001),前端只补**状态展示 + 手动重推 + 对账回查** |
|
||||
| P2 | **自动推送保留扫描兜底**。C102-1/2、C103-1/11 走"审批通过 → 置 unsynced → 扫描任务推送"链路(ADR-0001);2026-10-10 增补**审批通过即时推送**(§6),扫描任务降级为兜底补偿。前端只补**状态展示 + 手动重推 + 对账回查** |
|
||||
| P3 | **附件主存储仍在 TMS OSS**。表单上传逻辑不改;HCS 转传由后端推送链路自动完成(HcsAttachmentSupport 已实现)。前端新增的是**手动转传**(特批材料、确定性上传)与**HCS 附件回下载** |
|
||||
| P4 | **前端结算币种/合同分类不改**。两个下拉继续用本地字典(`currency_type`/`contractCategory`),存量值与交互零变化;C103-12 币种、C103-7 分类只在后端推送侧映射(`currency-name-mapping`/`template-mapping`,已实现),前端无触点 |
|
||||
| P5 | UI 照**"同步金蝶"按钮模式**(参考 `customer-archive.vue`、`formal-settlement-table-panel.vue` 的 kingdeeSyncStatus 列 + 推送按钮),HCS 用 `hcsSyncStatus` 等同构字段 |
|
||||
@@ -110,11 +110,11 @@ POST /hcs/contract/spec-apply (body:{ contractId, attachments: [{fdId,
|
||||
组装报文调 C103-8(报文模式预估照 C106-3:fd_contract_id=hcs_fd_id、fd_contract_no、
|
||||
fd_doc_subject="合同特批-"+contractNo、特批附件 mechanisms.attachment 带 fdBindType=ADD)
|
||||
→ 返回 data.fdId → hcs_spec_fd_id,状态置 processing;失败置 need 并记 hcs_spec_msg
|
||||
→ C103-8 v4 未含报文,联调前按上述预估实现并加 TODO 标注(§6-U1)
|
||||
→ C103-8 v4 未含报文,联调前按上述预估实现并加 TODO 标注(§7-U1)
|
||||
|
||||
POST /hcs/callback/spec-result (B103 合规→TMS 回调接收端点)
|
||||
公开端点(@PreAuth(AuthConstant.PERMIT_ALL) + 报文验签字段待合规方口径,先记录原文留痕)
|
||||
body:{ 合同标识/特批流程fdId, 审批结果, 审批意见 } ← 字段名待 B103 报文(§6-U1)
|
||||
body:{ 合同标识/特批流程fdId, 审批结果, 审批意见 } ← 字段名待 B103 报文(§7-U1)
|
||||
处理:定位合同 → 回写 hcs_spec_status=approved/rejected + hcs_spec_msg
|
||||
→ approved 时把合同 hcs_sync_status 重置 unsynced,由既有扫描任务自动走 C103-1(零新增链路)
|
||||
→ B103 报文未到位前端点先落库留痕不置状态,联调时接线
|
||||
@@ -241,11 +241,67 @@ export const applyContractSpec = data =>
|
||||
| F1 基础 | `GET /hcs/master-data/template` + `@PreAuth`/菜单 SQL | `api/business/hcs.js`;管理端主数据页 | 无 |
|
||||
| F2 状态与推送 | 无(端点已有) | 两列表页:状态列/筛选 + 推送合规按钮 | F1(hcs.js) |
|
||||
| F3 对账与附件 | `/hcs/risk`、`/hcs/attachment/upload`、`/hcs/attachment/download` | 对账弹窗 + 合规附件下载 + 手动转传 + 附件行 `hcsFdId` | F2 |
|
||||
| F4 特批 | 合同特批字段组 SQL;`POST /hcs/contract/spec-apply` + `POST /hcs/callback/spec-result`(B103);合同推送链路加 need 挂起判断 | 发起特批弹窗 + 特批状态标签 + 按钮联动 | C103-8/B103 报文(§6-U1,可先按预估模式实现联调替换) |
|
||||
| F4 特批 | 合同特批字段组 SQL;`POST /hcs/contract/spec-apply` + `POST /hcs/callback/spec-result`(B103);合同推送链路加 need 挂起判断 | 发起特批弹窗 + 特批状态标签 + 按钮联动 | C103-8/B103 报文(§7-U1,可先按预估模式实现联调替换) |
|
||||
| F5 即时推送(2026-10-10 增补,纯后端) | `HcsImmediatePushSupport` + 4 处审批入口触发(§6.3)+ 开关配置 | 无 | 无(复用既有 Support,扫描兜底保留) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 未决事项
|
||||
## 6. 审批通过即时推送(2026-10-10 增补,后端链路增强)
|
||||
|
||||
### 6.1 目标与原则
|
||||
|
||||
| # | 原则 |
|
||||
|---|---|
|
||||
| I1 | **秒级触达**:项目立项(C102-1/2)、合同(C103-1/11)审批通过(含变更通过)事务提交后立即异步推送,不再等扫描周期(原最长 ≤5 分钟) |
|
||||
| I2 | **ADR-0001 修订**:审批代码埋点由"零"放宽为"一行触发调用"(仅传 id,不含任何推送逻辑);推送逻辑仍全部收敛在 hcs support 包,扫描任务保留为**兜底**(失败重试、批量补偿、停机窗口漏单) |
|
||||
| I3 | **复用既有链路**:执行体直接调 `repushProject(id)` / `repushContract(id)`(已含 approved 校验、状态重置、失败 markFailed、高风险挂起判断),不新写报文与状态机 |
|
||||
|
||||
### 6.2 新增组件:HcsImmediatePushSupport(仅 1 个类)
|
||||
|
||||
位置 `org.springblade.transport.support.hcs`,职责只有一个:**事务提交后异步触发一次单条推送**。
|
||||
|
||||
```java
|
||||
public void pushProjectAfterCommit(Long id) // 项目 C102
|
||||
public void pushContractAfterCommit(Long id) // 合同 C103
|
||||
```
|
||||
|
||||
执行模型:
|
||||
|
||||
1. **afterCommit 触发**:审批方法在事务内调用本组件 → `TransactionSynchronizationManager.registerSynchronization` 注册 afterCommit 回调,审批事务回滚则不推送。MK 回调路径若未包事务(单条 updateById 自动提交)则直接提交异步,语义等价。
|
||||
2. **异步执行**:自建 2 线程 daemon 池(线程名 `hcs-immediate-push`,`@PreDestroy` 释放)。工程未启用 `@EnableAsync`,不为此新增全局配置;HCS 推送含风险校验+附件转传+起草可达 20s+,绝不能阻塞审批响应。
|
||||
3. **执行体**(与 HcsPushTask 同线程模型,无租户上下文,与定时扫描行为一致):
|
||||
- 开关:`thirdParty.hcs.push.immediate-enabled`(默认 false,灰度打开)
|
||||
- 短路:查库 `approvalStatus ∉ {approved, change_approved}` 或 `hcs_sync_status = synced` → 跳过(已被扫描/人工重推处理过)
|
||||
- 单条锁:Redis `setIfAbsent("blade:third:hcs:push:lock:{bizType}:{id}", TTL 120s)`,抢不到直接放弃(扫描兜底),持有值比对后释放(照 HcsPushTask 惯例)
|
||||
- 推送:`repushProject(id)` / `repushContract(id)`;异常仅 `log.error` 吞掉(Support 内部已 markFailed,扫描按 `hcs_retry_count < max-retry` 自动补偿,超限人工重推,D7 不变)
|
||||
|
||||
### 6.3 触发点(4 处,各加 1 行)
|
||||
|
||||
| 入口 | 位置 | 触发调用 |
|
||||
|---|---|---|
|
||||
| 本地审批 | `ProjectApplyServiceImpl.approve()`(事务内,含变更通过,现有置 unsynced 行之后) | `pushProjectAfterCommit(id)` |
|
||||
| 本地审批 | `ContractManageServiceImpl.approve()`(事务内,含变更通过 + 履约行置 add,`updateById` 之后) | `pushContractAfterCommit(id)` |
|
||||
| MK 回调 | `MkProcessBizHandlers.ProjectHandler.approveFromProcess()`(`updateById` 返回 true 后) | `pushProjectAfterCommit(id)` |
|
||||
| MK 回调 | `MkProcessBizHandlers.ContractHandler.approveFromProcess()`(同上) | `pushContractAfterCommit(id)` |
|
||||
|
||||
### 6.4 并发与失败语义
|
||||
|
||||
| 场景 | 语义 |
|
||||
|---|---|
|
||||
| 即时推送 vs 扫描任务并发 | 单条锁 + synced 短路 + HCS 侧判重(C102-3 / C103 判重、更新幂等)三重吸收;极端双推为更新语义,无脏数据 |
|
||||
| 即时推送失败 | `markFailed` → 扫描下轮自动补偿 → 超限人工重推(与现状完全一致,零新增重试机制) |
|
||||
| 高风险合同 | `pushContract` 既有 `hcs_spec_status ∈ {need, processing}` 挂起判断不变,即时推送同样被挂起,特批通过后恢复推送 |
|
||||
| 开关关闭 / 组件异常 | 完全回落扫描链路,行为与现状一致 |
|
||||
|
||||
### 6.5 配置
|
||||
|
||||
| 配置项 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `thirdParty.hcs.push.immediate-enabled` | false | 即时推送总开关,灰度打开;普通 `@Value` 重启生效即可(扫描兜底使风险可控,无需 @RefreshScope) |
|
||||
|
||||
---
|
||||
|
||||
## 7. 未决事项
|
||||
|
||||
| # | 事项 | 影响 | 当前处理 |
|
||||
|---|---|---|---|
|
||||
|
||||
Reference in New Issue
Block a user