增加退费功能

This commit is contained in:
wuweihua
2026-09-23 16:53:58 +08:00
parent 20363acefe
commit 75bf3223e2
7 changed files with 215 additions and 18 deletions
@@ -14,6 +14,13 @@ public interface IPrcOrderBillService
{
TmsPriceQuoteResult bill(TmsPriceBillRequest request);
/**
* 退费:保留原费用明细,在 prc_income_bill_fee 增加一条负金额(feeType=REFUND),重算应收并回补余额。
*
* @param orderNo 订单号
*/
TmsPriceQuoteResult refund(String orderNo);
List<PrcOrderBill> selectBillList(PrcOrderBill query);
/** 订单号下拉搜索选项(模糊匹配,去重限量) */
@@ -59,4 +66,3 @@ public interface IPrcOrderBillService
*/
TmsPriceQuoteResult updateCreditArrearsStatus(String billId);
}
@@ -120,6 +120,21 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
}
try
{
if (task.getBillTime() == null)
{
throw new ServiceException("task billTime empty (billing time from mobile required)");
}
BillTaskType type = BillTaskType.fromCode(task.getBillType());
if (BillTaskType.REFUND == type)
{
String refundOrderNo = resolveRefundOrderNo(task);
log.info("billing task REFUND path taskId={} orderNo={}", taskId, refundOrderNo);
TmsPriceQuoteResult result = refundWithDbRetry(taskId, refundOrderNo);
writeback(task, BillTaskSuccessStatus.SUCCESS, result == null ? null : result.getBillId(),
null);
return;
}
Map<String, Object> row = oracleUpstreamOrderService.findMainOrder(
task.getTransferNo(), task.getOrderNo(), task.getTrackingNo());
if (row == null || row.isEmpty())
@@ -158,19 +173,8 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
billReq.setOrderNo(task.getOrderNo());
}
// Billing date from task.bill_time (mobile), not Oracle CREATE_DATE.
if (task.getBillTime() == null)
{
throw new ServiceException("task billTime empty (billing time from mobile required)");
}
billReq.setBizDate(new SimpleDateFormat("yyyy-MM-dd").format(task.getBillTime()));
BillTaskType type = BillTaskType.fromCode(task.getBillType());
if (BillTaskType.REFUND == type)
{
// V1: refund still syncs latest Oracle snapshot into bill (business refine later).
log.info("billing task REFUND path uses bill upsert taskId={}", taskId);
}
TmsPriceQuoteResult result = billWithDbRetry(taskId, billReq);
writeback(task, BillTaskSuccessStatus.SUCCESS, result == null ? null : result.getBillId(),
null);
@@ -182,6 +186,69 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
}
}
/** Resolve orderNo for REFUND from task keys or existing bill (no Oracle quote required). */
private String resolveRefundOrderNo(PrcBillingTask task)
{
if (StringUtils.isNotEmpty(task.getOrderNo()))
{
return task.getOrderNo().trim();
}
if (StringUtils.isNotEmpty(task.getTransferNo()))
{
PrcOrderBill byTransfer = orderBillMapper.selectPrcOrderBillByTransferNo(task.getTransferNo().trim());
if (byTransfer != null && StringUtils.isNotEmpty(byTransfer.getOrderNo()))
{
return byTransfer.getOrderNo();
}
}
if (StringUtils.isNotEmpty(task.getTrackingNo()))
{
PrcOrderBill q = new PrcOrderBill();
q.setTrackingNo(task.getTrackingNo().trim());
List<PrcOrderBill> list = orderBillMapper.selectPrcOrderBillList(q);
if (list != null && !list.isEmpty() && StringUtils.isNotEmpty(list.get(0).getOrderNo()))
{
return list.get(0).getOrderNo();
}
}
throw new ServiceException("退费订单号无法解析");
}
private TmsPriceQuoteResult refundWithDbRetry(Long taskId, String orderNo)
{
RuntimeException last = null;
for (int attempt = 1; attempt <= BILL_DB_RETRY_MAX; attempt++)
{
try
{
return prcOrderBillService.refund(orderNo);
}
catch (RuntimeException e)
{
last = e;
if (attempt < BILL_DB_RETRY_MAX && isRetryableDbConflict(e))
{
long sleepMs = BILL_DB_RETRY_BASE_MS * attempt
+ (long) (Math.random() * BILL_DB_RETRY_BASE_MS);
log.warn("billing refund db conflict, retry taskId={} attempt={}/{} sleepMs={} msg={}",
taskId, attempt, BILL_DB_RETRY_MAX, sleepMs, cut(e.getMessage(), 200));
try
{
Thread.sleep(sleepMs);
}
catch (InterruptedException ie)
{
Thread.currentThread().interrupt();
throw e;
}
continue;
}
throw e;
}
}
throw last != null ? last : new ServiceException("billing refund retry exhausted");
}
/**
* Retry bill() on MySQL deadlock / lock wait (common when many tasks share one customer_account).
*/
@@ -5,6 +5,7 @@ import java.math.RoundingMode;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashSet;
@@ -21,6 +22,7 @@ import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.ar.enums.ArIncomeBillFeeStatus;
import com.ruoyi.ar.enums.ArOrderBillStatus;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DictUtils;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
@@ -65,6 +67,9 @@ import com.ruoyi.price.task.PrcCreditArrearsClearAsyncRunner;
@Service
public class PrcOrderBillServiceImpl implements IPrcOrderBillService
{
private static final String DICT_FEE_TYPE = "bms_fee_type";
private static final String FEE_TYPE_REFUND = "REFUND";
@Autowired
private ITmsPriceQuoteService tmsPriceQuoteService;
@@ -131,6 +136,102 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
}
}
/**
* 退费:不删除原费用明细;在 prc_income_bill_fee 增加一条负金额(feeType=REFUND),
* 重算应收合计并按差额回补账户余额。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public TmsPriceQuoteResult refund(String orderNo)
{
TmsPriceBillRequest failReq = new TmsPriceBillRequest();
try
{
if (StringUtils.isEmpty(orderNo))
{
throw new ServiceException(MessageUtils.message("price.bill.orderNoRequired"));
}
orderNo = orderNo.trim();
failReq.setOrderNo(orderNo);
PrcOrderBill bill = orderBillMapper.selectPrcOrderBillByOrderNo(orderNo);
if (bill == null)
{
throw new ServiceException(MessageUtils.message("price.bill.notFound"));
}
assertOrderBillEditable(bill);
List<PrcIncomeBillFee> fees = incomeBillFeeMapper.selectByBillId(bill.getBillId());
if (fees == null)
{
fees = new ArrayList<PrcIncomeBillFee>();
}
for (PrcIncomeBillFee row : fees)
{
if (row != null && FEE_TYPE_REFUND.equalsIgnoreCase(row.getFeeType()))
{
throw new ServiceException("订单已退费,不可重复退费");
}
}
BigDecimal feeSum = BigDecimal.ZERO;
for (PrcIncomeBillFee row : fees)
{
feeSum = feeSum.add(nz(row.getAmount()));
}
if (feeSum.compareTo(BigDecimal.ZERO) <= 0)
{
throw new ServiceException("应收总额已为 0,无需退费");
}
PrcIncomeBill incomeBill = incomeBillMapper.selectByBillId(bill.getBillId());
if (incomeBill == null)
{
incomeBill = persistIncomeBill(bill);
}
PrcIncomeBillFee refundFee = new PrcIncomeBillFee();
refundFee.setFeeId(UUID.randomUUID().toString());
refundFee.setBillId(bill.getBillId());
refundFee.setIncomeBillId(incomeBill == null ? null : incomeBill.getIncomeBillId());
String refundFeeType = FEE_TYPE_REFUND;
String refundFeeName = DictUtils.getDictLabel(DICT_FEE_TYPE, refundFeeType);
if (StringUtils.isEmpty(refundFeeName))
{
refundFeeName = "退费";
}
refundFee.setFeeType(refundFeeType);
refundFee.setFeeName(refundFeeName);
refundFee.setScId(Long.valueOf(0L));
refundFee.setAmount(feeSum.negate());
refundFee.setCurrency(StringUtils.isEmpty(bill.getCurrency()) ? "CNY" : bill.getCurrency());
refundFee.setRoundMode(bill.getRoundMode());
refundFee.setRemark("计费任务 REFUND 回补");
refundFee.setArBillStatus(ArIncomeBillFeeStatus.UNBILLED.getCode());
refundFee.setManualFlag("0");
incomeBillFeeMapper.insertPrcIncomeBillFee(refundFee);
TmsPriceQuoteResult result = refreshBillIncomeTotals(bill);
JSONObject req = new JSONObject();
req.put("action", "REFUND");
req.put("orderNo", orderNo);
req.put("billId", bill.getBillId());
req.put("refundAmount", feeSum.negate());
req.put("feeId", refundFee.getFeeId());
if (billSearchStore != null)
{
billSearchStore.save(EsLogType.BILL, orderNo, req.toJSONString(), toResponseJson(result));
}
return result;
}
catch (RuntimeException e)
{
EsBizLogSupport.saveFail(billSearchStore, EsLogType.BILL,
EsBizLogSupport.billFailBizId(failReq), failReq, e);
throw e;
}
}
@Override
public List<PrcOrderBill> selectBillList(PrcOrderBill query)
{
@@ -392,7 +493,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
BigDecimal amt = nz(row.getAmount());
incomeTotal = incomeTotal.add(amt);
if (!"BASIC_FREIGHT".equalsIgnoreCase(row.getFeeType())
&& !"REGISTRATION_FEE".equalsIgnoreCase(row.getFeeType()))
&& !"REGISTRATION_FEE".equalsIgnoreCase(row.getFeeType())
&& !FEE_TYPE_REFUND.equalsIgnoreCase(row.getFeeType()))
{
surchargeTotal = surchargeTotal.add(amt);
}
@@ -25,7 +25,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
ar_bill_status, manual_flag, create_time
from prc_income_bill_fee
where bill_id = #{billId}
order by field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE'), fee_id
order by field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE', 'REFUND'), fee_id
</select>
<select id="selectByBillIds" resultMap="PrcIncomeBillFeeResult">
@@ -36,7 +36,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<foreach collection="billIds" item="billId" open="(" separator="," close=")">
#{billId}
</foreach>
order by bill_id, field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE'), fee_id
order by bill_id, field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE', 'REFUND'), fee_id
</select>
<select id="selectUnbilledByBillIds" resultMap="PrcIncomeBillFeeResult">
@@ -48,7 +48,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<foreach collection="billIds" item="billId" open="(" separator="," close=")">
#{billId}
</foreach>
order by bill_id, field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE'), fee_id
order by bill_id, field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE', 'REFUND'), fee_id
</select>
<select id="selectByFeeId" parameterType="String" resultMap="PrcIncomeBillFeeResult">
@@ -22,7 +22,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, create_time
from prc_pay_bill_fee
where bill_id = #{billId}
order by field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE'), fee_id
order by field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE', 'REFUND'), fee_id
</select>
<insert id="insertPrcPayBillFee" parameterType="PrcPayBillFee">
@@ -21,7 +21,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select fee_id, quote_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, create_time
from prc_quote_fee
where quote_id = #{quoteId}
order by field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE'), fee_id
order by field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE', 'REFUND'), fee_id
</select>
<insert id="insertPrcQuoteFee" parameterType="PrcQuoteFee">
@@ -0,0 +1,22 @@
-- bms_fee_type: add REFUND (refund fee type)
SET NAMES utf8mb4;
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
SELECT N'费用类型', 'bms_fee_type', '0', 'admin', sysdate(), N'计费/附加费费用类型'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'bms_fee_type');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT 1, N'基础运费', 'BASIC_FREIGHT', 'bms_fee_type', '', 'primary', 'N', '0', 'admin', sysdate(), NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_fee_type' AND dict_value = 'BASIC_FREIGHT');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT 2, N'挂号费', 'REGISTRATION_FEE', 'bms_fee_type', '', 'info', 'N', '0', 'admin', sysdate(), NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_fee_type' AND dict_value = 'REGISTRATION_FEE');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT 3, N'附加费', 'SURCHARGE', 'bms_fee_type', '', 'warning', 'N', '0', 'admin', sysdate(), NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_fee_type' AND dict_value = 'SURCHARGE');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT 4, N'退费', 'REFUND', 'bms_fee_type', '', 'danger', 'N', '0', 'admin', sysdate(), N'计费任务 billType=REFUND 负金额明细'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_fee_type' AND dict_value = 'REFUND');