额度余额变动抽公共方法

This commit is contained in:
luojiaquan
2026-09-17 09:54:21 +08:00
parent e67dfbca7b
commit 85a3752107
7 changed files with 106 additions and 414 deletions
@@ -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<ArBill> 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()));
@@ -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<ArBill> 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(); }
@@ -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;
@@ -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);
}
@@ -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<CustomerAccount> 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();
@@ -114,6 +114,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="accountId" column="account_id" />
<result property="creditType" column="credit_type" />
<result property="changeType" column="change_type" />
<result property="bizType" column="biz_type" />
<result property="bizId" column="biz_id" />
<result property="bizNo" column="biz_no" />
<result property="direction" column="direction" />
<result property="beforeAmount" column="before_amount" />
<result property="changeAmount" column="change_amount" />
@@ -133,7 +136,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectCustomerCreditFlowVo">
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"
<if test="customerId != null"> and customer_id = #{customerId}</if>
<if test="creditType != null and creditType != ''"> and credit_type = #{creditType}</if>
<if test="changeType != null and changeType != ''"> and change_type = #{changeType}</if>
<if test="bizType != null and bizType != ''"> and biz_type = #{bizType}</if>
<if test="bizId != null"> and biz_id = #{bizId}</if>
<if test="params.beginTime != null and params.beginTime != ''"> and date(create_time) &gt;= #{params.beginTime}</if>
<if test="params.endTime != null and params.endTime != ''"> and date(create_time) &lt;= #{params.endTime}</if>
and del_flag = '0'
@@ -163,6 +168,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="accountId != null">account_id,</if>
<if test="creditType != null">credit_type,</if>
<if test="changeType != null">change_type,</if>
<if test="bizType != null">biz_type,</if>
<if test="bizId != null">biz_id,</if>
<if test="bizNo != null">biz_no,</if>
<if test="direction != null">direction,</if>
<if test="beforeAmount != null">before_amount,</if>
<if test="changeAmount != null">change_amount,</if>
@@ -183,6 +191,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="accountId != null">#{accountId},</if>
<if test="creditType != null">#{creditType},</if>
<if test="changeType != null">#{changeType},</if>
<if test="bizType != null">#{bizType},</if>
<if test="bizId != null">#{bizId},</if>
<if test="bizNo != null">#{bizNo},</if>
<if test="direction != null">#{direction},</if>
<if test="beforeAmount != null">#{beforeAmount},</if>
<if test="changeAmount != null">#{changeAmount},</if>
@@ -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)