实现扣减额度功能
This commit is contained in:
@@ -141,6 +141,7 @@ price.quote.hitCustomerFixedPackageDetail=命中客户套餐固定价格明细
|
|||||||
price.bill.orderNoRequired=订单号为空
|
price.bill.orderNoRequired=订单号为空
|
||||||
price.bill.billIdRequired=计费ID为空
|
price.bill.billIdRequired=计费ID为空
|
||||||
price.bill.notFound=计费记录不存在
|
price.bill.notFound=计费记录不存在
|
||||||
|
price.bill.customerRequired=订单同步缺少有效客户
|
||||||
price.bill.bizDateRequired=业务日期为空
|
price.bill.bizDateRequired=业务日期为空
|
||||||
|
|
||||||
price.set.version.effectiveTime.empty=版本生效时间不能为空
|
price.set.version.effectiveTime.empty=版本生效时间不能为空
|
||||||
|
|||||||
+12
-8
@@ -246,8 +246,8 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
|||||||
validateUsageCommand(command);
|
validateUsageCommand(command);
|
||||||
Long customerId = resolveCustomerId(command);
|
Long customerId = resolveCustomerId(command);
|
||||||
String currency = resolveCurrency(command.getCurrency());
|
String currency = resolveCurrency(command.getCurrency());
|
||||||
String username = SecurityUtils.getUsername();
|
String username = currentUsername();
|
||||||
Long userId = SecurityUtils.getUserId();
|
Long userId = currentUserId();
|
||||||
CustomerAccount account = lockAccountForUsage(customerId, currency, username);
|
CustomerAccount account = lockAccountForUsage(customerId, currency, username);
|
||||||
if (account == null)
|
if (account == null)
|
||||||
{
|
{
|
||||||
@@ -281,8 +281,8 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
|||||||
validateUsageCommand(command);
|
validateUsageCommand(command);
|
||||||
Long customerId = resolveCustomerId(command);
|
Long customerId = resolveCustomerId(command);
|
||||||
String currency = resolveCurrency(command.getCurrency());
|
String currency = resolveCurrency(command.getCurrency());
|
||||||
String username = SecurityUtils.getUsername();
|
String username = currentUsername();
|
||||||
Long userId = SecurityUtils.getUserId();
|
Long userId = currentUserId();
|
||||||
CustomerAccount account = lockAccountForUsage(customerId, currency, username);
|
CustomerAccount account = lockAccountForUsage(customerId, currency, username);
|
||||||
if (account == null)
|
if (account == null)
|
||||||
{
|
{
|
||||||
@@ -635,19 +635,23 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
|||||||
|
|
||||||
private String bizTypeLabel(String bizType)
|
private String bizTypeLabel(String bizType)
|
||||||
{
|
{
|
||||||
|
if (CustomerFundBizType.RECEIPT.equals(bizType))
|
||||||
|
{
|
||||||
|
return "\u6536\u6b3e";
|
||||||
|
}
|
||||||
if (CustomerFundBizType.AR_BILL.equals(bizType))
|
if (CustomerFundBizType.AR_BILL.equals(bizType))
|
||||||
{
|
{
|
||||||
return "应收账单";
|
return "\u5e94\u6536\u8d26\u5355";
|
||||||
}
|
}
|
||||||
if (CustomerFundBizType.ORDER.equals(bizType))
|
if (CustomerFundBizType.ORDER.equals(bizType))
|
||||||
{
|
{
|
||||||
return "计费订单";
|
return "\u8ba1\u8d39\u8ba2\u5355";
|
||||||
}
|
}
|
||||||
if (CustomerFundBizType.BILL_PAY.equals(bizType))
|
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 BILL = "BILL";
|
||||||
public static final String QUOTE = "QUOTE";
|
public static final String QUOTE = "QUOTE";
|
||||||
|
/** Order credit occupy/release */
|
||||||
|
public static final String CREDIT = "CREDIT";
|
||||||
|
|
||||||
private EsLogType()
|
private EsLogType()
|
||||||
{
|
{
|
||||||
|
|||||||
+103
@@ -28,6 +28,9 @@ import com.ruoyi.price.mapper.PrcPayBillFeeMapper;
|
|||||||
import com.ruoyi.price.search.EsBizLogSupport;
|
import com.ruoyi.price.search.EsBizLogSupport;
|
||||||
import com.ruoyi.price.search.EsLogType;
|
import com.ruoyi.price.search.EsLogType;
|
||||||
import com.ruoyi.price.search.IBillSearchStore;
|
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.IPrcOrderBillService;
|
||||||
import com.ruoyi.price.service.ITmsPriceQuoteService;
|
import com.ruoyi.price.service.ITmsPriceQuoteService;
|
||||||
|
|
||||||
@@ -49,6 +52,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IBillSearchStore billSearchStore;
|
private IBillSearchStore billSearchStore;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ICustomerAccountApi customerAccountApi;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public TmsPriceQuoteResult bill(TmsPriceBillRequest request)
|
public TmsPriceQuoteResult bill(TmsPriceBillRequest request)
|
||||||
@@ -121,6 +127,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
|||||||
String orderNo = request.getOrderNo();
|
String orderNo = request.getOrderNo();
|
||||||
String billKey = Md5Utils.hash(orderNo);
|
String billKey = Md5Utils.hash(orderNo);
|
||||||
PrcOrderBill existed = orderBillMapper.selectPrcOrderBillByOrderNo(orderNo);
|
PrcOrderBill existed = orderBillMapper.selectPrcOrderBillByOrderNo(orderNo);
|
||||||
|
BigDecimal previousIncome = existed == null ? BigDecimal.ZERO : nz(existed.getIncomeTotal());
|
||||||
PrcOrderBill bill = existed == null ? new PrcOrderBill() : existed;
|
PrcOrderBill bill = existed == null ? new PrcOrderBill() : existed;
|
||||||
if (existed == null)
|
if (existed == null)
|
||||||
{
|
{
|
||||||
@@ -209,9 +216,105 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
|||||||
{
|
{
|
||||||
incomeBillFeeMapper.insertBatch(fees);
|
incomeBillFeeMapper.insertBatch(fees);
|
||||||
}
|
}
|
||||||
|
syncOrderCredit(bill, previousIncome);
|
||||||
billSearchStore.save(EsLogType.BILL, orderNo, toRequestJson(request), toResponseJson(result));
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal nz(BigDecimal v)
|
||||||
|
{
|
||||||
|
return v == null ? BigDecimal.ZERO : v;
|
||||||
|
}
|
||||||
|
|
||||||
private List<PrcIncomeBillFee> toFeeRows(String billId, TmsPriceQuoteResult result)
|
private List<PrcIncomeBillFee> toFeeRows(String billId, TmsPriceQuoteResult result)
|
||||||
{
|
{
|
||||||
List<PrcIncomeBillFee> fees = new ArrayList<PrcIncomeBillFee>();
|
List<PrcIncomeBillFee> fees = new ArrayList<PrcIncomeBillFee>();
|
||||||
|
|||||||
@@ -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');
|
||||||
@@ -934,7 +934,9 @@ const payMethodOptions = [
|
|||||||
]
|
]
|
||||||
const bizTypeOptions = [
|
const bizTypeOptions = [
|
||||||
{ value: 'RECEIPT', label: '收款' },
|
{ value: 'RECEIPT', label: '收款' },
|
||||||
{ value: 'BILL_PAY', label: '账单支付' },
|
{ value: 'AR_BILL', label: '应收账单' },
|
||||||
|
{ value: 'BILL_PAY', label: '账单核销' },
|
||||||
|
{ value: 'ORDER', label: '计费订单' },
|
||||||
{ value: 'REFUND', label: '退款' },
|
{ value: 'REFUND', label: '退款' },
|
||||||
{ value: 'ADJUST', label: '手工调整' },
|
{ value: 'ADJUST', label: '手工调整' },
|
||||||
{ value: 'FREEZE', label: '冻结' },
|
{ value: 'FREEZE', label: '冻结' },
|
||||||
@@ -964,7 +966,7 @@ function receiptStatusTag(v) {
|
|||||||
return { PENDING: 'warning', CONFIRMED: 'success', CANCELLED: 'danger' }[v] || 'info'
|
return { PENDING: 'warning', CONFIRMED: 'success', CANCELLED: 'danger' }[v] || 'info'
|
||||||
}
|
}
|
||||||
const bizTypeTag = function(v) {
|
const bizTypeTag = function(v) {
|
||||||
return { RECEIPT: 'success', BILL_PAY: 'danger', REFUND: 'warning', ADJUST: 'info', FREEZE: 'info', UNFREEZE: '' }[v] || 'info'
|
return { RECEIPT: 'success', AR_BILL: 'primary', BILL_PAY: 'danger', ORDER: 'warning', REFUND: 'warning', ADJUST: 'info', FREEZE: 'info', UNFREEZE: '' }[v] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================= 额度相关枚举 ================= */
|
/* ================= 额度相关枚举 ================= */
|
||||||
|
|||||||
@@ -31,6 +31,12 @@
|
|||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="业务ID">{{ detail.bizId || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="业务ID">{{ detail.bizId || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="写入时间">{{ detail.updateTime || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="写入时间">{{ detail.updateTime || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item v-if="detail.logType === 'CREDIT'" label="扣减动作">
|
||||||
|
{{ creditActionLabel(detail) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item v-if="detail.logType === 'CREDIT'" label="金额">
|
||||||
|
{{ creditAmountLabel(detail) }}
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item v-if="detail.errorMessage" label="错误信息" :span="2">
|
<el-descriptions-item v-if="detail.errorMessage" label="错误信息" :span="2">
|
||||||
<span class="err-text">{{ detail.errorMessage }}</span>
|
<span class="err-text">{{ detail.errorMessage }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -66,7 +72,8 @@ const drawerTitle = ref('ES日志详情')
|
|||||||
|
|
||||||
const typeOptions = [
|
const typeOptions = [
|
||||||
{ label: '计费', value: 'BILL' },
|
{ label: '计费', value: 'BILL' },
|
||||||
{ label: '试算', value: 'QUOTE' }
|
{ label: '试算', value: 'QUOTE' },
|
||||||
|
{ label: '扣减额度', value: 'CREDIT' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
@@ -97,9 +104,31 @@ function typeLabel(type) {
|
|||||||
return found ? found.label : (type || '-')
|
return found ? found.label : (type || '-')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseReq(detail) {
|
||||||
|
if (!detail || !detail.requestJson) return {}
|
||||||
|
try {
|
||||||
|
return typeof detail.requestJson === 'object' ? detail.requestJson : JSON.parse(detail.requestJson)
|
||||||
|
} catch (e) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function creditActionLabel(detail) {
|
||||||
|
const req = parseReq(detail)
|
||||||
|
const map = { OCCUPY: '占用', RELEASE: '释放', SKIP: '跳过' }
|
||||||
|
return map[req.action] || req.action || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function creditAmountLabel(detail) {
|
||||||
|
const req = parseReq(detail)
|
||||||
|
if (req.amount == null || req.amount === '') return '-'
|
||||||
|
return String(req.amount) + (req.currency ? (' ' + req.currency) : '')
|
||||||
|
}
|
||||||
|
|
||||||
function typeTag(type) {
|
function typeTag(type) {
|
||||||
if (type === 'BILL') return 'success'
|
if (type === 'BILL') return 'success'
|
||||||
if (type === 'QUOTE') return 'warning'
|
if (type === 'QUOTE') return 'warning'
|
||||||
|
if (type === 'CREDIT') return 'danger'
|
||||||
return 'info'
|
return 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user