:feat: 新增项目金蝶同步结果回调并回填金蝶ID

- 新增 /hcs/projectLegal/result 免登回调端点(D902),按项目编号定位回填金蝶项目ID/编号,原文留痕 blade_hcs_check_log
- 合同 contractLegal/result 回调成功分支回填金蝶合同ID
- 项目/合同金蝶回调成功与失败均追加变更记录(changeRecordJson)
- 项目立项/合同列表支持按是否同步金蝶、是否同步合规筛选
- 新增增量SQL:blade_project_apply.kingdee_id/kingdee_number、blade_contract_manage.kingdee_id
This commit is contained in:
刘泉佳
2026-10-20 11:05:11 +08:00
parent 6e6e5913ba
commit fac4c112aa
13 changed files with 394 additions and 4 deletions
@@ -89,6 +89,11 @@ public final class HcsSyncConstant {
*/
public static final String CHECK_TYPE_SPEC = "spec";
/**
* 校验类型:项目金蝶同步结果回调(D902,projectLegal/result 推送留痕)
*/
public static final String CHECK_TYPE_KINGDEE = "kingdee";
/**
* 校验结果:通过
*/
@@ -36,11 +36,13 @@ import lombok.extern.slf4j.Slf4j;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.hcs.dto.HcsAttachmentRefDTO;
import org.springblade.hcs.dto.HcsContractLegalResultDTO;
import org.springblade.hcs.dto.HcsCurrencyVO;
import org.springblade.hcs.dto.HcsLegalResultDTO;
import org.springblade.hcs.dto.HcsProjectLegalResultDTO;
import org.springblade.hcs.dto.HcsTemplateVO;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.pojo.vo.CustomerRiskVO;
@@ -223,6 +225,17 @@ public class HcsSyncController {
return R.data(hcsCreditCheckSupport.queryCustomerRisk(customerCode));
}
/**
* 客商风险批量查询(C101-1,客商列表页展示用,前端接入设计 §3.3):
* 按客商编号集合逐个查询(口径同 /risk),展示用不作为安全边界,单个失败跳过不阻断整页
*/
@GetMapping("/risk/batch")
@ApiOperationSupport(order = 13)
@Operation(summary = "客商风险批量查询", description = "C101-1 按客商编号集合批量查询风险等级与黑名单状态(展示用),口径同单个查询,单个失败跳过不阻断")
public R<List<CustomerRiskVO>> riskBatch(@Parameter(description = "客商编号集合,逗号分隔", required = true) @RequestParam String customerCodes) {
return R.data(hcsCreditCheckSupport.queryCustomerRiskBatch(Func.toStrList(customerCodes)));
}
/**
* 附件上传到合同系统(C103-2 代理,前端接入设计 §3.2),返回合规附件ID
*/
@@ -293,4 +306,19 @@ public class HcsSyncController {
public R<String> paymentLegalResult(@RequestBody(required = false) HcsLegalResultDTO param) {
return R.data(hcsPaymentSyncSupport.handleLegalResult(param));
}
/**
* 项目金蝶同步结果回传接收(合规系统 projectLegal/result 推送,D902,原文留痕)
* <p>
* 免登免授权:blade.secure skip-url + auth permitAll 放行(doc/nacos/blade.yaml);
* iPaaS 网关注册映射:/gwzh/HTHG/projectLegal/result → /hcs/projectLegal/result;
* 报文 {sourceSystem, projectCode, requestId, success, kingdeeId, projectNumber, reason},
* 按 projectCode=TMS 项目编号定位,success=true 回填金蝶项目ID/编号,success=false 记录失败原因。
*/
@PostMapping("/projectLegal/result")
@ApiOperationSupport(order = 18)
@Operation(summary = "项目金蝶同步结果回传接收", description = "合规系统推送项目金蝶同步结果(D902),按项目编号定位并回填金蝶ID,原文留痕")
public R<String> projectLegalResult(@RequestBody(required = false) HcsProjectLegalResultDTO param) {
return R.data(hcsProjectSyncSupport.handleLegalResult(param));
}
}
@@ -482,9 +482,29 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
.orderByDesc(ContractManage::getCreateTime);
applyEffectiveType(queryWrapper, contractManage.getEffectiveType());
applyExpireScope(queryWrapper, contractManage.getExpireScope());
// 是否同步金蝶(kingdee_id 是否已回填)/ 是否同步合规(hcs_sync_status 是否 synced)
applySyncFlagFilter(queryWrapper, contractManage.getKingdeeSynced(), contractManage.getHcsSynced());
return queryWrapper;
}
/**
* 金蝶/合规同步与否筛选(列表搜索:kingdeeSynced 按 kingdee_id 空值判断,hcsSynced 按 hcs_sync_status=synced 判断)
*/
private void applySyncFlagFilter(LambdaQueryWrapper<ContractManage> queryWrapper, Boolean kingdeeSynced, Boolean hcsSynced) {
if (Boolean.TRUE.equals(kingdeeSynced)) {
queryWrapper.isNotNull(ContractManage::getKingdeeId).ne(ContractManage::getKingdeeId, "");
} else if (Boolean.FALSE.equals(kingdeeSynced)) {
queryWrapper.and(wrapper -> wrapper.isNull(ContractManage::getKingdeeId)
.or().eq(ContractManage::getKingdeeId, ""));
}
if (Boolean.TRUE.equals(hcsSynced)) {
queryWrapper.eq(ContractManage::getHcsSyncStatus, HcsSyncConstant.HCS_STATUS_SYNCED);
} else if (Boolean.FALSE.equals(hcsSynced)) {
queryWrapper.and(wrapper -> wrapper.isNull(ContractManage::getHcsSyncStatus)
.or().ne(ContractManage::getHcsSyncStatus, HcsSyncConstant.HCS_STATUS_SYNCED));
}
}
private void applyEffectiveType(LambdaQueryWrapper<ContractManage> queryWrapper, String effectiveType) {
if (Func.isEmpty(effectiveType)) {
return;
@@ -406,9 +406,29 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
.toList();
queryWrapper.in(ProjectApply::getId, matchedIds.isEmpty() ? List.of(-1L) : matchedIds);
}
// 是否同步金蝶(kingdee_id 是否已回填)/ 是否同步合规(hcs_sync_status 是否 synced)
applySyncFlagFilter(queryWrapper, projectApply.getKingdeeSynced(), projectApply.getHcsSynced());
return queryWrapper;
}
/**
* 金蝶/合规同步与否筛选(列表搜索:kingdeeSynced 按 kingdee_id 空值判断,hcsSynced 按 hcs_sync_status=synced 判断)
*/
private void applySyncFlagFilter(LambdaQueryWrapper<ProjectApply> queryWrapper, Boolean kingdeeSynced, Boolean hcsSynced) {
if (Boolean.TRUE.equals(kingdeeSynced)) {
queryWrapper.isNotNull(ProjectApply::getKingdeeId).ne(ProjectApply::getKingdeeId, "");
} else if (Boolean.FALSE.equals(kingdeeSynced)) {
queryWrapper.and(wrapper -> wrapper.isNull(ProjectApply::getKingdeeId)
.or().eq(ProjectApply::getKingdeeId, ""));
}
if (Boolean.TRUE.equals(hcsSynced)) {
queryWrapper.eq(ProjectApply::getHcsSyncStatus, HcsSyncConstant.HCS_STATUS_SYNCED);
} else if (Boolean.FALSE.equals(hcsSynced)) {
queryWrapper.and(wrapper -> wrapper.isNull(ProjectApply::getHcsSyncStatus)
.or().ne(ProjectApply::getHcsSyncStatus, HcsSyncConstant.HCS_STATUS_SYNCED));
}
}
private void fillFundUseRisk(List<ProjectApplyVO> records) {
if (records == null || records.isEmpty()) {
return;
@@ -45,8 +45,12 @@ import org.springblade.transport.pojo.entity.HcsContractCheckLog;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* HCS 合同特批同步支撑组件(合规特批结果接收:contractLegal/result 推送 + C103-4 定时查询兜底 + 人工确认)。
@@ -169,7 +173,7 @@ public class HcsContractSpecSyncSupport {
* 合同特批结果接收处理(合规 contractLegal/result 推送,2026-10-13 按合规方最新字段表修正报文结构):
* 特批完成后合规侧同步金蝶并回调,报文 {contractType, contractCode, requestId, success, reason, kingdeeId, sourceSystem=WCCK};
* sourceSystem 须为 WCCK(不符抛"来源系统标识错误"异常);contractCode=TMS 合同编号定位(requestId 仅请求头回显不可定位);
* success=true 联动恢复"审批通过"(kingdeeId 随留痕保存);success=false 联动合同审批驳回走重新提交闭环(reason 随原文留痕)
* success=true 联动恢复"审批通过"(金蝶合同ID回填 kingdee_id);success=false 联动合同审批驳回走重新提交闭环(reason 随原文留痕)
*
* @param param 合规推送报文
* @return 处理结果描述
@@ -193,9 +197,11 @@ public class HcsContractSpecSyncSupport {
return "已留痕:success 字段缺失(contractNo=" + contractManage.getContractNo() + ")";
}
boolean success = param.getSuccess();
// 成功:特批完成且金蝶同步成功,恢复"审批通过";kingdeeId 随 hcs_spec_msg/留痕保存
// 成功:特批完成且金蝶同步成功,恢复"审批通过";金蝶合同ID回填 kingdee_id 留痕保存
if (success) {
syncContractOnSpecResult(contractManage, true);
contractManage.setKingdeeId(param.getKingdeeId());
appendKingdeeResultRecord(contractManage, "金蝶同步成功:金蝶合同ID=" + param.getKingdeeId());
markSpec(contractManage, HcsSyncConstant.SPEC_STATUS_APPROVED,
"合同特批结果推送:金蝶同步成功,kingdeeId=" + param.getKingdeeId());
writeCallbackLog(contractManage, HcsSyncConstant.CHECK_RESULT_PASS, "合同特批结果推送:" + raw);
@@ -204,12 +210,49 @@ public class HcsContractSpecSyncSupport {
}
// 失败:特批流程已完成但金蝶同步失败,联动合同审批驳回,走重新编辑→提交→审批→重新特批闭环(失败单独重试机制待联调确认)
syncContractOnSpecResult(contractManage, false);
appendKingdeeResultRecord(contractManage, "金蝶同步失败:" + param.getReason());
markSpec(contractManage, HcsSyncConstant.SPEC_STATUS_REJECTED, "合同特批结果推送:金蝶同步失败:" + param.getReason());
writeCallbackLog(contractManage, HcsSyncConstant.CHECK_RESULT_FAIL, "合同特批结果推送:" + raw);
log.info("合同特批结果推送处理完成:contractNo={},结果=rejected,reason={}", contractManage.getContractNo(), param.getReason());
return "特批结果已处理:rejected";
}
/**
* 金蝶同步结果回填变更记录(changeRecordJson 追加一条;系统回调无登录用户,操作人记来源标识)
*/
private void appendKingdeeResultRecord(ContractManage contractManage, String changeContent) {
List<Object> records = StringUtil.isBlank(contractManage.getChangeRecordJson())
? new ArrayList<>() : new ArrayList<>(JSONUtil.toList(contractManage.getChangeRecordJson(), Object.class));
Map<String, Object> record = new LinkedHashMap<>();
record.put("changeDate", LocalDate.now().toString());
record.put("handlerUserName", "合规系统回调");
record.put("changeType", "金蝶同步结果");
record.put("changeContent", changeContent);
record.put("status", contractManage.getApprovalStatus());
record.put("statusName", approvalStatusName(contractManage.getApprovalStatus()));
records.add(record);
contractManage.setChangeRecordJson(JSONUtil.toJsonStr(records));
}
/**
* 审批状态中文名(变更记录展示用,未匹配回退原值)
*/
private String approvalStatusName(String approvalStatus) {
if (StringUtil.isBlank(approvalStatus)) {
return null;
}
return switch (approvalStatus) {
case "draft" -> "草稿";
case "reviewing" -> "审批中";
case "withdrawn" -> "已撤回";
case "approved" -> "审批通过";
case "rejected" -> "已驳回";
case "change_reviewing" -> "变更审批中";
case "change_approved" -> "变更审批通过";
default -> approvalStatus;
};
}
/**
* 按 ERP 合同编号定位合同(合规推送 contractCode,合同编号唯一)
*/
@@ -25,6 +25,8 @@
*/
package org.springblade.transport.support.hcs;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
@@ -36,6 +38,7 @@ import org.springblade.hcs.dto.HcsPageQueryDTO;
import org.springblade.hcs.dto.HcsPageVO;
import org.springblade.hcs.dto.HcsProjectAddDTO;
import org.springblade.hcs.dto.HcsProjectFormDataDTO;
import org.springblade.hcs.dto.HcsProjectLegalResultDTO;
import org.springblade.hcs.dto.HcsProjectPropsDTO;
import org.springblade.hcs.dto.HcsProjectUpdateDTO;
import org.springblade.hcs.dto.HcsProjectVO;
@@ -47,12 +50,17 @@ import org.springblade.hcs.exception.HcsException;
import org.springblade.hcs.feign.IHcsClient;
import org.springblade.transport.constant.HcsFieldConstant;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.mapper.HcsContractCheckLogMapper;
import org.springblade.transport.mapper.ProjectApplyMapper;
import org.springblade.transport.pojo.entity.HcsContractCheckLog;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -76,9 +84,15 @@ public class HcsProjectSyncSupport {
*/
private static final BigDecimal WAN_TO_YUAN = BigDecimal.valueOf(10000);
/**
* 项目金蝶同步结果回传来源系统标识(合规方接口文档约定固定传 WCCK)
*/
private static final String LEGAL_CALLBACK_SRC_SYSTEM = "WCCK";
private final IHcsClient hcsClient;
private final HcsProperties hcsProperties;
private final ProjectApplyMapper projectApplyMapper;
private final HcsContractCheckLogMapper hcsContractCheckLogMapper;
/**
* 推送项目立项到 HCS:判重 → 新建(C102-1)/ 更新(C102-2)→ 回填合规 fdId 置 synced
@@ -257,6 +271,125 @@ public class HcsProjectSyncSupport {
return page.getContent().get(0).getFdId();
}
/**
* 项目金蝶同步结果接收处理(合规 projectLegal/result 推送,D902):
* sourceSystem 须为 WCCK(不符抛"来源系统标识错误"异常);projectCode=TMS 项目编号定位(requestId 仅 X-ERP-Request-Id 回显不可定位);
* success=true 回填金蝶项目ID/编号(kingdeeId/kingdeeNumber);success=false 记录失败原因;原文留痕 blade_hcs_check_log
*
* @param param 合规推送报文
* @return 处理结果描述
*/
public String handleLegalResult(HcsProjectLegalResultDTO param) {
// 来源系统校验:仅接受合规方约定的 WCCK,防伪造回传
if (!LEGAL_CALLBACK_SRC_SYSTEM.equalsIgnoreCase(param == null ? null : param.getSourceSystem())) {
log.warn("项目金蝶同步结果推送来源系统标识错误:sourceSystem={}", param == null ? null : param.getSourceSystem());
throw new ServiceException("来源系统标识错误");
}
String raw = JSONUtil.toJsonStr(param);
ProjectApply projectApply = locateProjectByCode(param.getProjectCode());
if (projectApply == null) {
log.warn("项目金蝶同步结果推送未定位到项目:projectCode={},报文已留痕", param.getProjectCode());
writeCallbackLog(null, HcsSyncConstant.CHECK_RESULT_SKIP, "项目金蝶同步结果推送未定位到项目:" + raw);
return "已留痕:未定位到项目(projectCode=" + param.getProjectCode() + ")";
}
if (param.getSuccess() == null) {
log.warn("项目金蝶同步结果推送 success 字段缺失:projectCode={},报文已留痕", projectApply.getProjectCode());
writeCallbackLog(projectApply, HcsSyncConstant.CHECK_RESULT_SKIP, "项目金蝶同步结果推送 success 字段缺失:" + raw);
return "已留痕:success 字段缺失(projectCode=" + projectApply.getProjectCode() + ")";
}
if (param.getSuccess()) {
// 成功:回填金蝶项目ID/编号,同步结果信息随 hcs_sync_msg 留痕
projectApply.setKingdeeId(param.getKingdeeId());
projectApply.setKingdeeNumber(param.getProjectNumber());
projectApply.setHcsSyncMsg(HcsSyncConstant.truncateMsg("项目金蝶同步结果回调:金蝶同步成功,kingdeeId=" + param.getKingdeeId()
+ ",projectNumber=" + param.getProjectNumber()));
appendKingdeeResultRecord(projectApply, "金蝶同步成功:金蝶项目ID=" + param.getKingdeeId()
+ ",金蝶项目编号=" + param.getProjectNumber());
projectApplyMapper.updateById(projectApply);
writeCallbackLog(projectApply, HcsSyncConstant.CHECK_RESULT_PASS, "项目金蝶同步结果推送:" + raw);
log.info("项目金蝶同步结果推送处理完成:projectCode={},结果=success,kingdeeId={}", projectApply.getProjectCode(), param.getKingdeeId());
return "金蝶同步结果已处理:success";
}
// 失败:记录失败原因(金蝶同步在合规侧完成,TMS 不重推,失败重试由合规侧负责)
projectApply.setHcsSyncMsg(HcsSyncConstant.truncateMsg("项目金蝶同步结果回调:金蝶同步失败:" + param.getReason()));
appendKingdeeResultRecord(projectApply, "金蝶同步失败:" + param.getReason());
projectApplyMapper.updateById(projectApply);
writeCallbackLog(projectApply, HcsSyncConstant.CHECK_RESULT_FAIL, "项目金蝶同步结果推送:" + raw);
log.info("项目金蝶同步结果推送处理完成:projectCode={},结果=fail,reason={}", projectApply.getProjectCode(), param.getReason());
return "金蝶同步结果已处理:fail";
}
/**
* 金蝶同步结果回填变更记录(changeRecordJson 追加一条;系统回调无登录用户,操作人记来源标识)
*/
private void appendKingdeeResultRecord(ProjectApply projectApply, String changeContent) {
List<Object> records = StringUtil.isBlank(projectApply.getChangeRecordJson())
? new ArrayList<>() : new ArrayList<>(JSONUtil.toList(projectApply.getChangeRecordJson(), Object.class));
Map<String, Object> record = new LinkedHashMap<>();
record.put("changeDate", LocalDate.now().toString());
record.put("handlerUserName", "合规系统回调");
record.put("changeType", "金蝶同步结果");
record.put("changeContent", changeContent);
record.put("status", projectApply.getApprovalStatus());
record.put("statusName", approvalStatusName(projectApply.getApprovalStatus()));
records.add(record);
projectApply.setChangeRecordJson(JSONUtil.toJsonStr(records));
}
/**
* 审批状态中文名(变更记录展示用,未匹配回退原值)
*/
private String approvalStatusName(String approvalStatus) {
if (StringUtil.isBlank(approvalStatus)) {
return null;
}
return switch (approvalStatus) {
case "draft" -> "草稿";
case "reviewing" -> "审批中";
case "withdrawn" -> "已撤回";
case "approved" -> "审批通过";
case "rejected" -> "已驳回";
case "change_reviewing" -> "变更审批中";
case "change_rejected" -> "变更驳回";
case "change_approved" -> "变更审批通过";
case "voided" -> "已作废";
default -> approvalStatus;
};
}
/**
* 按 ERP 项目编号定位项目(合规推送 projectCode,对应 C102 fd_num)
*/
private ProjectApply locateProjectByCode(String projectCode) {
if (StringUtil.isBlank(projectCode)) {
return null;
}
return projectApplyMapper.selectOne(Wrappers.<ProjectApply>lambdaQuery()
.eq(ProjectApply::getProjectCode, projectCode.trim())
.last("limit 1"));
}
/**
* 项目金蝶同步结果推送留痕(blade_hcs_check_log,biz_type=project,项目未定位到时 bizId 为空,异常不影响主流程)
*/
private void writeCallbackLog(ProjectApply projectApply, String result, String msg) {
try {
HcsContractCheckLog checkLog = new HcsContractCheckLog();
if (projectApply != null) {
checkLog.setTenantId(projectApply.getTenantId());
checkLog.setBizId(projectApply.getId());
}
checkLog.setBizType(HcsSyncConstant.BIZ_TYPE_PROJECT);
checkLog.setCheckType(HcsSyncConstant.CHECK_TYPE_KINGDEE);
checkLog.setResult(result);
checkLog.setMsg(StringUtil.left(msg, HcsSyncConstant.SYNC_MSG_MAX_LENGTH));
checkLog.setCreateTime(LocalDateTime.now());
hcsContractCheckLogMapper.insert(checkLog);
} catch (Exception exception) {
log.error("项目金蝶同步结果推送留痕写入失败:projectCode={}", projectApply == null ? null : projectApply.getProjectCode(), exception);
}
}
/**
* 单条手动重推:重置为待同步并清零重试计数后立即推送(页面"推送合规/重推"按钮入口)
*