feat: HCS开票登记同步支撑(C111-1判重/C111-2新增/C111-3票据日期回填/应收结算链路合同回溯)

This commit is contained in:
刘泉佳
2026-09-27 23:57:38 +08:00
parent 165c22c2d7
commit e3b65697e9
2 changed files with 338 additions and 0 deletions
@@ -0,0 +1,334 @@
/**
* 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.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)
* <p>
* 审批通过即推 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<HcsIdVO> 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<HcsIdVO> 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<HcsPageVO<HcsInvoiceRegVO>> 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<HcsInvoiceRegVO> 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.<InvoiceApplicationSettlement>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.<InvoiceApplicationInvoice>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();
}
}
@@ -50,4 +50,8 @@ public class HcsInvoiceRegUpdateDTO extends HcsInvoiceRegAddDTO {
* 开票登记ID(必填,C111-2 新增返回,持久化于 hcs_fd_id)
*/
private String fdId;
/**
* 票据日期(毫秒时间戳,金蝶开票完成后回填)
*/
private Long fdBillDate;
}