客户余额加账单核销

This commit is contained in:
luojiaquan
2026-09-16 15:19:05 +08:00
parent f3be74b7a4
commit 2e39440540
15 changed files with 1639 additions and 5 deletions
@@ -1,5 +1,6 @@
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;
@@ -56,4 +57,36 @@ public interface ICustomerMoneyService
* -> 乐观锁更新额度 -> 回写客户档案额度字段 -> 写额度流水
*/
int adjustCredit(CustomerCreditFlow flow);
/**
* 账单确认时占用授信额度(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);
}
@@ -197,12 +197,8 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
BigDecimal frozenBalance = nz(account.getFrozenBalance());
account.setBalance(afterBalance);
if (CustomerReceipt.TYPE_REPAY.equals(receipt.getReceiptType()))
{
account.setUsedCredit(nz(account.getUsedCredit()).subtract(baseAmount).max(BigDecimal.ZERO));
}
account.setUpdateBy(username);
// 额度占用统一以账单为准:收款审核只增加余额,核销账单时才释放已用额度
int accountRows = customerMoneyMapper.updateAccountBalance(account);
if (accountRows == 0)
{
@@ -415,6 +411,175 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
return 1;
}
/* ================= 账单联动:额度占用 / 余额核销 ================= */
@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;
}
/* ================= 额度私有方法 ================= */
/**