新加额度池配置功能
This commit is contained in:
+10
-9
@@ -31,28 +31,29 @@ public interface ICustomerAccountApi
|
||||
CustomerAccount getAccount(Long customerId, String currency);
|
||||
|
||||
/**
|
||||
* 事务内行锁获取客户CNY账户,不存在则按客户档案额度自动初始化(不处理临时额度过期)。
|
||||
* 调用方拿到后如需最新临时额度,可再调 {@link #expireTempCreditIfNeeded}。
|
||||
* 事务内行锁获取客户CNY账户,不存在则按客户档案额度自动初始化(不处理授信过期)。
|
||||
* 调用方拿到后如需最新额度,可再调 {@link #expireDueCreditsIfNeeded}。
|
||||
*/
|
||||
CustomerAccount lockCnyAccount(Long customerId);
|
||||
|
||||
/**
|
||||
* 临时额度到期自动失效(额度清零 + 回写客户档案 + EXPIRE 额度流水)。
|
||||
* 入参必须是同一事务内行锁查出的账户;未过期时不做任何操作。
|
||||
* 授信(信用额度/临时额度)到期自动失效:过期项额度清零 + 释放角色额度池
|
||||
* + 回写客户档案 + 写 EXPIRE 额度流水。
|
||||
* 入参必须是同一事务内行锁查出的账户;均未过期时不做任何操作。
|
||||
*/
|
||||
void expireTempCreditIfNeeded(CustomerAccount account);
|
||||
void expireDueCreditsIfNeeded(CustomerAccount account);
|
||||
|
||||
/**
|
||||
* 扫描所有临时额度已到期(extra_credit>0 且失效日期早于今天)的CNY账户,
|
||||
* 逐户独立事务物化失效(行锁 -> 清零 -> 回写档案 -> EXPIRE流水)。
|
||||
* 扫描所有授信已到期(信用或临时额度>0 且失效日期早于今天)的CNY账户,
|
||||
* 逐户独立事务物化失效(行锁 -> 清零 -> 释放角色池 -> 回写档案 -> EXPIRE流水)。
|
||||
* 单户失败仅记录日志不影响其他户;定时任务每日调用,应用启动后也会执行一次。
|
||||
*
|
||||
* @return 实际发生失效的账户数
|
||||
*/
|
||||
int expireAllDueTempCredits();
|
||||
int expireAllDueCredits();
|
||||
|
||||
/**
|
||||
* 新建客户时初始化默认CNY账户(余额0,默认总额度1000 CNY,档案有值按档案),
|
||||
* 新建客户时初始化默认CNY账户(余额0,信用额度默认0,档案有值按档案),
|
||||
* 写 INIT 额度流水;CNY账户已存在时不重复创建。
|
||||
*/
|
||||
int initDefaultCnyAccount(Long customerId);
|
||||
|
||||
+83
-27
@@ -49,8 +49,8 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
|
||||
/** 本位币币种 */
|
||||
private static final String BASE_CURRENCY = BmsExchangeRate.BASE_CURRENCY;
|
||||
/** 新客户默认总额度(CNY) */
|
||||
private static final BigDecimal DEFAULT_CREDIT = new BigDecimal("1000");
|
||||
/** 新客户默认总额度(CNY):默认0,信用额度一律通过「申请-审批」分配 */
|
||||
private static final BigDecimal DEFAULT_CREDIT = BigDecimal.ZERO;
|
||||
/** 金额保留位数 */
|
||||
private static final int MONEY_SCALE = 2;
|
||||
|
||||
@@ -60,6 +60,10 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
@Autowired
|
||||
private BmsCustomerMapper bmsCustomerMapper;
|
||||
|
||||
/** 角色额度池台账:授信到期释放角色池 */
|
||||
@Autowired
|
||||
private com.ruoyi.customer.customermoney.service.ICreditQuotaService creditQuotaService;
|
||||
|
||||
@Autowired
|
||||
private PlatformTransactionManager transactionManager;
|
||||
|
||||
@@ -113,16 +117,16 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
throw new ServiceException("客户不存在");
|
||||
}
|
||||
BigDecimal initCredit = nz(customer.getCreditEd());
|
||||
if (initCredit.compareTo(BigDecimal.ZERO) <= 0)
|
||||
if (initCredit.compareTo(BigDecimal.ZERO) < 0)
|
||||
{
|
||||
initCredit = DEFAULT_CREDIT;
|
||||
initCredit = BigDecimal.ZERO;
|
||||
}
|
||||
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");
|
||||
initCredit, null, null, username, "新客户初始化账户,默认信用额度0(需通过额度申请审批分配)");
|
||||
if (initTemp.compareTo(BigDecimal.ZERO) > 0)
|
||||
{
|
||||
writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_TEMP,
|
||||
@@ -132,9 +136,9 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expireAllDueTempCredits()
|
||||
public int expireAllDueCredits()
|
||||
{
|
||||
List<Long> customerIds = customerMoneyMapper.selectExpiredTempCreditCustomerIds();
|
||||
List<Long> customerIds = customerMoneyMapper.selectExpiredCreditCustomerIds();
|
||||
if (customerIds == null || customerIds.isEmpty())
|
||||
{
|
||||
return 0;
|
||||
@@ -142,17 +146,18 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
int expired = 0;
|
||||
for (Long customerId : customerIds)
|
||||
{
|
||||
// 每户独立事务:行锁 -> 失效清零 -> 回写档案 -> EXPIRE流水,单户失败不影响其他户
|
||||
// 每户独立事务:行锁 -> 失效清零 -> 释放角色池 -> 回写档案 -> EXPIRE流水,单户失败不影响其他户
|
||||
try
|
||||
{
|
||||
Boolean changed = requireNewTxTemplate.execute(status -> {
|
||||
CustomerAccount locked = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY);
|
||||
if (locked == null || !isTempCreditExpired(locked))
|
||||
if (locked == null
|
||||
|| (!isTempCreditExpired(locked) && !isCreditExpired(locked)))
|
||||
{
|
||||
// 可能被其他节点/入口抢先处理
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
expireTempCreditIfNeeded(locked);
|
||||
expireDueCreditsIfNeeded(locked);
|
||||
return Boolean.TRUE;
|
||||
});
|
||||
if (Boolean.TRUE.equals(changed))
|
||||
@@ -162,25 +167,39 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("客户{}临时额度到期自动失效失败", customerId, e);
|
||||
log.error("客户{}授信到期自动失效失败", customerId, e);
|
||||
}
|
||||
}
|
||||
log.info("临时额度到期批量失效完成,扫描{}户,实际失效{}户", customerIds.size(), expired);
|
||||
log.info("授信到期批量失效完成,扫描{}户,实际失效{}户", customerIds.size(), expired);
|
||||
return expired;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void expireTempCreditIfNeeded(CustomerAccount account)
|
||||
public void expireDueCreditsIfNeeded(CustomerAccount account)
|
||||
{
|
||||
if (account == null || !isTempCreditExpired(account))
|
||||
if (account == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
boolean creditExpired = isCreditExpired(account);
|
||||
boolean tempExpired = isTempCreditExpired(account);
|
||||
if (!creditExpired && !tempExpired)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 定时任务场景无登录态,操作人回落为 system(人工触发的入口仍记录实际操作人)
|
||||
String username = currentUsername();
|
||||
Long userId = currentUserId();
|
||||
BigDecimal before = nz(account.getExtraCredit());
|
||||
account.setExtraCredit(BigDecimal.ZERO);
|
||||
BigDecimal beforeCredit = nz(account.getTotalCredit());
|
||||
BigDecimal beforeExtra = nz(account.getExtraCredit());
|
||||
if (creditExpired)
|
||||
{
|
||||
account.setTotalCredit(BigDecimal.ZERO);
|
||||
}
|
||||
if (tempExpired)
|
||||
{
|
||||
account.setExtraCredit(BigDecimal.ZERO);
|
||||
}
|
||||
account.setUpdateBy(username);
|
||||
int rows = customerMoneyMapper.updateAccountCredit(account);
|
||||
if (rows == 0)
|
||||
@@ -189,25 +208,49 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
}
|
||||
account.setVersion(account.getVersion() + 1);
|
||||
bmsCustomerMapper.updateCustomerCredit(account.getCustomerId(),
|
||||
nz(account.getTotalCredit()), BigDecimal.ZERO, username);
|
||||
nz(account.getTotalCredit()), nz(account.getExtraCredit()), username);
|
||||
|
||||
// 释放对应角色额度池(无台账背书的历史额度释放金额为0,不影响池余额)
|
||||
if (creditExpired)
|
||||
{
|
||||
creditQuotaService.releaseAll(account.getCustomerId(),
|
||||
CustomerCreditFlow.CREDIT_TYPE_CREDIT, username);
|
||||
writeExpireFlow(account, CustomerCreditFlow.CREDIT_TYPE_CREDIT, beforeCredit,
|
||||
account.getCreditEffectiveDate(), account.getCreditExpireDate(),
|
||||
username, userId, "信用额度到期自动失效");
|
||||
}
|
||||
if (tempExpired)
|
||||
{
|
||||
creditQuotaService.releaseAll(account.getCustomerId(),
|
||||
CustomerCreditFlow.CREDIT_TYPE_TEMP, username);
|
||||
writeExpireFlow(account, CustomerCreditFlow.CREDIT_TYPE_TEMP, beforeExtra,
|
||||
account.getExtraEffectiveDate(), account.getExtraExpireDate(),
|
||||
username, userId, "临时额度到期自动失效");
|
||||
}
|
||||
}
|
||||
|
||||
/** 到期失效写 EXPIRE 额度流水 */
|
||||
private void writeExpireFlow(CustomerAccount account, String creditType, BigDecimal before,
|
||||
Date effectiveDate, Date expireDate,
|
||||
String username, Long userId, String reason)
|
||||
{
|
||||
CustomerCreditFlow flow = new CustomerCreditFlow();
|
||||
flow.setFlowNo(generateNo("EDLS"));
|
||||
flow.setCustomerId(account.getCustomerId());
|
||||
flow.setAccountId(account.getId());
|
||||
flow.setCreditType(CustomerCreditFlow.CREDIT_TYPE_TEMP);
|
||||
flow.setCreditType(creditType);
|
||||
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.setEffectiveDate(effectiveDate);
|
||||
flow.setExpireDate(expireDate);
|
||||
flow.setReason(reason);
|
||||
flow.setOperatorId(userId);
|
||||
flow.setCreateBy(username);
|
||||
flow.setRemark("系统在访问账户时检测到临时额度已过失效日期,自动置零");
|
||||
flow.setRemark("系统在访问账户或定时任务中检测到额度已过失效日期,自动置零");
|
||||
flow.setDelFlag("0");
|
||||
customerMoneyMapper.insertCreditFlow(flow);
|
||||
}
|
||||
@@ -473,7 +516,7 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
if (BASE_CURRENCY.equals(currency))
|
||||
{
|
||||
CustomerAccount account = getOrInitCnyAccount(customerId, username);
|
||||
expireTempCreditIfNeeded(account);
|
||||
expireDueCreditsIfNeeded(account);
|
||||
return account;
|
||||
}
|
||||
return customerMoneyMapper.selectAccountForUpdate(customerId, currency);
|
||||
@@ -488,7 +531,7 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
if (BASE_CURRENCY.equals(currency))
|
||||
{
|
||||
account = getOrInitCnyAccount(customerId, username);
|
||||
expireTempCreditIfNeeded(account);
|
||||
expireDueCreditsIfNeeded(account);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -676,21 +719,24 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
BigDecimal rawExtra = account != null ? nz(account.getExtraCredit()) : BigDecimal.ZERO;
|
||||
BigDecimal usedCredit = account != null ? nz(account.getUsedCredit()) : BigDecimal.ZERO;
|
||||
|
||||
// 临时额度已过期则视为0(仅内存计算,不物化DB;由定时任务/真实资金入口落库)
|
||||
// 已过期的信用/临时额度视为0(仅内存计算,不物化DB;由定时任务/真实资金入口落库)
|
||||
boolean creditExpiredPending = isCreditExpired(account);
|
||||
boolean expiredPending = isTempCreditExpired(account);
|
||||
BigDecimal effectiveTotal = creditExpiredPending ? BigDecimal.ZERO : totalCredit;
|
||||
BigDecimal effectiveExtra = expiredPending ? BigDecimal.ZERO : rawExtra;
|
||||
|
||||
vo.setBalance(account != null ? nz(account.getBalance()) : BigDecimal.ZERO);
|
||||
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 = totalCredit.add(effectiveExtra).subtract(usedCredit);
|
||||
BigDecimal available = effectiveTotal.add(effectiveExtra).subtract(usedCredit);
|
||||
vo.setAvailableCredit(available);
|
||||
boolean overdue = available.compareTo(BigDecimal.ZERO) < 0;
|
||||
vo.setOverdue(overdue);
|
||||
vo.setExpiredTempCreditPending(expiredPending);
|
||||
vo.setExpiredTempCreditPending(expiredPending || creditExpiredPending);
|
||||
|
||||
boolean disabled = "1".equals(cust.getIsEnable());
|
||||
if (disabled && overdue)
|
||||
@@ -779,6 +825,16 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
.atZone(java.time.ZoneId.systemDefault()).toLocalDate());
|
||||
}
|
||||
|
||||
/** 信用额度是否已过期:口径同临时额度(失效日当天仍有效) */
|
||||
private boolean isCreditExpired(CustomerAccount account)
|
||||
{
|
||||
return account != null
|
||||
&& nz(account.getTotalCredit()).compareTo(BigDecimal.ZERO) > 0
|
||||
&& account.getCreditExpireDate() != null
|
||||
&& LocalDate.now().isAfter(account.getCreditExpireDate().toInstant()
|
||||
.atZone(java.time.ZoneId.systemDefault()).toLocalDate());
|
||||
}
|
||||
|
||||
private String resolveCurrency(String currency)
|
||||
{
|
||||
return StringUtils.hasText(currency) ? currency.trim().toUpperCase() : BASE_CURRENCY;
|
||||
|
||||
+9
-1
@@ -34,9 +34,17 @@ public class CustomerAccount extends BaseEntity
|
||||
/** 冻结余额 */
|
||||
private BigDecimal frozenBalance;
|
||||
|
||||
/** 总额度 */
|
||||
/** 总额度(信用额度,默认0;配置有效期后到期次日自动清零) */
|
||||
private BigDecimal totalCredit;
|
||||
|
||||
/** 信用额度生效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date creditEffectiveDate;
|
||||
|
||||
/** 信用额度失效日期(空=长期;到期次日自动清零) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date creditExpireDate;
|
||||
|
||||
/** 额外额度(临时额度) */
|
||||
private BigDecimal extraCredit;
|
||||
|
||||
|
||||
+6
@@ -79,6 +79,9 @@ public class CustomerCreditApply extends BaseEntity
|
||||
/** 审批人ID */
|
||||
private Long approveUserId;
|
||||
|
||||
/** 审批占用的额度池角色ID(NULL=超管审批不占池;调减/到期释放时不消费池) */
|
||||
private Long approveRoleId;
|
||||
|
||||
/** 审批时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date approveTime;
|
||||
@@ -99,4 +102,7 @@ public class CustomerCreditApply extends BaseEntity
|
||||
|
||||
/** 客户名称 */
|
||||
private String customerName;
|
||||
|
||||
/** 额度池角色名称(关联展示) */
|
||||
private String approveRoleName;
|
||||
}
|
||||
|
||||
+3
@@ -101,6 +101,9 @@ public class CustomerCreditFlow extends BaseEntity
|
||||
/** 操作人ID */
|
||||
private Long operatorId;
|
||||
|
||||
/** 额度池角色ID(ADJUST 审批调整时记录本次占用/释放的角色池;NULL=超管/系统) */
|
||||
private Long roleId;
|
||||
|
||||
/** 删除标志(0存在 1删除) */
|
||||
private String delFlag;
|
||||
}
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package com.ruoyi.customer.customermoney.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 客户授信额度分配台账 customer_credit_grant
|
||||
* 审批通过的净增额度占一行(占用角色额度池);调减/到期按 FIFO 核销行余额。
|
||||
* 角色池已用 = SUM(remaining WHERE role_id=? AND status='0')
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-20
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class CustomerCreditGrant extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 占用中 */
|
||||
public static final String STATUS_ACTIVE = "0";
|
||||
/** 已释放 */
|
||||
public static final String STATUS_RELEASED = "1";
|
||||
|
||||
/** 台账ID */
|
||||
private Long grantId;
|
||||
|
||||
/** 客户ID */
|
||||
private Long customerId;
|
||||
|
||||
/** 客户账户ID */
|
||||
private Long accountId;
|
||||
|
||||
/** 占用的额度池角色ID(NULL=超管审批/不占池) */
|
||||
private Long roleId;
|
||||
|
||||
/** 审批人ID */
|
||||
private Long approveUserId;
|
||||
|
||||
/** 额度申请单ID */
|
||||
private Long applyId;
|
||||
|
||||
/** 额度流水ID */
|
||||
private Long flowId;
|
||||
|
||||
/** 额度类型:CREDIT信用额度 TEMP临时额度 */
|
||||
private String creditType;
|
||||
|
||||
/** 原始占用池金额 */
|
||||
private BigDecimal amount;
|
||||
|
||||
/** 仍占用池的待释放余额 */
|
||||
private BigDecimal remaining;
|
||||
|
||||
/** 生效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date effectiveDate;
|
||||
|
||||
/** 失效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date expireDate;
|
||||
|
||||
/** 状态:0占用中 1已释放 */
|
||||
private String status;
|
||||
|
||||
/** 全部释放时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date releaseTime;
|
||||
|
||||
/** 删除标志 */
|
||||
private String delFlag;
|
||||
|
||||
/* -------- 展示字段 -------- */
|
||||
|
||||
/** 客户编码 */
|
||||
private String customerCode;
|
||||
|
||||
/** 客户名称 */
|
||||
private String customerName;
|
||||
|
||||
/** 角色名称 */
|
||||
private String roleName;
|
||||
|
||||
/** 审批人用户名 */
|
||||
private String approveBy;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.ruoyi.customer.customermoney.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 角色可分配授信额度池 sys_role_credit_quota
|
||||
* 一个角色一行;没有记录视为额度0(该角色用户不能审批任何增量授信)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-20
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SysRoleCreditQuota extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 角色ID */
|
||||
private Long roleId;
|
||||
|
||||
/** 可分配额度总额(CNY) */
|
||||
private BigDecimal totalQuota;
|
||||
|
||||
/** 乐观锁版本号 */
|
||||
private Integer version;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.ruoyi.customer.customermoney.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerCreditGrant;
|
||||
import com.ruoyi.customer.customermoney.domain.SysRoleCreditQuota;
|
||||
import com.ruoyi.customer.customermoney.vo.RoleCreditQuotaVo;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 角色额度池 / 授信分配台账 Mapper
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-20
|
||||
*/
|
||||
public interface CreditQuotaMapper
|
||||
{
|
||||
/* ================= 角色额度池 ================= */
|
||||
|
||||
/** 全部角色的额度池(左连接,未配置额度的角色总额度按0返回;used 由台账 remaining 实时求和) */
|
||||
List<RoleCreditQuotaVo> selectRoleQuotaList();
|
||||
|
||||
/** 指定用户名下正常角色的额度池(sys_user_role 关联) */
|
||||
List<RoleCreditQuotaVo> selectRoleQuotaListByUserId(@Param("userId") Long userId);
|
||||
|
||||
/** 行锁取额度池配置(不存在返回null) */
|
||||
SysRoleCreditQuota selectQuotaForUpdate(@Param("roleId") Long roleId);
|
||||
|
||||
/** 角色池已分配额(占用中台账 remaining 合计;调用前需先持有该池行锁) */
|
||||
BigDecimal sumActiveRemaining(@Param("roleId") Long roleId);
|
||||
|
||||
/** 新建额度池配置 */
|
||||
int insertQuota(SysRoleCreditQuota quota);
|
||||
|
||||
/** 修改额度池总额度(乐观锁) */
|
||||
int updateQuota(SysRoleCreditQuota quota);
|
||||
|
||||
/* ================= 授信分配台账 ================= */
|
||||
|
||||
/** 台账查询(带客户名/角色名) */
|
||||
List<CustomerCreditGrant> selectGrantList(CustomerCreditGrant query);
|
||||
|
||||
/** 行锁取某客户某额度类型的占用中台账(FIFO:按 grant_id 升序) */
|
||||
List<CustomerCreditGrant> selectActiveGrantsForUpdate(@Param("customerId") Long customerId,
|
||||
@Param("creditType") String creditType);
|
||||
|
||||
/** 新增台账 */
|
||||
int insertGrant(CustomerCreditGrant grant);
|
||||
|
||||
/** 更新台账剩余余额/状态(乐观条件 remaining 不变,防并发核销) */
|
||||
int updateGrantRemaining(CustomerCreditGrant grant);
|
||||
}
|
||||
+2
-2
@@ -29,10 +29,10 @@ public interface CustomerMoneyMapper
|
||||
List<CustomerAccount> selectAccountListByCustomerId(@Param("customerId") Long customerId);
|
||||
|
||||
/**
|
||||
* 扫描临时额度已到期的CNY账户客户ID(extra_credit>0 且失效日期早于今天;失效日当天仍有效)。
|
||||
* 扫描授信已到期的CNY账户客户ID(信用或临时额度失效日早于今天;失效日当天仍有效)。
|
||||
* 供定时任务批量物化失效使用,结果集不加锁,逐户处理时再行锁。
|
||||
*/
|
||||
List<Long> selectExpiredTempCreditCustomerIds();
|
||||
List<Long> selectExpiredCreditCustomerIds();
|
||||
|
||||
/** 新增账户 */
|
||||
int insertAccount(CustomerAccount account);
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.ruoyi.customer.customermoney.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerAccount;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerCreditGrant;
|
||||
import com.ruoyi.customer.customermoney.vo.RoleCreditQuotaVo;
|
||||
|
||||
/**
|
||||
* 角色额度池 Service
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-20
|
||||
*/
|
||||
public interface ICreditQuotaService
|
||||
{
|
||||
/** 全部角色额度池(管理员配置用) */
|
||||
List<RoleCreditQuotaVo> listRoleQuotas();
|
||||
|
||||
/** 当前用户可使用的角色额度池(审批选池用;超管返回空列表由调用方放行) */
|
||||
List<RoleCreditQuotaVo> myPools(Long userId);
|
||||
|
||||
/** 设置角色可分配额度总额(不能小于该池已分配额) */
|
||||
void setRoleQuota(Long roleId, BigDecimal totalQuota, String remark);
|
||||
|
||||
/** 额度池占用台账查询 */
|
||||
List<CustomerCreditGrant> listGrants(CustomerCreditGrant query);
|
||||
|
||||
/**
|
||||
* 审批净增额度:占用角色额度池并写台账。
|
||||
* roleId 为 null(超管审批)时台账角色为空、不占用任何池。
|
||||
* 必须在客户账户行锁之后调用;内部对角色池加行锁防并发超配。
|
||||
*/
|
||||
void consume(CustomerAccount account, String creditType, BigDecimal amount,
|
||||
Date effectiveDate, Date expireDate, Long roleId,
|
||||
Long applyId, Long flowId, String username, Long userId);
|
||||
|
||||
/**
|
||||
* 审批调减:按 FIFO 核销该客户该类型的占用台账,释放对应角色池。
|
||||
* 历史无台账背书的额度(建档直配/超管授予)核销时不释放池。
|
||||
* 返回实际从池中释放的金额(可能小于 amount)。
|
||||
*/
|
||||
BigDecimal releaseFifo(Long customerId, String creditType, BigDecimal amount, String username);
|
||||
|
||||
/**
|
||||
* 到期自动失效:释放该客户该类型全部占用台账,返回释放总额。
|
||||
*/
|
||||
BigDecimal releaseAll(Long customerId, String creditType, String username);
|
||||
}
|
||||
+3
-2
@@ -19,7 +19,7 @@ public interface ICustomerMoneyService
|
||||
List<CustomerAccount> selectAccountList(Long customerId);
|
||||
|
||||
/**
|
||||
* 新建客户时初始化默认CNY账户(余额0,默认总额度1000 CNY),并写 INIT 额度流水。
|
||||
* 新建客户时初始化默认CNY账户(余额0,信用额度默认0),并写 INIT 额度流水。
|
||||
* 已存在CNY账户时不重复创建。
|
||||
*
|
||||
* @param customerId 客户ID
|
||||
@@ -60,8 +60,9 @@ public interface ICustomerMoneyService
|
||||
/**
|
||||
* 审批通过额度调整申请:PENDING -> APPROVED。
|
||||
* 事务内重新锁定CNY账户并按审批时的实际额度计算/校验,通过后调整额度、回写客户档案、写额度流水。
|
||||
* 净增额度需占用 approveRoleId 对应角色额度池(超管传 null 不占池;净减/到期释放不占池)。
|
||||
*/
|
||||
int approveCreditApply(Long applyId, String approveRemark);
|
||||
int approveCreditApply(Long applyId, String approveRemark, Long approveRoleId);
|
||||
|
||||
/** 审批驳回:PENDING -> REJECTED,驳回原因必填 */
|
||||
int rejectCreditApply(Long applyId, String rejectReason);
|
||||
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
package com.ruoyi.customer.customermoney.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import com.ruoyi.common.core.domain.entity.SysRole;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerAccount;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerCreditGrant;
|
||||
import com.ruoyi.customer.customermoney.domain.SysRoleCreditQuota;
|
||||
import com.ruoyi.customer.customermoney.mapper.CreditQuotaMapper;
|
||||
import com.ruoyi.customer.customermoney.service.ICreditQuotaService;
|
||||
import com.ruoyi.customer.customermoney.vo.RoleCreditQuotaVo;
|
||||
import com.ruoyi.system.mapper.SysRoleMapper;
|
||||
|
||||
/**
|
||||
* 角色额度池 Service实现
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-20
|
||||
*/
|
||||
@Service
|
||||
public class CreditQuotaServiceImpl implements ICreditQuotaService
|
||||
{
|
||||
private static final int MONEY_SCALE = 2;
|
||||
|
||||
@Autowired
|
||||
private CreditQuotaMapper creditQuotaMapper;
|
||||
|
||||
@Autowired
|
||||
private SysRoleMapper sysRoleMapper;
|
||||
|
||||
@Override
|
||||
public List<RoleCreditQuotaVo> listRoleQuotas()
|
||||
{
|
||||
List<RoleCreditQuotaVo> list = creditQuotaMapper.selectRoleQuotaList();
|
||||
list.forEach(this::fillAvailable);
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RoleCreditQuotaVo> myPools(Long userId)
|
||||
{
|
||||
if (userId == null)
|
||||
{
|
||||
throw new ServiceException("无法获取当前登录用户");
|
||||
}
|
||||
List<RoleCreditQuotaVo> list = creditQuotaMapper.selectRoleQuotaListByUserId(userId);
|
||||
list.forEach(this::fillAvailable);
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void setRoleQuota(Long roleId, BigDecimal totalQuota, String remark)
|
||||
{
|
||||
if (roleId == null)
|
||||
{
|
||||
throw new ServiceException("角色ID不能为空");
|
||||
}
|
||||
SysRole role = sysRoleMapper.selectRoleById(roleId);
|
||||
if (role == null || "2".equals(role.getDelFlag()))
|
||||
{
|
||||
throw new ServiceException("角色不存在或已删除");
|
||||
}
|
||||
BigDecimal total = nz(totalQuota).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
if (total.compareTo(BigDecimal.ZERO) < 0)
|
||||
{
|
||||
throw new ServiceException("可分配额度总额不能为负数");
|
||||
}
|
||||
String username = SecurityUtils.getUsername();
|
||||
|
||||
SysRoleCreditQuota quota = creditQuotaMapper.selectQuotaForUpdate(roleId);
|
||||
if (quota == null)
|
||||
{
|
||||
SysRoleCreditQuota newQuota = new SysRoleCreditQuota();
|
||||
newQuota.setRoleId(roleId);
|
||||
newQuota.setTotalQuota(total);
|
||||
newQuota.setVersion(0);
|
||||
newQuota.setCreateBy(username);
|
||||
newQuota.setRemark(StringUtils.hasText(remark) ? remark.trim() : null);
|
||||
creditQuotaMapper.insertQuota(newQuota);
|
||||
return;
|
||||
}
|
||||
BigDecimal used = nz(creditQuotaMapper.sumActiveRemaining(roleId));
|
||||
if (total.compareTo(used) < 0)
|
||||
{
|
||||
throw new ServiceException("可分配额度总额不能小于该角色已分配额度:" + used.toPlainString() + " CNY");
|
||||
}
|
||||
quota.setTotalQuota(total);
|
||||
quota.setUpdateBy(username);
|
||||
quota.setRemark(StringUtils.hasText(remark) ? remark.trim() : null);
|
||||
if (creditQuotaMapper.updateQuota(quota) == 0)
|
||||
{
|
||||
throw new ServiceException("额度池已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CustomerCreditGrant> listGrants(CustomerCreditGrant query)
|
||||
{
|
||||
return creditQuotaMapper.selectGrantList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void consume(CustomerAccount account, String creditType, BigDecimal amount,
|
||||
Date effectiveDate, Date expireDate, Long roleId,
|
||||
Long applyId, Long flowId, String username, Long userId)
|
||||
{
|
||||
if (account == null || account.getId() == null)
|
||||
{
|
||||
throw new ServiceException("客户账户不存在,无法分配额度");
|
||||
}
|
||||
BigDecimal delta = nz(amount).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
if (delta.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CustomerCreditGrant grant = new CustomerCreditGrant();
|
||||
grant.setCustomerId(account.getCustomerId());
|
||||
grant.setAccountId(account.getId());
|
||||
grant.setApproveUserId(userId);
|
||||
grant.setApplyId(applyId);
|
||||
grant.setFlowId(flowId);
|
||||
grant.setCreditType(creditType);
|
||||
grant.setAmount(delta);
|
||||
grant.setRemaining(delta);
|
||||
grant.setEffectiveDate(effectiveDate);
|
||||
grant.setExpireDate(expireDate);
|
||||
grant.setStatus(CustomerCreditGrant.STATUS_ACTIVE);
|
||||
grant.setCreateBy(username);
|
||||
|
||||
if (roleId == null)
|
||||
{
|
||||
// 超管审批:不占用任何角色池
|
||||
creditQuotaMapper.insertGrant(grant);
|
||||
return;
|
||||
}
|
||||
// 校验角色归属与状态
|
||||
SysRole role = sysRoleMapper.selectRoleById(roleId);
|
||||
if (role == null || "2".equals(role.getDelFlag()) || !"0".equals(role.getStatus()))
|
||||
{
|
||||
throw new ServiceException("额度池角色不存在或已停用");
|
||||
}
|
||||
List<Long> myRoleIds = sysRoleMapper.selectRoleListByUserId(userId)
|
||||
.stream().filter(id -> id != null && id.equals(roleId)).collect(Collectors.toList());
|
||||
if (myRoleIds.isEmpty())
|
||||
{
|
||||
throw new ServiceException("不能使用不属于自己的角色额度池");
|
||||
}
|
||||
// 池行锁 -> 已分配实时汇总 -> 容量校验,防并发超配
|
||||
SysRoleCreditQuota quota = creditQuotaMapper.selectQuotaForUpdate(roleId);
|
||||
if (quota == null)
|
||||
{
|
||||
throw new ServiceException("角色【" + role.getRoleName() + "】未配置可分配额度总额,请联系管理员");
|
||||
}
|
||||
BigDecimal used = nz(creditQuotaMapper.sumActiveRemaining(roleId));
|
||||
BigDecimal available = nz(quota.getTotalQuota()).subtract(used);
|
||||
if (available.compareTo(delta) < 0)
|
||||
{
|
||||
throw new ServiceException("角色【" + role.getRoleName() + "】额度池不足:总额度 "
|
||||
+ nz(quota.getTotalQuota()).toPlainString() + ",已分配 " + used.toPlainString()
|
||||
+ ",剩余 " + available.toPlainString() + ",本次需分配 " + delta.toPlainString());
|
||||
}
|
||||
grant.setRoleId(roleId);
|
||||
creditQuotaMapper.insertGrant(grant);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public BigDecimal releaseFifo(Long customerId, String creditType, BigDecimal amount, String username)
|
||||
{
|
||||
BigDecimal need = nz(amount);
|
||||
if (need.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
List<CustomerCreditGrant> grants = creditQuotaMapper.selectActiveGrantsForUpdate(customerId, creditType);
|
||||
if (grants.isEmpty())
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
lockQuotaRows(grants);
|
||||
|
||||
BigDecimal released = BigDecimal.ZERO;
|
||||
Date now = new Date();
|
||||
for (CustomerCreditGrant grant : grants)
|
||||
{
|
||||
if (need.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
BigDecimal remaining = nz(grant.getRemaining());
|
||||
if (remaining.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BigDecimal take = remaining.min(need);
|
||||
BigDecimal after = remaining.subtract(take).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
grant.setRemaining(after);
|
||||
grant.setUpdateBy(username);
|
||||
if (after.compareTo(BigDecimal.ZERO) == 0)
|
||||
{
|
||||
grant.setStatus(CustomerCreditGrant.STATUS_RELEASED);
|
||||
grant.setReleaseTime(now);
|
||||
}
|
||||
else
|
||||
{
|
||||
grant.setStatus(CustomerCreditGrant.STATUS_ACTIVE);
|
||||
grant.setReleaseTime(null);
|
||||
}
|
||||
creditQuotaMapper.updateGrantRemaining(grant);
|
||||
need = need.subtract(take);
|
||||
released = released.add(take);
|
||||
}
|
||||
return released.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public BigDecimal releaseAll(Long customerId, String creditType, String username)
|
||||
{
|
||||
List<CustomerCreditGrant> grants = creditQuotaMapper.selectActiveGrantsForUpdate(customerId, creditType);
|
||||
if (grants.isEmpty())
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
lockQuotaRows(grants);
|
||||
|
||||
BigDecimal released = BigDecimal.ZERO;
|
||||
Date now = new Date();
|
||||
for (CustomerCreditGrant grant : grants)
|
||||
{
|
||||
BigDecimal remaining = nz(grant.getRemaining());
|
||||
if (remaining.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
grant.setRemaining(BigDecimal.ZERO);
|
||||
grant.setStatus(CustomerCreditGrant.STATUS_RELEASED);
|
||||
grant.setReleaseTime(now);
|
||||
grant.setUpdateBy(username);
|
||||
creditQuotaMapper.updateGrantRemaining(grant);
|
||||
released = released.add(remaining);
|
||||
}
|
||||
return released.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对台账涉及的非空角色池按 role_id 升序加行锁,
|
||||
* 保证与池占用(consume)互斥且避免多角色交叉死锁。
|
||||
*/
|
||||
private void lockQuotaRows(List<CustomerCreditGrant> grants)
|
||||
{
|
||||
List<Long> roleIds = grants.stream()
|
||||
.map(CustomerCreditGrant::getRoleId)
|
||||
.filter(id -> id != null)
|
||||
.distinct()
|
||||
.sorted(Comparator.naturalOrder())
|
||||
.collect(Collectors.toList());
|
||||
for (Long roleId : roleIds)
|
||||
{
|
||||
// 池配置理论上一定存在(占用时已校验);若被误删则空过,不影响台账核销
|
||||
creditQuotaMapper.selectQuotaForUpdate(roleId);
|
||||
}
|
||||
}
|
||||
|
||||
private void fillAvailable(RoleCreditQuotaVo vo)
|
||||
{
|
||||
BigDecimal total = nz(vo.getTotalQuota());
|
||||
BigDecimal used = nz(vo.getUsedQuota());
|
||||
vo.setTotalQuota(total);
|
||||
vo.setUsedQuota(used);
|
||||
vo.setAvailableQuota(total.subtract(used).setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
||||
}
|
||||
|
||||
private BigDecimal nz(BigDecimal value)
|
||||
{
|
||||
return value == null ? BigDecimal.ZERO : value;
|
||||
}
|
||||
}
|
||||
+70
-29
@@ -28,6 +28,7 @@ import com.ruoyi.customer.customermoney.domain.CustomerCreditFlow;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerReceipt;
|
||||
import com.ruoyi.customer.customermoney.mapper.CustomerCreditApplyMapper;
|
||||
import com.ruoyi.customer.customermoney.mapper.CustomerMoneyMapper;
|
||||
import com.ruoyi.customer.customermoney.service.ICreditQuotaService;
|
||||
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
|
||||
import com.ruoyi.system.domain.BmsExchangeRate;
|
||||
import com.ruoyi.system.service.IBmsExchangeRateService;
|
||||
@@ -64,19 +65,24 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
@Autowired
|
||||
private ICustomerAccountApi customerAccountApi;
|
||||
|
||||
/** 角色额度池:审批增量占用/调减与到期释放 */
|
||||
@Autowired
|
||||
private ICreditQuotaService creditQuotaService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<CustomerAccount> selectAccountList(Long customerId)
|
||||
{
|
||||
List<CustomerAccount> list = customerMoneyMapper.selectAccountListByCustomerId(customerId);
|
||||
// 查询时顺带处理临时额度到期自动失效(仅CNY账户),保证卡片展示为最新值
|
||||
// 查询时顺带处理授信到期自动失效(仅CNY账户),保证卡片展示为最新值
|
||||
for (int i = 0; i < list.size(); i++)
|
||||
{
|
||||
CustomerAccount account = list.get(i);
|
||||
if (BASE_CURRENCY.equals(account.getCurrency()) && isTempCreditExpired(account))
|
||||
if (BASE_CURRENCY.equals(account.getCurrency())
|
||||
&& (isTempCreditExpired(account) || isCreditExpired(account)))
|
||||
{
|
||||
CustomerAccount locked = customerAccountApi.lockCnyAccount(customerId);
|
||||
customerAccountApi.expireTempCreditIfNeeded(locked);
|
||||
customerAccountApi.expireDueCreditsIfNeeded(locked);
|
||||
if (locked != null)
|
||||
{
|
||||
list.set(i, locked);
|
||||
@@ -331,10 +337,11 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
|
||||
/**
|
||||
* 执行额度调整(额度申请审批通过后调用)。
|
||||
* 锁CNY账户 -> 按审批时实际额度计算并校验 -> 乐观锁更新 -> 回写客户档案 -> 写额度流水。
|
||||
* 锁CNY账户 -> 按审批时实际额度计算并校验 -> 乐观锁更新 -> 回写客户档案 -> 写额度流水 -> 角色额度池占用/释放。
|
||||
* roleId 为本次审批选择的额度池角色(超管/净减时可为null);applyId 为审批申请单ID。
|
||||
* 返回已落库的额度流水(含ID)。
|
||||
*/
|
||||
private CustomerCreditFlow executeAdjustCredit(CustomerCreditFlow cmd)
|
||||
private CustomerCreditFlow executeAdjustCredit(CustomerCreditFlow cmd, Long roleId, Long applyId)
|
||||
{
|
||||
String username = SecurityUtils.getUsername();
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
@@ -365,18 +372,18 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
}
|
||||
after = after.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
|
||||
// 3. 临时额度有效期处理(不允许预约未来生效,失效日必须晚于生效日)
|
||||
// 3. 有效期处理:信用/临时同口径——不允许预约未来生效;信用额度失效日期必填,临时额度空=长期
|
||||
Date effectiveDate = null;
|
||||
Date expireDate = null;
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate eff = cmd.getEffectiveDate() != null ? toLocalDate(cmd.getEffectiveDate()) : today;
|
||||
effectiveDate = toSqlDate(eff);
|
||||
if (cmd.getExpireDate() != null)
|
||||
{
|
||||
expireDate = toSqlDate(toLocalDate(cmd.getExpireDate()));
|
||||
}
|
||||
if (isTemp)
|
||||
{
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate eff = cmd.getEffectiveDate() != null ? toLocalDate(cmd.getEffectiveDate()) : today;
|
||||
effectiveDate = toSqlDate(eff);
|
||||
if (cmd.getExpireDate() != null)
|
||||
{
|
||||
expireDate = toSqlDate(toLocalDate(cmd.getExpireDate()));
|
||||
}
|
||||
account.setExtraCredit(after);
|
||||
account.setExtraEffectiveDate(effectiveDate);
|
||||
account.setExtraExpireDate(expireDate);
|
||||
@@ -384,6 +391,8 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
else
|
||||
{
|
||||
account.setTotalCredit(after);
|
||||
account.setCreditEffectiveDate(effectiveDate);
|
||||
account.setCreditExpireDate(expireDate);
|
||||
}
|
||||
account.setUpdateBy(username);
|
||||
|
||||
@@ -417,10 +426,29 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
flow.setReason(reason);
|
||||
flow.setAdjustMode(adjustMode);
|
||||
flow.setOperatorId(userId);
|
||||
// 非超管审批且未指定角色池的净增申请不允许通过(在 consume 中强校验)
|
||||
flow.setRoleId(SecurityUtils.isAdmin(userId) ? null : roleId);
|
||||
flow.setCreateBy(username);
|
||||
flow.setRemark(cmd.getRemark());
|
||||
flow.setDelFlag("0");
|
||||
customerMoneyMapper.insertCreditFlow(flow);
|
||||
|
||||
// 7. 角色额度池占用/释放(账户行锁已持有;池锁在台账服务内部按角色加锁)
|
||||
BigDecimal netDelta = after.subtract(before);
|
||||
if (netDelta.compareTo(BigDecimal.ZERO) > 0)
|
||||
{
|
||||
Long poolRoleId = SecurityUtils.isAdmin(userId) ? null : roleId;
|
||||
if (poolRoleId == null && !SecurityUtils.isAdmin(userId))
|
||||
{
|
||||
throw new ServiceException("请选择本次额度分配占用的角色额度池");
|
||||
}
|
||||
creditQuotaService.consume(account, creditType, netDelta,
|
||||
effectiveDate, expireDate, poolRoleId, applyId, flow.getId(), username, userId);
|
||||
}
|
||||
else if (netDelta.compareTo(BigDecimal.ZERO) < 0)
|
||||
{
|
||||
creditQuotaService.releaseFifo(customerId, creditType, netDelta.negate(), username);
|
||||
}
|
||||
return flow;
|
||||
}
|
||||
|
||||
@@ -477,7 +505,7 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int approveCreditApply(Long applyId, String approveRemark)
|
||||
public int approveCreditApply(Long applyId, String approveRemark, Long approveRoleId)
|
||||
{
|
||||
if (applyId == null)
|
||||
{
|
||||
@@ -505,7 +533,7 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
cmd.setRemark(apply.getRemark());
|
||||
cmd.setEffectiveDate(apply.getEffectiveDate());
|
||||
cmd.setExpireDate(apply.getExpireDate());
|
||||
CustomerCreditFlow flow = executeAdjustCredit(cmd);
|
||||
CustomerCreditFlow flow = executeAdjustCredit(cmd, approveRoleId, applyId);
|
||||
|
||||
CustomerCreditApply update = new CustomerCreditApply();
|
||||
update.setApplyId(applyId);
|
||||
@@ -513,6 +541,8 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
update.setFlowId(flow.getId());
|
||||
update.setApproveBy(username);
|
||||
update.setApproveUserId(userId);
|
||||
// 超管审批不占池;非超管净减时 roleId 为 null 也正常(只释放不占用)
|
||||
update.setApproveRoleId(SecurityUtils.isAdmin(userId) ? null : approveRoleId);
|
||||
update.setApproveRemark(approveRemark);
|
||||
int rows = creditApplyMapper.updateApproved(update);
|
||||
if (rows == 0)
|
||||
@@ -628,7 +658,7 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
boolean isTemp = CustomerCreditFlow.CREDIT_TYPE_TEMP.equals(creditType);
|
||||
|
||||
CustomerAccount account = customerAccountApi.lockCnyAccount(cmd.getCustomerId());
|
||||
customerAccountApi.expireTempCreditIfNeeded(account);
|
||||
customerAccountApi.expireDueCreditsIfNeeded(account);
|
||||
|
||||
BigDecimal before = isTemp ? nz(account.getExtraCredit()) : nz(account.getTotalCredit());
|
||||
BigDecimal otherCredit = isTemp ? nz(account.getTotalCredit()) : nz(account.getExtraCredit());
|
||||
@@ -660,21 +690,23 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
throw new ServiceException("额度未发生变化,无需调整");
|
||||
}
|
||||
|
||||
if (isTemp)
|
||||
if (!isTemp && cmd.getExpireDate() == null)
|
||||
{
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate eff = cmd.getEffectiveDate() != null ? toLocalDate(cmd.getEffectiveDate()) : today;
|
||||
if (eff.isAfter(today))
|
||||
// 信用额度必须设置失效日期(到期自动清零);临时额度不填=长期
|
||||
throw new ServiceException("请选择信用额度失效日期");
|
||||
}
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate eff = cmd.getEffectiveDate() != null ? toLocalDate(cmd.getEffectiveDate()) : today;
|
||||
if (eff.isAfter(today))
|
||||
{
|
||||
throw new ServiceException("生效日期不能晚于今天,需要未来生效请在生效当天再提交");
|
||||
}
|
||||
if (cmd.getExpireDate() != null)
|
||||
{
|
||||
LocalDate exp = toLocalDate(cmd.getExpireDate());
|
||||
if (!exp.isAfter(eff))
|
||||
{
|
||||
throw new ServiceException("生效日期不能晚于今天,需要未来生效请在生效当天再提交");
|
||||
}
|
||||
if (cmd.getExpireDate() != null)
|
||||
{
|
||||
LocalDate exp = toLocalDate(cmd.getExpireDate());
|
||||
if (!exp.isAfter(eff))
|
||||
{
|
||||
throw new ServiceException("失效日期必须晚于生效日期");
|
||||
}
|
||||
throw new ServiceException("失效日期必须晚于生效日期");
|
||||
}
|
||||
}
|
||||
return account;
|
||||
@@ -701,6 +733,15 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
&& LocalDate.now().isAfter(toLocalDate(account.getExtraExpireDate()));
|
||||
}
|
||||
|
||||
/** 信用额度是否已过期:口径同临时额度(失效日当天仍有效) */
|
||||
private boolean isCreditExpired(CustomerAccount account)
|
||||
{
|
||||
return account != null
|
||||
&& nz(account.getTotalCredit()).compareTo(BigDecimal.ZERO) > 0
|
||||
&& account.getCreditExpireDate() != null
|
||||
&& LocalDate.now().isAfter(toLocalDate(account.getCreditExpireDate()));
|
||||
}
|
||||
|
||||
private LocalDate toLocalDate(Date date)
|
||||
{
|
||||
return date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
|
||||
|
||||
+7
-7
@@ -10,11 +10,11 @@ import org.springframework.stereotype.Component;
|
||||
import com.ruoyi.customer.customermoney.api.ICustomerAccountApi;
|
||||
|
||||
/**
|
||||
* 客户临时额度到期自动失效任务。
|
||||
* 客户授信(信用额度/临时额度)到期自动失效任务。
|
||||
*
|
||||
* <p>每天 01:30 扫描临时额度已到期(extra_credit>0 且失效日期早于今天)的CNY账户,
|
||||
* 逐户独立事务物化失效:额度清零 -> 回写客户档案 -> 写 EXPIRE 额度流水。
|
||||
* 失效日当天临时额度仍有效,故严格按「早于今天」判断。</p>
|
||||
* <p>每天 01:30 扫描授信已到期(信用或临时额度>0 且失效日期早于今天)的CNY账户,
|
||||
* 逐户独立事务物化失效:额度清零 -> 释放角色额度池 -> 回写客户档案 -> 写 EXPIRE 额度流水。
|
||||
* 失效日当天额度仍有效,故严格按「早于今天」判断。</p>
|
||||
*
|
||||
* <p>除惰性触发(访问资金接口时即查即失效)外,本任务保证展示口径一致:
|
||||
* 客户列表可用额度是账户表现算,不经过惰性入口,需由本任务把到期状态落库。
|
||||
@@ -51,12 +51,12 @@ public class TempCreditExpireTask implements ApplicationRunner
|
||||
{
|
||||
try
|
||||
{
|
||||
int count = customerAccountApi.expireAllDueTempCredits();
|
||||
log.info("客户临时额度到期失效任务执行完成,失效账户数={}", count);
|
||||
int count = customerAccountApi.expireAllDueCredits();
|
||||
log.info("客户授信到期失效任务执行完成,失效账户数={}", count);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("客户临时额度到期失效任务执行异常", e);
|
||||
log.error("客户授信到期失效任务执行异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -44,6 +44,8 @@ public class CustomerAccountStatusVO implements Serializable
|
||||
private BigDecimal frozenBalance;
|
||||
/** 授信总额度(CNY) */
|
||||
private BigDecimal totalCredit;
|
||||
/** 授信总额度实际有效值(过期未物化时为0,接口计算得出,不物化DB) */
|
||||
private BigDecimal effectiveTotalCredit;
|
||||
/** 临时额度原值(CNY,DB 中 extra_credit 字段) */
|
||||
private BigDecimal extraCredit;
|
||||
/** 临时额度实际有效值(过期时为0,接口计算得出,不物化DB) */
|
||||
@@ -78,6 +80,8 @@ public class CustomerAccountStatusVO implements Serializable
|
||||
public void setFrozenBalance(BigDecimal frozenBalance) { this.frozenBalance = frozenBalance; }
|
||||
public BigDecimal getTotalCredit() { return totalCredit; }
|
||||
public void setTotalCredit(BigDecimal totalCredit) { this.totalCredit = totalCredit; }
|
||||
public BigDecimal getEffectiveTotalCredit() { return effectiveTotalCredit; }
|
||||
public void setEffectiveTotalCredit(BigDecimal effectiveTotalCredit) { this.effectiveTotalCredit = effectiveTotalCredit; }
|
||||
public BigDecimal getExtraCredit() { return extraCredit; }
|
||||
public void setExtraCredit(BigDecimal extraCredit) { this.extraCredit = extraCredit; }
|
||||
public BigDecimal getEffectiveExtraCredit() { return effectiveExtraCredit; }
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.ruoyi.customer.customermoney.vo;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 角色额度池视图(角色管理/审批选池用)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-20
|
||||
*/
|
||||
@Data
|
||||
public class RoleCreditQuotaVo
|
||||
{
|
||||
/** 角色ID */
|
||||
private Long roleId;
|
||||
|
||||
/** 角色名称 */
|
||||
private String roleName;
|
||||
|
||||
/** 权限字符 */
|
||||
private String roleKey;
|
||||
|
||||
/** 角色状态(0正常 1停用) */
|
||||
private String status;
|
||||
|
||||
/** 可分配额度总额;未配置记录时为0 */
|
||||
private BigDecimal totalQuota;
|
||||
|
||||
/** 已分配(占用中台账余额合计) */
|
||||
private BigDecimal usedQuota;
|
||||
|
||||
/** 剩余可分配 */
|
||||
private BigDecimal availableQuota;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
Reference in New Issue
Block a user