feat: HCS项目立项同步支撑(C102-3判重/C102-1新建/C102-2更新/合规fdId回填与失败补偿回写)
This commit is contained in:
+309
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user