This commit is contained in:
luojiaquan
2026-09-17 17:27:42 +08:00
8 changed files with 203 additions and 13 deletions
@@ -141,6 +141,8 @@ price.quote.hitCustomerFixedPackageDetail=命中客户套餐固定价格明细
price.bill.orderNoRequired=订单号为空
price.bill.billIdRequired=计费ID为空
price.bill.notFound=计费记录不存在
price.bill.customerRequired=订单同步缺少有效客户
price.bill.alreadyInArBill=订单{0}已生成应收账单,作废后方可重新计费
price.bill.bizDateRequired=业务日期为空
price.set.version.effectiveTime.empty=版本生效时间不能为空
@@ -247,8 +247,8 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
validateUsageCommand(command);
Long customerId = resolveCustomerId(command);
String currency = resolveCurrency(command.getCurrency());
String username = SecurityUtils.getUsername();
Long userId = SecurityUtils.getUserId();
String username = currentUsername();
Long userId = currentUserId();
CustomerAccount account = lockAccountForUsage(customerId, currency, username);
if (account == null)
{
@@ -282,8 +282,8 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
validateUsageCommand(command);
Long customerId = resolveCustomerId(command);
String currency = resolveCurrency(command.getCurrency());
String username = SecurityUtils.getUsername();
Long userId = SecurityUtils.getUserId();
String username = currentUsername();
Long userId = currentUserId();
CustomerAccount account = lockAccountForUsage(customerId, currency, username);
if (account == null)
{
@@ -720,19 +720,23 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
private String bizTypeLabel(String bizType)
{
if (CustomerFundBizType.RECEIPT.equals(bizType))
{
return "\u6536\u6b3e";
}
if (CustomerFundBizType.AR_BILL.equals(bizType))
{
return "应收账单";
return "\u5e94\u6536\u8d26\u5355";
}
if (CustomerFundBizType.ORDER.equals(bizType))
{
return "计费订单";
return "\u8ba1\u8d39\u8ba2\u5355";
}
if (CustomerFundBizType.BILL_PAY.equals(bizType))
{
return "账单核销";
return "\u8d26\u5355\u6838\u9500";
}
return "业务";
return "\u4e1a\u52a1";
}
/** 临时额度是否已过期:有失效日期且今天已晚于失效日(失效日当天仍有效) */
@@ -8,6 +8,8 @@ public final class EsLogType
{
public static final String BILL = "BILL";
public static final String QUOTE = "QUOTE";
/** Order credit occupy/release */
public static final String CREDIT = "CREDIT";
private EsLogType()
{
@@ -12,6 +12,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.ar.enums.ArOrderBillStatus;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.StringUtils;
@@ -28,6 +29,9 @@ import com.ruoyi.price.mapper.PrcPayBillFeeMapper;
import com.ruoyi.price.search.EsBizLogSupport;
import com.ruoyi.price.search.EsLogType;
import com.ruoyi.price.search.IBillSearchStore;
import com.ruoyi.customer.customermoney.api.CreditUsageCommand;
import com.ruoyi.customer.customermoney.api.CustomerFundBizType;
import com.ruoyi.customer.customermoney.api.ICustomerAccountApi;
import com.ruoyi.price.service.IPrcOrderBillService;
import com.ruoyi.price.service.ITmsPriceQuoteService;
@@ -49,6 +53,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
@Autowired
private IBillSearchStore billSearchStore;
@Autowired
private ICustomerAccountApi customerAccountApi;
@Override
@Transactional(rollbackFor = Exception.class)
public TmsPriceQuoteResult bill(TmsPriceBillRequest request)
@@ -65,6 +72,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
throw new ServiceException(MessageUtils.message("price.bill.bizDateRequired"));
}
Date bizDate = resolveBizDate(request.getBizDate());
assertOrderBillEditable(request.getOrderNo());
BigDecimal weight = request.getWeight();
TmsPriceQuoteResult result = tmsPriceQuoteService.calculate(request);
persistBill(request, weight, bizDate, result);
@@ -121,6 +129,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
String orderNo = request.getOrderNo();
String billKey = Md5Utils.hash(orderNo);
PrcOrderBill existed = orderBillMapper.selectPrcOrderBillByOrderNo(orderNo);
assertOrderBillEditable(existed);
BigDecimal previousIncome = existed == null ? BigDecimal.ZERO : nz(existed.getIncomeTotal());
PrcOrderBill bill = existed == null ? new PrcOrderBill() : existed;
if (existed == null)
{
@@ -197,6 +207,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
result.setFromCache(Boolean.valueOf(existed != null));
if (existed == null)
{
bill.setArBillStatus(ArOrderBillStatus.UNBILLED.getCode());
orderBillMapper.insertPrcOrderBill(bill);
}
else
@@ -209,9 +220,127 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
{
incomeBillFeeMapper.insertBatch(fees);
}
syncOrderCredit(bill, previousIncome);
billSearchStore.save(EsLogType.BILL, orderNo, toRequestJson(request), toResponseJson(result));
}
/**
* Occupy/release credit by income total delta (bizType=ORDER).
* Always write CREDIT log to ES (SUCCESS / FAIL), then rethrow on failure.
*/
private void syncOrderCredit(PrcOrderBill bill, BigDecimal previousIncome)
{
if (bill == null)
{
return;
}
Long customerId = bill.getCustomerId();
if (customerId == null || customerId.longValue() <= 0L)
{
ServiceException ex = new ServiceException(MessageUtils.message("price.bill.customerRequired"));
saveCreditEsLog(bill, null, "SKIP", false, EsBizLogSupport.errorText(ex));
throw ex;
}
BigDecimal oldAmt = nz(previousIncome);
BigDecimal newAmt = nz(bill.getIncomeTotal());
BigDecimal delta = newAmt.subtract(oldAmt);
if (delta.compareTo(BigDecimal.ZERO) == 0)
{
return;
}
CreditUsageCommand cmd = new CreditUsageCommand();
cmd.setCustomerId(customerId);
cmd.setCustomerCode(bill.getCustomerCode());
cmd.setCurrency(StringUtils.isEmpty(bill.getCurrency()) ? "CNY" : bill.getCurrency());
cmd.setBizType(CustomerFundBizType.ORDER);
cmd.setBizNo(bill.getOrderNo());
cmd.setCheckAvailable(Boolean.TRUE);
String action = delta.compareTo(BigDecimal.ZERO) > 0 ? "OCCUPY" : "RELEASE";
try
{
if (delta.compareTo(BigDecimal.ZERO) > 0)
{
cmd.setAmount(delta);
customerAccountApi.occupyCredit(cmd);
}
else
{
cmd.setAmount(delta.abs());
customerAccountApi.releaseCredit(cmd);
}
saveCreditEsLog(bill, cmd, action, true, null);
}
catch (RuntimeException e)
{
saveCreditEsLog(bill, cmd, action, false, EsBizLogSupport.errorText(e));
throw e;
}
}
/** CREDIT ES log; does not set ATTR_FAIL_LOGGED so BILL fail can still be written. */
private void saveCreditEsLog(PrcOrderBill bill, CreditUsageCommand cmd, String action, boolean success,
String errorMessage)
{
if (billSearchStore == null || bill == null || StringUtils.isEmpty(bill.getOrderNo()))
{
return;
}
JSONObject req = new JSONObject();
req.put("orderNo", bill.getOrderNo());
req.put("billId", bill.getBillId());
req.put("customerId", bill.getCustomerId());
req.put("customerCode", bill.getCustomerCode());
req.put("currency", bill.getCurrency());
req.put("incomeTotal", bill.getIncomeTotal());
req.put("action", action);
req.put("bizType", CustomerFundBizType.ORDER);
if (cmd != null)
{
req.put("amount", cmd.getAmount());
req.put("checkAvailable", cmd.getCheckAvailable());
}
JSONObject resp = new JSONObject();
resp.put("success", Boolean.valueOf(success));
if (success)
{
resp.put("msg", action);
}
else
{
resp.put("msg", errorMessage);
}
String status = success ? EsBizLogSupport.STATUS_SUCCESS : EsBizLogSupport.STATUS_FAIL;
String err = success ? null : EsBizLogSupport.cut(errorMessage);
billSearchStore.save(EsLogType.CREDIT, bill.getOrderNo(), req.toJSONString(), resp.toJSONString(), status, err);
}
/** Block re-bill when order already claimed by an AR bill (BILLED). Void restores UNBILLED. */
private void assertOrderBillEditable(String orderNo)
{
if (StringUtils.isEmpty(orderNo))
{
return;
}
assertOrderBillEditable(orderBillMapper.selectPrcOrderBillByOrderNo(orderNo.trim()));
}
private void assertOrderBillEditable(PrcOrderBill existed)
{
if (existed == null)
{
return;
}
if (ArOrderBillStatus.BILLED.getCode().equals(existed.getArBillStatus()))
{
throw new ServiceException(MessageUtils.message("price.bill.alreadyInArBill", existed.getOrderNo()));
}
}
private static BigDecimal nz(BigDecimal v)
{
return v == null ? BigDecimal.ZERO : v;
}
private List<PrcIncomeBillFee> toFeeRows(String billId, TmsPriceQuoteResult result)
{
List<PrcIncomeBillFee> fees = new ArrayList<PrcIncomeBillFee>();
@@ -167,7 +167,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
set_type, bind_priority, customer_priceset_id, product_country_id,
weight_min, weight_max, weight_label, effective_date, charge_weight,
freight_rate, base_freight_rate, discount_percent, registration_fee,
freight_amount, surcharge_amount, income_total, pay_total, currency, round_mode,
freight_amount, surcharge_amount, income_total, pay_total, currency, ar_bill_status, round_mode,
create_time
) values (
#{billId}, #{billKey}, #{orderNo}, #{trackingNo}, #{customerId}, #{customerCode}, #{customerName},
@@ -178,7 +178,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{setType}, #{bindPriority}, #{customerPricesetId}, #{productCountryId},
#{weightMin}, #{weightMax}, #{weightLabel}, #{effectiveDate}, #{chargeWeight},
#{freightRate}, #{baseFreightRate}, #{discountPercent}, #{registrationFee},
#{freightAmount}, #{surchargeAmount}, #{incomeTotal}, #{payTotal}, #{currency}, #{roundMode},
#{freightAmount}, #{surchargeAmount}, #{incomeTotal}, #{payTotal}, #{currency}, #{arBillStatus}, #{roundMode},
sysdate()
)
</insert>
@@ -0,0 +1,22 @@
-- customer fund biz_type dict (CustomerFundBizType)
-- dict_type: customer_fund_biz_type
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
SELECT '客户资金业务类型', 'customer_fund_biz_type', '0', 'admin', sysdate(), 'ICustomerAccountApi bizType'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'customer_fund_biz_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, '收款', 'RECEIPT', 'customer_fund_biz_type', '', 'success', 'N', '0', 'admin', sysdate(), NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'customer_fund_biz_type' AND dict_value = 'RECEIPT');
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, '应收账单', 'AR_BILL', 'customer_fund_biz_type', '', 'primary', 'N', '0', 'admin', sysdate(), NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'customer_fund_biz_type' AND dict_value = 'AR_BILL');
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, '账单核销', 'BILL_PAY', 'customer_fund_biz_type', '', 'danger', 'N', '0', 'admin', sysdate(), NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'customer_fund_biz_type' AND dict_value = 'BILL_PAY');
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, '计费订单', 'ORDER', 'customer_fund_biz_type', '', 'warning', 'Y', '0', 'admin', sysdate(), 'TMS bill sync occupy/release'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'customer_fund_biz_type' AND dict_value = 'ORDER');