feat: HCS开票登记同步支撑(C111-1判重/C111-2新增/C111-3票据日期回填/应收结算链路合同回溯)
This commit is contained in:
+334
@@ -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();
|
||||
}
|
||||
}
|
||||
+4
@@ -50,4 +50,8 @@ public class HcsInvoiceRegUpdateDTO extends HcsInvoiceRegAddDTO {
|
||||
* 开票登记ID(必填,C111-2 新增返回,持久化于 hcs_fd_id)
|
||||
*/
|
||||
private String fdId;
|
||||
/**
|
||||
* 票据日期(毫秒时间戳,金蝶开票完成后回填)
|
||||
*/
|
||||
private Long fdBillDate;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user