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();
diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/task/TempCreditExpireTask.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/task/TempCreditExpireTask.java
index 172a8570..12e67a7d 100644
--- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/task/TempCreditExpireTask.java
+++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/task/TempCreditExpireTask.java
@@ -10,11 +10,11 @@ import org.springframework.stereotype.Component;
import com.ruoyi.customer.customermoney.api.ICustomerAccountApi;
/**
- * 客户临时额度到期自动失效任务。
+ * 客户授信(信用额度/临时额度)到期自动失效任务。
*
- * 每天 01:30 扫描临时额度已到期(extra_credit>0 且失效日期早于今天)的CNY账户,
- * 逐户独立事务物化失效:额度清零 -> 回写客户档案 -> 写 EXPIRE 额度流水。
- * 失效日当天临时额度仍有效,故严格按「早于今天」判断。
+ * 每天 01:30 扫描授信已到期(信用或临时额度>0 且失效日期早于今天)的CNY账户,
+ * 逐户独立事务物化失效:额度清零 -> 释放角色额度池 -> 回写客户档案 -> 写 EXPIRE 额度流水。
+ * 失效日当天额度仍有效,故严格按「早于今天」判断。
*
* 除惰性触发(访问资金接口时即查即失效)外,本任务保证展示口径一致:
* 客户列表可用额度是账户表现算,不经过惰性入口,需由本任务把到期状态落库。
@@ -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);
}
}
}
diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/vo/CustomerAccountStatusVO.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/vo/CustomerAccountStatusVO.java
index 77ea62bf..7ac3303b 100644
--- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/vo/CustomerAccountStatusVO.java
+++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/vo/CustomerAccountStatusVO.java
@@ -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; }
diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/vo/RoleCreditQuotaVo.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/vo/RoleCreditQuotaVo.java
new file mode 100644
index 00000000..70fbff97
--- /dev/null
+++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/vo/RoleCreditQuotaVo.java
@@ -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;
+}
diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/customer/CreditQuotaMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/customer/CreditQuotaMapper.xml
new file mode 100644
index 00000000..f4b9a3b9
--- /dev/null
+++ b/bms-back/ruoyi-system/src/main/resources/mapper/customer/CreditQuotaMapper.xml
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ insert into sys_role_credit_quota (role_id, total_quota, version, create_by, create_time, remark)
+ values (#{roleId}, #{totalQuota}, 0, #{createBy}, sysdate(), #{remark})
+ on duplicate key update total_quota = values(total_quota),
+ remark = values(remark),
+ update_by = values(create_by),
+ update_time = now()
+
+
+
+ update sys_role_credit_quota
+ set total_quota = #{totalQuota},
+ version = version + 1,
+ update_by = #{updateBy},
+ update_time = now(),
+ remark = #{remark}
+ where role_id = #{roleId} and version = #{version}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ select g.grant_id, g.customer_id, g.account_id, g.role_id, g.approve_user_id, g.apply_id, g.flow_id,
+ g.credit_type, g.amount, g.remaining, g.effective_date, g.expire_date, g.status, g.release_time,
+ g.create_by, g.create_time, g.update_by, g.update_time, g.remark, g.del_flag,
+ c.customer_code, c.customer_name,
+ r.role_name,
+ u.user_name as approve_by
+ from customer_credit_grant g
+ left join bms_customer c on c.customer_id = g.customer_id
+ left join sys_role r on r.role_id = g.role_id
+ left join sys_user u on u.user_id = g.approve_user_id
+
+
+
+
+
+
+
+ insert into customer_credit_grant (
+ customer_id, account_id, role_id, approve_user_id, apply_id, flow_id,
+ credit_type, amount, remaining, effective_date, expire_date, status,
+ create_by, create_time, remark, del_flag
+ ) values (
+ #{customerId}, #{accountId}, #{roleId}, #{approveUserId}, #{applyId}, #{flowId},
+ #{creditType}, #{amount}, #{remaining}, #{effectiveDate}, #{expireDate}, #{status},
+ #{createBy}, sysdate(), #{remark}, '0'
+ )
+
+
+
+ update customer_credit_grant
+ set remaining = #{remaining},
+ status = #{status},
+ release_time = #{releaseTime},
+ update_by = #{updateBy},
+ update_time = now()
+ where grant_id = #{grantId}
+
+
diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerCreditApplyMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerCreditApplyMapper.xml
index aac36186..dcdb9135 100644
--- a/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerCreditApplyMapper.xml
+++ b/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerCreditApplyMapper.xml
@@ -21,6 +21,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+
+
@@ -38,11 +40,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select a.apply_id, a.apply_no, a.customer_id, a.account_id, a.credit_type, a.adjust_mode,
a.change_amount, a.effective_date, a.expire_date, a.reason, a.status,
a.apply_by, a.apply_user_id, a.apply_time,
- a.approve_by, a.approve_user_id, a.approve_time, a.approve_remark, a.flow_id,
+ a.approve_by, a.approve_user_id, a.approve_role_id, a.approve_time, a.approve_remark, a.flow_id,
a.create_by, a.create_time, a.update_by, a.update_time, a.remark, a.del_flag,
- c.customer_code, c.customer_name
+ c.customer_code, c.customer_name,
+ r.role_name as approve_role_name
from customer_credit_apply a
left join bms_customer c on c.customer_id = a.customer_id
+ left join sys_role r on r.role_id = a.approve_role_id
+
+
@@ -27,7 +29,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
- select id, customer_id, currency, balance, frozen_balance, total_credit, extra_credit,
+ select id, customer_id, currency, balance, frozen_balance, total_credit,
+ credit_effective_date, credit_expire_date, extra_credit,
extra_effective_date, extra_expire_date, used_credit,
tax, version, create_by, create_time, update_by, update_time, remark, del_flag
from customer_account
@@ -49,15 +52,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where customer_id = #{customerId} and del_flag = '0'
-
-