diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBalanceWriteoffServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBalanceWriteoffServiceImpl.java index 9404a573..e70965bd 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBalanceWriteoffServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBalanceWriteoffServiceImpl.java @@ -23,8 +23,10 @@ import com.ruoyi.ar.service.IArBillLogService; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.customer.customermoney.api.BalanceChangeCommand; +import com.ruoyi.customer.customermoney.api.CustomerFundBizType; +import com.ruoyi.customer.customermoney.api.ICustomerAccountApi; import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow; -import com.ruoyi.customer.customermoney.service.ICustomerMoneyService; /** * 应收账单余额核销 Service实现 @@ -48,7 +50,7 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService private IArBillLogService logService; @Autowired - private ICustomerMoneyService customerMoneyService; + private ICustomerAccountApi customerAccountApi; @Override public List selectUnsettledBills(Long customerId) @@ -125,8 +127,15 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService // 3. 扣减账户余额并释放等额已用额度,写 BILL_PAY 扣款流水(余额不足在此处拦截) String flowRemark = "余额核销账单:" + bill.getBillNo() + (StringUtils.hasText(writeoff.getRemark()) ? "(" + writeoff.getRemark() + ")" : ""); - CustomerBalanceFlow flow = customerMoneyService.payBill(bill.getCustomerId(), currency, - amount, writeoff.getWriteoffId(), bill.getBillNo(), flowRemark); + BalanceChangeCommand payCmd = new BalanceChangeCommand(); + payCmd.setCustomerId(bill.getCustomerId()); + payCmd.setCurrency(currency); + payCmd.setAmount(amount); + payCmd.setBizType(CustomerFundBizType.BILL_PAY); + payCmd.setBizId(writeoff.getWriteoffId()); + payCmd.setBizNo(bill.getBillNo()); + payCmd.setRemark(flowRemark); + CustomerBalanceFlow flow = customerAccountApi.deductBalance(payCmd); writeoffMapper.updateFlowRefs(writeoff.getWriteoffId(), flow.getAccountId(), flow.getId()); // 4. 更新账单已核销/未核销缓存与状态(乐观条件更新,状态被并发改动则整体回滚) @@ -178,8 +187,16 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService // 2. 余额回补、已用额度重新占用,写反向 BILL_PAY 流水 String flowRemark = "反核销回补账单:" + bill.getBillNo() + "(" + cancelRemark + ")"; - CustomerBalanceFlow reverseFlow = customerMoneyService.reverseBillPay(writeoff.getCustomerId(), - writeoff.getCurrency(), nvl(writeoff.getWriteoffAmount()), writeoffId, bill.getBillNo(), flowRemark); + BalanceChangeCommand reverseCmd = new BalanceChangeCommand(); + reverseCmd.setCustomerId(writeoff.getCustomerId()); + reverseCmd.setCurrency(writeoff.getCurrency()); + reverseCmd.setAmount(nvl(writeoff.getWriteoffAmount())); + reverseCmd.setBizType(CustomerFundBizType.BILL_PAY); + reverseCmd.setBizId(writeoffId); + reverseCmd.setBizNo(bill.getBillNo()); + reverseCmd.setRemark(flowRemark); + reverseCmd.setReoccupyUsedCredit(Boolean.TRUE); + CustomerBalanceFlow reverseFlow = customerAccountApi.refundBalance(reverseCmd); // 3. 账单核销金额回退,状态按剩余已核销额重算(归零则回到已确认) BigDecimal written = nvl(bill.getWrittenOffAmount()).subtract(nvl(writeoff.getWriteoffAmount())); diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillServiceImpl.java index 836cddf0..90a8b7da 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillServiceImpl.java @@ -58,7 +58,9 @@ import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.file.FileUtils; import com.ruoyi.common.utils.file.FileUploadUtils; import com.ruoyi.common.utils.uuid.IdUtils; -import com.ruoyi.customer.customermoney.service.ICustomerMoneyService; +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.domain.PrcIncomeBillFee; import com.ruoyi.price.domain.PrcOrderBill; import com.ruoyi.price.mapper.PrcIncomeBillFeeMapper; @@ -81,7 +83,7 @@ public class ArBillServiceImpl implements IArBillService @Autowired private IArBillLogService logService; @Autowired private PrcOrderBillMapper sourceBillMapper; @Autowired private PrcIncomeBillFeeMapper sourceFeeMapper; - @Autowired private ICustomerMoneyService customerMoneyService; + @Autowired private ICustomerAccountApi customerAccountApi; @Override public List selectArBillList(ArBillQuery query) @@ -279,9 +281,9 @@ public class ArBillServiceImpl implements IArBillService throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", bill.getStatus(), "账单状态已发生变化,请刷新后重试", operator, requestId); } - // 账单确认:应收形成,按账单金额占用客户授信额度(账户不存在时仅CNY会自动初始化) - customerMoneyService.occupyCredit(bill.getCustomerId(), bill.getCurrency(), - nvl(bill.getTotalAmount())); + // 账单确认:应收形成,按账单金额占用客户授信额度(账户不存在时仅CNY会自动初始化)。 + // 不校验可用额度,兼容历史 used_credit 可高于总额度、外币账户静默跳过的口径 + customerAccountApi.occupyCredit(buildCreditCommand(bill)); recordSuccess(bill, "CONFIRM", "确认应收账单", ArBillStatus.DRAFT.getCode(), ArBillStatus.CONFIRMED.getCode(), requestId, operator); return selectArBillById(arBillId); } @@ -322,8 +324,7 @@ public class ArBillServiceImpl implements IArBillService // 账单作废:已确认账单占用的授信额度全额释放(可作废的CONFIRMED账单必然零核销) if (currentStatus == ArBillStatus.CONFIRMED) { - customerMoneyService.releaseCredit(bill.getCustomerId(), bill.getCurrency(), - nvl(bill.getTotalAmount())); + customerAccountApi.releaseCredit(buildCreditCommand(bill)); } if (!sourceBillIds.isEmpty()) { @@ -860,6 +861,21 @@ public class ArBillServiceImpl implements IArBillService private BigDecimal decimal(BigDecimal value, int scale) { return value == null ? null : value.setScale(scale, java.math.RoundingMode.HALF_UP); } private BigDecimal nvl(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } + + /** 构造账单授信占用/释放命令(确认占用、作废释放共用,关联账单单据便于流水追溯) */ + private CreditUsageCommand buildCreditCommand(ArBill bill) + { + CreditUsageCommand command = new CreditUsageCommand(); + command.setCustomerId(bill.getCustomerId()); + command.setCurrency(bill.getCurrency()); + command.setAmount(nvl(bill.getTotalAmount())); + command.setBizType(CustomerFundBizType.AR_BILL); + command.setBizId(bill.getArBillId()); + command.setBizNo(bill.getBillNo()); + // 兼容历史口径:不校验可用额度(允许 used_credit 高于总额度、外币账户静默跳过) + command.setCheckAvailable(Boolean.FALSE); + return command; + } private String safeFilePart(String value) { return StringUtils.isEmpty(value) ? "客户" : value.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5_-]", "_"); } private String maskAccount(String value) { if (StringUtils.isEmpty(value)) return ""; return value.length() <= 4 ? "****" : "****" + value.substring(value.length() - 4); } private String sha256(byte[] data) throws NoSuchAlgorithmException { byte[] digest = MessageDigest.getInstance("SHA-256").digest(data); StringBuilder sb = new StringBuilder(); for (byte b : digest) sb.append(String.format("%02x", b)); return sb.toString(); } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerCreditFlow.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerCreditFlow.java index 0bce0f0c..25ce1c4c 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerCreditFlow.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerCreditFlow.java @@ -30,6 +30,10 @@ public class CustomerCreditFlow extends BaseEntity public static final String CHANGE_ADJUST = "ADJUST"; /** 变动类型:到期自动失效 */ public static final String CHANGE_EXPIRE = "EXPIRE"; + /** 变动类型:业务单据占用授信(已用额度增加,如账单确认/计费订单开单) */ + public static final String CHANGE_OCCUPY = "OCCUPY"; + /** 变动类型:业务单据释放授信(已用额度减少,如账单作废/计费变动核减) */ + public static final String CHANGE_RELEASE = "RELEASE"; /** 调整方式:设置为 */ public static final String MODE_SET = "SET"; @@ -53,9 +57,18 @@ public class CustomerCreditFlow extends BaseEntity /** 额度类型:CREDIT信用额度 TEMP临时额度 */ private String creditType; - /** 变动类型:INIT初始化 ADJUST调整 EXPIRE到期失效 */ + /** 变动类型:INIT初始化 ADJUST调整 EXPIRE到期失效 OCCUPY授信占用 RELEASE授信释放 */ private String changeType; + /** 业务来源类型:AR_BILL应收账单 ORDER计费订单(仅占用/释放流水) */ + private String bizType; + + /** 关联业务单据ID */ + private Long bizId; + + /** 关联业务单据号 */ + private String bizNo; + /** 变动方向:1增加 -1减少 0持平 */ private Integer direction; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/ICustomerMoneyService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/ICustomerMoneyService.java index 2a955384..27365aab 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/ICustomerMoneyService.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/ICustomerMoneyService.java @@ -1,6 +1,5 @@ package com.ruoyi.customer.customermoney.service; -import java.math.BigDecimal; import java.util.List; import com.ruoyi.customer.customermoney.domain.CustomerAccount; import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow; @@ -75,36 +74,4 @@ public interface ICustomerMoneyService /** 查询额度调整申请详情 */ CustomerCreditApply selectCreditApplyById(Long applyId); - - /** - * 账单确认时占用授信额度(used_credit += amount)。该币种账户不存在时跳过。 - * 调用方必须已持有账单行锁,本方法内部再锁账户(加锁顺序:账单 -> 账户)。 - */ - void occupyCredit(Long customerId, String currency, BigDecimal amount); - - /** - * 账单作废时释放授信额度(used_credit -= amount,不小于0)。该币种账户不存在时跳过。 - * 调用方必须已持有账单行锁。 - */ - void releaseCredit(Long customerId, String currency, BigDecimal amount); - - /** - * 用账户余额核销账单:校验余额充足 -> balance -= amount、used_credit -= amount - * -> 写一条 BILL_PAY 扣款流水(direction=-1)。返回已落库的流水(含ID)。 - * - * @param bizId 余额核销单ID - * @param bizNo 账单号 - */ - CustomerBalanceFlow payBill(Long customerId, String currency, BigDecimal amount, - Long bizId, String bizNo, String remark); - - /** - * 反核销回补:balance += amount、used_credit += amount(重新占用授信) - * -> 写一条 BILL_PAY 回补流水(direction=1)。返回已落库的流水(含ID)。 - * - * @param bizId 余额核销单ID - * @param bizNo 账单号 - */ - CustomerBalanceFlow reverseBillPay(Long customerId, String currency, BigDecimal amount, - Long bizId, String bizNo, String remark); } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/impl/CustomerMoneyServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/impl/CustomerMoneyServiceImpl.java index ad9bb948..92739fd2 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/impl/CustomerMoneyServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/impl/CustomerMoneyServiceImpl.java @@ -18,6 +18,9 @@ import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.customer.customerinfo.domain.BmsCustomer; import com.ruoyi.customer.customerinfo.domain.BmsCustomerAttachment; import com.ruoyi.customer.customerinfo.mapper.BmsCustomerMapper; +import com.ruoyi.customer.customermoney.api.BalanceChangeCommand; +import com.ruoyi.customer.customermoney.api.CustomerFundBizType; +import com.ruoyi.customer.customermoney.api.ICustomerAccountApi; import com.ruoyi.customer.customermoney.domain.CustomerAccount; import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow; import com.ruoyi.customer.customermoney.domain.CustomerCreditApply; @@ -42,8 +45,6 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService /** 本位币币种 */ private static final String BASE_CURRENCY = BmsExchangeRate.BASE_CURRENCY; - /** 新客户默认总额度(CNY) */ - private static final BigDecimal DEFAULT_CREDIT = new BigDecimal("1000"); /** 金额保留位数 */ private static final int MONEY_SCALE = 2; @@ -59,6 +60,10 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService @Autowired private IBmsExchangeRateService exchangeRateService; + /** 客户资金公共服务:账户/余额/授信额度变动统一收口 */ + @Autowired + private ICustomerAccountApi customerAccountApi; + @Override @Transactional(rollbackFor = Exception.class) public List selectAccountList(Long customerId) @@ -70,8 +75,8 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService CustomerAccount account = list.get(i); if (BASE_CURRENCY.equals(account.getCurrency()) && isTempCreditExpired(account)) { - CustomerAccount locked = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY); - expireTempCreditIfNeeded(locked); + CustomerAccount locked = customerAccountApi.lockCnyAccount(customerId); + customerAccountApi.expireTempCreditIfNeeded(locked); if (locked != null) { list.set(i, locked); @@ -193,46 +198,24 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService ? receipt.getBaseAmount() : originalAmount.multiply(rate).setScale(MONEY_SCALE, RoundingMode.HALF_UP); - // 1. 行锁锁定客户本位币(CNY)账户,不存在则按客户档案额度自动初始化。无论收什么币种,一律折算 CNY 入账 - CustomerAccount account = getOrInitCnyAccount(receipt.getCustomerId(), username); - - // 2. 本位币余额变动(预充值/混合增加余额;还款核销释放已用额度) - BigDecimal beforeBalance = nz(account.getBalance()); - BigDecimal afterBalance = beforeBalance.add(baseAmount); - BigDecimal frozenBalance = nz(account.getFrozenBalance()); - - account.setBalance(afterBalance); - account.setUpdateBy(username); + // 1. 本位币入账(公共资金服务内完成:锁户/初始化 -> 余额变动 -> 余额流水)。 // 额度占用统一以账单为准:收款审核只增加余额,核销账单时才释放已用额度 - int accountRows = customerMoneyMapper.updateAccountBalance(account); - if (accountRows == 0) - { - throw new ServiceException("账户余额已被其他操作变动,请刷新后重试"); - } + BalanceChangeCommand fundCmd = new BalanceChangeCommand(); + fundCmd.setCustomerId(receipt.getCustomerId()); + // 流水保留收款原币;无论收什么币种,一律锁CNY账户折算入账 + fundCmd.setCurrency(currency); + fundCmd.setAccountCurrency(BASE_CURRENCY); + fundCmd.setAmount(originalAmount); + fundCmd.setExchangeRate(rate); + fundCmd.setBaseAmount(baseAmount); + fundCmd.setBizType(CustomerFundBizType.RECEIPT); + fundCmd.setBizId(receipt.getId()); + fundCmd.setBizNo(receipt.getReceiptNo()); + fundCmd.setReoccupyUsedCredit(false); + fundCmd.setRemark("收款单审核通过:" + receipt.getReceiptNo()); + customerAccountApi.refundBalance(fundCmd); - // 3. 写余额流水:原币金额留痕,本位币金额参与余额变动 - CustomerBalanceFlow flow = new CustomerBalanceFlow(); - flow.setFlowNo(generateNo("LS")); - flow.setCustomerId(receipt.getCustomerId()); - flow.setAccountId(account.getId()); - flow.setCurrency(currency); - flow.setExchangeRate(rate); - flow.setBaseAmount(baseAmount); - flow.setBizType(CustomerBalanceFlow.BIZ_RECEIPT); - flow.setBizId(receipt.getId()); - flow.setBizNo(receipt.getReceiptNo()); - flow.setDirection(1); - flow.setAmount(originalAmount); - flow.setBeforeBalance(beforeBalance); - flow.setAfterBalance(afterBalance); - flow.setBeforeFrozenBalance(frozenBalance); - flow.setAfterFrozenBalance(frozenBalance); - flow.setOperatorId(userId); - flow.setCreateBy(username); - flow.setRemark("收款单审核通过:" + receipt.getReceiptNo()); - customerMoneyMapper.insertFlow(flow); - - // 4. 收款单置为已确认 + // 2. 收款单置为已确认 receipt.setStatus(CustomerReceipt.STATUS_CONFIRMED); receipt.setConfirmedAt(new Date()); receipt.setConfirmedBy(userId); @@ -571,8 +554,8 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService cmd.setReason(reason); boolean isTemp = CustomerCreditFlow.CREDIT_TYPE_TEMP.equals(creditType); - CustomerAccount account = getOrInitCnyAccount(cmd.getCustomerId(), username); - expireTempCreditIfNeeded(account); + CustomerAccount account = customerAccountApi.lockCnyAccount(cmd.getCustomerId()); + customerAccountApi.expireTempCreditIfNeeded(account); BigDecimal before = isTemp ? nz(account.getExtraCredit()) : nz(account.getTotalCredit()); BigDecimal otherCredit = isTemp ? nz(account.getTotalCredit()) : nz(account.getExtraCredit()); @@ -624,292 +607,19 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService return account; } - /* ================= 账单联动:额度占用 / 余额核销 ================= */ - - @Override - @Transactional(rollbackFor = Exception.class) - public void occupyCredit(Long customerId, String currency, BigDecimal amount) - { - if (customerId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) - { - return; - } - String ccny = StringUtils.hasText(currency) ? currency.trim().toUpperCase() : BASE_CURRENCY; - CustomerAccount account = lockAccountForBill(customerId, ccny, SecurityUtils.getUsername()); - if (account == null) - { - return; - } - account.setUsedCredit(nz(account.getUsedCredit()).add(amount).setScale(MONEY_SCALE, RoundingMode.HALF_UP)); - account.setUpdateBy(SecurityUtils.getUsername()); - if (customerMoneyMapper.updateAccountBalance(account) == 0) - { - throw new ServiceException("账户已被其他操作变动,请刷新后重试"); - } - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void releaseCredit(Long customerId, String currency, BigDecimal amount) - { - if (customerId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) - { - return; - } - String ccny = StringUtils.hasText(currency) ? currency.trim().toUpperCase() : BASE_CURRENCY; - CustomerAccount account = lockAccountForBill(customerId, ccny, SecurityUtils.getUsername()); - if (account == null) - { - return; - } - account.setUsedCredit(nz(account.getUsedCredit()).subtract(amount).max(BigDecimal.ZERO) - .setScale(MONEY_SCALE, RoundingMode.HALF_UP)); - account.setUpdateBy(SecurityUtils.getUsername()); - if (customerMoneyMapper.updateAccountBalance(account) == 0) - { - throw new ServiceException("账户已被其他操作变动,请刷新后重试"); - } - } - - @Override - @Transactional(rollbackFor = Exception.class) - public CustomerBalanceFlow payBill(Long customerId, String currency, BigDecimal amount, - Long bizId, String bizNo, String remark) - { - if (customerId == null) - { - throw new ServiceException("客户ID不能为空"); - } - if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) - { - throw new ServiceException("核销金额必须大于0"); - } - String ccny = StringUtils.hasText(currency) ? currency.trim().toUpperCase() : BASE_CURRENCY; - String username = SecurityUtils.getUsername(); - Long userId = SecurityUtils.getUserId(); - CustomerAccount account = lockAccountForBill(customerId, ccny, username); - if (account == null) - { - throw new ServiceException("客户没有" + ccny + "账户,无法用余额核销"); - } - - BigDecimal beforeBalance = nz(account.getBalance()); - if (beforeBalance.compareTo(amount) < 0) - { - throw new ServiceException("账户余额不足,当前" + ccny + "余额:" + beforeBalance.toPlainString()); - } - BigDecimal afterBalance = beforeBalance.subtract(amount); - BigDecimal frozenBalance = nz(account.getFrozenBalance()); - // 核销账单 = 应收被清偿,同步释放等额授信占用 - account.setBalance(afterBalance); - account.setUsedCredit(nz(account.getUsedCredit()).subtract(amount).max(BigDecimal.ZERO) - .setScale(MONEY_SCALE, RoundingMode.HALF_UP)); - account.setUpdateBy(username); - if (customerMoneyMapper.updateAccountBalance(account) == 0) - { - throw new ServiceException("账户余额已被其他操作变动,请刷新后重试"); - } - - CustomerBalanceFlow flow = newBalanceFlow(account, ccny, amount, -1, - beforeBalance, afterBalance, frozenBalance, frozenBalance, - bizId, bizNo, username, userId, remark); - customerMoneyMapper.insertFlow(flow); - return flow; - } - - @Override - @Transactional(rollbackFor = Exception.class) - public CustomerBalanceFlow reverseBillPay(Long customerId, String currency, BigDecimal amount, - Long bizId, String bizNo, String remark) - { - if (customerId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) - { - throw new ServiceException("反核销金额不正确"); - } - String ccny = StringUtils.hasText(currency) ? currency.trim().toUpperCase() : BASE_CURRENCY; - String username = SecurityUtils.getUsername(); - Long userId = SecurityUtils.getUserId(); - CustomerAccount account = lockAccountForBill(customerId, ccny, username); - if (account == null) - { - throw new ServiceException("客户" + ccny + "账户不存在,无法反核销"); - } - - BigDecimal beforeBalance = nz(account.getBalance()); - BigDecimal afterBalance = beforeBalance.add(amount); - BigDecimal frozenBalance = nz(account.getFrozenBalance()); - // 账单恢复为未核销,授信占用同步加回(允许与存量数据一样出现超出总额度的占用状态) - account.setBalance(afterBalance); - account.setUsedCredit(nz(account.getUsedCredit()).add(amount).setScale(MONEY_SCALE, RoundingMode.HALF_UP)); - account.setUpdateBy(username); - if (customerMoneyMapper.updateAccountBalance(account) == 0) - { - throw new ServiceException("账户余额已被其他操作变动,请刷新后重试"); - } - - CustomerBalanceFlow flow = newBalanceFlow(account, ccny, amount, 1, - beforeBalance, afterBalance, frozenBalance, frozenBalance, - bizId, bizNo, username, userId, remark); - customerMoneyMapper.insertFlow(flow); - return flow; - } - - /** 账单联动场景锁定账户:CNY不存在则初始化(先处理临时额度过期),外币账户不存在返回null */ - private CustomerAccount lockAccountForBill(Long customerId, String currency, String username) - { - if (BASE_CURRENCY.equals(currency)) - { - CustomerAccount account = getOrInitCnyAccount(customerId, username); - expireTempCreditIfNeeded(account); - return account; - } - return customerMoneyMapper.selectAccountForUpdate(customerId, currency); - } - - private CustomerBalanceFlow newBalanceFlow(CustomerAccount account, String currency, BigDecimal amount, - int direction, BigDecimal beforeBalance, BigDecimal afterBalance, - BigDecimal beforeFrozen, BigDecimal afterFrozen, - Long bizId, String bizNo, String username, Long userId, String remark) - { - CustomerBalanceFlow flow = new CustomerBalanceFlow(); - flow.setFlowNo(generateNo("LS")); - flow.setCustomerId(account.getCustomerId()); - flow.setAccountId(account.getId()); - flow.setCurrency(currency); - flow.setExchangeRate(BigDecimal.ONE); - flow.setBaseAmount(amount); - flow.setBizType(CustomerBalanceFlow.BIZ_BILL_PAY); - flow.setBizId(bizId); - flow.setBizNo(bizNo); - flow.setDirection(direction); - flow.setAmount(amount); - flow.setBeforeBalance(beforeBalance); - flow.setAfterBalance(afterBalance); - flow.setBeforeFrozenBalance(beforeFrozen); - flow.setAfterFrozenBalance(afterFrozen); - flow.setOperatorId(userId); - flow.setCreateBy(username); - flow.setRemark(remark); - return flow; - } - /* ================= 额度私有方法 ================= */ /** - * 新建客户时默认初始化CNY账户:余额0,总额度默认1000 CNY - * (客户档案已填信用额度且大于0时按档案值),并写 INIT 流水;已存在则跳过。 + * 新建客户时默认初始化CNY账户(委托公共资金服务统一收口) */ @Override @Transactional(rollbackFor = Exception.class) public int initDefaultCnyAccount(Long customerId) { - if (customerId == null) - { - throw new ServiceException("客户ID不能为空"); - } - String username = SecurityUtils.getUsername(); - CustomerAccount account = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY); - if (account != null) - { - return 1; - } - BmsCustomer customer = bmsCustomerMapper.selectRawCreditByCustomerId(customerId); - if (customer == null) - { - throw new ServiceException("客户不存在"); - } - BigDecimal initCredit = nz(customer.getCreditEd()); - if (initCredit.compareTo(BigDecimal.ZERO) <= 0) - { - initCredit = DEFAULT_CREDIT; - } - BigDecimal initTemp = nz(customer.getTempEd()); - account = createCnyAccount(customerId, initCredit, initTemp, username); - // 档案额度与实际初始额度保持一致 - bmsCustomerMapper.updateCustomerCredit(customerId, initCredit, initTemp, username); - writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_CREDIT, - initCredit, null, null, username, "新客户默认初始化额度1000 CNY"); - if (initTemp.compareTo(BigDecimal.ZERO) > 0) - { - writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_TEMP, - initTemp, null, null, username, "新客户初始化临时额度"); - } - return 1; + return customerAccountApi.initDefaultCnyAccount(customerId); } - /** 行锁获取客户CNY账户,不存在则按客户档案当前额度初始化创建 */ - private CustomerAccount getOrInitCnyAccount(Long customerId, String username) - { - CustomerAccount account = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY); - if (account == null) - { - BmsCustomer customer = bmsCustomerMapper.selectRawCreditByCustomerId(customerId); - if (customer == null) - { - throw new ServiceException("客户不存在"); - } - BigDecimal initCredit = nz(customer.getCreditEd()); - BigDecimal initTemp = nz(customer.getTempEd()); - account = createCnyAccount(customerId, initCredit, initTemp, username); - writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_CREDIT, - initCredit, null, null, username, "首次创建CNY账户,从客户档案同步初始额度"); - if (initTemp.compareTo(BigDecimal.ZERO) > 0) - { - writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_TEMP, - initTemp, null, null, username, "首次创建CNY账户,从客户档案同步临时额度"); - } - } - return account; - } - - /** 创建CNY账户(余额0,已用0) */ - private CustomerAccount createCnyAccount(Long customerId, BigDecimal totalCredit, - BigDecimal extraCredit, String username) - { - CustomerAccount account = new CustomerAccount(); - account.setCustomerId(customerId); - account.setCurrency(BASE_CURRENCY); - account.setBalance(BigDecimal.ZERO); - account.setFrozenBalance(BigDecimal.ZERO); - account.setTotalCredit(totalCredit); - account.setExtraCredit(extraCredit); - account.setUsedCredit(BigDecimal.ZERO); - account.setTax(BigDecimal.ZERO); - account.setVersion(0); - account.setCreateBy(username); - customerMoneyMapper.insertAccount(account); - return account; - } - - /** - * 账户首次初始化时写额度 INIT 流水(从客户档案额度同步) - */ - private void writeInitFlow(CustomerAccount account, String creditType, - BigDecimal initAmount, Date effectiveDate, Date expireDate, - String username, String remark) - { - CustomerCreditFlow flow = new CustomerCreditFlow(); - flow.setFlowNo(generateNo("EDLS")); - flow.setCustomerId(account.getCustomerId()); - flow.setAccountId(account.getId()); - flow.setCreditType(creditType); - flow.setChangeType(CustomerCreditFlow.CHANGE_INIT); - flow.setDirection(initAmount.compareTo(BigDecimal.ZERO) > 0 ? 1 : 0); - flow.setBeforeAmount(BigDecimal.ZERO); - flow.setChangeAmount(initAmount); - flow.setAfterAmount(initAmount); - flow.setUsedCredit(nz(account.getUsedCredit())); - flow.setEffectiveDate(effectiveDate); - flow.setExpireDate(expireDate); - flow.setReason("账户初始化"); - flow.setOperatorId(SecurityUtils.getUserId()); - flow.setCreateBy(username); - flow.setRemark(remark); - flow.setDelFlag("0"); - customerMoneyMapper.insertCreditFlow(flow); - } - - /** 临时额度是否已过期:有失效日期且今天已晚于失效日(失效日当天仍有效) */ + /** 临时额度是否已过期:有失效日期且今天已晚于失效日(失效日当天仍有效)。仅只读判断,不落库 */ private boolean isTempCreditExpired(CustomerAccount account) { return account != null @@ -918,50 +628,6 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService && LocalDate.now().isAfter(toLocalDate(account.getExtraExpireDate())); } - /** - * 临时额度到期自动失效:额度清零 -> 回写客户档案 -> 写EXPIRE流水。 - * 入参必须是事务内行锁查出的账户。 - */ - private void expireTempCreditIfNeeded(CustomerAccount account) - { - if (account == null || !isTempCreditExpired(account)) - { - return; - } - String username = SecurityUtils.getUsername(); - BigDecimal before = nz(account.getExtraCredit()); - account.setExtraCredit(BigDecimal.ZERO); - account.setUpdateBy(username); - int rows = customerMoneyMapper.updateAccountCredit(account); - if (rows == 0) - { - throw new ServiceException("账户额度已被其他操作变动,请刷新后重试"); - } - account.setVersion(account.getVersion() + 1); - bmsCustomerMapper.updateCustomerCredit(account.getCustomerId(), - nz(account.getTotalCredit()), BigDecimal.ZERO, username); - - CustomerCreditFlow flow = new CustomerCreditFlow(); - flow.setFlowNo(generateNo("EDLS")); - flow.setCustomerId(account.getCustomerId()); - flow.setAccountId(account.getId()); - flow.setCreditType(CustomerCreditFlow.CREDIT_TYPE_TEMP); - flow.setChangeType(CustomerCreditFlow.CHANGE_EXPIRE); - flow.setDirection(-1); - flow.setBeforeAmount(before); - flow.setChangeAmount(before); - flow.setAfterAmount(BigDecimal.ZERO); - flow.setUsedCredit(nz(account.getUsedCredit())); - flow.setEffectiveDate(account.getExtraEffectiveDate()); - flow.setExpireDate(account.getExtraExpireDate()); - flow.setReason("临时额度到期自动失效"); - flow.setOperatorId(SecurityUtils.getUserId()); - flow.setCreateBy(username); - flow.setRemark("系统在访问账户时检测到临时额度已过失效日期,自动置零"); - flow.setDelFlag("0"); - customerMoneyMapper.insertCreditFlow(flow); - } - private LocalDate toLocalDate(Date date) { return date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(); diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerMoneyMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerMoneyMapper.xml index c307ca04..2bbb8aed 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerMoneyMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerMoneyMapper.xml @@ -114,6 +114,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + @@ -133,7 +136,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - select id, flow_no, customer_id, account_id, credit_type, change_type, direction, + select id, flow_no, customer_id, account_id, credit_type, change_type, biz_type, biz_id, biz_no, direction, before_amount, change_amount, after_amount, used_credit, effective_date, expire_date, reason, adjust_mode, operator_id, create_by, create_time, update_by, update_time, remark, del_flag @@ -147,6 +150,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and customer_id = #{customerId} and credit_type = #{creditType} and change_type = #{changeType} + and biz_type = #{bizType} + and biz_id = #{bizId} and date(create_time) >= #{params.beginTime} and date(create_time) <= #{params.endTime} and del_flag = '0' @@ -163,6 +168,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" account_id, credit_type, change_type, + biz_type, + biz_id, + biz_no, direction, before_amount, change_amount, @@ -183,6 +191,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{accountId}, #{creditType}, #{changeType}, + #{bizType}, + #{bizId}, + #{bizNo}, #{direction}, #{beforeAmount}, #{changeAmount}, diff --git a/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue b/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue index 62403c19..ca77ecf9 100644 --- a/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue +++ b/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue @@ -972,7 +972,9 @@ const creditTypeOptions = [ const creditChangeTypeOptions = [ { value: 'INIT', label: '初始化' }, { value: 'ADJUST', label: '手工调整' }, - { value: 'EXPIRE', label: '到期失效' } + { value: 'EXPIRE', label: '到期失效' }, + { value: 'OCCUPY', label: '授信占用' }, + { value: 'RELEASE', label: '授信释放' } ] const adjustModeOptions = [ { value: 'SET', label: '设置为' }, @@ -980,7 +982,7 @@ const adjustModeOptions = [ { value: 'SUB', label: '减少' } ] function creditChangeTag(v) { - return { INIT: '', ADJUST: 'primary', EXPIRE: 'danger' }[v] || 'info' + return { INIT: '', ADJUST: 'primary', EXPIRE: 'danger', OCCUPY: 'warning', RELEASE: 'success' }[v] || 'info' } function formatMoney(val) { const num = Number(val)