diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java new file mode 100644 index 00000000..6a162c95 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsProjectSyncSupport.java @@ -0,0 +1,309 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * 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) + *

+ * 触发与链路:审批通过(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 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 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> 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 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); + } +}