From 75bf3223e276a58bbff5f740fe8407302dd2f7a5 Mon Sep 17 00:00:00 2001 From: wuweihua <88888888> Date: Wed, 23 Sep 2026 16:53:58 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=80=80=E8=B4=B9=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../price/service/IPrcOrderBillService.java | 8 +- .../impl/PrcBillingTaskServiceImpl.java | 89 +++++++++++++-- .../service/impl/PrcOrderBillServiceImpl.java | 104 +++++++++++++++++- .../mapper/price/PrcIncomeBillFeeMapper.xml | 6 +- .../mapper/price/PrcPayBillFeeMapper.xml | 2 +- .../mapper/price/PrcQuoteFeeMapper.xml | 2 +- .../sql/price/dict_bms_fee_type_refund.sql | 22 ++++ 7 files changed, 215 insertions(+), 18 deletions(-) create mode 100644 bms-back/sql/price/dict_bms_fee_type_refund.sql diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcOrderBillService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcOrderBillService.java index 2668c25f..abc6cd75 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcOrderBillService.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcOrderBillService.java @@ -14,6 +14,13 @@ public interface IPrcOrderBillService { TmsPriceQuoteResult bill(TmsPriceBillRequest request); + /** + * 退费:保留原费用明细,在 prc_income_bill_fee 增加一条负金额(feeType=REFUND),重算应收并回补余额。 + * + * @param orderNo 订单号 + */ + TmsPriceQuoteResult refund(String orderNo); + List selectBillList(PrcOrderBill query); /** 订单号下拉搜索选项(模糊匹配,去重限量) */ @@ -59,4 +66,3 @@ public interface IPrcOrderBillService */ TmsPriceQuoteResult updateCreditArrearsStatus(String billId); } - diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcBillingTaskServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcBillingTaskServiceImpl.java index 1cc3885b..d68cae85 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcBillingTaskServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcBillingTaskServiceImpl.java @@ -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 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 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). */ diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java index e6e3a0e1..3af0d884 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java @@ -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 fees = incomeBillFeeMapper.selectByBillId(bill.getBillId()); + if (fees == null) + { + fees = new ArrayList(); + } + 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 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); } diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcIncomeBillFeeMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcIncomeBillFeeMapper.xml index afe80c2f..06b8f9d2 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcIncomeBillFeeMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcIncomeBillFeeMapper.xml @@ -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 diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteFeeMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteFeeMapper.xml index 8caa76b5..485e4769 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteFeeMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteFeeMapper.xml @@ -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 diff --git a/bms-back/sql/price/dict_bms_fee_type_refund.sql b/bms-back/sql/price/dict_bms_fee_type_refund.sql new file mode 100644 index 00000000..e2a68dc0 --- /dev/null +++ b/bms-back/sql/price/dict_bms_fee_type_refund.sql @@ -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');