feat: 付款记录回写金蝶电子回单文件名并支持凭证A801-2下载
This commit is contained in:
+2
@@ -42,4 +42,6 @@ public class PaymentApplicationRecord extends TenantEntity {
|
|||||||
private String kingdeeBillNo;
|
private String kingdeeBillNo;
|
||||||
/** 金蝶付款处理单id(cas_paybill.id,A103-4 回写,应付单 preentry 预付抵扣取数用) */
|
/** 金蝶付款处理单id(cas_paybill.id,A103-4 回写,应付单 preentry 预付抵扣取数用) */
|
||||||
private String kingdeeBillId;
|
private String kingdeeBillId;
|
||||||
|
/** 付款凭证文件名(A103-4 isgetreceipt=true 时取 bankcheckentity.awrp_receiptno.uploadfilename,A801-2 下载键) */
|
||||||
|
private String voucherFileName;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -24,6 +24,7 @@ import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springblade.core.boot.ctrl.BladeController;
|
import org.springblade.core.boot.ctrl.BladeController;
|
||||||
import org.springblade.core.mp.support.Condition;
|
import org.springblade.core.mp.support.Condition;
|
||||||
import org.springblade.core.mp.support.Query;
|
import org.springblade.core.mp.support.Query;
|
||||||
@@ -147,4 +148,13 @@ public class PaymentApplicationController extends BladeController {
|
|||||||
public R<String> syncKingdeeResult(@RequestBody(required = false) PaymentApplicationSyncResultRequest request) {
|
public R<String> syncKingdeeResult(@RequestBody(required = false) PaymentApplicationSyncResultRequest request) {
|
||||||
return R.data(paymentApplicationService.syncKingdeePaymentResult(request == null ? null : request.getPaymentNos()));
|
return R.data(paymentApplicationService.syncKingdeePaymentResult(request == null ? null : request.getPaymentNos()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/record/download-voucher")
|
||||||
|
@ApiOperationSupport(order = 13)
|
||||||
|
@Operation(summary = "付款记录付款凭证下载(A801-2 流式回源)",
|
||||||
|
description = "按付款记录落库的凭证文件名(A103-4 bankcheckentity.awrp_receiptno.uploadfilename)回源金蝶文件流;"
|
||||||
|
+ "成功返回文件流,失败返回 JSON 错误")
|
||||||
|
public void downloadRecordVoucher(@RequestParam Long recordId, HttpServletResponse response) {
|
||||||
|
paymentApplicationService.downloadRecordVoucher(recordId, response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -20,6 +20,7 @@
|
|||||||
package org.springblade.transport.service;
|
package org.springblade.transport.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springblade.core.mp.base.BaseService;
|
import org.springblade.core.mp.base.BaseService;
|
||||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyVO;
|
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyVO;
|
||||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayBillVO;
|
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayBillVO;
|
||||||
@@ -125,6 +126,14 @@ public interface IPaymentApplicationService extends BaseService<PaymentApplicati
|
|||||||
*/
|
*/
|
||||||
void applyPayBillRecord(PaymentApplication entity, BfiPayBillVO payBill);
|
void applyPayBillRecord(PaymentApplication entity, BfiPayBillVO payBill);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 付款记录付款凭证下载(A801-2 流式回源):按付款记录落库的凭证文件名回源金蝶文件流
|
||||||
|
*
|
||||||
|
* @param recordId 付款记录id
|
||||||
|
* @param response HTTP 响应(成功写文件流,失败写 JSON 错误)
|
||||||
|
*/
|
||||||
|
void downloadRecordVoucher(Long recordId, HttpServletResponse response);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 付款方式是否为汇票付款(按业务字典 pay_method 条目文本匹配"汇票")
|
* 付款方式是否为汇票付款(按业务字典 pay_method 条目文本匹配"汇票")
|
||||||
*
|
*
|
||||||
|
|||||||
+122
@@ -24,13 +24,17 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
|||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import feign.Response;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springblade.core.log.exception.ServiceException;
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||||
import org.springblade.core.secure.utils.AuthUtil;
|
import org.springblade.core.secure.utils.AuthUtil;
|
||||||
|
import org.springblade.core.tool.api.R;
|
||||||
import org.springblade.core.tool.jackson.JsonUtil;
|
import org.springblade.core.tool.jackson.JsonUtil;
|
||||||
import org.springblade.core.tool.utils.BeanUtil;
|
import org.springblade.core.tool.utils.BeanUtil;
|
||||||
import org.springblade.core.tool.utils.Func;
|
import org.springblade.core.tool.utils.Func;
|
||||||
import org.springblade.core.tool.utils.SpringUtil;
|
import org.springblade.core.tool.utils.SpringUtil;
|
||||||
|
import org.springblade.core.tool.utils.WebUtil;
|
||||||
import org.springblade.system.cache.DictBizCache;
|
import org.springblade.system.cache.DictBizCache;
|
||||||
import org.springblade.system.pojo.entity.DictBiz;
|
import org.springblade.system.pojo.entity.DictBiz;
|
||||||
import org.springblade.thirdparty.bfi.constant.BfiConstant;
|
import org.springblade.thirdparty.bfi.constant.BfiConstant;
|
||||||
@@ -44,6 +48,7 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO;
|
|||||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
|
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
|
||||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyEntryVO;
|
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyEntryVO;
|
||||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyVO;
|
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyVO;
|
||||||
|
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayBillReceiptVO;
|
||||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayBillVO;
|
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayBillVO;
|
||||||
import org.springblade.thirdparty.bfi.service.IBfiService;
|
import org.springblade.thirdparty.bfi.service.IBfiService;
|
||||||
import org.springblade.thirdparty.bfi.util.BfiUtil;
|
import org.springblade.thirdparty.bfi.util.BfiUtil;
|
||||||
@@ -118,6 +123,9 @@ import org.springframework.context.ApplicationEventPublisher;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
@@ -126,6 +134,7 @@ import java.time.LocalDate;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@@ -150,6 +159,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
|||||||
private static final int PAY_RESULT_PAGE_SIZE = 200;
|
private static final int PAY_RESULT_PAGE_SIZE = 200;
|
||||||
/** 历史模拟付款记录单号前缀 */
|
/** 历史模拟付款记录单号前缀 */
|
||||||
private static final String MOCK_PAYMENT_RECORD_PREFIX = "MOCK-PAY-";
|
private static final String MOCK_PAYMENT_RECORD_PREFIX = "MOCK-PAY-";
|
||||||
|
/** A801-2 凭证文件流式拷贝缓冲区大小 */
|
||||||
|
private static final int VOUCHER_DOWNLOAD_BUFFER_SIZE = 8 * 1024;
|
||||||
/** 金蝶付款结果回写 single-flight 锁键前缀(多副本定时任务与手动触发共用) */
|
/** 金蝶付款结果回写 single-flight 锁键前缀(多副本定时任务与手动触发共用) */
|
||||||
private static final String PAY_RESULT_SYNC_LOCK_PREFIX = "transport:payment-application:kingdee-pay-result-sync:";
|
private static final String PAY_RESULT_SYNC_LOCK_PREFIX = "transport:payment-application:kingdee-pay-result-sync:";
|
||||||
private final PaymentApplicationInvoiceMapper invoiceMapper;
|
private final PaymentApplicationInvoiceMapper invoiceMapper;
|
||||||
@@ -1098,6 +1109,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
|||||||
record.setKingdeeBillNo(billNo);
|
record.setKingdeeBillNo(billNo);
|
||||||
// 金蝶付款处理单内码(cas_paybill.id)落库:财务应付单 preentry 预付抵扣(y_billid)取数用
|
// 金蝶付款处理单内码(cas_paybill.id)落库:财务应付单 preentry 预付抵扣(y_billid)取数用
|
||||||
record.setKingdeeBillId(payBill.getId() == null ? null : String.valueOf(payBill.getId()));
|
record.setKingdeeBillId(payBill.getId() == null ? null : String.valueOf(payBill.getId()));
|
||||||
|
// 付款凭证:isgetreceipt=true 表示金蝶已生成电子回单,落库回单文件名(A801-2 文件下载键)
|
||||||
|
record.setVoucherFileName(this.resolveReceiptFileKey(payBill));
|
||||||
// 付款处理单查询结果整体留档(凭证号、银行流水、电子回单等信息),便于追溯
|
// 付款处理单查询结果整体留档(凭证号、银行流水、电子回单等信息),便于追溯
|
||||||
record.setVoucherJson(JsonUtil.toJson(payBill));
|
record.setVoucherJson(JsonUtil.toJson(payBill));
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
@@ -1107,6 +1120,115 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取电子回单文件名(A801-2 文件下载键):isgetreceipt=true 且 bankcheckentity.awrp_receiptno
|
||||||
|
* 存在时取第一个非空 uploadfilename;否则返回 null(回写置空,保持重复回写的幂等)
|
||||||
|
*
|
||||||
|
* @param payBill A103-4 查询返回的付款处理单
|
||||||
|
* @return 电子回单文件名,无回单返回 null
|
||||||
|
*/
|
||||||
|
private String resolveReceiptFileKey(BfiPayBillVO payBill) {
|
||||||
|
if (!Boolean.TRUE.equals(payBill.getIsgetreceipt()) || Func.isEmpty(payBill.getBankcheckentity())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return payBill.getBankcheckentity().stream()
|
||||||
|
.filter(bankCheck -> Func.isNotEmpty(bankCheck.getAwrpReceiptno()))
|
||||||
|
.flatMap(bankCheck -> bankCheck.getAwrpReceiptno().stream())
|
||||||
|
.map(BfiPayBillReceiptVO::getUploadfilename)
|
||||||
|
.filter(Func::isNotBlank)
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void downloadRecordVoucher(Long recordId, HttpServletResponse response) {
|
||||||
|
PaymentApplicationRecord record = recordMapper.selectById(recordId);
|
||||||
|
String fileKey = record == null ? null : record.getVoucherFileName();
|
||||||
|
if (Func.isBlank(fileKey)) {
|
||||||
|
WebUtil.renderJson(response, R.fail("该付款记录没有付款凭证"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
Response bfiResponse = bfiService.downloadInvoice(fileKey);
|
||||||
|
try {
|
||||||
|
String contentType = this.firstHeader(bfiResponse, "Content-Type");
|
||||||
|
// A801-2 失败以 JSON 体返回(如 {"status":false,"errorCode":"FILE_IS_NOT_EXIST"}),先校验再写流:
|
||||||
|
// 写流前的失败统一以 JSON 返回,前端按响应 Content-Type 识别提示,浏览器不落损坏文件
|
||||||
|
if (contentType != null && contentType.contains("application/json")) {
|
||||||
|
throw new ServiceException("调用BFI文件下载失败:" + this.readBfiJsonMessage(bfiResponse));
|
||||||
|
}
|
||||||
|
// 电子回单为 PDF 版式,uploadfilename 无扩展名,按付款单号命名
|
||||||
|
String fileName = record.getPaymentNo() + "-付款凭证.pdf";
|
||||||
|
// BFI 文件流统一回 octet-stream:PDF 版式须显式声明 MIME,否则浏览器 blob 不渲染
|
||||||
|
if (Func.isBlank(contentType) || contentType.contains("octet-stream")) {
|
||||||
|
contentType = "application/pdf";
|
||||||
|
}
|
||||||
|
// 文件名按 RFC 5987 编码(+ 号转 %20),中文不乱码
|
||||||
|
String encodedName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20");
|
||||||
|
response.setContentType(contentType);
|
||||||
|
String contentLength = this.firstHeader(bfiResponse, "Content-Length");
|
||||||
|
if (contentLength != null) {
|
||||||
|
try {
|
||||||
|
response.setContentLengthLong(Long.parseLong(contentLength));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// 非法长度头按无长度处理,走分块传输
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.setHeader("Content-Disposition",
|
||||||
|
"attachment; filename=\"" + encodedName + "\"; filename*=UTF-8''" + encodedName);
|
||||||
|
int totalBytes = 0;
|
||||||
|
try (InputStream inputStream = bfiResponse.body().asInputStream();
|
||||||
|
OutputStream outputStream = response.getOutputStream()) {
|
||||||
|
byte[] buffer = new byte[VOUCHER_DOWNLOAD_BUFFER_SIZE];
|
||||||
|
int read;
|
||||||
|
while ((read = inputStream.read(buffer)) != -1) {
|
||||||
|
outputStream.write(buffer, 0, read);
|
||||||
|
totalBytes += read;
|
||||||
|
}
|
||||||
|
outputStream.flush();
|
||||||
|
}
|
||||||
|
log.info("付款记录[{}]付款凭证下载完成:文件[{}],{}字节,耗时{}ms",
|
||||||
|
recordId, fileKey, totalBytes, System.currentTimeMillis() - start);
|
||||||
|
} finally {
|
||||||
|
bfiResponse.close();
|
||||||
|
}
|
||||||
|
} catch (ServiceException exception) {
|
||||||
|
log.warn("付款记录[{}]付款凭证下载失败:{}", recordId, exception.getMessage());
|
||||||
|
WebUtil.renderJson(response, R.fail(exception.getMessage()));
|
||||||
|
} catch (IOException exception) {
|
||||||
|
// 传输中断类失败(客户端断开等):响应通道已不可用,仅记录日志
|
||||||
|
log.error("付款记录[{}]付款凭证下载传输异常", recordId, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读取 A801-2 JSON 错误体提示信息(优先 message,兜底 errorCode) */
|
||||||
|
private String readBfiJsonMessage(Response response) {
|
||||||
|
try (InputStream inputStream = response.body().asInputStream()) {
|
||||||
|
Object parsed = JsonUtil.parse(new String(inputStream.readAllBytes(), StandardCharsets.UTF_8), Object.class);
|
||||||
|
if (parsed instanceof Map<?, ?> map) {
|
||||||
|
for (String key : List.of("message", "errorCode")) {
|
||||||
|
Object value = map.get(key);
|
||||||
|
if (value != null && Func.isNotBlank(String.valueOf(value))) {
|
||||||
|
return String.valueOf(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception exception) {
|
||||||
|
log.warn("解析BFI文件下载错误响应失败", exception);
|
||||||
|
}
|
||||||
|
return "响应体为JSON错误";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 大小写不敏感取第一个响应头值 */
|
||||||
|
private String firstHeader(Response response, String name) {
|
||||||
|
for (Map.Entry<String, Collection<String>> entry : response.headers().entrySet()) {
|
||||||
|
if (name.equalsIgnoreCase(entry.getKey()) && Func.isNotEmpty(entry.getValue())) {
|
||||||
|
return entry.getValue().iterator().next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A103-2 返回状态映射本地金蝶同步状态:
|
* A103-2 返回状态映射本地金蝶同步状态:
|
||||||
* paystatus=Alreadypay 已付款(终态)优先;billstatus=D 已关闭(终态)次之;
|
* paystatus=Alreadypay 已付款(终态)优先;billstatus=D 已关闭(终态)次之;
|
||||||
|
|||||||
+4
@@ -555,6 +555,10 @@ public class BfiPayBillVO implements Serializable {
|
|||||||
* 单据同步id
|
* 单据同步id
|
||||||
*/
|
*/
|
||||||
private String transbillid;
|
private String transbillid;
|
||||||
|
/**
|
||||||
|
* 是否已有电子回单[true:有, false:无]
|
||||||
|
*/
|
||||||
|
private Boolean isgetreceipt;
|
||||||
/**
|
/**
|
||||||
* 分录
|
* 分录
|
||||||
*/
|
*/
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
-- 付款记录表:付款凭证文件名落库(付款凭证下载 A801-2 用)
|
||||||
|
-- 背景:A103-4 付款处理查询返回 isgetreceipt(true=金蝶已有电子回单),
|
||||||
|
-- 回单文件名取 bankcheckentity.awrp_receiptno.uploadfilename(即 A801-2 文件下载键),
|
||||||
|
-- 前端按付款记录 voucherFileName 判定是否展示凭证下载,调用
|
||||||
|
-- GET /payment-application/record/download-voucher?recordId= 流式回源下载
|
||||||
|
-- 幂等可重复执行:逐列查 information_schema,已存在则跳过;存量数据从 voucher_json 回填(仅填空值)
|
||||||
|
|
||||||
|
-- ① 列 voucher_file_name
|
||||||
|
SET @ddl = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_payment_application_record' AND COLUMN_NAME = 'voucher_file_name') = 0,
|
||||||
|
'ALTER TABLE `blade_payment_application_record` ADD COLUMN `voucher_file_name` varchar(128) DEFAULT NULL COMMENT ''付款凭证文件名(A103-4 bankcheckentity.awrp_receiptno.uploadfilename,A801-2下载键)'' AFTER `kingdee_bill_id`',
|
||||||
|
'SELECT 1'));
|
||||||
|
PREPARE stmt FROM @ddl;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- ② 存量回填:从 A103-4 留档报文 voucher_json 提取电子回单文件名(仅填空值,天然可重复执行)
|
||||||
|
-- 仅回填 isgetreceipt=true 且回单文件名非空的记录
|
||||||
|
UPDATE `blade_payment_application_record`
|
||||||
|
SET `voucher_file_name` = JSON_UNQUOTE(JSON_EXTRACT(`voucher_json`, '$.bankcheckentity[0].awrp_receiptno[0].uploadfilename'))
|
||||||
|
WHERE `voucher_file_name` IS NULL
|
||||||
|
AND `voucher_json` IS NOT NULL
|
||||||
|
AND JSON_VALID(`voucher_json`)
|
||||||
|
AND JSON_EXTRACT(`voucher_json`, '$.isgetreceipt') = TRUE
|
||||||
|
AND JSON_EXTRACT(`voucher_json`, '$.bankcheckentity[0].awrp_receiptno[0].uploadfilename') IS NOT NULL
|
||||||
|
AND JSON_UNQUOTE(JSON_EXTRACT(`voucher_json`, '$.bankcheckentity[0].awrp_receiptno[0].uploadfilename')) <> '';
|
||||||
Reference in New Issue
Block a user