diff --git a/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties b/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties index f4b3b054..44a153d3 100644 --- a/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties +++ b/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties @@ -141,6 +141,7 @@ price.quote.hitCustomerFixedPackageDetail=命中客户套餐固定价格明细 price.bill.orderNoRequired=订单号为空 price.bill.billIdRequired=计费ID为空 price.bill.notFound=计费记录不存在 +price.bill.customerRequired=订单同步缺少有效客户 price.bill.bizDateRequired=业务日期为空 price.set.version.effectiveTime.empty=版本生效时间不能为空 diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/api/impl/CustomerAccountApiImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/api/impl/CustomerAccountApiImpl.java index d8730a26..1813c31b 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/api/impl/CustomerAccountApiImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/api/impl/CustomerAccountApiImpl.java @@ -246,8 +246,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) { @@ -281,8 +281,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) { @@ -635,19 +635,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"; } /** 临时额度是否已过期:有失效日期且今天已晚于失效日(失效日当天仍有效) */ diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/EsLogType.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/EsLogType.java index f6b00c0f..83491128 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/EsLogType.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/EsLogType.java @@ -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() { 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 092a5b46..d415e4db 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 @@ -28,6 +28,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 +52,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService @Autowired private IBillSearchStore billSearchStore; + @Autowired + private ICustomerAccountApi customerAccountApi; + @Override @Transactional(rollbackFor = Exception.class) public TmsPriceQuoteResult bill(TmsPriceBillRequest request) @@ -121,6 +127,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService String orderNo = request.getOrderNo(); String billKey = Md5Utils.hash(orderNo); PrcOrderBill existed = orderBillMapper.selectPrcOrderBillByOrderNo(orderNo); + BigDecimal previousIncome = existed == null ? BigDecimal.ZERO : nz(existed.getIncomeTotal()); PrcOrderBill bill = existed == null ? new PrcOrderBill() : existed; if (existed == null) { @@ -209,9 +216,105 @@ 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); + } + + private static BigDecimal nz(BigDecimal v) + { + return v == null ? BigDecimal.ZERO : v; + } + private List toFeeRows(String billId, TmsPriceQuoteResult result) { List fees = new ArrayList(); diff --git a/bms-back/sql/customer+supplier/dict_customer_fund_biz_type.sql b/bms-back/sql/customer+supplier/dict_customer_fund_biz_type.sql new file mode 100644 index 00000000..ebbda3a0 --- /dev/null +++ b/bms-back/sql/customer+supplier/dict_customer_fund_biz_type.sql @@ -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'); diff --git a/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue b/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue index d4b2e825..cce1dfdb 100644 --- a/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue +++ b/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue @@ -934,7 +934,9 @@ const payMethodOptions = [ ] const bizTypeOptions = [ { value: 'RECEIPT', label: '收款' }, - { value: 'BILL_PAY', label: '账单支付' }, + { value: 'AR_BILL', label: '应收账单' }, + { value: 'BILL_PAY', label: '账单核销' }, + { value: 'ORDER', label: '计费订单' }, { value: 'REFUND', label: '退款' }, { value: 'ADJUST', label: '手工调整' }, { value: 'FREEZE', label: '冻结' }, @@ -964,7 +966,7 @@ function receiptStatusTag(v) { return { PENDING: 'warning', CONFIRMED: 'success', CANCELLED: 'danger' }[v] || 'info' } 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' } /* ================= 额度相关枚举 ================= */ diff --git a/bms-front/src/views/monitor/eslog/index.vue b/bms-front/src/views/monitor/eslog/index.vue index 737c03ff..4540000a 100644 --- a/bms-front/src/views/monitor/eslog/index.vue +++ b/bms-front/src/views/monitor/eslog/index.vue @@ -31,6 +31,12 @@ {{ detail.bizId || '-' }} {{ detail.updateTime || '-' }} + + {{ creditActionLabel(detail) }} + + + {{ creditAmountLabel(detail) }} + {{ detail.errorMessage }} @@ -66,7 +72,8 @@ const drawerTitle = ref('ES日志详情') const typeOptions = [ { label: '计费', value: 'BILL' }, - { label: '试算', value: 'QUOTE' } + { label: '试算', value: 'QUOTE' }, + { label: '扣减额度', value: 'CREDIT' } ] const statusOptions = [ @@ -97,9 +104,31 @@ function typeLabel(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) { if (type === 'BILL') return 'success' if (type === 'QUOTE') return 'warning' + if (type === 'CREDIT') return 'danger' return 'info' }