feat: HCS推送任务按业务类型拆分独立调度(各任务独立开关与cron错峰、任务级分布式锁、关键日志与配置说明)

This commit is contained in:
刘泉佳
2026-10-09 12:16:29 +08:00
parent 4cd7fb24df
commit 6c645603b6
3 changed files with 106 additions and 33 deletions
@@ -171,9 +171,9 @@ public final class HcsSyncConstant {
public static final long ATTACHMENT_CACHE_TTL_DAYS = 7L;
/**
* 推送任务分布式锁 key(多实例部署防重扫)
* 推送任务分布式锁 key 前缀(多实例部署防重扫,完整 key = 前缀 + 业务类型,任务间互不阻塞)
*/
public static final String PUSH_TASK_LOCK_KEY = "blade:third:hcs:push:task:lock";
public static final String PUSH_TASK_LOCK_KEY_PREFIX = "blade:third:hcs:push:task:lock:";
private HcsSyncConstant() {
}
@@ -54,17 +54,28 @@ import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;
/**
* HCS 合规推送定时扫描任务(设计文档 §5.0,ADR-0001)
* <p>
* 审批代码零埋点:扫描只认 approvalStatus=approved/change_approved 且 hcs_sync_status=unsynced/failed(重试未超限),
* 与审批入口(/approve、MK 回调)无关;失败落 failed 由下轮扫描自动补偿,超限仅人工重推(D7)。
* 任务顺序:项目 → 合同(内含风险校验→附件转传→C103-1→履约规则 C103-9 行级同步)→ 付款特批/履约记录。
* 多实例部署时以 Redis 分布式锁防重扫(照既有 BladeRedis 用法,单实例部署行为与既有金蝶任务一致)。
* 扫描与推送逻辑收敛在 pushUnsyncedProjects/pushUnsyncedContracts/pushUnsyncedPayments,供手动端点(HcsSyncController)复用。
* <p>
* 调度配置(Nacos thirdParty.hcs.push.*):sync-enabled 总开关(默认关,灰度打开)、cron、batch-size、max-retry。
* 五类任务各自独立调度、独立开关(Nacos thirdParty.hcs.push.{bizType}.*):
* <pre>
* bizType 说明 配置节点
* project 项目立项 C102 thirdParty.hcs.push.project.*
* contract 合同 C103(内含风险校验/附件转传/履约规则) thirdParty.hcs.push.contract.*
* payment 付款特批 C106-3 + 履约记录 C108 thirdParty.hcs.push.payment.*
* invoice-application 开票登记 C111(含金蝶开票回填) thirdParty.hcs.push.invoice-application.*
* invoice-receipt 收票单 C109 thirdParty.hcs.push.invoice-receipt.*
* </pre>
* 每个任务配置项:sync-enabled 开关(默认关,灰度打开)、cron 调度(默认每 5 分钟,各任务分钟位错峰
* 0/1/2/3/4,避免同刻并发扫描与 HCS 接口流量尖峰);batch-size/max-retry 为全局配置(thirdParty.hcs.push.*),
* 见 {@link HcsProperties.Push}。
* 多实例部署时按任务隔离 Redis 分布式锁防重扫(setIfAbsent + 持有值比对释放,TTL 240s),任务间互不阻塞。
* 扫描与推送逻辑收敛在 pushUnsyncedProjects/pushUnsyncedContracts/pushUnsyncedPayments 等方法,供手动端点(HcsSyncController)复用。
*
* @author Chill
*/
@@ -74,7 +85,7 @@ import java.util.UUID;
public class HcsPushTask {
/**
* 分布式锁 TTL(秒):须小于 cron 间隔(默认 5 分钟)且大于单轮预期执行时长,实例崩溃后锁自动过期
* 分布式锁 TTL(秒):须小于各任务 cron 间隔(默认 5 分钟)且大于单轮预期执行时长,实例崩溃后锁自动过期
*/
private static final long PUSH_TASK_LOCK_TTL_SECONDS = 240L;
@@ -97,37 +108,107 @@ public class HcsPushTask {
private final BladeRedis bladeRedis;
/**
* HCS 推送任务开关,默认关闭(灰度打开)
* 项目立项(C102)推送开关,默认关闭(Nacos thirdParty.hcs.push.project.sync-enabled,灰度打开)
*/
@Value("${thirdParty.hcs.push.sync-enabled:false}")
private boolean syncEnabled;
@Value("${thirdParty.hcs.push.project.sync-enabled:false}")
private boolean projectSyncEnabled;
/**
* HCS 推送调度,默认每 5 分钟(准实时首推)
* 合同(C103)推送开关,默认关闭(Nacos thirdParty.hcs.push.contract.sync-enabled,灰度打开)
*/
@Scheduled(cron = "${thirdParty.hcs.push.cron:0 */5 * * * ?}")
public void pushHcs() {
@Value("${thirdParty.hcs.push.contract.sync-enabled:false}")
private boolean contractSyncEnabled;
/**
* 付款链(C106 特批 / C108 履约记录)推送开关,默认关闭(Nacos thirdParty.hcs.push.payment.sync-enabled,灰度打开)
*/
@Value("${thirdParty.hcs.push.payment.sync-enabled:false}")
private boolean paymentSyncEnabled;
/**
* 开票登记(C111)推送开关,默认关闭(Nacos thirdParty.hcs.push.invoice-application.sync-enabled,灰度打开)
*/
@Value("${thirdParty.hcs.push.invoice-application.sync-enabled:false}")
private boolean invoiceApplicationSyncEnabled;
/**
* 收票单(C109)推送开关,默认关闭(Nacos thirdParty.hcs.push.invoice-receipt.sync-enabled,灰度打开)
*/
@Value("${thirdParty.hcs.push.invoice-receipt.sync-enabled:false}")
private boolean invoiceReceiptSyncEnabled;
/**
* 项目立项(C102)定时推送:扫描审批通过且待同步/待补偿的项目,独立开关与 cron 调度
*/
@Scheduled(cron = "${thirdParty.hcs.push.project.cron:0 */5 * * * ?}")
public void pushHcsProject() {
this.runOnce("项目立项", HcsSyncConstant.BIZ_TYPE_PROJECT, projectSyncEnabled, this::pushUnsyncedProjects);
}
/**
* 合同(C103)定时推送:扫描审批通过且待同步/待补偿的合同(含风险校验→附件转传→C103-1→履约规则 C103-9 行级同步),
* 独立开关与 cron 调度
*/
@Scheduled(cron = "${thirdParty.hcs.push.contract.cron:0 1/5 * * * ?}")
public void pushHcsContract() {
this.runOnce("合同", HcsSyncConstant.BIZ_TYPE_CONTRACT, contractSyncEnabled, this::pushUnsyncedContracts);
}
/**
* 付款链(C106 特批 / C108 履约记录)定时推送:扫描审批通过且待同步/待补偿的付款申请,独立开关与 cron 调度
*/
@Scheduled(cron = "${thirdParty.hcs.push.payment.cron:0 2/5 * * * ?}")
public void pushHcsPayment() {
this.runOnce("付款链", HcsSyncConstant.BIZ_TYPE_PAYMENT, paymentSyncEnabled, this::pushUnsyncedPayments);
}
/**
* 开票登记(C111)定时推送:扫描审批通过且待同步/待补偿的开票申请(含金蝶开票完成后回填),独立开关与 cron 调度
*/
@Scheduled(cron = "${thirdParty.hcs.push.invoice-application.cron:0 3/5 * * * ?}")
public void pushHcsInvoiceApplication() {
this.runOnce("开票登记", HcsSyncConstant.BIZ_TYPE_INVOICE_APPLICATION, invoiceApplicationSyncEnabled, this::pushUnsyncedInvoiceApplications);
}
/**
* 收票单(C109)定时推送:扫描审批通过且待同步/待补偿的收票单,独立开关与 cron 调度
*/
@Scheduled(cron = "${thirdParty.hcs.push.invoice-receipt.cron:0 4/5 * * * ?}")
public void pushHcsInvoiceReceipt() {
this.runOnce("收票单", HcsSyncConstant.BIZ_TYPE_INVOICE_RECEIPT, invoiceReceiptSyncEnabled, this::pushUnsyncedInvoiceReceipts);
}
/**
* 单任务执行模板:开关校验 → 分布式锁抢锁 → 执行推送 → 持有值比对后释放。
* 锁按业务类型隔离,各任务可并行调度互不阻塞;TTL 见 {@link #PUSH_TASK_LOCK_TTL_SECONDS}。
*
* @param taskName 任务中文名(用于日志定位)
* @param bizType 业务类型(同时作为锁 key 后缀)
* @param syncEnabled 该任务的独立开关(thirdParty.hcs.push.{bizType}.sync-enabled)
* @param pushAction 推送动作,返回结果汇总描述
*/
private void runOnce(String taskName, String bizType, boolean syncEnabled, Supplier<String> pushAction) {
if (!syncEnabled) {
// 开关未开启属常态(灰度),debug 级别避免轮询刷屏
log.debug("HCS{}推送任务未开启(thirdParty.hcs.push.{}.sync-enabled),本轮跳过", taskName, bizType);
return;
}
// 多实例部署防重扫:setIfAbsent 抢锁,持有值比对后释放
String lockKey = HcsSyncConstant.PUSH_TASK_LOCK_KEY_PREFIX + bizType;
String lockHolder = UUID.randomUUID().toString();
Boolean locked = bladeRedis.getValueOps()
.setIfAbsent(HcsSyncConstant.PUSH_TASK_LOCK_KEY, lockHolder, Duration.ofSeconds(PUSH_TASK_LOCK_TTL_SECONDS));
.setIfAbsent(lockKey, lockHolder, Duration.ofSeconds(PUSH_TASK_LOCK_TTL_SECONDS));
if (!Boolean.TRUE.equals(locked)) {
log.info("HCS推送任务其他实例执行中,本轮跳过");
log.info("HCS{}推送任务其他实例执行中(lockKey={}),本轮跳过", taskName, lockKey);
return;
}
try {
// 任务顺序(设计文档 §5.0):项目 → 合同 → 付款 → 开票 → 收票;各类互不影响
log.info("定时推送HCS项目立项:{}", pushUnsyncedProjects());
log.info("定时推送HCS合同:{}", pushUnsyncedContracts());
log.info("定时推送HCS付款链:{}", pushUnsyncedPayments());
log.info("定时推送HCS开票登记:{}", pushUnsyncedInvoiceApplications());
log.info("定时推送HCS收票单:{}", pushUnsyncedInvoiceReceipts());
log.info("HCS{}推送任务开始执行", taskName);
String summary = pushAction.get();
log.info("HCS{}推送任务执行完成:{}", taskName, summary);
} finally {
if (lockHolder.equals(bladeRedis.get(HcsSyncConstant.PUSH_TASK_LOCK_KEY))) {
bladeRedis.del(HcsSyncConstant.PUSH_TASK_LOCK_KEY);
if (lockHolder.equals(bladeRedis.get(lockKey))) {
bladeRedis.del(lockKey);
}
}
}
@@ -112,19 +112,11 @@ public class HcsProperties {
@Data
public static class Push {
/**
* 推送任务总开关(默认关,灰度打开)
*/
private boolean syncEnabled = false;
/**
* 扫描调度 cron,默认每 5 分钟
*/
private String cron = "0 */5 * * * ?";
/**
* 每类单据单轮扫描批量
* 每类单据单轮扫描批量(全局)
*/
private int batchSize = 50;
/**
* 推送失败自动重试上限,超限仅人工重推
* 推送失败自动重试上限,超限仅人工重推(全局)
*/
private int maxRetry = 5;
}