feat: 开票结果回写落库发票文件地址并支持正式结算单详情查看下载
- A303-5 VO 补数电 PDF/XML 直链字段,回写落库四个发票文件地址(板式文件/预览图/数电PDF/数电XML) - 已落库但缺地址的历史记录由回写任务字段级补空,不覆盖已有值 - 正式结算单详情新增 issuedInvoices:经结算单-开票申请关联查出回写发票,只读不参与匹配金额 - 新增建表补丁 blade_invoice_application_invoice_urls_20260930.sql - 同步更新金蝶附件上传下载链路梳理文档(新增 3.4 销项直链节)
This commit is contained in:
+13
@@ -33,6 +33,7 @@ import java.time.LocalDateTime;
|
||||
* 开票申请金蝶发票结果实体类
|
||||
* <p>
|
||||
* 定时任务经 A303-5 开票申请发票查询回写的发票明细(发票号唯一键幂等落库),
|
||||
* 发票文件地址(板式文件/数电PDF/XML/预览图)独立落库供详情页查看下载,
|
||||
* 原文 JSON 整体留档便于联调与追溯
|
||||
*
|
||||
* @author Chill
|
||||
@@ -61,6 +62,18 @@ public class InvoiceApplicationInvoice extends TenantEntity {
|
||||
@Schema(description = "价税合计")
|
||||
private BigDecimal invoiceAmount;
|
||||
|
||||
@Schema(description = "板式文件下载地址(数电票为OFD,航信百望税盘普通电票为PDF,电子专票/UKey电票为OFD)")
|
||||
private String invoiceFileUrl;
|
||||
|
||||
@Schema(description = "PDF图片预览地址")
|
||||
private String invoiceImageUrl;
|
||||
|
||||
@Schema(description = "数电PDF下载地址(发票云5.0.025版本返回)")
|
||||
private String invoicePdfFileUrl;
|
||||
|
||||
@Schema(description = "数电XML下载地址(发票云5.0.025版本返回)")
|
||||
private String invoiceXmlFileUrl;
|
||||
|
||||
@Schema(description = "A303-5 返回原文JSON留档")
|
||||
private String rawJson;
|
||||
|
||||
|
||||
+4
@@ -9,6 +9,7 @@
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlement;
|
||||
@@ -18,6 +19,7 @@ import org.springblade.transport.pojo.entity.FormalSettlementInvoice;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementSource;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementPayment;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serial;
|
||||
@@ -49,6 +51,8 @@ public class FormalSettlementVO extends FormalSettlement {
|
||||
@TableField(exist = false) private List<FormalSettlementSummaryFee> summaryFees;
|
||||
@TableField(exist = false) private List<FormalSettlementPayment> payments;
|
||||
@TableField(exist = false) private List<FormalSettlementInvoice> invoices;
|
||||
@TableField(exist = false) @Schema(description = "开票申请经金蝶回写的发票结果(含文件地址,只读展示)")
|
||||
private List<InvoiceApplicationInvoice> issuedInvoices;
|
||||
@TableField(exist = false) private List<PaymentApplicationVO> paymentApplications;
|
||||
@TableField(exist = false) private List<SettlementAdjustmentVO> adjustments;
|
||||
@TableField(exist = false) private List<FormalSettlementChangeRecord> changeRecords;
|
||||
|
||||
+35
@@ -28,6 +28,9 @@ import org.springblade.transport.mapper.FormalSettlementSummaryFeeMapper;
|
||||
import org.springblade.transport.mapper.FormalSettlementPaymentMapper;
|
||||
import org.springblade.transport.mapper.InvoiceReceiptMapper;
|
||||
import org.springblade.transport.mapper.InvoiceReceiptSettlementMapper;
|
||||
import org.springblade.transport.mapper.InvoiceApplicationInvoiceMapper;
|
||||
import org.springblade.transport.mapper.InvoiceApplicationMapper;
|
||||
import org.springblade.transport.mapper.InvoiceApplicationSettlementMapper;
|
||||
import org.springblade.transport.mapper.PreSettlementDetailMapper;
|
||||
import org.springblade.transport.mapper.PreSettlementDetailFeeMapper;
|
||||
import org.springblade.transport.mapper.PreSettlementMapper;
|
||||
@@ -58,6 +61,9 @@ import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementPayment;
|
||||
import org.springblade.transport.pojo.entity.InvoiceReceipt;
|
||||
import org.springblade.transport.pojo.entity.InvoiceReceiptSettlement;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplication;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.PreSettlement;
|
||||
import org.springblade.transport.pojo.entity.PreSettlementDetail;
|
||||
@@ -129,6 +135,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
private final FormalSettlementInvoiceMapper invoiceMapper;
|
||||
private final InvoiceReceiptMapper invoiceReceiptMapper;
|
||||
private final InvoiceReceiptSettlementMapper invoiceReceiptSettlementMapper;
|
||||
private final InvoiceApplicationMapper invoiceApplicationMapper;
|
||||
private final InvoiceApplicationInvoiceMapper invoiceApplicationInvoiceMapper;
|
||||
private final InvoiceApplicationSettlementMapper invoiceApplicationSettlementMapper;
|
||||
private final FormalSettlementChangeRecordMapper changeRecordMapper;
|
||||
private final FormalSettlementDetailMapper detailMapper;
|
||||
private final FormalSettlementDetailFeeMapper detailFeeMapper;
|
||||
@@ -269,6 +278,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
vo.setPayments(paymentMapper.selectList(Wrappers.<FormalSettlementPayment>lambdaQuery()
|
||||
.eq(FormalSettlementPayment::getFormalSettlementId, id).orderByDesc(FormalSettlementPayment::getCreateTime)));
|
||||
vo.setInvoices(listInvoices(settlement));
|
||||
vo.setIssuedInvoices(listIssuedInvoices(id));
|
||||
Set<Long> relationApplicationIds = paymentApplicationSettlementMapper.selectList(
|
||||
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
.select(PaymentApplicationSettlement::getPaymentApplicationId)
|
||||
@@ -386,6 +396,31 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
return normalizedInvoiceNo.isEmpty() ? fallback : normalizedInvoiceNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开票申请经金蝶回写的发票结果(A303-5 落库,含板式文件/数电PDF等文件地址):
|
||||
* 通过结算单与开票申请的关联关系查出,仅保留有效(非作废未删除)开票申请的发票,
|
||||
* 只读展示于详情页,不落 blade_formal_settlement_invoice、不参与匹配金额计算
|
||||
*/
|
||||
private List<InvoiceApplicationInvoice> listIssuedInvoices(Long settlementId) {
|
||||
List<Long> applicationIds = invoiceApplicationSettlementMapper.selectList(
|
||||
Wrappers.<InvoiceApplicationSettlement>lambdaQuery()
|
||||
.select(InvoiceApplicationSettlement::getInvoiceApplicationId)
|
||||
.eq(InvoiceApplicationSettlement::getFormalSettlementId, settlementId)
|
||||
).stream().map(InvoiceApplicationSettlement::getInvoiceApplicationId)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
if (applicationIds.isEmpty()) return List.of();
|
||||
Set<Long> activeApplicationIds = invoiceApplicationMapper.selectBatchIds(applicationIds).stream()
|
||||
.filter(application -> !TransportBusiConstant.VOIDED.equals(application.getApprovalStatus())
|
||||
&& !Objects.equals(application.getIsDeleted(), 1))
|
||||
.map(InvoiceApplication::getId).collect(Collectors.toSet());
|
||||
if (activeApplicationIds.isEmpty()) return List.of();
|
||||
return invoiceApplicationInvoiceMapper.selectList(
|
||||
Wrappers.<InvoiceApplicationInvoice>lambdaQuery()
|
||||
.in(InvoiceApplicationInvoice::getInvoiceApplicationId, activeApplicationIds)
|
||||
.orderByAsc(InvoiceApplicationInvoice::getInvoiceApplicationId)
|
||||
.orderByAsc(InvoiceApplicationInvoice::getCreateTime));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long saveDraft(FormalSettlementSaveRequest request) {
|
||||
|
||||
+69
-29
@@ -941,11 +941,12 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
continue;
|
||||
}
|
||||
log.info("金蝶开票结果回写批次查询成功:serialNo=[{}],返回单据数={}", batchSerialNos, result.getData().size());
|
||||
// 批量查询当前批次已存在的发票号码(避免 N+1)
|
||||
Set<String> existingInvoiceNos = batchQueryExistingInvoiceNos(batch);
|
||||
log.debug("金蝶开票结果回写:批次已存在发票号码数={}", existingInvoiceNos.size());
|
||||
// 收集待插入的发票记录和待更新的开票申请
|
||||
// 批量查询当前批次已落库的发票记录(按发票号索引,用于幂等去重与缺文件地址补齐)
|
||||
Map<String, InvoiceApplicationInvoice> existingInvoiceMap = batchQueryExistingInvoices(batch);
|
||||
log.debug("金蝶开票结果回写:批次已存在发票记录数={}", existingInvoiceMap.size());
|
||||
// 收集待插入的发票记录、待补文件地址的已有发票记录和待更新的开票申请
|
||||
List<InvoiceApplicationInvoice> toInsertInvoices = new ArrayList<>();
|
||||
List<InvoiceApplicationInvoice> toPatchInvoices = new ArrayList<>();
|
||||
List<InvoiceApplication> toUpdateApplications = new ArrayList<>();
|
||||
for (BfiInvoiceApplyOpenApiBillVO bill : result.getData()) {
|
||||
String billNo = bill.getBillNo();
|
||||
@@ -959,24 +960,32 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
if (Func.isBlank(invoiceNo)) {
|
||||
continue;
|
||||
}
|
||||
// 使用批量查询结果判断是否已存在
|
||||
if (existingInvoiceNos.contains(invoiceNo)) {
|
||||
log.debug("金蝶开票结果回写:发票号码[{}]已存在,跳过", invoiceNo);
|
||||
continue;
|
||||
InvoiceApplicationInvoice existing = existingInvoiceMap.get(invoiceNo);
|
||||
if (existing == null) {
|
||||
InvoiceApplicationInvoice record = new InvoiceApplicationInvoice();
|
||||
record.setInvoiceApplicationId(application.getId());
|
||||
record.setSerialNo(billNo);
|
||||
record.setInvoiceNo(invoiceNo);
|
||||
record.setInvoiceDate(parseInvoiceDateTimestamp(invoice.getInvoiceDate()));
|
||||
record.setInvoiceAmount(invoice.getIncludeTaxAmount());
|
||||
// 发票文件地址独立落库:板式文件/预览图/数电PDF/数电XML,供正式结算单详情查看下载
|
||||
record.setInvoiceFileUrl(invoice.getInvoiceFileUrl());
|
||||
record.setInvoiceImageUrl(invoice.getInvoiceImageUrl());
|
||||
record.setInvoicePdfFileUrl(invoice.getInvoicePdfFileUrl());
|
||||
record.setInvoiceXmlFileUrl(invoice.getInvoiceXmlFileUrl());
|
||||
record.setRawJson(JsonUtil.toJson(invoice));
|
||||
toInsertInvoices.add(record);
|
||||
inserted++;
|
||||
// 标记为已存在,避免同批次重复插入
|
||||
existingInvoiceMap.put(invoiceNo, record);
|
||||
log.info("金蝶开票结果回写:写入发票,申请单号=[{}],单据编号=[{}],发票号码=[{}],含税金额=[{}]",
|
||||
application.getApplicationNo(), billNo, invoiceNo, invoice.getIncludeTaxAmount());
|
||||
} else if (fillMissingInvoiceFileUrls(existing, invoice)) {
|
||||
// 历史已落库但缺文件地址的记录按本次返回补齐(字段级补空,不覆盖已有值)
|
||||
toPatchInvoices.add(existing);
|
||||
log.info("金蝶开票结果回写:补齐发票文件地址,申请单号=[{}],单据编号=[{}],发票号码=[{}]",
|
||||
application.getApplicationNo(), billNo, invoiceNo);
|
||||
}
|
||||
InvoiceApplicationInvoice record = new InvoiceApplicationInvoice();
|
||||
record.setInvoiceApplicationId(application.getId());
|
||||
record.setSerialNo(billNo);
|
||||
record.setInvoiceNo(invoiceNo);
|
||||
record.setInvoiceDate(parseInvoiceDateTimestamp(invoice.getInvoiceDate()));
|
||||
record.setInvoiceAmount(invoice.getIncludeTaxAmount());
|
||||
record.setRawJson(JsonUtil.toJson(invoice));
|
||||
toInsertInvoices.add(record);
|
||||
inserted++;
|
||||
// 标记为已存在,避免同批次重复插入
|
||||
existingInvoiceNos.add(invoiceNo);
|
||||
log.info("金蝶开票结果回写:写入发票,申请单号=[{}],单据编号=[{}],发票号码=[{}],含税金额=[{}]",
|
||||
application.getApplicationNo(), billNo, invoiceNo, invoice.getIncludeTaxAmount());
|
||||
}
|
||||
if (inserted > 0) {
|
||||
invoiceWritten += inserted;
|
||||
@@ -992,6 +1001,11 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
Db.saveBatch(toInsertInvoices);
|
||||
log.info("金蝶开票结果回写:批量插入发票记录{}条", toInsertInvoices.size());
|
||||
}
|
||||
// 批量补齐历史记录缺失的发票文件地址
|
||||
if (!toPatchInvoices.isEmpty()) {
|
||||
Db.updateBatchById(toPatchInvoices);
|
||||
log.info("金蝶开票结果回写:批量补齐发票文件地址{}条", toPatchInvoices.size());
|
||||
}
|
||||
// 批量更新开票申请状态
|
||||
if (!toUpdateApplications.isEmpty()) {
|
||||
updateBatchById(toUpdateApplications);
|
||||
@@ -1010,20 +1024,46 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询当前批次已存在的发票号码
|
||||
* 批量查询当前批次已落库的发票记录,按发票号索引
|
||||
*
|
||||
* @param serialNos 单据编号列表
|
||||
* @return 已存在的发票号码集合
|
||||
* @return 发票号 → 已落库发票记录
|
||||
*/
|
||||
private Set<String> batchQueryExistingInvoiceNos(List<String> serialNos) {
|
||||
private Map<String, InvoiceApplicationInvoice> batchQueryExistingInvoices(List<String> serialNos) {
|
||||
List<InvoiceApplicationInvoice> existingList = invoiceApplicationInvoiceMapper.selectList(
|
||||
Wrappers.<InvoiceApplicationInvoice>lambdaQuery()
|
||||
.in(InvoiceApplicationInvoice::getSerialNo, serialNos)
|
||||
.select(InvoiceApplicationInvoice::getInvoiceNo));
|
||||
.in(InvoiceApplicationInvoice::getSerialNo, serialNos));
|
||||
return existingList.stream()
|
||||
.map(InvoiceApplicationInvoice::getInvoiceNo)
|
||||
.filter(Func::isNotBlank)
|
||||
.collect(Collectors.toSet());
|
||||
.filter(record -> Func.isNotBlank(record.getInvoiceNo()))
|
||||
.collect(Collectors.toMap(InvoiceApplicationInvoice::getInvoiceNo, Function.identity(), (left, right) -> left));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 A303-5 返回的发票文件地址补齐到已落库记录(仅补空字段,不覆盖已有值)
|
||||
*
|
||||
* @param record 已落库发票记录
|
||||
* @param invoice A303-5 返回发票明细
|
||||
* @return 发生补齐时返回 true
|
||||
*/
|
||||
private boolean fillMissingInvoiceFileUrls(InvoiceApplicationInvoice record, BfiInvoiceApplyOpenApiInvoiceVO invoice) {
|
||||
boolean patched = false;
|
||||
if (Func.isBlank(record.getInvoiceFileUrl()) && Func.isNotBlank(invoice.getInvoiceFileUrl())) {
|
||||
record.setInvoiceFileUrl(invoice.getInvoiceFileUrl());
|
||||
patched = true;
|
||||
}
|
||||
if (Func.isBlank(record.getInvoiceImageUrl()) && Func.isNotBlank(invoice.getInvoiceImageUrl())) {
|
||||
record.setInvoiceImageUrl(invoice.getInvoiceImageUrl());
|
||||
patched = true;
|
||||
}
|
||||
if (Func.isBlank(record.getInvoicePdfFileUrl()) && Func.isNotBlank(invoice.getInvoicePdfFileUrl())) {
|
||||
record.setInvoicePdfFileUrl(invoice.getInvoicePdfFileUrl());
|
||||
patched = true;
|
||||
}
|
||||
if (Func.isBlank(record.getInvoiceXmlFileUrl()) && Func.isNotBlank(invoice.getInvoiceXmlFileUrl())) {
|
||||
record.setInvoiceXmlFileUrl(invoice.getInvoiceXmlFileUrl());
|
||||
patched = true;
|
||||
}
|
||||
return patched;
|
||||
}
|
||||
|
||||
/** 从票易通原文 Map 中按候选键顺序取首个非空文本值 */
|
||||
|
||||
+8
@@ -217,6 +217,14 @@ public class BfiInvoiceApplyOpenApiInvoiceVO implements Serializable {
|
||||
* 发票图片URL
|
||||
*/
|
||||
private String invoiceImageUrl;
|
||||
/**
|
||||
* 数电PDF下载地址(发票云5.0.025版本返回)
|
||||
*/
|
||||
private String invoicePdfFileUrl;
|
||||
/**
|
||||
* 数电XML下载地址(发票云5.0.025版本返回)
|
||||
*/
|
||||
private String invoiceXmlFileUrl;
|
||||
/**
|
||||
* 开具状态
|
||||
*/
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## 一、结论摘要
|
||||
|
||||
1. **文件级下载目前只有一种:票池发票文件(发票影像 PDF)**。判断正确——全代码库中唯一返回文件流的金蝶接口是 A801-2「文件下载」(`IBfiClient.invoiceDownload`),它的消费方只有两处,都围绕票池发票文件:① 附件同步链路内「下载回传」收票单;② 面向前端的票池发票文件下载端点(2026-09-27 新增,当前未提交)。
|
||||
1. **文件级下载目前只有一种:票池发票文件(发票影像 PDF)**。判断正确——全代码库中唯一返回文件流的金蝶接口是 A801-2「文件下载」(`IBfiClient.invoiceDownload`),它的消费方只有两处,都围绕票池发票文件:① 附件同步链路内「下载回传」收票单;② 面向前端的票池发票文件下载端点(2026-09-27 新增,当前未提交)。销项开票发票(A303-5 回写)不走文件流接口:A303-5 响应自带发票云文件直链 URL,落库后前端直连查看/下载(2026-09-30 补充,见 3.4)。
|
||||
2. 除文件外,系统还从金蝶「下载」**数据**(JSON 查询结果):票池发票元数据(A801-1)、收票单结果(A801-3)、付款申请/付款处理结果(A103-2/A103-4)、应付/应收单结果(A102-4/A302-4)、开票申请单与开票结果(A303-4/A303-5)、客商(A902-1)、汇率(A801-4)。这些用于幂等预检、星瀚内码反查、状态/结果定时回写。
|
||||
3. **上传与下载基本能对应上,但有三处缺口**:① 已上传金蝶的附件(`blade_kingdee_attachment_sync.kingdee_path`)没有回源下载通道(A801-2 是否兼容 A902 返回的 path 未验证,二期待金蝶确认);② 收款认领链路(A301-1/2/3)客户端已实现、业务零接线;③ 客商资质附件上传(A902)二期预留,entityNumber 未确认——客商附件目前既无上传也无下载。
|
||||
4. 每一个「推送/保存」类接口都有对应的「查询」类接口做幂等预检或结果回写,数据面上传下载成对;文件面只有「下载发票→上传回传」一条闭环。
|
||||
@@ -45,7 +45,7 @@
|
||||
| A103-2 | 付款申请单查询 | `queryPayApply` | PaymentApplicationServiceImpl:743 | 付款结果定时回写;单条状态回查 |
|
||||
| A103-4 | 付款处理查询 | `queryPayBill` | PaymentApplicationServiceImpl:833 | 付款处理结果勾稽回写 |
|
||||
| A303-4 | 开票申请单查询 | `queryInvoiceApply` | InvoiceApplicationServiceImpl:581 | 推送幂等预检;附件内码反查 |
|
||||
| A303-5 | 开票申请发票查询(OpenApi) | `queryInvoiceApplyInvoice` | InvoiceApplicationServiceImpl:857 | 开票结果回写(发票号落库) |
|
||||
| A303-5 | 开票申请发票查询(OpenApi) | `queryInvoiceApplyInvoice` | InvoiceApplicationServiceImpl.executeInvoiceResultSync | 开票结果回写(发票号 + 板式文件/数电PDF/XML文件地址落库,2026-09-30 扩展) |
|
||||
| A902-1 | 客户查询 | `queryCustomer` | CustomerArchiveServiceImpl:554、821;BfiCustomerController:70 | 推送前置幂等复查;定时同步复查;客商列表穿透查询 |
|
||||
| A801-4 | 汇率查询 | `queryExchangeRate` | BfiExchangeRateController:63(GET /bfi/exchange-rate) | 前端汇率展示 |
|
||||
|
||||
@@ -96,7 +96,16 @@
|
||||
|
||||
前端三处入口(票池弹窗附件列、正式结算认领发票行、付款申请发票附件行)经 `kingdee-attachment.js` 分流:`source === 'piaozone'` 或「有 path 且非 http(s) 开头」判为金蝶型走该端点;OSS 直链附件维持现有 `downloadFileByUrl` 行为不变。
|
||||
|
||||
### 3.4 数据下载(查询回写)的触发机制
|
||||
### 3.4 用途三:销项开票发票文件直链查看/下载(2026-09-30 新增)
|
||||
|
||||
开票申请链路的销项发票文件不走 A801-2:A303-5 开票结果查询响应自带发票云文件直链——`invoiceFileUrl`(板式文件:数电票 OFD、航信百望税盘普通电票 PDF、电子专票/UKey 电票 OFD)、`invoiceImageUrl`(PDF 图片预览地址)、`invoicePdfFileUrl`/`invoiceXmlFileUrl`(数电 PDF/XML,发票云 5.0.025+ 返回)。
|
||||
|
||||
- **落库**:`executeInvoiceResultSync` 回写时将四个地址独立落 `blade_invoice_application_invoice`(补丁 `blade_invoice_application_invoice_urls_20260930.sql`),原文 JSON 照旧留档;已落库但缺地址的历史记录由同任务字段级补空(只补空不覆盖已有值)。
|
||||
- **展示**:正式结算单详情 `FormalSettlementVO.issuedInvoices`(`FormalSettlementServiceImpl.listIssuedInvoices`:结算单 → 开票申请关联 → 发票结果,仅保留非作废未删除申请),前端 `formal-settlement-editor.vue` 发票信息卡片内只读表格「开票发票」提供查看/下载。该列表只读、不落 `blade_formal_settlement_invoice`、不参与匹配金额计算,避免被 `rebuildInvoices` 全删重建逻辑误持久化。
|
||||
- **下载通道**:URL 为 http(s) 直链,按 3.3 前端分流约定归入「OSS 直链」一类——前端 `window.open`(查看,数电 PDF 优先)/ `downloadFileByUrl`(下载,按直链扩展名命名)直连,不经 A801-2 代理,不新增文件流接口。
|
||||
- **时效性**:直链有效期未知,与 5.2 缺口 #4(票池文件时效性)同源待金蝶确认;回写补齐策略为仅补空不刷新,若金蝶确认直链会过期需另行设计刷新机制。
|
||||
|
||||
### 3.5 数据下载(查询回写)的触发机制
|
||||
|
||||
| 定时任务(PaymentKingdeeResultTask / CustomerKingdeeSyncTask) | cron 默认 | 拉取内容 | 回写目标 |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -152,6 +161,7 @@
|
||||
| **用户附件(OSS 直链)** | http(s) 下载(60s 超时、20MB 封顶)→ A902 上传 5 类单据 | 不经金蝶:前端直连 OSS(`downloadFileByUrl` 现有行为) | ✅ 各自闭环,两类附件按字段特征分流互不干扰 |
|
||||
| **单据数据面** | A103-1/3、A102-2、A302-2、A303-2/6、A303-1、A902-2 | A103-2/4、A102-4、A302-4、A801-3、A303-4/5、A902-1(幂等预检 + 内码反查 + 结果回写) | ✅ 每个推送接口都有查询接口配套,成对出现 |
|
||||
| **票池元数据** | (消费方)收票登记落镜像、A303-2 分录取数 | A801-1 实时查询(选票弹窗 + 回源) | ✅ 闭环 |
|
||||
| **销项开票发票文件** | (数据面)A303-1 开票申请推送 | A303-5 回写发票结果含文件直链,落库后前端直连查看/下载(不经 A801-2) | ✅ 闭环(2026-09-30 补充,见 3.4) |
|
||||
|
||||
### 5.2 对不上的缺口(单向或未启用)
|
||||
|
||||
@@ -160,7 +170,7 @@
|
||||
| 1 | **已上传附件的金蝶侧回源下载** | A902 上传成功后 `kingdee_path` 已落 `blade_kingdee_attachment_sync`,但没有任何代码用它发起 A801-2 下载 | TMS 侧附件若 OSS 丢失/清理,无法从金蝶恢复。A902 返回 path(`211e6ce5c9805c00`)与 A801-2 path(`234e244b2c803400`)同为 16 位 hex,疑似同命名空间,但兼容性**未验证**——2026-09-27 设计「不做的事」第 1 条明确列为二期待确认项 |
|
||||
| 2 | **收款认领链路** | A301-1 收款单查询、A301-2 认领公告查询、A301-3 结果推送在 `IBfiClient`/`BfiServiceImpl` 已完整实现,业务层零调用 | 金蝶收款数据下载 → TMS 认领 → 结果回推整条链路待启用;启用前 A301 系列不产生任何下载 |
|
||||
| 3 | **客商资质附件** | `KingdeeAttachmentConstant.ENTITY_NUMBERS` 仅预留注释(CustomerArchiveServiceImpl 侧无任何附件上传接线);客商的 entityNumber/controlKey 待金蝶确认 | 客商附件目前既不上传也不下载,二期接入时补一个 biz_type 条目即可复用现有架构 |
|
||||
| 4 | 票池文件时效性 | 镜像 `attachmentsJson` 落库后 path 是否会过期未知;下载时镜像快照过期(红冲重开)报「附件不存在」**不自动刷新**,与上传取快照口径一致 | 需金蝶确认 `downloadUrl` 有效期与失效表现,决定是否要补 A801-1 定时刷新镜像机制 |
|
||||
| 4 | 票池文件时效性 | 镜像 `attachmentsJson` 落库后 path 是否会过期未知;下载时镜像快照过期(红冲重开)报「附件不存在」**不自动刷新**,与上传取快照口径一致。2026-09-30 落库的 A303-5 销项发票云直链(见 3.4)同样受时效性约束,回写仅补空不刷新 | 需金蝶确认 `downloadUrl`/发票直链有效期与失效表现,决定是否要补定时刷新镜像机制 |
|
||||
| 5 | 附件面板反向查询 | 金蝶无「查询单据附件列表」接口接入 | 附件同步状态只能以本地同步表为准,无法与金蝶侧附件面板对账 |
|
||||
|
||||
---
|
||||
@@ -197,6 +207,7 @@
|
||||
| 附件同步主流程 | `org.springblade.transport.support.KingdeeAttachmentSyncSupport` |
|
||||
| 附件同步表 | `blade_kingdee_attachment_sync`(实体 `KingdeeAttachmentSync`,建表 `doc/sql/transport/blade_kingdee_attachment_sync_20260924.sql`) |
|
||||
| 票池镜像 | `blade_kingdee_invoice_pool`(`KingdeeInvoicePool`,`InvoiceReceiptServiceImpl` 内 upsert) |
|
||||
| 开票回写发票结果 | `blade_invoice_application_invoice`(实体 `InvoiceApplicationInvoice`,建表 `blade_invoice_application_invoice_20260920.sql` + 文件地址补丁 `blade_invoice_application_invoice_urls_20260930.sql`) |
|
||||
| 前端下载端点 | `InvoiceReceiptController.downloadPoolAttachment`(GET /invoice-receipt/invoice-pool/attachment/download) |
|
||||
| entityNumber 映射 | `org.springblade.transport.constant.KingdeeAttachmentConstant` |
|
||||
| 定时回写任务 | `org.springblade.transport.task.PaymentKingdeeResultTask`、`CustomerKingdeeSyncTask` |
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
-- ============================================================
|
||||
-- blade_invoice_application_invoice_urls_20260930.sql
|
||||
-- ============================================================
|
||||
-- 开票申请金蝶发票结果表补充发票文件下载地址(A303-5 返回,正式结算单详情查看/下载发票):
|
||||
-- invoice_file_url 板式文件下载地址(数电票为OFD,航信百望税盘普通电票为PDF,电子专票/UKey电票为OFD)
|
||||
-- invoice_image_url PDF图片预览地址
|
||||
-- invoice_pdf_file_url 数电PDF下载地址(发票云5.0.025版本返回)
|
||||
-- invoice_xml_file_url 数电XML下载地址(发票云5.0.025版本返回)
|
||||
-- 以下结构升级支持重复执行
|
||||
|
||||
-- ① blade_invoice_application_invoice.invoice_file_url
|
||||
SET @ddl_sql = (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `blade_invoice_application_invoice` ADD COLUMN `invoice_file_url` varchar(1024) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''板式文件下载地址(数电票为OFD,普通电票为PDF,电子专票为OFD)'' AFTER `invoice_amount`',
|
||||
'SELECT 1')
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'blade_invoice_application_invoice'
|
||||
AND COLUMN_NAME = 'invoice_file_url'
|
||||
);
|
||||
PREPARE ddl_stmt FROM @ddl_sql;
|
||||
EXECUTE ddl_stmt;
|
||||
DEALLOCATE PREPARE ddl_stmt;
|
||||
|
||||
-- ② blade_invoice_application_invoice.invoice_image_url
|
||||
SET @ddl_sql = (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `blade_invoice_application_invoice` ADD COLUMN `invoice_image_url` varchar(1024) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''PDF图片预览地址'' AFTER `invoice_file_url`',
|
||||
'SELECT 1')
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'blade_invoice_application_invoice'
|
||||
AND COLUMN_NAME = 'invoice_image_url'
|
||||
);
|
||||
PREPARE ddl_stmt FROM @ddl_sql;
|
||||
EXECUTE ddl_stmt;
|
||||
DEALLOCATE PREPARE ddl_stmt;
|
||||
|
||||
-- ③ blade_invoice_application_invoice.invoice_pdf_file_url
|
||||
SET @ddl_sql = (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `blade_invoice_application_invoice` ADD COLUMN `invoice_pdf_file_url` varchar(1024) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''数电PDF下载地址(发票云5.0.025版本返回)'' AFTER `invoice_image_url`',
|
||||
'SELECT 1')
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'blade_invoice_application_invoice'
|
||||
AND COLUMN_NAME = 'invoice_pdf_file_url'
|
||||
);
|
||||
PREPARE ddl_stmt FROM @ddl_sql;
|
||||
EXECUTE ddl_stmt;
|
||||
DEALLOCATE PREPARE ddl_stmt;
|
||||
|
||||
-- ④ blade_invoice_application_invoice.invoice_xml_file_url
|
||||
SET @ddl_sql = (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `blade_invoice_application_invoice` ADD COLUMN `invoice_xml_file_url` varchar(1024) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''数电XML下载地址(发票云5.0.025版本返回)'' AFTER `invoice_pdf_file_url`',
|
||||
'SELECT 1')
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'blade_invoice_application_invoice'
|
||||
AND COLUMN_NAME = 'invoice_xml_file_url'
|
||||
);
|
||||
PREPARE ddl_stmt FROM @ddl_sql;
|
||||
EXECUTE ddl_stmt;
|
||||
DEALLOCATE PREPARE ddl_stmt;
|
||||
Reference in New Issue
Block a user