额度余额变动抽公共方法
This commit is contained in:
+23
-6
@@ -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()));
|
||||
|
||||
+23
-7
@@ -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(); }
|
||||
|
||||
+14
-1
@@ -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;
|
||||
|
||||
|
||||
-33
@@ -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);
|
||||
}
|
||||
|
||||
+30
-364
@@ -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) >= #{params.beginTime}</if>
|
||||
<if test="params.endTime != null and params.endTime != ''"> and date(create_time) <= #{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)
|
||||
|
||||
Reference in New Issue
Block a user