diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoiceRegSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoiceRegSyncSupport.java new file mode 100644 index 00000000..f4ca4d03 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoiceRegSyncSupport.java @@ -0,0 +1,334 @@ +/** + * 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.log.exception.ServiceException; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.hcs.config.HcsProperties; +import org.springblade.hcs.dto.HcsIdVO; +import org.springblade.hcs.dto.HcsInvoiceRegAddDTO; +import org.springblade.hcs.dto.HcsInvoiceRegUpdateDTO; +import org.springblade.hcs.dto.HcsPageQueryDTO; +import org.springblade.hcs.dto.HcsPageVO; +import org.springblade.hcs.dto.HcsInvoiceRegVO; +import org.springblade.hcs.dto.HcsResult; +import org.springblade.hcs.exception.HcsException; +import org.springblade.hcs.feign.IHcsClient; +import org.springblade.transport.constant.KingdeeBfiConstant; +import org.springblade.system.cache.SysCache; +import org.springblade.system.cache.UserCache; +import org.springblade.system.pojo.entity.Dept; +import org.springblade.system.pojo.entity.User; +import org.springblade.transport.constant.HcsSyncConstant; +import org.springblade.transport.mapper.InvoiceApplicationInvoiceMapper; +import org.springblade.transport.mapper.InvoiceApplicationMapper; +import org.springblade.transport.mapper.InvoiceApplicationSettlementMapper; +import org.springblade.transport.mapper.FormalSettlementMapper; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.InvoiceApplication; +import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice; +import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * HCS 开票登记同步支撑组件(C111-1 查询判重 / C111-2 新增 / C111-3 更新,设计文档 §5.7,D12) + *

+ * 审批通过即推 C111-2(票据日期空);金蝶开票完成(kingdeeStatus=invoiced,由金蝶结果回写置待同步) + * → C111-3 回填 fdBillDate/fdStatus。判重 C111-1 conditions={"fdNumber": applicationNo}(§9-U4 联调确认)。 + * fdContractNumber 缺口:InvoiceApplication 无合同字段,经应收结算关联表回溯合同编号(§9-U10,回溯不到失败提示)。 + * + * @author Chill + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class HcsInvoiceRegSyncSupport { + + private final IHcsClient hcsClient; + private final HcsProperties hcsProperties; + private final InvoiceApplicationMapper invoiceApplicationMapper; + private final InvoiceApplicationSettlementMapper invoiceApplicationSettlementMapper; + private final InvoiceApplicationInvoiceMapper invoiceApplicationInvoiceMapper; + private final FormalSettlementMapper formalSettlementMapper; + private final HcsAttachmentSupport hcsAttachmentSupport; + + /** + * 推送开票登记:无合规ID 走判重+C111-2 新增(金蝶已开票完成时紧接着 C111-3),有合规ID 走 C111-3 + * + * @param application 开票申请单(approvalStatus=approved) + * @return 推送结果描述 + */ + public String pushInvoiceRegistration(InvoiceApplication application) { + if (StringUtil.isBlank(application.getHcsFdId())) { + String existFdId = findExistFdId(application); + if (StringUtil.isNotBlank(existFdId)) { + markSynced(application, existFdId, "判重命中,回填已有合规开票登记ID"); + return "判重命中,回填已有合规开票登记ID"; + } + String fdId = addInvoiceRegistration(application); + // 开票登记ID 立即持久化(调用方必须保存) + markSynced(application, fdId, "C111-2 开票登记新增成功"); + String result = "C111-2 开票登记新增成功"; + // 金蝶已开票完成(新增前回写竞态):紧接着回填票据日期 + if (KingdeeBfiConstant.KINGDEE_STATUS_INVOICED.equals(application.getKingdeeStatus())) { + result += ";" + updateInvoiceRegistration(application); + } + return result; + } + String result = updateInvoiceRegistration(application); + markSynced(application, application.getHcsFdId(), "C111-3 开票登记更新成功"); + return result; + } + + /** + * 单条手动重推:重置为待同步并清零重试计数后立即推送 + * + * @param id 开票申请单ID + * @return 推送结果描述 + */ + public String repushInvoiceRegistration(Long id) { + InvoiceApplication application = invoiceApplicationMapper.selectById(id); + if (application == null) { + throw new ServiceException("开票申请单不存在:" + id); + } + if (!"approved".equals(application.getApprovalStatus())) { + throw new ServiceException("仅审批通过的开票申请可推送合规(当前状态:" + application.getApprovalStatus() + ")"); + } + application.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED); + application.setHcsRetryCount(0); + invoiceApplicationMapper.updateById(application); + try { + return pushInvoiceRegistration(application); + } catch (Exception exception) { + String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage(); + markFailed(application, message); + throw new ServiceException("HCS开票登记推送失败:" + message); + } + } + + /** + * C111-2 新增开票登记,返回合规 fdId(字段标注照 §3.4 规范,映射见设计文档 §5.7) + */ + private String addInvoiceRegistration(InvoiceApplication application) { + HcsInvoiceRegAddDTO param = new HcsInvoiceRegAddDTO(); + // 单据编号:开票申请单号(同租户唯一,判重候选键) + param.setFdNumber(application.getApplicationNo()); + // 登记日期:applicationDate(毫秒时间戳) + param.setFdRegisterDate(toMillis(application.getApplicationDate())); + // 【必填】合同编号:经应收结算链路回溯(§9-U10 关联字段确认后细化) + param.setFdContractNumber(requireContractNumber(application)); + // 发票类型:invoice-type-mapping 字典映射(§9-U3,未配置不传) + param.setFdInvoiceType(hcsProperties.getInvoice().getInvoiceTypeMapping().get(application.getInvoiceType())); + if (param.getFdInvoiceType() == null) { + log.warn("HCS开票登记发票类型映射缺失:invoiceType=[{}] 不在 invoice.invoice-type-mapping,fdInvoiceType 不传(§9-U3)", + application.getInvoiceType()); + } + // 【必填】结算币种:currency-id-mapping(TMS 币种码 → HCS 币种fdId,经 C103-12 缓存取值) + param.setFdCurrencyId(requireCurrencyId(application)); + // 开票方(销方):issuerName + param.setFdInvoiceParty(application.getIssuerName()); + // 款项性质:配置 invoice.fund-nature-default(默认 progress) + param.setFdFundNature(hcsProperties.getInvoice().getFundNatureDefault()); + // 开票金额(含税):invoiceAmount + param.setFdInvoiceAmount(application.getInvoiceAmount()); + // 备注:remark + param.setFdRemark(application.getRemark()); + // 状态:新增固定 0(已开票) + param.setFdStatus(0); + // 第三方创建人编码:申请人工号(User.code) + param.setFdThirdCreatorCode(resolveCreatorCode(application)); + // 第三方创建部门编码:申请人部门编码 + param.setFdThirdCreatorDeptCode(resolveDeptCode(application)); + // 附件ID列表:attachmentsJson 经 C103-2 转传 + if (hcsProperties.getAttachment().isTransferEnabled() && StringUtil.isNotBlank(application.getAttachmentsJson())) { + param.setFdAttIds(hcsAttachmentSupport.transferAttachments(application.getAttachmentsJson(), resolveCreatorCode(application))); + } + log.info("HCS开票登记新增开始:applicationNo={}", application.getApplicationNo()); + HcsResult result = hcsClient.invoiceRegistrationAdd(param); + if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdId())) { + log.error("HCS开票登记新增失败响应:{},applicationNo={}", result == null ? "无响应" : result.getMsg(), application.getApplicationNo()); + throw new HcsException("C111-2 开票登记新增失败:" + (result == null ? "响应为空" : result.getMsg())); + } + return result.getData().getFdId(); + } + + /** + * C111-3 更新开票登记:回填票据日期与状态(金蝶开票完成后) + */ + private String updateInvoiceRegistration(InvoiceApplication application) { + HcsInvoiceRegUpdateDTO param = new HcsInvoiceRegUpdateDTO(); + // 【必填】开票登记ID(C111-2 新增返回) + param.setFdId(application.getHcsFdId()); + // 票据日期:金蝶回写发票的最新开票日期(无发票记录取回写时间,待联调确认口径) + param.setFdBillDate(latestInvoiceDate(application)); + // 状态:0 已开票 + param.setFdStatus(0); + log.info("HCS开票登记更新开始:applicationNo={},fdId={}", application.getApplicationNo(), application.getHcsFdId()); + HcsResult result = hcsClient.invoiceRegistrationUpdate(param); + if (result == null || !result.isSuccess()) { + log.error("HCS开票登记更新失败响应:{},applicationNo={}", result == null ? "无响应" : result.getMsg(), application.getApplicationNo()); + throw new HcsException("C111-3 开票登记更新失败:" + (result == null ? "响应为空" : result.getMsg())); + } + return "C111-3 开票登记更新成功"; + } + + /** + * C111-1 开票登记列表判重(conditions={"fdNumber": applicationNo},§9-U4 查询能力联调确认) + */ + private String findExistFdId(InvoiceApplication application) { + HcsPageQueryDTO query = new HcsPageQueryDTO(); + query.setColumns(List.of("fdId", "fdNumber")); + // 判重条件:单据编号精确匹配 + query.setConditions(Map.of("fdNumber", Objects.toString(application.getApplicationNo(), ""))); + query.setOffset(0); + query.setPageSize(10); + HcsResult> result = hcsClient.invoiceRegistrationList(query); + if (result == null || !result.isSuccess()) { + log.error("HCS开票登记判重查询失败响应:{},applicationNo={}", result == null ? "无响应" : result.getMsg(), application.getApplicationNo()); + throw new HcsException("C111-1 开票登记判重查询失败:" + (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(); + } + + /** + * 应收结算链路回溯合同编号:开票申请 → 应收结算关联 → 正式结算单合同编号(§9-U10) + */ + private String requireContractNumber(InvoiceApplication application) { + InvoiceApplicationSettlement relation = invoiceApplicationSettlementMapper.selectOne(Wrappers.lambdaQuery() + .eq(InvoiceApplicationSettlement::getInvoiceApplicationId, application.getId()) + .last("limit 1")); + if (relation != null && relation.getFormalSettlementId() != null) { + FormalSettlement settlement = formalSettlementMapper.selectById(relation.getFormalSettlementId()); + if (settlement != null && StringUtil.isNotBlank(settlement.getContractNo())) { + return settlement.getContractNo(); + } + } + throw new HcsException("C111 开票登记推送失败,应收结算链路回溯不到合同编号(applicationNo=" + application.getApplicationNo() + + ",§9-U10 待确认关联字段)"); + } + + /** + * 结算币种ID(currency-id-mapping,必填;TMS 无币种字段按 RMB 取映射) + */ + private String requireCurrencyId(InvoiceApplication application) { + String currencyFdId = hcsProperties.getInvoice().getCurrencyIdMapping().get("RMB"); + if (StringUtil.isBlank(currencyFdId)) { + throw new HcsException("C111 开票登记推送失败,币种ID 映射未配置(invoice.currency-id-mapping RMB,§9-U3)"); + } + return currencyFdId; + } + + /** + * 金蝶回写发票的最新开票日期(毫秒),无发票记录取金蝶回写时间 + */ + private Long latestInvoiceDate(InvoiceApplication application) { + InvoiceApplicationInvoice invoice = invoiceApplicationInvoiceMapper.selectOne(Wrappers.lambdaQuery() + .eq(InvoiceApplicationInvoice::getInvoiceApplicationId, application.getId()) + .orderByDesc(InvoiceApplicationInvoice::getInvoiceDate) + .last("limit 1")); + LocalDateTime invoiceDate = invoice != null && invoice.getInvoiceDate() != null + ? invoice.getInvoiceDate() : application.getSyncedTime(); + return toMillis(invoiceDate); + } + + /** + * 回填整体同步状态置 synced + */ + public void markSynced(InvoiceApplication application, String fdId, String message) { + application.setHcsFdId(fdId); + application.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_SYNCED); + application.setHcsRetryCount(0); + application.setHcsSyncTime(LocalDateTime.now()); + application.setHcsSyncMsg(message); + invoiceApplicationMapper.updateById(application); + log.info("HCS开票登记推送成功:applicationNo={},fdId={},{}", application.getApplicationNo(), fdId, message); + } + + /** + * 推送失败回写:failed + 重试次数+1 + 失败原因 + */ + public void markFailed(InvoiceApplication application, String message) { + application.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_FAILED); + application.setHcsRetryCount(application.getHcsRetryCount() == null ? 1 : application.getHcsRetryCount() + 1); + application.setHcsSyncTime(LocalDateTime.now()); + application.setHcsSyncMsg(message); + invoiceApplicationMapper.updateById(application); + log.error("HCS开票登记推送失败:applicationNo={},retryCount={},{}", application.getApplicationNo(), application.getHcsRetryCount(), message); + } + + /** + * 申请人工号(User.code,fdThirdCreatorCode) + */ + private String resolveCreatorCode(InvoiceApplication application) { + User user = application.getCreateUser() == null ? null : UserCache.getUser(application.getCreateUser()); + return user == null ? null : user.getCode(); + } + + /** + * 申请人部门编码(fdThirdCreatorDeptCode) + */ + private String resolveDeptCode(InvoiceApplication application) { + User user = application.getCreateUser() == null ? null : UserCache.getUser(application.getCreateUser()); + if (user == null) { + return null; + } + // 用户冗余部门编码优先(可能逗号分隔,取第一个) + if (StringUtil.isNotBlank(user.getDeptCodes())) { + return user.getDeptCodes().split(",")[0].trim(); + } + Long deptId = Func.toLong(user.getDeptId() == null ? null : user.getDeptId().split(",")[0].trim()); + Dept dept = deptId == null ? null : SysCache.getDept(deptId); + return dept == null ? null : dept.getDeptCode(); + } + + /** + * LocalDateTime/LocalDate → 毫秒时间戳 + */ + private Long toMillis(LocalDateTime localDateTime) { + return localDateTime == null ? null : localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } + + /** + * LocalDate → 毫秒时间戳 + */ + private Long toMillis(java.time.LocalDate localDate) { + return localDate == null ? null : localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } +} diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegUpdateDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegUpdateDTO.java index 44465dfb..0795fe05 100644 --- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegUpdateDTO.java +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsInvoiceRegUpdateDTO.java @@ -50,4 +50,8 @@ public class HcsInvoiceRegUpdateDTO extends HcsInvoiceRegAddDTO { * 开票登记ID(必填,C111-2 新增返回,持久化于 hcs_fd_id) */ private String fdId; + /** + * 票据日期(毫秒时间戳,金蝶开票完成后回填) + */ + private Long fdBillDate; }