修改
This commit is contained in:
@@ -47,6 +47,9 @@ user:
|
||||
|
||||
# Spring配置
|
||||
spring:
|
||||
# 允许循环引用(price 模块 PrcOrderBillServiceImpl <-> PrcCreditArrearsClearAsyncRunner 互依赖)
|
||||
main:
|
||||
allow-circular-references: true
|
||||
# 资源信息
|
||||
messages:
|
||||
# 国际化资源文件路径
|
||||
|
||||
+13
-45
@@ -28,10 +28,6 @@ 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;
|
||||
|
||||
/**
|
||||
* 应收账单余额核销 Service实现
|
||||
@@ -53,9 +49,6 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
@Autowired
|
||||
private IArBillLogService logService;
|
||||
|
||||
@Autowired
|
||||
private ICustomerAccountApi customerAccountApi;
|
||||
|
||||
/**
|
||||
* 按客户查询允许余额核销的未结清账单。
|
||||
*/
|
||||
@@ -76,8 +69,9 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行余额核销主流程:锁账单、登记核销单、扣减客户余额、更新账单金额和状态。
|
||||
* 任一步失败都会回滚当前事务,避免核销单、资金流水和账单状态不一致。
|
||||
* 执行余额核销主流程:锁账单、登记核销单、更新账单金额和状态。
|
||||
* 口径变更(2026-09-23):核销不再变动账户余额与已用额度,资金变动由收款/计费入口产生。
|
||||
* 任一步失败都会回滚当前事务,避免核销单与账单状态不一致。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -131,20 +125,8 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
writeoff.setCreateBy(operator);
|
||||
writeoffMapper.insertArBalanceWriteoff(writeoff);
|
||||
|
||||
// 3. 扣减账户余额并释放等额已用额度,写 BILL_PAY 扣款流水(余额不足在此处拦截)
|
||||
String flowRemark = "余额核销账单:" + bill.getBillNo()
|
||||
+ (StringUtils.hasText(writeoff.getRemark()) ? "(" + writeoff.getRemark() + ")" : "");
|
||||
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());
|
||||
|
||||
// 3. 账单核销口径变更(2026-09-23):核销不再变动账户余额与已用额度,
|
||||
// 仅更新账单已核销/未核销金额与状态,资金变动由收款/计费入口产生。
|
||||
// 4. 更新账单已核销/未核销缓存与状态(乐观条件更新,状态被并发改动则整体回滚)
|
||||
BigDecimal written = nvl(bill.getWrittenOffAmount()).add(amount);
|
||||
BigDecimal newUnwritten = unwritten.subtract(amount);
|
||||
@@ -155,16 +137,14 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
}
|
||||
|
||||
recordLog(bill.getArBillId(), writeoff.getWriteoffId(), ArLogOperationType.BALANCE_WRITEOFF,
|
||||
"账户余额核销账单 " + amount.toPlainString() + " " + currency,
|
||||
"余额核销账单 " + amount.toPlainString() + " " + currency,
|
||||
fromStatus, nextStatus, operator, requestId);
|
||||
writeoff.setAccountId(flow.getAccountId());
|
||||
writeoff.setBalanceFlowId(flow.getId());
|
||||
return writeoff;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行反核销主流程:锁定核销单和账单、回补客户余额、恢复账单未核销金额,
|
||||
* 最后将原核销记录标记为已取消。
|
||||
* 执行反核销主流程:锁定核销单和账单、恢复账单未核销金额,
|
||||
* 最后将原核销记录标记为已取消(口径变更后不再回补账户余额、不再重新占用已用额度)。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -190,20 +170,8 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
String cancelRemark = StringUtils.hasText(remark) ? remark.trim() : "取消余额核销";
|
||||
String fromStatus = bill.getStatus();
|
||||
|
||||
// 2. 余额回补、已用额度重新占用,写反向 BILL_PAY 流水
|
||||
String flowRemark = "反核销回补账单:" + bill.getBillNo() + "(" + cancelRemark + ")";
|
||||
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. 账单核销金额回退,状态按剩余已核销额重算(归零则回到已确认)
|
||||
// 2. 账单核销金额回退,状态按剩余已核销额重算(归零则回到已确认)
|
||||
// 口径变更(2026-09-23):反核销不再回补账户余额、不再重新占用已用额度。
|
||||
BigDecimal written = nvl(bill.getWrittenOffAmount()).subtract(nvl(writeoff.getWriteoffAmount()));
|
||||
if (written.compareTo(BigDecimal.ZERO) < 0) {
|
||||
written = BigDecimal.ZERO;
|
||||
@@ -215,13 +183,13 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
throw new ServiceException("账单状态已发生变化,请刷新后重试");
|
||||
}
|
||||
|
||||
// 4. 核销单置为CANCELLED并登记回补流水
|
||||
if (writeoffMapper.updateCancelled(writeoffId, reverseFlow.getId(), operator, cancelRemark,
|
||||
// 3. 核销单置为CANCELLED(口径变更后无回补流水,reverse_flow_id 置空)
|
||||
if (writeoffMapper.updateCancelled(writeoffId, null, operator, cancelRemark,
|
||||
ArBalanceWriteoffStatus.CANCELLED.getCode(), ArBalanceWriteoffStatus.EFFECTIVE.getCode()) != 1) {
|
||||
throw new ServiceException("核销记录状态已变化,请刷新后重试");
|
||||
}
|
||||
recordLog(bill.getArBillId(), writeoffId, ArLogOperationType.CANCEL_BALANCE_WRITEOFF,
|
||||
"反核销回补账单:" + cancelRemark,
|
||||
"反核销取消核销:" + cancelRemark,
|
||||
fromStatus, nextStatus, operator, requestId);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -52,10 +52,10 @@ public class BalanceChangeCommand
|
||||
/** 流水备注 */
|
||||
private String remark;
|
||||
|
||||
/** 扣减余额时是否同步释放等额已用额度(账单核销场景为true),默认true */
|
||||
/** 扣减余额时是否同步释放等额已用额度(口径变更 2026-09-23 后已忽略,保留兼容历史调用) */
|
||||
private Boolean releaseUsedCredit = Boolean.TRUE;
|
||||
|
||||
/** 回补余额时是否同步重新占用等额已用额度(反核销场景为true,收款入账为false),默认false */
|
||||
/** 回补余额时是否同步重新占用等额已用额度(口径变更 2026-09-23 后已忽略,保留兼容历史调用) */
|
||||
private Boolean reoccupyUsedCredit = Boolean.FALSE;
|
||||
|
||||
/** 扣减时是否校验余额充足(不足抛异常),默认true */
|
||||
|
||||
+14
-13
@@ -59,25 +59,27 @@ public interface ICustomerAccountApi
|
||||
int initDefaultCnyAccount(Long customerId);
|
||||
|
||||
/**
|
||||
* 占用授信额度:used_credit += amount,写 OCCUPY 额度流水(关联来源单据)。
|
||||
* 命令中 customerId 与 customerCode 二选一;超额不拦截,允许可用额度(总额度-已占用)为负;
|
||||
* CNY账户不存在自动初始化,外币账户不存在时静默跳过(返回null,兼容历史口径)。
|
||||
* 计费占用:扣减账户余额(balance -= amount),写 -1 余额流水(口径变更 2026-09-23)。
|
||||
* 不再变动 used_credit;额度仅在调整/到期失效时变化。允许负余额(欠费)继续计费。
|
||||
* 命令中 customerId 与 customerCode 二选一;
|
||||
* CNY账户不存在自动初始化,外币账户不存在时静默跳过(返回null)。
|
||||
*
|
||||
* @return 已落库的额度流水(含ID);账户不存在跳过场景返回null
|
||||
* @return 口径变更后不再产生额度流水,固定返回 null;账户不存在跳过场景也返回null
|
||||
*/
|
||||
CustomerCreditFlow occupyCredit(CreditUsageCommand command);
|
||||
|
||||
/**
|
||||
* 释放授信额度:used_credit -= amount(不小于0),写 RELEASE 额度流水。
|
||||
* 命令中 customerId 与 customerCode 二选一;账户不存在时静默跳过(返回null)。
|
||||
* 计费释放:回补账户余额(balance += amount),写 +1 余额流水(口径变更 2026-09-23)。
|
||||
* 不再变动 used_credit。命令中 customerId 与 customerCode 二选一;
|
||||
* 账户不存在时静默跳过(返回null)。
|
||||
*
|
||||
* @return 已落库的额度流水(含ID);账户不存在跳过场景返回null
|
||||
* @return 口径变更后不再产生额度流水,固定返回 null;账户不存在跳过场景也返回null
|
||||
*/
|
||||
CustomerCreditFlow releaseCredit(CreditUsageCommand command);
|
||||
|
||||
/**
|
||||
* 扣减账户余额:balance -= amount,写 -1 余额流水;
|
||||
* releaseUsedCredit=true 时同步释放等额已用额度(余额清偿应收场景)。
|
||||
* 扣减账户余额:balance -= amount,写 -1 余额流水。
|
||||
* 口径变更后不再联动 used_credit(releaseUsedCredit 字段已忽略)。
|
||||
* 账户不存在或余额不足(checkBalanceEnough=true)抛 ServiceException。
|
||||
*
|
||||
* @return 已落库的余额流水(含ID、账户ID)
|
||||
@@ -85,9 +87,8 @@ public interface ICustomerAccountApi
|
||||
CustomerBalanceFlow deductBalance(BalanceChangeCommand command);
|
||||
|
||||
/**
|
||||
* 回补/入账账户余额:balance += amount,写 +1 余额流水;
|
||||
* reoccupyUsedCredit=true 时同步加回等额已用额度(反核销场景),
|
||||
* 收款审核入账等场景保持false。
|
||||
* 回补/入账账户余额:balance += amount,写 +1 余额流水。
|
||||
* 口径变更后不再联动 used_credit(reoccupyUsedCredit 字段已忽略)。
|
||||
*
|
||||
* @return 已落库的余额流水(含ID、账户ID)
|
||||
*/
|
||||
@@ -98,7 +99,7 @@ public interface ICustomerAccountApi
|
||||
* 不锁账户也不物化临时额度失效——临时额度已过期时按"视为0"计算可用额度,但 DB 保持原状,
|
||||
* 由定时任务或后续真实资金入口落库。
|
||||
*
|
||||
* <p>欠费定义:可用额度 < 0(已用额度超过授信总额+有效临时额度)。</p>
|
||||
* <p>欠费定义:可用额度 < 0(账户余额 + 信用额度 + 有效临时额度 不足)。</p>
|
||||
*
|
||||
* @return 账号状态VO;客户不存在时返回 null
|
||||
*/
|
||||
|
||||
+32
-135
@@ -281,7 +281,9 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 授信额度占用 / 释放 ================= */
|
||||
/* ================= 计费扣减 / 回补余额(口径变更:不再变动 used_credit) ================= */
|
||||
// 2026-09-23 口径变更:计费占用/释放改为扣减/回补账户余额,允许负余额(欠费)继续计费。
|
||||
// 额度(totalCredit/extraCredit)仅在额度调整或到期失效时变化,计费不再影响 used_credit。
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -292,10 +294,6 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
String currency = resolveCurrency(command.getCurrency());
|
||||
String username = currentUsername();
|
||||
Long userId = currentUserId();
|
||||
// 计费开单准入:客户被停用(含删除)时直接拦截,与账户状态接口口径保持一致。
|
||||
// 仅占用(新增计费)校验;释放/核销等清欠动作不受限,避免客户被停用后无法平账。
|
||||
//9月20 调整,客户被停用不拦截计费,继续占用信用额度,
|
||||
// assertCustomerActive(customerId);
|
||||
CustomerAccount account = lockAccountForUsage(customerId, currency, username);
|
||||
if (account == null)
|
||||
{
|
||||
@@ -303,18 +301,22 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
return null;
|
||||
}
|
||||
BigDecimal amount = money(command.getAmount());
|
||||
BigDecimal beforeUsed = nz(account.getUsedCredit());
|
||||
// 计费策略(2026-09-20 调整):超额客户不拦截计费,继续占用信用额度,
|
||||
// 允许可用额度(总额度-已占用)为负;后续基于负额度做超额提醒,不做硬拦截。
|
||||
BigDecimal afterUsed = beforeUsed.add(amount).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
account.setUsedCredit(afterUsed);
|
||||
BigDecimal beforeBalance = nz(account.getBalance());
|
||||
// 计费口径变更:occupyCredit 改为扣减账户余额,允许负余额(欠费),不再变动 used_credit
|
||||
BigDecimal afterBalance = beforeBalance.subtract(amount).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
account.setBalance(afterBalance);
|
||||
account.setUpdateBy(username);
|
||||
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
||||
{
|
||||
throw new ServiceException("账户已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
return writeUsedCreditFlow(account, command, CustomerCreditFlow.CHANGE_OCCUPY, 1,
|
||||
beforeUsed, afterUsed, amount, username, userId);
|
||||
// 写余额流水(计费扣减),不再写额度流水
|
||||
BigDecimal frozenBalance = nz(account.getFrozenBalance());
|
||||
CustomerBalanceFlow flow = newBalanceFlow(account, toBalanceCommand(command, currency, "计费扣减余额"),
|
||||
currency, amount, -1, beforeBalance, afterBalance, frozenBalance, frozenBalance, username, userId);
|
||||
customerMoneyMapper.insertFlow(flow);
|
||||
// 接口契约返回额度流水;口径变更后不再产生额度变动,返回 null(计费侧未使用返回值)
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -332,22 +334,20 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
return null;
|
||||
}
|
||||
BigDecimal amount = money(command.getAmount());
|
||||
BigDecimal beforeUsed = nz(account.getUsedCredit());
|
||||
BigDecimal afterUsed = beforeUsed.subtract(amount).max(BigDecimal.ZERO)
|
||||
.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
if (afterUsed.compareTo(beforeUsed) == 0)
|
||||
{
|
||||
// 无实际占用可释放,仍返回null不产生流水
|
||||
return null;
|
||||
}
|
||||
account.setUsedCredit(afterUsed);
|
||||
BigDecimal beforeBalance = nz(account.getBalance());
|
||||
// 计费口径变更:releaseCredit 改为回补账户余额,不再变动 used_credit
|
||||
BigDecimal afterBalance = beforeBalance.add(amount).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
account.setBalance(afterBalance);
|
||||
account.setUpdateBy(username);
|
||||
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
||||
{
|
||||
throw new ServiceException("账户已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
return writeUsedCreditFlow(account, command, CustomerCreditFlow.CHANGE_RELEASE, -1,
|
||||
beforeUsed, afterUsed, amount, username, userId);
|
||||
BigDecimal frozenBalance = nz(account.getFrozenBalance());
|
||||
CustomerBalanceFlow flow = newBalanceFlow(account, toBalanceCommand(command, currency, "计费回补余额"),
|
||||
currency, amount, 1, beforeBalance, afterBalance, frozenBalance, frozenBalance, username, userId);
|
||||
customerMoneyMapper.insertFlow(flow);
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ================= 余额扣减 / 回补 ================= */
|
||||
@@ -376,12 +376,6 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
BigDecimal afterBalance = beforeBalance.subtract(changeAmount);
|
||||
BigDecimal frozenBalance = nz(account.getFrozenBalance());
|
||||
account.setBalance(afterBalance);
|
||||
BigDecimal beforeUsedCredit = nz(account.getUsedCredit());
|
||||
if (Boolean.TRUE.equals(command.getReleaseUsedCredit()))
|
||||
{
|
||||
account.setUsedCredit(beforeUsedCredit.subtract(changeAmount).max(BigDecimal.ZERO)
|
||||
.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
||||
}
|
||||
account.setUpdateBy(username);
|
||||
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
||||
{
|
||||
@@ -391,18 +385,6 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
CustomerBalanceFlow flow = newBalanceFlow(account, command, flowCurrency, originalAmount, -1,
|
||||
beforeBalance, afterBalance, frozenBalance, frozenBalance, username, userId);
|
||||
customerMoneyMapper.insertFlow(flow);
|
||||
|
||||
// 核销扣款联动释放已用额度:补写额度流水(RELEASE),保证 used_credit 变动台账完整
|
||||
BigDecimal afterUsedCredit = nz(account.getUsedCredit());
|
||||
if (Boolean.TRUE.equals(command.getReleaseUsedCredit())
|
||||
&& afterUsedCredit.compareTo(beforeUsedCredit) < 0)
|
||||
{
|
||||
writeUsedCreditFlow(account, toCreditUsageCommand(command, accountCurrency, null),
|
||||
CustomerCreditFlow.CHANGE_RELEASE, -1,
|
||||
beforeUsedCredit, afterUsedCredit,
|
||||
beforeUsedCredit.subtract(afterUsedCredit),
|
||||
username, userId);
|
||||
}
|
||||
return flow;
|
||||
}
|
||||
|
||||
@@ -425,13 +407,6 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
BigDecimal afterBalance = beforeBalance.add(changeAmount);
|
||||
BigDecimal frozenBalance = nz(account.getFrozenBalance());
|
||||
account.setBalance(afterBalance);
|
||||
BigDecimal beforeUsedCredit = nz(account.getUsedCredit());
|
||||
if (Boolean.TRUE.equals(command.getReoccupyUsedCredit()))
|
||||
{
|
||||
// 反核销回补:账单恢复未清偿,授信占用同步加回(允许出现高于总额度的占用存量)
|
||||
account.setUsedCredit(beforeUsedCredit.add(changeAmount)
|
||||
.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
||||
}
|
||||
account.setUpdateBy(username);
|
||||
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
||||
{
|
||||
@@ -441,66 +416,25 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
CustomerBalanceFlow flow = newBalanceFlow(account, command, flowCurrency, originalAmount, 1,
|
||||
beforeBalance, afterBalance, frozenBalance, frozenBalance, username, userId);
|
||||
customerMoneyMapper.insertFlow(flow);
|
||||
|
||||
// 反核销回补联动重新占用已用额度:补写额度流水(OCCUPY),与核销释放流水方向相反、可勾稽
|
||||
BigDecimal afterUsedCredit = nz(account.getUsedCredit());
|
||||
if (Boolean.TRUE.equals(command.getReoccupyUsedCredit())
|
||||
&& afterUsedCredit.compareTo(beforeUsedCredit) > 0)
|
||||
{
|
||||
String reoccupyReason = "反核销重新占用授信额度"
|
||||
+ (StringUtils.hasText(command.getBizNo()) ? ":" + command.getBizNo() : "");
|
||||
writeUsedCreditFlow(account, toCreditUsageCommand(command, accountCurrency, reoccupyReason),
|
||||
CustomerCreditFlow.CHANGE_OCCUPY, 1,
|
||||
beforeUsedCredit, afterUsedCredit,
|
||||
afterUsedCredit.subtract(beforeUsedCredit),
|
||||
username, userId);
|
||||
}
|
||||
return flow;
|
||||
}
|
||||
|
||||
/* ================= 内部实现 ================= */
|
||||
|
||||
/** 余额变动命令 -> 额度占用命令(复用业务单据信息,金额取实际联动额由调用方写流水时传入) */
|
||||
private CreditUsageCommand toCreditUsageCommand(BalanceChangeCommand source, String accountCurrency, String reason)
|
||||
/** 额度占用命令 -> 余额变动命令(计费口径变更后,occupy/release 复用余额流水落库) */
|
||||
private BalanceChangeCommand toBalanceCommand(CreditUsageCommand source, String currency, String reason)
|
||||
{
|
||||
CreditUsageCommand target = new CreditUsageCommand();
|
||||
BalanceChangeCommand target = new BalanceChangeCommand();
|
||||
target.setCustomerId(source.getCustomerId());
|
||||
target.setCurrency(accountCurrency);
|
||||
target.setCurrency(currency);
|
||||
target.setAmount(money(source.getAmount()));
|
||||
target.setBizType(source.getBizType());
|
||||
target.setBizId(source.getBizId());
|
||||
target.setBizNo(source.getBizNo());
|
||||
target.setReason(reason);
|
||||
target.setRemark(reason);
|
||||
return target;
|
||||
}
|
||||
|
||||
/** 额度占用/释放写额度流水(before/after 为已用额度快照) */
|
||||
private CustomerCreditFlow writeUsedCreditFlow(CustomerAccount account, CreditUsageCommand command,
|
||||
String changeType, int direction, BigDecimal beforeUsed, BigDecimal afterUsed,
|
||||
BigDecimal amount, String username, Long userId)
|
||||
{
|
||||
CustomerCreditFlow flow = new CustomerCreditFlow();
|
||||
flow.setFlowNo(generateNo("EDLS"));
|
||||
flow.setCustomerId(account.getCustomerId());
|
||||
flow.setAccountId(account.getId());
|
||||
flow.setCreditType(CustomerCreditFlow.CREDIT_TYPE_CREDIT);
|
||||
flow.setChangeType(changeType);
|
||||
flow.setBizType(command.getBizType());
|
||||
flow.setBizId(command.getBizId());
|
||||
flow.setBizNo(command.getBizNo());
|
||||
flow.setDirection(direction);
|
||||
flow.setBeforeAmount(beforeUsed);
|
||||
flow.setChangeAmount(amount);
|
||||
flow.setAfterAmount(afterUsed);
|
||||
flow.setUsedCredit(afterUsed);
|
||||
flow.setReason(resolveUsageReason(command, changeType));
|
||||
flow.setOperatorId(userId);
|
||||
flow.setCreateBy(username);
|
||||
flow.setDelFlag("0");
|
||||
customerMoneyMapper.insertCreditFlow(flow);
|
||||
return flow;
|
||||
}
|
||||
|
||||
/** 余额变动写余额流水(原币金额留痕,本位币金额参与余额变动) */
|
||||
private CustomerBalanceFlow newBalanceFlow(CustomerAccount account, BalanceChangeCommand command,
|
||||
String currency, BigDecimal amount, int direction,
|
||||
@@ -792,9 +726,9 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
vo.setIsEnable(cust.getIsEnable());
|
||||
vo.setIsEnableLabel("0".equals(cust.getIsEnable()) ? "正常" : "停用");
|
||||
|
||||
BigDecimal balance = account != null ? nz(account.getBalance()) : BigDecimal.ZERO;
|
||||
BigDecimal totalCredit = account != null ? nz(account.getTotalCredit()) : BigDecimal.ZERO;
|
||||
BigDecimal rawExtra = account != null ? nz(account.getExtraCredit()) : BigDecimal.ZERO;
|
||||
BigDecimal usedCredit = account != null ? nz(account.getUsedCredit()) : BigDecimal.ZERO;
|
||||
|
||||
// 已过期的信用/临时额度视为0(仅内存计算,不物化DB;由定时任务/真实资金入口落库)
|
||||
boolean creditExpiredPending = isCreditExpired(account);
|
||||
@@ -802,14 +736,14 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
BigDecimal effectiveTotal = creditExpiredPending ? BigDecimal.ZERO : totalCredit;
|
||||
BigDecimal effectiveExtra = expiredPending ? BigDecimal.ZERO : rawExtra;
|
||||
|
||||
vo.setBalance(account != null ? nz(account.getBalance()) : BigDecimal.ZERO);
|
||||
vo.setBalance(balance);
|
||||
vo.setFrozenBalance(account != null ? nz(account.getFrozenBalance()) : BigDecimal.ZERO);
|
||||
vo.setTotalCredit(totalCredit);
|
||||
vo.setEffectiveTotalCredit(effectiveTotal);
|
||||
vo.setExtraCredit(rawExtra);
|
||||
vo.setEffectiveExtraCredit(effectiveExtra);
|
||||
vo.setUsedCredit(usedCredit);
|
||||
BigDecimal available = effectiveTotal.add(effectiveExtra).subtract(usedCredit);
|
||||
// 可用额度 = 账户余额 + 信用额度 + 有效临时额度(2026-09-23 口径:计费扣减余额,已用额度不再参与)
|
||||
BigDecimal available = balance.add(effectiveTotal).add(effectiveExtra);
|
||||
vo.setAvailableCredit(available);
|
||||
boolean overdue = available.compareTo(BigDecimal.ZERO) < 0;
|
||||
vo.setOverdue(overdue);
|
||||
@@ -855,43 +789,6 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveUsageReason(CreditUsageCommand command, String changeType)
|
||||
{
|
||||
if (StringUtils.hasText(command.getReason()))
|
||||
{
|
||||
return command.getReason().trim();
|
||||
}
|
||||
String action = CustomerCreditFlow.CHANGE_OCCUPY.equals(changeType) ? "占用授信额度" : "释放授信额度";
|
||||
String biz = bizTypeLabel(command.getBizType());
|
||||
String reason = biz + action;
|
||||
if (StringUtils.hasText(command.getBizNo()))
|
||||
{
|
||||
reason = reason + ":" + command.getBizNo();
|
||||
}
|
||||
return reason;
|
||||
}
|
||||
|
||||
private String bizTypeLabel(String bizType)
|
||||
{
|
||||
if (CustomerFundBizType.RECEIPT.equals(bizType))
|
||||
{
|
||||
return "\u6536\u6b3e";
|
||||
}
|
||||
if (CustomerFundBizType.AR_BILL.equals(bizType))
|
||||
{
|
||||
return "\u5e94\u6536\u8d26\u5355";
|
||||
}
|
||||
if (CustomerFundBizType.ORDER.equals(bizType))
|
||||
{
|
||||
return "\u8ba1\u8d39\u8ba2\u5355";
|
||||
}
|
||||
if (CustomerFundBizType.BILL_PAY.equals(bizType))
|
||||
{
|
||||
return "\u8d26\u5355\u6838\u9500";
|
||||
}
|
||||
return "\u4e1a\u52a1";
|
||||
}
|
||||
|
||||
/** 临时额度是否已过期:有失效日期且今天已晚于失效日(失效日当天仍有效) */
|
||||
private boolean isTempCreditExpired(CustomerAccount account)
|
||||
{
|
||||
|
||||
+1
-6
@@ -700,7 +700,6 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
customerAccountApi.expireDueCreditsIfNeeded(account);
|
||||
|
||||
BigDecimal before = isTemp ? nz(account.getExtraCredit()) : nz(account.getTotalCredit());
|
||||
BigDecimal otherCredit = isTemp ? nz(account.getTotalCredit()) : nz(account.getExtraCredit());
|
||||
BigDecimal after;
|
||||
switch (adjustMode)
|
||||
{
|
||||
@@ -719,11 +718,7 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
{
|
||||
throw new ServiceException("调整后额度不能为负数");
|
||||
}
|
||||
if (otherCredit.add(after).compareTo(nz(account.getUsedCredit())) < 0)
|
||||
{
|
||||
throw new ServiceException("调整后授信总额度不能小于已用额度:"
|
||||
+ nz(account.getUsedCredit()).toPlainString());
|
||||
}
|
||||
// 口径变更(2026-09-23):计费不再变动 used_credit,额度调整无需校验已用额度
|
||||
if (after.compareTo(before) == 0)
|
||||
{
|
||||
throw new ServiceException("额度未发生变化,无需调整");
|
||||
|
||||
+2
-6
@@ -9,7 +9,7 @@ import java.math.BigDecimal;
|
||||
* <p>只读轻量,不锁账户也不物化临时额度失效:临时额度已过期时按"视为0"计算可用额度,
|
||||
* 但 DB 保持原状——由定时任务或后续真实资金入口落库。</p>
|
||||
*
|
||||
* <p>欠费定义:availableCredit < 0(已用额度超过授信总额+有效临时额度)。</p>
|
||||
* <p>欠费定义:availableCredit < 0(可用额度 = 账户余额 + 信用额度 + 有效临时额度)。</p>
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-17
|
||||
@@ -50,9 +50,7 @@ public class CustomerAccountStatusVO implements Serializable
|
||||
private BigDecimal extraCredit;
|
||||
/** 临时额度实际有效值(过期时为0,接口计算得出,不物化DB) */
|
||||
private BigDecimal effectiveExtraCredit;
|
||||
/** 已用额度(CNY) */
|
||||
private BigDecimal usedCredit;
|
||||
/** 可用额度(总额度 + 有效临时额度 - 已用额度;可能为负) */
|
||||
/** 可用额度(账户余额 + 授信总额度 + 有效临时额度;可能为负表示欠费) */
|
||||
private BigDecimal availableCredit;
|
||||
|
||||
/** 是否欠费(可用额度 < 0) */
|
||||
@@ -86,8 +84,6 @@ public class CustomerAccountStatusVO implements Serializable
|
||||
public void setExtraCredit(BigDecimal extraCredit) { this.extraCredit = extraCredit; }
|
||||
public BigDecimal getEffectiveExtraCredit() { return effectiveExtraCredit; }
|
||||
public void setEffectiveExtraCredit(BigDecimal effectiveExtraCredit) { this.effectiveExtraCredit = effectiveExtraCredit; }
|
||||
public BigDecimal getUsedCredit() { return usedCredit; }
|
||||
public void setUsedCredit(BigDecimal usedCredit) { this.usedCredit = usedCredit; }
|
||||
public BigDecimal getAvailableCredit() { return availableCredit; }
|
||||
public void setAvailableCredit(BigDecimal availableCredit) { this.availableCredit = availableCredit; }
|
||||
public boolean isOverdue() { return overdue; }
|
||||
|
||||
+18
-47
@@ -62,12 +62,6 @@
|
||||
<div class="balance-sub" :style="tempCreditPeriodExpired ? 'color: var(--el-color-danger);' : ''">{{ tempCreditPeriodText }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="4">
|
||||
<el-card shadow="hover" class="balance-card">
|
||||
<div class="balance-label">已用额度</div>
|
||||
<div class="balance-value danger">{{ formatMoney(currentAccount.usedCredit) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="4">
|
||||
<el-card shadow="hover" class="balance-card">
|
||||
<div class="balance-label">可用额度</div>
|
||||
@@ -796,7 +790,7 @@
|
||||
>
|
||||
<el-alert type="info" :closable="false" class="credit-tip">
|
||||
<div>当前{{ creditDialog.form.creditType === 'TEMP' ? '临时额度' : '信用额度' }}:
|
||||
<b>{{ formatMoney(creditBefore) }}</b>,已用额度:<b>{{ formatMoney(currentAccount.usedCredit) }}</b></div>
|
||||
<b>{{ formatMoney(creditBefore) }}</b>,账户余额:<b>{{ formatMoney(currentAccount.balance) }}</b>,可用额度:<b>{{ availableCredit }}</b></div>
|
||||
</el-alert>
|
||||
<el-form ref="creditFormRef" :model="creditDialog.form" :rules="creditRules" label-width="100px">
|
||||
<el-row>
|
||||
@@ -853,7 +847,7 @@
|
||||
<div class="rate-tip">
|
||||
{{ creditDialog.form.creditType === 'TEMP'
|
||||
? '失效日当天仍有效,次日访问账户时自动清零并记到期流水'
|
||||
: '信用额度失效日期必填;失效日当天仍有效,次日自动清零,已用额度未回款时将形成超额欠费(可继续计费,请及时催收)' }}
|
||||
: '信用额度失效日期必填;失效日当天仍有效,次日自动清零;额度失效后可用额度相应减少,若账户余额为负将形成欠费(可继续计费,请及时催收回款)' }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -877,17 +871,12 @@
|
||||
<b :class="creditAfter < 0 ? 'money-out' : 'money-in'">{{ formatMoney(creditAfter) }}</b>
|
||||
</div>
|
||||
<div>调整后授信总额:<b>{{ formatMoney(totalCreditAfter) }}</b>
|
||||
(可用 = 授信总额 - 已用 {{ formatMoney(currentAccount.usedCredit) }})</div>
|
||||
(可用 = 账户余额 {{ formatMoney(currentAccount.balance) }} + 授信总额 + 额外额度)</div>
|
||||
<el-alert
|
||||
v-if="creditAfter < 0"
|
||||
type="error" :closable="false"
|
||||
title="调整后额度不能为负数" class="credit-alert"
|
||||
/>
|
||||
<el-alert
|
||||
v-else-if="totalCreditAfter < (Number(currentAccount.usedCredit) || 0)"
|
||||
type="error" :closable="false"
|
||||
title="授信总额不能小于已用额度" class="credit-alert"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
@@ -960,9 +949,9 @@
|
||||
placeholder="选填,最多255字" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-alert type="warning" :closable="false">
|
||||
<el-alert type="info" :closable="false">
|
||||
<template title>
|
||||
核销后账户余额变为 CNY {{ woAfterBalance }},同步释放等额已用额度;余额不足时无法提交。
|
||||
核销仅更新账单已核销/未核销金额与状态,不再扣减账户余额;资金变动由收款/计费入口产生。
|
||||
</template>
|
||||
</el-alert>
|
||||
<template #footer>
|
||||
@@ -975,8 +964,8 @@
|
||||
<el-dialog title="反核销(取消余额核销)" v-model="cancelWoDialog.visible" width="480px" append-to-body>
|
||||
<el-alert type="warning" :closable="false" class="credit-tip">
|
||||
<span v-if="cancelWoDialog.row">
|
||||
反核销后,核销金额 CNY {{ formatMoney(cancelWoDialog.row.writeoffAmount) }} 将退回客户账户余额,
|
||||
账单「{{ cancelWoDialog.row.billNo }}」恢复为未核销,已用额度同步加回。
|
||||
反核销后,账单「{{ cancelWoDialog.row.billNo }}」恢复为未核销状态(核销金额 CNY {{ formatMoney(cancelWoDialog.row.writeoffAmount) }});
|
||||
口径变更后不再回补账户余额、不再变动已用额度。
|
||||
</span>
|
||||
</el-alert>
|
||||
<el-form label-width="100px" class="wo-form">
|
||||
@@ -1090,8 +1079,9 @@ function creditChangeTag(v) {
|
||||
return { INIT: '', ADJUST: 'primary', EXPIRE: 'danger', OCCUPY: 'warning', RELEASE: 'success' }[v] || 'info'
|
||||
}
|
||||
/**
|
||||
* 是否为授信占用/释放流水。
|
||||
* 占用/释放流水的 变动前/变动额/变动后 记的是【已用额度】维度;
|
||||
* 是否为授信占用/释放流水(历史数据)。
|
||||
* 口径变更(2026-09-23)后计费不再产生 OCCUPY/RELEASE 流水,改为写余额流水;
|
||||
* 历史占用/释放流水的 变动前/变动额/变动后 记的是【已用额度】维度,
|
||||
* INIT/ADJUST/EXPIRE 流水这三列记的是【授信额度】(信用/临时额度)维度。
|
||||
*/
|
||||
function isCreditUsageFlow(changeType) {
|
||||
@@ -1127,17 +1117,17 @@ const currentAccount = computed(() => {
|
||||
return accountList.value.find(a => a.currency === 'CNY') || {}
|
||||
})
|
||||
|
||||
/** 可用额度数值 = 总额度 + 临时额度 - 已用额度(可能为负,表示临时额度到期/占用超授信总额) */
|
||||
/** 可用额度数值 = 账户余额 + 信用额度 + 额外额度(可能为负,表示欠费;口径变更后不再扣减已用额度) */
|
||||
const availableCreditValue = computed(() => {
|
||||
const a = currentAccount.value
|
||||
const balance = Number(a.balance) || 0
|
||||
const total = Number(a.totalCredit) || 0
|
||||
const extra = Number(a.extraCredit) || 0
|
||||
const used = Number(a.usedCredit) || 0
|
||||
return total + extra - used
|
||||
return balance + total + extra
|
||||
})
|
||||
/** 可用额度展示文本 */
|
||||
const availableCredit = computed(() => formatMoney(availableCreditValue.value))
|
||||
/** 是否处于超额状态(已用额度超过授信总额,可用为负;不拦截计费,后续做超额提醒) */
|
||||
/** 是否处于欠费状态(可用额度为负;不拦截计费,允许负余额继续计费) */
|
||||
const availableOverLimit = computed(() => availableCreditValue.value < 0)
|
||||
|
||||
/** 临时额度有效期文案 */
|
||||
@@ -1274,11 +1264,6 @@ function submitAdjustCredit() {
|
||||
proxy.$modal.msgError('调整后额度不能为负数')
|
||||
return
|
||||
}
|
||||
const used = Number(currentAccount.value.usedCredit) || 0
|
||||
if (totalCreditAfter.value < used) {
|
||||
proxy.$modal.msgError('调整后授信总额不能小于已用额度 ' + formatMoney(used))
|
||||
return
|
||||
}
|
||||
if (creditAfter.value === creditBefore.value) {
|
||||
proxy.$modal.msgError('额度未发生变化,无需调整')
|
||||
return
|
||||
@@ -1852,12 +1837,6 @@ function loadBillWriteoffData() {
|
||||
const woSubmitLoading = ref(false)
|
||||
const woDialog = reactive({ visible: false, bill: null, form: {}, accountLoading: false })
|
||||
let writeoffAccountRequestSerial = 0
|
||||
/** 核销后余额预览(不为负则可提交) */
|
||||
const woAfterBalance = computed(() => {
|
||||
const balance = Number(currentAccount.value.balance) || 0
|
||||
const amount = Number(woDialog.form.writeoffAmount) || 0
|
||||
return formatMoney(balance - amount)
|
||||
})
|
||||
|
||||
async function openWriteoffDialog(row) {
|
||||
const customerId = Number(route.query.customerId)
|
||||
@@ -1868,28 +1847,25 @@ async function openWriteoffDialog(row) {
|
||||
|
||||
woDialog.bill = row
|
||||
const unwritten = Number(row.unwrittenAmount) || 0
|
||||
// 口径变更后核销不再扣减余额,默认核销金额取账单未核销金额
|
||||
woDialog.form = {
|
||||
arBillId: row.arBillId,
|
||||
customerId,
|
||||
// 余额接口返回前先显示 0,避免使用旧余额或在余额为 0 时错误带出整笔未核销金额。
|
||||
writeoffAmount: 0,
|
||||
writeoffAmount: unwritten,
|
||||
writeoffDate: proxy.parseTime(new Date(), '{y}-{m}-{d}'),
|
||||
remark: ''
|
||||
}
|
||||
woDialog.visible = true
|
||||
|
||||
// 必须等待余额刷新完成后再计算默认金额,避免异步请求尚未返回就读取旧余额。
|
||||
// 仍刷新账户余额供展示,但不再限制核销金额
|
||||
const requestSerial = ++writeoffAccountRequestSerial
|
||||
woDialog.accountLoading = true
|
||||
try {
|
||||
const res = await listAccount(customerId)
|
||||
if (requestSerial !== writeoffAccountRequestSerial) return
|
||||
accountList.value = res.data || []
|
||||
const balance = Math.max(0, Number(currentAccount.value.balance) || 0)
|
||||
woDialog.form.writeoffAmount = Math.min(unwritten, balance)
|
||||
} catch (e) {
|
||||
if (requestSerial !== writeoffAccountRequestSerial) return
|
||||
woDialog.form.writeoffAmount = 0
|
||||
proxy.$modal.msgError('账户余额刷新失败,请稍后重试')
|
||||
} finally {
|
||||
if (requestSerial === writeoffAccountRequestSerial) woDialog.accountLoading = false
|
||||
@@ -1915,11 +1891,6 @@ function submitWriteoff() {
|
||||
proxy.$modal.msgError('核销金额不能超过账单未核销金额 ' + formatMoney(unwritten))
|
||||
return
|
||||
}
|
||||
const balance = Number(currentAccount.value.balance) || 0
|
||||
if (amount > balance) {
|
||||
proxy.$modal.msgError('账户余额不足,当前余额 CNY ' + formatMoney(balance) + ',请先登记收款并审核')
|
||||
return
|
||||
}
|
||||
woSubmitLoading.value = true
|
||||
balanceWriteoff(woDialog.form).then(() => {
|
||||
proxy.$modal.msgSuccess('核销成功:账单已核销 CNY ' + formatMoney(amount))
|
||||
@@ -1949,7 +1920,7 @@ function submitCancelWriteoff() {
|
||||
}
|
||||
cancelWoLoading.value = true
|
||||
cancelBalanceWriteoff(cancelWoDialog.row.writeoffId, remark).then(() => {
|
||||
proxy.$modal.msgSuccess('反核销成功,余额已退回')
|
||||
proxy.$modal.msgSuccess('反核销成功,账单已恢复未核销')
|
||||
cancelWoDialog.visible = false
|
||||
loadBillWriteoffData()
|
||||
getFlowList()
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<el-table-column v-if="isColVisible('creditEd')" label="信用额度" align="center" prop="creditEd">
|
||||
<template #default="scope">
|
||||
<span v-if="Number(scope.row.creditEd) < 0" style="color: var(--el-color-danger); font-weight: 600;"
|
||||
title="已用额度超过授信总额(如临时额度已到期),客户回款核销后恢复">
|
||||
title="信用额度异常(如临时额度到期失效),请核查客户额度">
|
||||
{{ scope.row.creditEd }}
|
||||
</span>
|
||||
<span v-else>{{ scope.row.creditEd }}</span>
|
||||
|
||||
Reference in New Issue
Block a user