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..7047dc50 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,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=版本生效时间不能为空 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 ad15e3e1..a3b8587b 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 @@ -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"; } /** 临时额度是否已过期:有失效日期且今天已晚于失效日(失效日当天仍有效) */ 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..52120ef5 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 @@ -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 toFeeRows(String billId, TmsPriceQuoteResult result) { List fees = new ArrayList(); diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml index e32eda16..6747106f 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml @@ -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() ) 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 3c9e88e5..9847c1af 100644 --- a/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue +++ b/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue @@ -943,7 +943,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: '冻结' }, @@ -973,7 +975,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' }