# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
This commit is contained in:
wuweihua
2026-10-27 01:10:31 +08:00
2 changed files with 91 additions and 7 deletions
@@ -151,7 +151,9 @@ public interface IPrcOrderBillService
/** /**
* Full rebill from local prc_order_bill only (no Oracle upstream). * 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); TmsPriceQuoteResult rebillLocalByBillId(String billId);
@@ -255,11 +255,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
} }
failReq.setTransferNo(bill.getTransferNo()); failReq.setTransferNo(bill.getTransferNo());
assertOrderBillEditable(bill); assertOrderBillEditable(bill);
if (BillRefundStatus.REFUNDED.getCode().equalsIgnoreCase(bill.getRefundStatus())) // Duplicate guard is fee_type=REFUND on prc_income_bill_fee (refund_status alone may be stale).
{
throw new ServiceException("\u8ba2\u5355\u5df2\u9000\u8d39\uff0c\u4e0d\u53ef\u91cd\u590d\u9000\u8d39");
}
List<PrcIncomeBillFee> fees = incomeBillFeeMapper.selectByBillId(bill.getBillId()); List<PrcIncomeBillFee> fees = incomeBillFeeMapper.selectByBillId(bill.getBillId());
if (fees == null) if (fees == null)
{ {
@@ -752,8 +748,11 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
private static final long LOCAL_REBILL_DB_RETRY_BASE_MS = 80L; 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). * MySQL deadlock can be retried cleanly (same idea as billing-task billWithDbRetry).
* <p>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 @Override
public TmsPriceQuoteResult rebillLocalByBillId(String billId) public TmsPriceQuoteResult rebillLocalByBillId(String billId)
@@ -775,10 +774,57 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
{ {
throw new ServiceException(MessageUtils.message("price.bill.bizDateRequired")); 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); TmsPriceBillRequest req = buildBillRequestFromBill(bill);
return billLocalWithDbRetry(billId.trim(), req); 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<PrcIncomeBillFee> 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) private TmsPriceQuoteResult billLocalWithDbRetry(String billId, TmsPriceBillRequest billReq)
{ {
RuntimeException last = null; RuntimeException last = null;
@@ -815,6 +861,42 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
throw last != null ? last : new ServiceException("local rebill retry exhausted"); 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). */ /** Deadlock / lock wait — safe to retry whole bill txn (parallel local rebill). */
private static boolean isRetryableDbConflict(Throwable e) private static boolean isRetryableDbConflict(Throwable e)
{ {