feat: HCS项目立项同步支撑(C102-3判重/C102-1新建/C102-2更新/合规fdId回填与失败补偿回写)

This commit is contained in:
刘泉佳
2026-09-27 22:51:42 +08:00
parent f01cd9ec71
commit c34a5b5bc5
@@ -0,0 +1,309 @@
/**
* 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 com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.hcs.config.HcsProperties;
import org.springblade.hcs.dto.HcsBpmOperationDTO;
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.HcsProjectPropsDTO;
import org.springblade.hcs.dto.HcsProjectUpdateDTO;
import org.springblade.hcs.dto.HcsProjectVO;
import org.springblade.hcs.dto.HcsRefVO;
import org.springblade.hcs.dto.HcsFormSaveVO;
import org.springblade.hcs.dto.HcsResult;
import org.springblade.hcs.dto.HcsSaveOptionDTO;
import org.springblade.hcs.dto.HcsUserRefVO;
import org.springblade.hcs.exception.HcsException;
import org.springblade.hcs.feign.IHcsClient;
import org.springblade.system.cache.UserCache;
import org.springblade.system.pojo.entity.User;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.mapper.ProjectApplyMapper;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* HCS 项目立项(C102)同步支撑组件(设计文档 §5.1)
* <p>
* 触发与链路:审批通过(approved)→ C102-1 新建;变更审批通过(change_approved,hcs_fd_id 已存在)→ C102-2 更新。
* 推送前判重:C102-3 conditions={"fd_num": projectCode},命中则直接回填 fdId 置 synced(跳过新建,§9-U4 查询能力联调确认)。
* 失败不在此回写,由调用方(HcsPushTask/手动端点)统一 {@link #markFailed} 落 failed 状态进补偿重试。
*
* @author Chill
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class HcsProjectSyncSupport {
/**
* TMS 万元 → HCS 元换算倍率
*/
private static final BigDecimal WAN_TO_YUAN = BigDecimal.valueOf(10000);
private final IHcsClient hcsClient;
private final HcsProperties hcsProperties;
private final ProjectApplyMapper projectApplyMapper;
/**
* 推送项目立项到 HCS:判重 → 新建(C102-1)/ 更新(C102-2)→ 回填合规 fdId 置 synced
*
* @param projectApply 项目立项单(approvalStatus 须为 approved/change_approved)
* @return 推送结果描述(供任务汇总与 hcs_sync_msg 落库)
*/
public String pushProject(ProjectApply projectApply) {
// 首推先判重(C102-3),命中直接回填跳过新建;已有合规ID(变更单)走更新
if (StringUtil.isBlank(projectApply.getHcsFdId())) {
String existFdId = findExistFdId(projectApply);
if (StringUtil.isNotBlank(existFdId)) {
markSynced(projectApply, existFdId, "判重命中,回填已有合规项目ID");
return "判重命中,回填已有合规项目ID";
}
String fdId = addProject(projectApply);
markSynced(projectApply, fdId, "C102-1 项目立项新建成功");
return "C102-1 项目立项新建成功";
}
updateProject(projectApply);
markSynced(projectApply, projectApply.getHcsFdId(), "C102-2 项目立项更新成功");
return "C102-2 项目立项更新成功";
}
/**
* C102-1 项目立项新建,返回合规项目 fdId
*/
private String addProject(ProjectApply projectApply) {
String operatorLoginName = requireLoginName(projectApply, "C102-1");
HcsProjectAddDTO param = new HcsProjectAddDTO();
HcsBpmOperationDTO bpmOperation = new HcsBpmOperationDTO();
// 操作类型:固定 drafter_submit(以起草人身份直接提交流程)
bpmOperation.setOperationType("drafter_submit");
// 【必填】立项表单模板编码:配置 thirdParty.hcs.project.template-code(环境值,§9-U9)
bpmOperation.setTemplateCode(requireTemplateCode());
param.setBpmOperation(bpmOperation);
// 是否校验权限:接口调用固定 false
param.setCheckAuth(false);
// 【必填】当前操作人:项目经办人登录名(§9-U2 登录名同源性联调确认)
param.setCurUser(HcsUserRefVO.ofLoginName(operatorLoginName));
param.setFormData(buildFormData(projectApply, null));
HcsSaveOptionDTO saveOption = new HcsSaveOptionDTO();
// saveDraft=false 直接提交
saveOption.setSaveDraft(false);
param.setSaveOption(saveOption);
log.info("HCS项目立项推送开始:{}(C102-1 新建)", projectApply.getProjectCode());
HcsResult<HcsFormSaveVO> result = hcsClient.projectAdd(param);
if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdId())) {
log.error("HCS项目立项新建失败响应:{},projectCode={}", result == null ? "无响应" : result.getMsg(), projectApply.getProjectCode());
throw new HcsException("C102-1 项目立项新建失败:" + (result == null ? "响应为空" : result.getMsg()));
}
return result.getData().getFdId();
}
/**
* C102-2 项目立项更新(入参同新建,去掉 bpmOperation/saveOption,formData.fdId 必填)
*/
private void updateProject(ProjectApply projectApply) {
String operatorLoginName = requireLoginName(projectApply, "C102-2");
HcsProjectUpdateDTO param = new HcsProjectUpdateDTO();
// 是否校验权限:接口调用固定 false
param.setCheckAuth(false);
// 【必填】当前操作人:项目经办人登录名
param.setCurUser(HcsUserRefVO.ofLoginName(operatorLoginName));
// 更新必须携带合规项目 fdId
param.setFormData(buildFormData(projectApply, projectApply.getHcsFdId()));
log.info("HCS项目立项推送开始:{}(C102-2 更新)", projectApply.getProjectCode());
HcsResult<Void> result = hcsClient.projectUpdate(param);
if (result == null || !result.isSuccess()) {
log.error("HCS项目立项更新失败响应:{},projectCode={}", result == null ? "无响应" : result.getMsg(), projectApply.getProjectCode());
throw new HcsException("C102-2 项目立项更新失败:" + (result == null ? "响应为空" : result.getMsg()));
}
}
/**
* 组装项目立项 formData(新建/更新共用,字段标注照 §3.4 规范,映射表见设计文档 §5.1)
*/
private HcsProjectFormDataDTO buildFormData(ProjectApply projectApply, String fdId) {
HcsProjectFormDataDTO formData = new HcsProjectFormDataDTO();
// 合规项目 fdId(C102-2 更新必填,C102-1 不传)
formData.setFdId(fdId);
HcsProjectPropsDTO props = new HcsProjectPropsDTO();
// 【必填】项目名称:ProjectApply.projectName
props.setFdProName(projectApply.getProjectName());
// 项目简称:ProjectApply.projectShortName
props.setFdProSimpleName(projectApply.getProjectShortName());
// 【必填】项目编号:ProjectApply.projectCode(判重候选键)
props.setFdNum(projectApply.getProjectCode());
// 创建单位:TMS 开题部门名称(HCS 组织 fdId 与 TMS 不同源,先传名称,§9-U2)
props.setFdCreatorUnit(HcsRefVO.ofName(projectApply.getBusinessDeptName()));
// 项目负责人:负责人登录名 {fdLoginName}(§9-U2)
props.setFdHead(resolveUserRef(projectApply.getPrincipalUserId()));
// 承接部门:TMS 承接部门名称(同 fd_creator_unit 先传名称)
props.setFdTakerDept(HcsRefVO.ofName(projectApply.getUndertakeDeptName()));
// 承接人:项目经办人登录名 {fdLoginName}
props.setFdTaker(resolveUserRef(projectApply.getHandlerUserId()));
// 付款额度:TMS fundLimit(万元)× 10000 转 HCS 元
props.setFdPayLimit(toYuan(projectApply.getFundLimit()));
// 开票额度:TMS receivableLimit(万元)× 10000 转 HCS 元
props.setFdOutInvoiceLimit(toYuan(projectApply.getReceivableLimit()));
// 项目建设内容:ProjectApply.situationRemark
props.setFdProContent(projectApply.getSituationRemark());
// 是否可用:固定 true
props.setFdIsAvailable(true);
// 可用单位列表:与创建单位一致,先传名称
props.setFdAvailUnits(List.of(HcsRefVO.ofName(projectApply.getBusinessDeptName())));
// 结算类型:TMS settlementMode 原值(字典映射待联调,§9-U3)
props.setFdSettlementType(projectApply.getSettlementMode());
// 计划保证金:TMS fundDemand(万元)× 10000 转 HCS 元(变更映射曾名"履约保证金",语义联调确认)
props.setFdProPlanDeposit(toYuan(projectApply.getFundDemand()));
// 是否自营项目:一期固定 "1"(联调确认)
props.setFdIsSelfPro("1");
// TODO 待确认:项目收入目标 fd_pro_income_goal,TMS 无直接对应字段,一期不传(§5.1)
// 金蝶同步状态:固定 "1"
props.setFdKingdeeSyncStatus("1");
// 来源标识:fd_sources,配置 thirdParty.hcs.src-system(约定值 TMS,D14)
props.setFdSources(hcsProperties.getSrcSystem());
// 来源系统主键 ID:TMS 项目立项单主键(D14,对账/幂等候选键)
props.setFdKingdeeId(String.valueOf(projectApply.getId()));
// 创建人:项目经办人登录名(扫描推送无登录上下文,取单据承办人,§9-U2)
props.setFdCreator(resolveUserRef(projectApply.getHandlerUserId()));
formData.setDynamicProps(props);
return formData;
}
/**
* C102-3 项目立项查询判重(conditions={"fd_num": projectCode},§9-U4 查询能力联调确认)
*/
private String findExistFdId(ProjectApply projectApply) {
HcsPageQueryDTO query = new HcsPageQueryDTO();
// C102-3 入参 columns 用下划线字段名
query.setColumns(List.of("fd_id", "fd_num"));
// 判重条件:项目编号精确匹配
query.setConditions(Map.of("fd_num", Objects.toString(projectApply.getProjectCode(), "")));
query.setOffset(0);
query.setPageSize(10);
query.setSorts(Map.of("fd_create_time", "desc"));
// 查询需携带当前操作人(复用单据承办人登录名)
query.setCurUser(HcsUserRefVO.ofLoginName(requireLoginName(projectApply, "C102-3")));
HcsResult<HcsPageVO<HcsProjectVO>> result = hcsClient.projectList(query);
if (result == null || !result.isSuccess()) {
log.error("HCS项目立项判重查询失败响应:{},projectCode={}", result == null ? "无响应" : result.getMsg(), projectApply.getProjectCode());
throw new HcsException("C102-3 项目立项判重查询失败:" + (result == null ? "响应为空" : result.getMsg()));
}
HcsPageVO<HcsProjectVO> page = result.getData();
if (page == null || page.getContent() == null || page.getContent().isEmpty()) {
return null;
}
return page.getContent().get(0).getFdId();
}
/**
* 回填合规 fdId 并置 synced
*/
public void markSynced(ProjectApply projectApply, String fdId, String message) {
projectApply.setHcsFdId(fdId);
projectApply.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_SYNCED);
projectApply.setHcsRetryCount(0);
projectApply.setHcsSyncTime(LocalDateTime.now());
projectApply.setHcsSyncMsg(message);
projectApplyMapper.updateById(projectApply);
log.info("HCS项目立项推送成功:projectCode={},fdId={},{}", projectApply.getProjectCode(), fdId, message);
}
/**
* 推送失败回写:failed + 重试次数+1 + 失败原因(供扫描任务按 hcs_retry_count < max-retry 自动补偿)
*/
public void markFailed(ProjectApply projectApply, String message) {
projectApply.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_FAILED);
projectApply.setHcsRetryCount(projectApply.getHcsRetryCount() == null ? 1 : projectApply.getHcsRetryCount() + 1);
projectApply.setHcsSyncTime(LocalDateTime.now());
projectApply.setHcsSyncMsg(message);
projectApplyMapper.updateById(projectApply);
log.error("HCS项目立项推送失败:projectCode={},retryCount={},{}", projectApply.getProjectCode(), projectApply.getHcsRetryCount(), message);
}
/**
* 立项表单模板编码(未配置直接失败,提示配置项,§9-U9)
*/
private String requireTemplateCode() {
String templateCode = hcsProperties.getProject().getTemplateCode();
if (StringUtil.isBlank(templateCode)) {
throw new HcsException("HCS项目立项推送失败,立项表单模板编码未配置(thirdParty.hcs.project.template-code,§9-U9)");
}
return templateCode;
}
/**
* 项目经办人登录名(curUser 必填,解析不到直接失败并提示)
*/
private String requireLoginName(ProjectApply projectApply, String scene) {
String loginName = resolveLoginName(projectApply.getHandlerUserId());
if (StringUtil.isBlank(loginName)) {
throw new HcsException("HCS项目立项推送失败(" + scene + "),经办人登录名解析不到(handlerUserId=" + projectApply.getHandlerUserId() + ",§9-U2)");
}
return loginName;
}
/**
* 按用户ID 解析登录名引用(查不到返回 null,调用方按字段必填性兜底)
*/
private HcsUserRefVO resolveUserRef(Long userId) {
String loginName = resolveLoginName(userId);
return StringUtil.isBlank(loginName) ? null : HcsUserRefVO.ofLoginName(loginName);
}
/**
* 按用户ID 解析登录名(UserCache 惯例)
*/
private String resolveLoginName(Long userId) {
if (userId == null) {
return null;
}
User user = UserCache.getUser(userId);
return user == null ? null : user.getAccount();
}
/**
* TMS 万元 → HCS 元
*/
private BigDecimal toYuan(BigDecimal wanAmount) {
return wanAmount == null ? null : wanAmount.multiply(WAN_TO_YUAN);
}
}