# 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).
* 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);
@@ -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<PrcIncomeBillFee> 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).
* <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
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<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)
{
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)
{