diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcOrderBillService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcOrderBillService.java index 20091b13..d71ce032 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcOrderBillService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcOrderBillService.java @@ -151,7 +151,9 @@ public interface IPrcOrderBillService /** * Full rebill from local prc_order_bill only (no Oracle upstream). - * Rebuilds TmsPriceBillRequest from the bill row then calls {@link #bill}. + * Refund-like local status (order_status 9/10 or refund_status=REFUNDED): + * skip when prc_income_bill_fee already has fee_type=REFUND; otherwise {@link #refund}. + * Else rebuilds request from the bill row then {@link #bill}. */ TmsPriceQuoteResult rebillLocalByBillId(String billId); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcOrderBillServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcOrderBillServiceImpl.java index f4899dda..2b6c581b 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcOrderBillServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcOrderBillServiceImpl.java @@ -255,11 +255,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService } failReq.setTransferNo(bill.getTransferNo()); assertOrderBillEditable(bill); - if (BillRefundStatus.REFUNDED.getCode().equalsIgnoreCase(bill.getRefundStatus())) - { - throw new ServiceException("\u8ba2\u5355\u5df2\u9000\u8d39\uff0c\u4e0d\u53ef\u91cd\u590d\u9000\u8d39"); - } - + // Duplicate guard is fee_type=REFUND on prc_income_bill_fee (refund_status alone may be stale). List fees = incomeBillFeeMapper.selectByBillId(bill.getBillId()); if (fees == null) { @@ -752,8 +748,11 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService private static final long LOCAL_REBILL_DB_RETRY_BASE_MS = 80L; /** - * Local rebill without outer txn: each {@link #bill} starts its own transaction so + * Local rebill without outer txn: each {@link #bill}/{@link #refund} starts its own transaction so * MySQL deadlock can be retried cleanly (same idea as billing-task billWithDbRetry). + *

If local order is refund-like (order_status 9/10 or refund_status=REFUNDED): + * skip when prc_income_bill_fee already has fee_type=REFUND; otherwise call {@link #refund}. + * Otherwise normal {@link #bill}. */ @Override public TmsPriceQuoteResult rebillLocalByBillId(String billId) @@ -775,10 +774,57 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService { throw new ServiceException(MessageUtils.message("price.bill.bizDateRequired")); } + if (isLocalRefundCandidate(bill)) + { + if (hasRefundFeeLine(bill.getBillId())) + { + log.info("local rebill skip refund, REFUND fee exists billId={} orderNo={} orderStatus={} refundStatus={}", + billId, bill.getOrderNo(), bill.getOrderStatus(), bill.getRefundStatus()); + return toResult(bill.getBillId()); + } + log.info("local rebill -> refund path billId={} orderNo={} orderStatus={} refundStatus={}", + billId, bill.getOrderNo(), bill.getOrderStatus(), bill.getRefundStatus()); + return refundLocalWithDbRetry(bill.getOrderNo().trim()); + } TmsPriceBillRequest req = buildBillRequestFromBill(bill); return billLocalWithDbRetry(billId.trim(), req); } + /** order_status 9/10 or refund_status=REFUNDED. */ + private static boolean isLocalRefundCandidate(PrcOrderBill bill) + { + if (bill == null) + { + return false; + } + if (BillOrderStatus.requiresFeeRefund(bill.getOrderStatus())) + { + return true; + } + return BillRefundStatus.REFUNDED.getCode().equalsIgnoreCase(bill.getRefundStatus()); + } + + private boolean hasRefundFeeLine(String billId) + { + if (StringUtils.isEmpty(billId)) + { + return false; + } + List fees = incomeBillFeeMapper.selectByBillId(billId); + if (fees == null || fees.isEmpty()) + { + return false; + } + for (PrcIncomeBillFee row : fees) + { + if (row != null && FEE_TYPE_REFUND.equalsIgnoreCase(row.getFeeType())) + { + return true; + } + } + return false; + } + private TmsPriceQuoteResult billLocalWithDbRetry(String billId, TmsPriceBillRequest billReq) { RuntimeException last = null; @@ -815,6 +861,42 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService throw last != null ? last : new ServiceException("local rebill retry exhausted"); } + private TmsPriceQuoteResult refundLocalWithDbRetry(String orderNo) + { + RuntimeException last = null; + for (int attempt = 1; attempt <= LOCAL_REBILL_DB_RETRY_MAX; attempt++) + { + try + { + return self.refund(orderNo); + } + catch (RuntimeException e) + { + last = e; + if (attempt < LOCAL_REBILL_DB_RETRY_MAX && isRetryableDbConflict(e)) + { + long sleepMs = LOCAL_REBILL_DB_RETRY_BASE_MS * attempt + + (long) (Math.random() * LOCAL_REBILL_DB_RETRY_BASE_MS); + log.warn("local rebill refund db conflict, retry orderNo={} attempt={}/{} sleepMs={} msg={}", + orderNo, Integer.valueOf(attempt), Integer.valueOf(LOCAL_REBILL_DB_RETRY_MAX), + Long.valueOf(sleepMs), StringUtils.substring(e.getMessage(), 0, 200)); + try + { + Thread.sleep(sleepMs); + } + catch (InterruptedException ie) + { + Thread.currentThread().interrupt(); + throw e; + } + continue; + } + throw e; + } + } + throw last != null ? last : new ServiceException("local rebill refund retry exhausted"); + } + /** Deadlock / lock wait — safe to retry whole bill txn (parallel local rebill). */ private static boolean isRetryableDbConflict(Throwable e) {