✨ A303-5开票结果回写支持Base64解码响应并补充关键日志

This commit is contained in:
刘泉佳
2026-09-22 14:07:25 +08:00
parent 430fc6b5a7
commit 1268561fd9
7 changed files with 454 additions and 28 deletions
@@ -44,6 +44,8 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceApplyQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceApplySaveDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceApplySaveEntryDTO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoiceApplyOpenApiBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoiceApplyOpenApiInvoiceVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoiceApplyVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiOpenApiResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
@@ -738,6 +740,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
if (Func.isEmpty(applications)) {
return "金蝶开票结果回写:无待回写开票申请";
}
log.info("金蝶开票结果回写开始:待回写开票申请{}笔", applications.size());
// 按 serialNo(金蝶开票申请单编号)分批(≤50/批)调 A303-5,销方税号与 A303-1 同源(开票单位→客商档案),按税号分组查询
Map<String, InvoiceApplication> applicationBySerialNo = new LinkedHashMap<>();
applications.forEach(application -> {
@@ -766,6 +769,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
}
serialNosBySellerTaxNo.computeIfAbsent(sellerTaxNo, key -> new ArrayList<>()).add(entry.getKey());
}
log.info("金蝶开票结果回写:按销方税号分组完成,共{}组,销方税号无法解析跳过{}笔", serialNosBySellerTaxNo.size(), unresolved);
int invoiceWritten = 0;
int failed = 0;
int invoicedCount = 0;
@@ -773,34 +777,28 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
List<String> groupSerialNos = group.getValue();
for (int from = 0; from < groupSerialNos.size(); from += INVOICE_RESULT_QUERY_BATCH) {
List<String> batch = groupSerialNos.subList(from, Math.min(from + INVOICE_RESULT_QUERY_BATCH, groupSerialNos.size()));
String batchSerialNos = String.join(",", batch);
log.info("金蝶开票结果回写批次查询:销方税号=[{}],单据编号=[{}],数量={}", group.getKey(), batchSerialNos, batch.size());
try {
BfiInvoiceApplyOpenApiQueryDTO param = new BfiInvoiceApplyOpenApiQueryDTO();
param.setSerialNo(String.join(",", batch));
param.setSerialNo(batchSerialNos);
param.setSellerTaxpayerId(group.getKey());
BfiOpenApiResultVO<List<Object>> result = bfiService.queryInvoiceApplyInvoice(param);
BfiOpenApiResultVO<List<BfiInvoiceApplyOpenApiBillVO>> result = bfiService.queryInvoiceApplyInvoice(param);
if (result == null || !result.isSuccess() || result.getData() == null) {
failed++;
log.warn("金蝶开票结果回写批次查询未成功:serialNo=[{}]", String.join(",", batch));
log.warn("金蝶开票结果回写批次查询未成功:serialNo=[{}]", batchSerialNos);
continue;
}
for (Object billItem : result.getData()) {
if (!(billItem instanceof Map)) {
continue;
}
Map<?, ?> billMap = (Map<?, ?>) billItem;
String billNo = firstText(billMap, "billNo", "billno");
log.info("金蝶开票结果回写批次查询成功:serialNo=[{}],返回单据数={}", batchSerialNos, result.getData().size());
for (BfiInvoiceApplyOpenApiBillVO bill : result.getData()) {
String billNo = bill.getBillNo();
InvoiceApplication application = applicationBySerialNo.get(billNo);
if (application == null || !(billMap.get("invoiceList") instanceof List)) {
if (application == null || bill.getInvoiceList() == null) {
continue;
}
int inserted = 0;
for (Object invoiceItem : (List<?>) billMap.get("invoiceList")) {
if (!(invoiceItem instanceof Map)) {
continue;
}
Map<?, ?> invoice = (Map<?, ?>) invoiceItem;
// 宽松提取关键字段(票易通原文,字段名联调校准后收敛),原文整体 JSON 留档
String invoiceNo = firstText(invoice, "invoiceNo", "invoice_no", "invoiceNumber");
for (BfiInvoiceApplyOpenApiInvoiceVO invoice : bill.getInvoiceList()) {
String invoiceNo = invoice.getInvoiceNumber();
if (Func.isBlank(invoiceNo)) {
continue;
}
@@ -809,17 +807,20 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
.eq(InvoiceApplicationInvoice::getInvoiceApplicationId, application.getId())
.eq(InvoiceApplicationInvoice::getInvoiceNo, invoiceNo));
if (exists != null && exists > 0) {
log.debug("金蝶开票结果回写:发票号码[{}]已存在,跳过", invoiceNo);
continue;
}
InvoiceApplicationInvoice record = new InvoiceApplicationInvoice();
record.setInvoiceApplicationId(application.getId());
record.setSerialNo(billNo);
record.setInvoiceNo(invoiceNo);
record.setInvoiceDate(parseInvoiceDate(firstText(invoice, "invoiceDate", "invoice_date", "issueDate", "billingDate")));
record.setInvoiceAmount(parseDecimal(firstText(invoice, "amountWithTax", "totalAmount", "invoiceAmount", "priceTaxTotal")));
record.setInvoiceDate(parseInvoiceDateTimestamp(invoice.getInvoiceDate()));
record.setInvoiceAmount(invoice.getIncludeTaxAmount());
record.setRawJson(JsonUtil.toJson(invoice));
invoiceApplicationInvoiceMapper.insert(record);
inserted++;
log.info("金蝶开票结果回写:写入发票,申请单号=[{}],单据编号=[{}],发票号码=[{}],含税金额=[{}]",
application.getApplicationNo(), billNo, invoiceNo, invoice.getIncludeTaxAmount());
}
if (inserted > 0) {
invoiceWritten += inserted;
@@ -827,14 +828,17 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
application.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_INVOICED);
updateById(application);
invoicedCount++;
log.info("金蝶开票结果回写:开票申请[{}]状态推进为invoiced,本次写入发票{}张", application.getApplicationNo(), inserted);
}
}
} catch (Exception exception) {
failed++;
log.error("金蝶开票结果回写批次异常:serialNo=[{}]", String.join(",", batch), exception);
log.error("金蝶开票结果回写批次异常:serialNo=[{}]", batchSerialNos, exception);
}
}
}
log.info("金蝶开票结果回写完成:待查开票申请{}笔(销方税号无法解析跳过{}笔),回写发票{}张(开票完成{}笔,失败批次{}个)",
applications.size(), unresolved, invoiceWritten, invoicedCount, failed);
return "金蝶开票结果回写完成:待查开票申请" + applications.size() + "笔(销方税号无法解析跳过" + unresolved
+ "笔),回写发票" + invoiceWritten + "张(开票完成" + invoicedCount + "笔,失败批次" + failed + "个)";
}
@@ -877,6 +881,19 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
return null;
}
/** 解析时间戳格式的开票日期(毫秒时间戳),无法解析返回 null */
private LocalDateTime parseInvoiceDateTimestamp(Long timestamp) {
if (timestamp == null) {
return null;
}
try {
return LocalDateTime.ofInstant(java.time.Instant.ofEpochMilli(timestamp), java.time.ZoneId.systemDefault());
} catch (Exception exception) {
log.warn("金蝶开票结果开票日期时间戳解析失败:[{}]", timestamp);
return null;
}
}
/** 宽松解析金额文本,无法解析返回 null */
private BigDecimal parseDecimal(String text) {
if (Func.isBlank(text)) {
@@ -361,10 +361,10 @@ public interface IBfiClient {
*
* @param accessToken 访问令牌,通过 getToken 获取
* @param param 请求参数,data 为原文 JSON 的 Base64 编码
* @return 发票查询结果
* @return 发票查询结果,data 为 Base64 编码字符串
*/
@PostMapping(INVOICE_APPLY_OPEN_API_URL)
BfiOpenApiResultVO<List<Object>> invoiceApplyOpenApi(@RequestHeader(BfiConstant.HEADER_ACCESS_TOKEN) String accessToken, @RequestBody BfiInvoiceApplyOpenApiBodyDTO param);
BfiOpenApiResultVO<String> invoiceApplyOpenApi(@RequestHeader(BfiConstant.HEADER_ACCESS_TOKEN) String accessToken, @RequestBody BfiInvoiceApplyOpenApiBodyDTO param);
/**
* A303-2 收票单新增(批量保存)
@@ -0,0 +1,51 @@
/**
* 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.thirdparty.bfi.pojo.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 业财融合(BFI)A303-5 开票申请发票查询返回单据信息
*
* @author Chill
*/
@Data
public class BfiInvoiceApplyOpenApiBillVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 单据编号
*/
private String billNo;
/**
* 发票列表
*/
private List<BfiInvoiceApplyOpenApiInvoiceVO> invoiceList;
}
@@ -0,0 +1,341 @@
/**
* 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.thirdparty.bfi.pojo.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* 业财融合(BFI)A303-5 开票申请发票查询返回发票明细
*
* @author Chill
*/
@Data
public class BfiInvoiceApplyOpenApiInvoiceVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 发票ID
*/
private Long id;
/**
* 发票代码
*/
private String invoiceCode;
/**
* 发票号码
*/
private String invoiceNumber;
/**
* 开票日期(时间戳)
*/
private Long invoiceDate;
/**
* 发票类型
*/
private String invoiceType;
/**
* 发票状态
*/
private Integer invoiceStatus;
/**
* 发票属性
*/
private Integer invoiceProperty;
/**
* 发票来源
*/
private String invoiceSource;
/**
* 单据编号
*/
private String billNo;
/**
* 流水号
*/
private String serialNo;
/**
* 购方名称
*/
private String buyerName;
/**
* 购方纳税人识别号
*/
private String buyerTaxpayerId;
/**
* 购方地址及电话
*/
private String buyerAddressAndTel;
/**
* 购方银行及账号
*/
private String buyerBankAndAccount;
/**
* 购方性质
*/
private Integer buyerProperty;
/**
* 购方收件人邮箱
*/
private String buyerRecipientMail;
/**
* 购方收件人电话
*/
private String buyerRecipientPhone;
/**
* 销方名称
*/
private String sellerName;
/**
* 销方纳税人识别号
*/
private String sellerTaxpayerId;
/**
* 销方地址及电话
*/
private String sellerAddressAndTel;
/**
* 销方银行及账号
*/
private String sellerBankAndAccount;
/**
* 不含税金额
*/
private BigDecimal totalAmount;
/**
* 税额
*/
private BigDecimal totalTaxAmount;
/**
* 价税合计
*/
private BigDecimal includeTaxAmount;
/**
* 可红冲金额
*/
private BigDecimal canRedAmount;
/**
* 可红冲税额
*/
private BigDecimal canRedTaxAmount;
/**
* 合并差额税额
*/
private BigDecimal mergeDifferTaxAmount;
/**
* 开票人
*/
private String drawer;
/**
* 收款人
*/
private String payee;
/**
* 复核人
*/
private String reviewer;
/**
* 作废人
*/
private String canceler;
/**
* 备注
*/
private String remark;
/**
* 校验码
*/
private String checkCode;
/**
* 合同编号
*/
private String contractNo;
/**
* 设备编号
*/
private String deviceNo;
/**
* 终端编号
*/
private String terminalNo;
/**
* 机构名称
*/
private String orgName;
/**
* 原发票代码
*/
private String originalInvoiceCode;
/**
* 原发票号码
*/
private String originalInvoiceNumber;
/**
* 红冲单据号
*/
private String redInfoBillNo;
/**
* 红冲原因
*/
private String redReason;
/**
* 作废原因
*/
private String abolishReason;
/**
* 发票文件URL
*/
private String invoiceFileUrl;
/**
* 发票图片URL
*/
private String invoiceImageUrl;
/**
* 开具状态
*/
private String issueStatus;
/**
* 下载标记
*/
private Integer downloadFlag;
/**
* 打印标记
*/
private Integer printFlag;
/**
* 库存标记
*/
private Integer inventoryFlag;
/**
* 特殊红冲标记
*/
private Integer specialRedFlag;
/**
* 拆分合并标记
*/
private String splitOrMergeFlag;
/**
* 商品明细列表
*/
private List<BfiInvoiceApplyOpenApiGoodsVO> goods;
/**
* 业财融合(BFI)A303-5 开票申请发票查询返回商品明细
*/
@Data
public static class BfiInvoiceApplyOpenApiGoodsVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 明细ID
*/
private String detailId;
/**
* 明细行号
*/
private Integer detailRowNo;
/**
* 序号
*/
private Integer seq;
/**
* 商品名称
*/
private String goodsName;
/**
* 规格型号
*/
private String specification;
/**
* 单位
*/
private String units;
/**
* 数量
*/
private String quantity;
/**
* 单价
*/
private String price;
/**
* 金额
*/
private BigDecimal amount;
/**
* 税率
*/
private String taxRate;
/**
* 税额
*/
private BigDecimal taxAmount;
/**
* 含税金额
*/
private BigDecimal includeTaxAmount;
/**
* 含税单价
*/
private String includeTaxPrice;
/**
* 行性质
*/
private Integer lineProperty;
/**
* 税收分类编码
*/
private String revenueCode;
/**
* 税收分类名称
*/
private String revenueName;
/**
* 单据来源ID
*/
private String billSourceId;
/**
* 优惠标记
*/
private Integer privilegeFlag;
/**
* 优惠内容
*/
private String privilegeContent;
/**
* 征税标记
*/
private String taxFlag;
/**
* 零税率标记
*/
private String zeroTaxRateFlag;
}
}
@@ -53,6 +53,7 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiClaimAnnounceVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiCustomerVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiExchangeRateVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoiceApplyOpenApiBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoiceApplyVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoicePoolVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiOpenApiResultVO;
@@ -231,9 +232,9 @@ public interface IBfiService {
* A303-5 开票申请发票查询
*
* @param param 查询原文参数,单据编号多个用英文逗号隔开,上限50个
* @return 发票查询结果,data 内容参考票易通文档
* @return 发票查询结果,data 为单据及发票明细列表
*/
BfiOpenApiResultVO<List<Object>> queryInvoiceApplyInvoice(BfiInvoiceApplyOpenApiQueryDTO param);
BfiOpenApiResultVO<List<BfiInvoiceApplyOpenApiBillVO>> queryInvoiceApplyInvoice(BfiInvoiceApplyOpenApiQueryDTO param);
/**
* A303-2 收票单新增(批量保存)
@@ -25,6 +25,7 @@
*/
package org.springblade.thirdparty.bfi.service.impl;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import feign.Response;
import lombok.RequiredArgsConstructor;
@@ -76,6 +77,7 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiClaimAnnounceVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiCustomerVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiExchangeRateVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoiceApplyOpenApiBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoiceApplyVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoicePoolVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiOpenApiResultVO;
@@ -398,7 +400,7 @@ public class BfiServiceImpl implements IBfiService {
}
@Override
public BfiOpenApiResultVO<List<Object>> queryInvoiceApplyInvoice(BfiInvoiceApplyOpenApiQueryDTO param) {
public BfiOpenApiResultVO<List<BfiInvoiceApplyOpenApiBillVO>> queryInvoiceApplyInvoice(BfiInvoiceApplyOpenApiQueryDTO param) {
if (param == null || StringUtil.isBlank(param.getSerialNo()) || StringUtil.isBlank(param.getSellerTaxpayerId())) {
throw new BfiException("查询BFI开票申请发票失败,单据编号与销方税号不能为空");
}
@@ -409,7 +411,18 @@ public class BfiServiceImpl implements IBfiService {
body.setInterfaceCode(BfiConstant.INVOICE_APPLY_OPEN_API_CODE);
body.setData(data);
String accessToken = getAccessToken();
BfiOpenApiResultVO<List<Object>> result = commonOpenApiCall("开票申请发票查询", () -> client.invoiceApplyOpenApi(accessToken, body));
BfiOpenApiResultVO<String> rawResult = commonOpenApiCall("开票申请发票查询", () -> client.invoiceApplyOpenApi(accessToken, body));
// 响应 data 为 Base64 编码字符串,需解码后解析为 JSON
BfiOpenApiResultVO<List<BfiInvoiceApplyOpenApiBillVO>> result = new BfiOpenApiResultVO<>();
result.setErrorCode(rawResult.getErrorCode());
result.setSuccess(rawResult.getSuccess());
if (rawResult.isSuccess() && StringUtil.isNotBlank(rawResult.getData())) {
String decodedJson = new String(Base64.getDecoder().decode(rawResult.getData()), StandardCharsets.UTF_8);
JSONObject decodedObj = JSONUtil.parseObj(decodedJson);
result.setErrorCode(decodedObj.getStr("errorCode"));
result.setSuccess(decodedObj.getBool("success"));
result.setData(decodedObj.getBeanList("data", BfiInvoiceApplyOpenApiBillVO.class));
}
return result;
}
@@ -385,10 +385,13 @@ class BfiServiceImplTest {
@Test
void queryInvoiceApplyInvoiceShouldEncodeBase64Data() {
mockAccessToken();
BfiOpenApiResultVO<List<Object>> openApiResult = new BfiOpenApiResultVO<>();
// 模拟接口返回 base64 编码的 data
String responseData = "{\"data\":[{\"billNo\":\"KPSQ-1\",\"invoiceList\":[]}],\"errorCode\":\"0\",\"success\":true}";
String encodedData = Base64.getEncoder().encodeToString(responseData.getBytes(StandardCharsets.UTF_8));
BfiOpenApiResultVO<String> openApiResult = new BfiOpenApiResultVO<>();
openApiResult.setSuccess(true);
openApiResult.setErrorCode("S0000");
openApiResult.setData(List.of());
openApiResult.setData(encodedData);
when(client.invoiceApplyOpenApi(anyString(), any())).thenReturn(openApiResult);
BfiInvoiceApplyOpenApiQueryDTO param = new BfiInvoiceApplyOpenApiQueryDTO();
param.setSerialNo("KPSQ-1,KPSQ-2");