From f12cff1789bebd9429473df2a24883d01eb026bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Fri, 6 Nov 2026 17:39:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BB=98=E6=AC=BE=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E5=9B=9E=E5=86=99=E9=87=91=E8=9D=B6=E7=94=B5=E5=AD=90=E5=9B=9E?= =?UTF-8?q?=E5=8D=95=E6=96=87=E4=BB=B6=E5=90=8D=E5=B9=B6=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=87=AD=E8=AF=81A801-2=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pojo/entity/PaymentApplicationRecord.java | 2 + .../PaymentApplicationController.java | 10 ++ .../service/IPaymentApplicationService.java | 9 ++ .../impl/PaymentApplicationServiceImpl.java | 122 ++++++++++++++++++ .../thirdparty/bfi/pojo/vo/BfiPayBillVO.java | 4 + ...tion_record_voucher_file_name_20261106.sql | 27 ++++ 6 files changed, 174 insertions(+) create mode 100644 doc/sql/transport/lqj/temp/29_blade_payment_application_record_voucher_file_name_20261106.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationRecord.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationRecord.java index 9d97e3d9..c51d0ed4 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationRecord.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationRecord.java @@ -42,4 +42,6 @@ public class PaymentApplicationRecord extends TenantEntity { private String kingdeeBillNo; /** 金蝶付款处理单id(cas_paybill.id,A103-4 回写,应付单 preentry 预付抵扣取数用) */ private String kingdeeBillId; + /** 付款凭证文件名(A103-4 isgetreceipt=true 时取 bankcheckentity.awrp_receiptno.uploadfilename,A801-2 下载键) */ + private String voucherFileName; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java index dad47f59..0a1a9ccc 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java @@ -24,6 +24,7 @@ import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; +import jakarta.servlet.http.HttpServletResponse; import org.springblade.core.boot.ctrl.BladeController; import org.springblade.core.mp.support.Condition; import org.springblade.core.mp.support.Query; @@ -147,4 +148,13 @@ public class PaymentApplicationController extends BladeController { public R syncKingdeeResult(@RequestBody(required = false) PaymentApplicationSyncResultRequest request) { 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); + } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java index 1cf969ba..e9b575f2 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java @@ -20,6 +20,7 @@ package org.springblade.transport.service; import com.baomidou.mybatisplus.core.metadata.IPage; +import jakarta.servlet.http.HttpServletResponse; import org.springblade.core.mp.base.BaseService; import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiPayBillVO; @@ -125,6 +126,14 @@ public interface IPaymentApplicationService extends BaseService 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> entry : response.headers().entrySet()) { + if (name.equalsIgnoreCase(entry.getKey()) && Func.isNotEmpty(entry.getValue())) { + return entry.getValue().iterator().next(); + } + } + return null; + } + /** * A103-2 返回状态映射本地金蝶同步状态: * paystatus=Alreadypay 已付款(终态)优先;billstatus=D 已关闭(终态)次之; diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiPayBillVO.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiPayBillVO.java index b43e0d8b..c07ffc85 100644 --- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiPayBillVO.java +++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiPayBillVO.java @@ -555,6 +555,10 @@ public class BfiPayBillVO implements Serializable { * 单据同步id */ private String transbillid; + /** + * 是否已有电子回单[true:有, false:无] + */ + private Boolean isgetreceipt; /** * 分录 */ diff --git a/doc/sql/transport/lqj/temp/29_blade_payment_application_record_voucher_file_name_20261106.sql b/doc/sql/transport/lqj/temp/29_blade_payment_application_record_voucher_file_name_20261106.sql new file mode 100644 index 00000000..279c4163 --- /dev/null +++ b/doc/sql/transport/lqj/temp/29_blade_payment_application_record_voucher_file_name_20261106.sql @@ -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')) <> '';