|
|
@@ -0,0 +1,255 @@
|
|
|
+package com.malk.minglei.service.impl.banking;
|
|
|
+
|
|
|
+import com.malk.minglei.config.BankingProperties;
|
|
|
+import com.malk.minglei.dto.banking.BankTransactionQueryRequest;
|
|
|
+import com.malk.minglei.dto.banking.ElectronicBillDownloadRequest;
|
|
|
+import com.malk.minglei.dto.banking.ElectronicBillDownloadResult;
|
|
|
+import com.malk.minglei.dto.banking.PendingBankingPayment;
|
|
|
+import com.malk.minglei.service.banking.BankingService;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.scheduling.annotation.Scheduled;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import org.springframework.web.util.UriUtils;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 定时补偿查询异步支付状态,并在支付成功后下载电子回单。
|
|
|
+ */
|
|
|
+@Component
|
|
|
+public class BankingPaymentStatusScheduler {
|
|
|
+
|
|
|
+ private static final Logger LOGGER = LoggerFactory.getLogger(BankingPaymentStatusScheduler.class);
|
|
|
+ private static final String PAYMENT_SUCCESS_STATUS = "X0000";
|
|
|
+ private static final int TRANSACTION_LOOKBACK_DAYS = 7;
|
|
|
+ private static final DateTimeFormatter ACCOUNTING_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
|
|
|
+ private static final DateTimeFormatter UPDATED_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
+
|
|
|
+ private final BankingPaymentTrackingRepository trackingRepository;
|
|
|
+ private final BankingService bankingService;
|
|
|
+ private final BankingProperties bankingProperties;
|
|
|
+ private final BankingPaymentYidaWriteBackService yidaWriteBackService;
|
|
|
+
|
|
|
+ public BankingPaymentStatusScheduler(BankingPaymentTrackingRepository trackingRepository,
|
|
|
+ BankingService bankingService,
|
|
|
+ BankingProperties bankingProperties,
|
|
|
+ BankingPaymentYidaWriteBackService yidaWriteBackService) {
|
|
|
+ this.trackingRepository = trackingRepository;
|
|
|
+ this.bankingService = bankingService;
|
|
|
+ this.bankingProperties = bankingProperties;
|
|
|
+ this.yidaWriteBackService = yidaWriteBackService;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 每五分钟查询未完成支付;支付已成功而回单未生成时只重试回单下载。
|
|
|
+ */
|
|
|
+ @Scheduled(fixedDelayString = "${minglei.banking.pending-payment.schedule-fixed-delay-millis:300000}")
|
|
|
+ public void queryOutstandingPaymentStatuses() {
|
|
|
+ LOGGER.info("Banking payment status scheduler triggered");
|
|
|
+ if (!trackingRepository.isAvailable()) {
|
|
|
+ LOGGER.warn("Banking payment status scheduler skipped: employee roster datasource is unavailable");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ List<PendingBankingPayment> payments = trackingRepository.findOutstandingPayments();
|
|
|
+ LOGGER.info("Banking payment status scheduler found {} outstanding payment(s)", payments.size());
|
|
|
+ for (PendingBankingPayment payment : payments) {
|
|
|
+ processPayment(payment);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void processPayment(PendingBankingPayment payment) {
|
|
|
+ try {
|
|
|
+ if (!hasText(payment.getPaymentAccountNo())) {
|
|
|
+ LOGGER.warn("Banking payment skipped because payment account is empty businessTransNo={}",
|
|
|
+ payment.getBusinessTransNo());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> transactionResponse = bankingService.queryTransactions(
|
|
|
+ buildTransactionQuery(payment.getPaymentAccountNo()));
|
|
|
+ Map<String, Object> matchedTransaction = findMatchingTransaction(transactionResponse, payment);
|
|
|
+ if (matchedTransaction == null) {
|
|
|
+ LOGGER.info("No matching banking transaction found businessTransNo={} paymentAccountNo={}",
|
|
|
+ payment.getBusinessTransNo(), payment.getPaymentAccountNo());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ String uniqueNumber = extractPaymentDataText(matchedTransaction, "uniqueNumber");
|
|
|
+ if (!hasText(uniqueNumber)) {
|
|
|
+ LOGGER.warn("Matching banking transaction has no uniqueNumber businessTransNo={}",
|
|
|
+ payment.getBusinessTransNo());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ downloadReceipt(payment, uniqueNumber);
|
|
|
+ } catch (RuntimeException exception) {
|
|
|
+ trackingRepository.recordQueryFailure(payment.getBusinessTransNo());
|
|
|
+ LOGGER.error("Banking transaction compensation failed businessTransNo={}",
|
|
|
+ payment.getBusinessTransNo(), exception);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private BankTransactionQueryRequest buildTransactionQuery(String paymentAccountNo) {
|
|
|
+ LocalDate today = LocalDate.now();
|
|
|
+ LocalDate fromDate = today.minusDays(TRANSACTION_LOOKBACK_DAYS);
|
|
|
+ LocalDateTime now = LocalDateTime.now();
|
|
|
+ BankTransactionQueryRequest request = new BankTransactionQueryRequest();
|
|
|
+ request.setAccountNo(paymentAccountNo);
|
|
|
+ request.setFromDateId(fromDate.format(ACCOUNTING_DATE_FORMATTER));
|
|
|
+ request.setToDateId(today.format(ACCOUNTING_DATE_FORMATTER));
|
|
|
+ request.setFromUpdatedTime(fromDate.atStartOfDay().format(UPDATED_TIME_FORMATTER));
|
|
|
+ request.setToUpdatedTime(now.format(UPDATED_TIME_FORMATTER));
|
|
|
+ request.setPageNow("1");
|
|
|
+ request.setPageSize("100");
|
|
|
+ return request;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> findMatchingTransaction(Map<String, Object> response,
|
|
|
+ PendingBankingPayment payment) {
|
|
|
+ for (Map<String, Object> transaction : extractTransactionRecords(response)) {
|
|
|
+ if (matchesTransaction(transaction, payment)) {
|
|
|
+ return transaction;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean matchesTransaction(Map<String, Object> transaction, PendingBankingPayment payment) {
|
|
|
+ return amountsEqual(transaction.get("drAmount"), payment.getPayAmount())
|
|
|
+ && equalsText(transaction.get("accountNo"), payment.getPaymentAccountNo())
|
|
|
+ && equalsText(transaction.get("customerAccountNo"), payment.getCustomerAccountNo())
|
|
|
+ && equalsText(transaction.get("customerName"), payment.getCustomerName())
|
|
|
+ && matchesText(transaction.get("customerBank"), payment.getCustomerBank())
|
|
|
+ && equalsText(transaction.get("userMemo"), payment.getRemake());
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean amountsEqual(Object actualValue, BigDecimal expectedValue) {
|
|
|
+ if (actualValue == null || expectedValue == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return new BigDecimal(String.valueOf(actualValue)).compareTo(expectedValue) == 0;
|
|
|
+ } catch (NumberFormatException exception) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean matchesText(Object actualValue, String expectedValue) {
|
|
|
+ if (!hasText(expectedValue) || actualValue == null || !hasText(String.valueOf(actualValue))) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String actual = normalizeText(String.valueOf(actualValue));
|
|
|
+ String expected = normalizeText(expectedValue);
|
|
|
+ return actual.equals(expected) || actual.contains(expected) || expected.contains(actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean equalsText(Object actualValue, String expectedValue) {
|
|
|
+ if (!hasText(expectedValue) || actualValue == null || !hasText(String.valueOf(actualValue))) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return normalizeText(String.valueOf(actualValue)).equals(normalizeText(expectedValue));
|
|
|
+ }
|
|
|
+
|
|
|
+ private String normalizeText(String value) {
|
|
|
+ return value == null ? "" : value.replaceAll("\\s+", "").trim().toLowerCase();
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map<String, Object>> extractTransactionRecords(Map<String, Object> response) {
|
|
|
+ if (response == null) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ List<Map<String, Object>> records = new ArrayList<Map<String, Object>>();
|
|
|
+ collectTransactionRecords(response.get("data"), records);
|
|
|
+ if (records.isEmpty()) {
|
|
|
+ collectTransactionRecords(response, records);
|
|
|
+ }
|
|
|
+ return records;
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private void collectTransactionRecords(Object value, List<Map<String, Object>> records) {
|
|
|
+ if (value instanceof List) {
|
|
|
+ List<?> list = (List<?>) value;
|
|
|
+ if (isTransactionList(list)) {
|
|
|
+ for (Object item : list) {
|
|
|
+ records.add((Map<String, Object>) item);
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ for (Object item : list) {
|
|
|
+ collectTransactionRecords(item, records);
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (value instanceof Map) {
|
|
|
+ for (Object child : ((Map<?, ?>) value).values()) {
|
|
|
+ collectTransactionRecords(child, records);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isTransactionList(List<?> list) {
|
|
|
+ if (list.isEmpty()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ for (Object item : list) {
|
|
|
+ if (item instanceof Map && (((Map<?, ?>) item).containsKey("uniqueNumber")
|
|
|
+ || ((Map<?, ?>) item).containsKey("drAmount"))) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void downloadReceipt(PendingBankingPayment payment, String uniqueNumber) {
|
|
|
+ String businessTransNo = payment.getBusinessTransNo();
|
|
|
+ try {
|
|
|
+ ElectronicBillDownloadRequest request = new ElectronicBillDownloadRequest();
|
|
|
+ if (uniqueNumber != null && !uniqueNumber.trim().isEmpty()) {
|
|
|
+ request.setUniqueNumber(uniqueNumber);
|
|
|
+ } else {
|
|
|
+ request.setBusinessTransNo(businessTransNo);
|
|
|
+ }
|
|
|
+ ElectronicBillDownloadResult receipt = bankingService.downloadElectronicBill(request);
|
|
|
+ ElectronicBillDownloadResult receiptWithUrls = new ElectronicBillDownloadResult(
|
|
|
+ receipt.getFileName(), receipt.getFilePath(), receipt.getFileSize(), receipt.getContentType(),
|
|
|
+ buildReceiptUrl(receipt.getFileName(), "preview"), buildReceiptUrl(receipt.getFileName(), "download"));
|
|
|
+ yidaWriteBackService.writePaymentResult(payment.getFormID(), payment.getFormInstanceId(),
|
|
|
+ PAYMENT_SUCCESS_STATUS, receiptWithUrls);
|
|
|
+ trackingRepository.markCompleted(businessTransNo,
|
|
|
+ receiptWithUrls.getPreviewUrl(), receiptWithUrls.getDownloadUrl());
|
|
|
+ LOGGER.info("Banking electronic bill downloaded businessTransNo={} fileName={}", businessTransNo,
|
|
|
+ receipt.getFileName());
|
|
|
+ } catch (RuntimeException exception) {
|
|
|
+ trackingRepository.markReceiptDownloadFailed(businessTransNo);
|
|
|
+ // 回单下载、文件保存和宜搭链接回写任一环节失败时均保留重试状态,日志必须包含原因以便排查。
|
|
|
+ LOGGER.warn("Banking payment succeeded but electronic bill is unavailable businessTransNo={}",
|
|
|
+ businessTransNo, exception);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String extractPaymentDataText(Map<String, Object> response, String fieldName) {
|
|
|
+ Object value = response == null ? null : response.get(fieldName);
|
|
|
+ return value == null ? null : String.valueOf(value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasText(String value) {
|
|
|
+ return value != null && !value.trim().isEmpty();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildReceiptUrl(String fileName, String option) {
|
|
|
+ return bankingProperties.getPublicBaseUrl().replaceAll("/+$", "")
|
|
|
+ + "/api/banking/electronic-bills/files/"
|
|
|
+ + UriUtils.encodePathSegment(fileName, StandardCharsets.UTF_8)
|
|
|
+ + "?option=" + option;
|
|
|
+ }
|
|
|
+
|
|
|
+}
|