feat: HCS推送任务与手动端点(定时扫描首推/补偿+分布式锁、单条重推/批量推送/主数据刷新端点)
This commit is contained in:
+106
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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.controller;
|
||||
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.constant.HcsSyncConstant;
|
||||
import org.springblade.transport.support.hcs.HcsContractSyncSupport;
|
||||
import org.springblade.transport.support.hcs.HcsMasterDataSupport;
|
||||
import org.springblade.transport.support.hcs.HcsProjectSyncSupport;
|
||||
import org.springblade.transport.task.HcsPushTask;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* HCS 合规推送端点(设计文档 §5.0 手动入口)
|
||||
* <p>
|
||||
* 前端列表页"推送合规/重推"按钮(照"同步金蝶"按钮模式):
|
||||
* 单条重推重置 hcs_sync_status=unsynced 并清零重试计数后立即推送;批量端点复用定时扫描逻辑。
|
||||
* 排查推送问题以 hcs_sync_status/hcs_sync_msg 为准,不以审批日志为准(ADR-0001)。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RequestMapping("hcs")
|
||||
@Tag(name = "HCS合规推送", description = "HCS合规推送端点")
|
||||
public class HcsSyncController {
|
||||
|
||||
private final HcsPushTask hcsPushTask;
|
||||
private final HcsProjectSyncSupport hcsProjectSyncSupport;
|
||||
private final HcsContractSyncSupport hcsContractSyncSupport;
|
||||
private final HcsMasterDataSupport hcsMasterDataSupport;
|
||||
|
||||
/**
|
||||
* 单条重推(bizType:project=项目立项 / contract=合同)
|
||||
*/
|
||||
@PostMapping("/sync/{bizType}/{id}")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "单条推送/重推合规", description = "重置待同步状态后立即推送,bizType:project=项目立项、contract=合同")
|
||||
public R<String> repush(@Parameter(description = "业务类型", required = true) @PathVariable String bizType,
|
||||
@Parameter(description = "单据ID", required = true) @PathVariable Long id) {
|
||||
String result = switch (bizType) {
|
||||
case HcsSyncConstant.BIZ_TYPE_PROJECT -> hcsProjectSyncSupport.repushProject(id);
|
||||
case HcsSyncConstant.BIZ_TYPE_CONTRACT -> hcsContractSyncSupport.repushContract(id);
|
||||
default -> throw new IllegalArgumentException("不支持的HCS推送业务类型:" + bizType);
|
||||
};
|
||||
return R.data(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量推送(bizType:project=项目立项 / contract=合同;复用定时扫描逻辑,单轮批量上限 push.batch-size)
|
||||
*/
|
||||
@PostMapping("/sync/{bizType}")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "批量推送合规", description = "扫描待同步/待补偿单据推送一轮,bizType:project=项目立项、contract=合同")
|
||||
public R<String> pushBatch(@Parameter(description = "业务类型", required = true) @PathVariable String bizType) {
|
||||
String result = switch (bizType) {
|
||||
case HcsSyncConstant.BIZ_TYPE_PROJECT -> hcsPushTask.pushUnsyncedProjects();
|
||||
case HcsSyncConstant.BIZ_TYPE_CONTRACT -> hcsPushTask.pushUnsyncedContracts();
|
||||
default -> throw new IllegalArgumentException("不支持的HCS推送业务类型:" + bizType);
|
||||
};
|
||||
return R.data(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新主数据缓存(C103-6 模板 / C103-7 分类 / C103-12 币种)
|
||||
*/
|
||||
@PostMapping("/master-data/refresh")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "刷新HCS主数据缓存", description = "全量重拉模板/分类/币种列表并缓存,同时校验模板映射配置")
|
||||
public R<String> refreshMasterData() {
|
||||
return R.data(hcsMasterDataSupport.refreshAll());
|
||||
}
|
||||
}
|
||||
+27
@@ -28,6 +28,7 @@ package org.springblade.transport.support.hcs;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.hcs.config.HcsProperties;
|
||||
import org.springblade.hcs.dto.HcsContractDraftDTO;
|
||||
@@ -342,6 +343,32 @@ public class HcsContractSyncSupport {
|
||||
return result.getData().getFdContractId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条手动重推:重置为待同步并清零重试计数后立即推送(页面"推送合规/重推"按钮入口)
|
||||
*
|
||||
* @param id 合同单ID
|
||||
* @return 推送结果描述
|
||||
*/
|
||||
public String repushContract(Long id) {
|
||||
ContractManage contractManage = contractManageMapper.selectById(id);
|
||||
if (contractManage == null) {
|
||||
throw new ServiceException("合同单不存在:" + id);
|
||||
}
|
||||
if (!List.of("approved", "change_approved").contains(contractManage.getApprovalStatus())) {
|
||||
throw new ServiceException("仅审批通过的合同可推送合规(当前状态:" + contractManage.getApprovalStatus() + ")");
|
||||
}
|
||||
contractManage.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
|
||||
contractManage.setHcsRetryCount(0);
|
||||
contractManageMapper.updateById(contractManage);
|
||||
try {
|
||||
return pushContract(contractManage);
|
||||
} catch (Exception exception) {
|
||||
String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage();
|
||||
markFailed(contractManage, message);
|
||||
throw new ServiceException("HCS合同推送失败:" + message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回填合规 fdId 并置 synced
|
||||
*/
|
||||
|
||||
+27
@@ -28,6 +28,7 @@ package org.springblade.transport.support.hcs;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.hcs.config.HcsProperties;
|
||||
import org.springblade.hcs.dto.HcsBpmOperationDTO;
|
||||
@@ -234,6 +235,32 @@ public class HcsProjectSyncSupport {
|
||||
return page.getContent().get(0).getFdId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条手动重推:重置为待同步并清零重试计数后立即推送(页面"推送合规/重推"按钮入口)
|
||||
*
|
||||
* @param id 项目立项单ID
|
||||
* @return 推送结果描述
|
||||
*/
|
||||
public String repushProject(Long id) {
|
||||
ProjectApply projectApply = projectApplyMapper.selectById(id);
|
||||
if (projectApply == null) {
|
||||
throw new ServiceException("项目立项单不存在:" + id);
|
||||
}
|
||||
if (!List.of("approved", "change_approved").contains(projectApply.getApprovalStatus())) {
|
||||
throw new ServiceException("仅审批通过的项目立项可推送合规(当前状态:" + projectApply.getApprovalStatus() + ")");
|
||||
}
|
||||
projectApply.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
|
||||
projectApply.setHcsRetryCount(0);
|
||||
projectApplyMapper.updateById(projectApply);
|
||||
try {
|
||||
return pushProject(projectApply);
|
||||
} catch (Exception exception) {
|
||||
String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage();
|
||||
markFailed(projectApply, message);
|
||||
throw new ServiceException("HCS项目立项推送失败:" + message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回填合规 fdId 并置 synced
|
||||
*/
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 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.task;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.redis.cache.BladeRedis;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.hcs.config.HcsProperties;
|
||||
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.springblade.transport.support.hcs.HcsContractSyncSupport;
|
||||
import org.springblade.transport.support.hcs.HcsProjectSyncSupport;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* HCS 合规推送定时扫描任务(设计文档 §5.0,ADR-0001)
|
||||
* <p>
|
||||
* 审批代码零埋点:扫描只认 approvalStatus=approved/change_approved 且 hcs_sync_status=unsynced/failed(重试未超限),
|
||||
* 与审批入口(/approve、MK 回调)无关;失败落 failed 由下轮扫描自动补偿,超限仅人工重推(D7)。
|
||||
* 任务顺序:项目 → 合同(内含风险校验→附件转传→C103-1→履约规则 P2 接线)。
|
||||
* 多实例部署时以 Redis 分布式锁防重扫(照既有 BladeRedis 用法,单实例部署行为与既有金蝶任务一致)。
|
||||
* 扫描与推送逻辑收敛在 pushUnsyncedProjects/pushUnsyncedContracts,供手动端点(HcsSyncController)复用。
|
||||
* <p>
|
||||
* 调度配置(Nacos thirdParty.hcs.push.*):sync-enabled 总开关(默认关,灰度打开)、cron、batch-size、max-retry。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class HcsPushTask {
|
||||
|
||||
/**
|
||||
* 分布式锁 TTL(秒):须小于 cron 间隔(默认 5 分钟)且大于单轮预期执行时长,实例崩溃后锁自动过期
|
||||
*/
|
||||
private static final long PUSH_TASK_LOCK_TTL_SECONDS = 240L;
|
||||
|
||||
/**
|
||||
* 审批通过状态集合(含变更通过)
|
||||
*/
|
||||
private static final List<String> APPROVED_STATUSES = List.of("approved", "change_approved");
|
||||
|
||||
private final HcsProperties hcsProperties;
|
||||
private final ProjectApplyMapper projectApplyMapper;
|
||||
private final ContractManageMapper contractManageMapper;
|
||||
private final HcsProjectSyncSupport hcsProjectSyncSupport;
|
||||
private final HcsContractSyncSupport hcsContractSyncSupport;
|
||||
private final BladeRedis bladeRedis;
|
||||
|
||||
/**
|
||||
* HCS 推送任务开关,默认关闭(灰度打开)
|
||||
*/
|
||||
@Value("${thirdParty.hcs.push.sync-enabled:false}")
|
||||
private boolean syncEnabled;
|
||||
|
||||
/**
|
||||
* HCS 推送调度,默认每 5 分钟(准实时首推)
|
||||
*/
|
||||
@Scheduled(cron = "${thirdParty.hcs.push.cron:0 */5 * * * ?}")
|
||||
public void pushHcs() {
|
||||
if (!syncEnabled) {
|
||||
return;
|
||||
}
|
||||
// 多实例部署防重扫:setIfAbsent 抢锁,持有值比对后释放
|
||||
String lockHolder = UUID.randomUUID().toString();
|
||||
Boolean locked = bladeRedis.getValueOps()
|
||||
.setIfAbsent(HcsSyncConstant.PUSH_TASK_LOCK_KEY, lockHolder, Duration.ofSeconds(PUSH_TASK_LOCK_TTL_SECONDS));
|
||||
if (!Boolean.TRUE.equals(locked)) {
|
||||
log.info("HCS推送任务其他实例执行中,本轮跳过");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 任务顺序(设计文档 §5.0):项目 → 合同;两类互不影响
|
||||
log.info("定时推送HCS项目立项:{}", pushUnsyncedProjects());
|
||||
log.info("定时推送HCS合同:{}", pushUnsyncedContracts());
|
||||
} finally {
|
||||
if (lockHolder.equals(bladeRedis.get(HcsSyncConstant.PUSH_TASK_LOCK_KEY))) {
|
||||
bladeRedis.del(HcsSyncConstant.PUSH_TASK_LOCK_KEY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描并推送待同步/待补偿的项目立项(C102),返回结果汇总
|
||||
*
|
||||
* @return 汇总描述
|
||||
*/
|
||||
public String pushUnsyncedProjects() {
|
||||
int maxRetry = hcsProperties.getPush().getMaxRetry();
|
||||
List<ProjectApply> projectList = projectApplyMapper.selectList(Wrappers.<ProjectApply>lambdaQuery()
|
||||
.in(ProjectApply::getApprovalStatus, APPROVED_STATUSES)
|
||||
.in(ProjectApply::getHcsSyncStatus, List.of(HcsSyncConstant.HCS_STATUS_UNSYNCED, HcsSyncConstant.HCS_STATUS_FAILED))
|
||||
.and(wrapper -> wrapper.isNull(ProjectApply::getHcsRetryCount).or().lt(ProjectApply::getHcsRetryCount, maxRetry))
|
||||
.last("limit " + hcsProperties.getPush().getBatchSize()));
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
for (ProjectApply projectApply : projectList) {
|
||||
try {
|
||||
hcsProjectSyncSupport.pushProject(projectApply);
|
||||
successCount++;
|
||||
} catch (Exception exception) {
|
||||
String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage();
|
||||
hcsProjectSyncSupport.markFailed(projectApply, message);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
String summary = String.format("扫描 %d 条,成功 %d 条,失败 %d 条(重试上限 %d)", projectList.size(), successCount, failCount, maxRetry);
|
||||
if (failCount > 0) {
|
||||
log.warn("HCS项目立项推送存在失败:{}", summary);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描并推送待同步/待补偿的合同(C103),返回结果汇总
|
||||
*
|
||||
* @return 汇总描述
|
||||
*/
|
||||
public String pushUnsyncedContracts() {
|
||||
int maxRetry = hcsProperties.getPush().getMaxRetry();
|
||||
List<ContractManage> contractList = contractManageMapper.selectList(Wrappers.<ContractManage>lambdaQuery()
|
||||
.in(ContractManage::getApprovalStatus, APPROVED_STATUSES)
|
||||
.in(ContractManage::getHcsSyncStatus, List.of(HcsSyncConstant.HCS_STATUS_UNSYNCED, HcsSyncConstant.HCS_STATUS_FAILED))
|
||||
.and(wrapper -> wrapper.isNull(ContractManage::getHcsRetryCount).or().lt(ContractManage::getHcsRetryCount, maxRetry))
|
||||
.last("limit " + hcsProperties.getPush().getBatchSize()));
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
for (ContractManage contractManage : contractList) {
|
||||
try {
|
||||
hcsContractSyncSupport.pushContract(contractManage);
|
||||
successCount++;
|
||||
} catch (Exception exception) {
|
||||
String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage();
|
||||
hcsContractSyncSupport.markFailed(contractManage, message);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
String summary = String.format("扫描 %d 条,成功 %d 条,失败 %d 条(重试上限 %d)", contractList.size(), successCount, failCount, maxRetry);
|
||||
if (failCount > 0) {
|
||||
log.warn("HCS合同推送存在失败:{}", summary);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user