diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/api/ICustomerAccountApi.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/api/ICustomerAccountApi.java index 548ab301..bd1812fa 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/api/ICustomerAccountApi.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/api/ICustomerAccountApi.java @@ -41,6 +41,15 @@ public interface ICustomerAccountApi */ void expireTempCreditIfNeeded(CustomerAccount account); + /** + * 扫描所有临时额度已到期(extra_credit>0 且失效日期早于今天)的CNY账户, + * 逐户独立事务物化失效(行锁 -> 清零 -> 回写档案 -> EXPIRE流水)。 + * 单户失败仅记录日志不影响其他户;定时任务每日调用,应用启动后也会执行一次。 + * + * @return 实际发生失效的账户数 + */ + int expireAllDueTempCredits(); + /** * 新建客户时初始化默认CNY账户(余额0,默认总额度1000 CNY,档案有值按档案), * 写 INIT 额度流水;CNY账户已存在时不重复创建。 diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/api/impl/CustomerAccountApiImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/api/impl/CustomerAccountApiImpl.java index 8322327c..d8730a26 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/api/impl/CustomerAccountApiImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/api/impl/CustomerAccountApiImpl.java @@ -8,9 +8,15 @@ import java.time.format.DateTimeFormatter; import java.util.Date; import java.util.List; import java.util.concurrent.ThreadLocalRandom; +import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.StringUtils; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.SecurityUtils; @@ -36,6 +42,8 @@ import com.ruoyi.system.domain.BmsExchangeRate; @Service public class CustomerAccountApiImpl implements ICustomerAccountApi { + private static final Logger log = LoggerFactory.getLogger(CustomerAccountApiImpl.class); + private static final DateTimeFormatter NO_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"); /** 本位币币种 */ @@ -51,6 +59,19 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi @Autowired private BmsCustomerMapper bmsCustomerMapper; + @Autowired + private PlatformTransactionManager transactionManager; + + /** 逐户独立事务模板(REQUIRES_NEW):批量失效时单户失败回滚不影响其他户 */ + private TransactionTemplate requireNewTxTemplate; + + @PostConstruct + public void initTxTemplate() + { + requireNewTxTemplate = new TransactionTemplate(transactionManager); + requireNewTxTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + } + @Override public CustomerAccount getAccount(Long customerId, String currency) { @@ -109,6 +130,44 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi return 1; } + @Override + public int expireAllDueTempCredits() + { + List customerIds = customerMoneyMapper.selectExpiredTempCreditCustomerIds(); + if (customerIds == null || customerIds.isEmpty()) + { + return 0; + } + int expired = 0; + for (Long customerId : customerIds) + { + // 每户独立事务:行锁 -> 失效清零 -> 回写档案 -> EXPIRE流水,单户失败不影响其他户 + try + { + Boolean changed = requireNewTxTemplate.execute(status -> { + CustomerAccount locked = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY); + if (locked == null || !isTempCreditExpired(locked)) + { + // 可能被其他节点/入口抢先处理 + return Boolean.FALSE; + } + expireTempCreditIfNeeded(locked); + return Boolean.TRUE; + }); + if (Boolean.TRUE.equals(changed)) + { + expired++; + } + } + catch (Exception e) + { + log.error("客户{}临时额度到期自动失效失败", customerId, e); + } + } + log.info("临时额度到期批量失效完成,扫描{}户,实际失效{}户", customerIds.size(), expired); + return expired; + } + @Override public void expireTempCreditIfNeeded(CustomerAccount account) { @@ -116,7 +175,9 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi { return; } - String username = SecurityUtils.getUsername(); + // 定时任务场景无登录态,操作人回落为 system(人工触发的入口仍记录实际操作人) + String username = currentUsername(); + Long userId = currentUserId(); BigDecimal before = nz(account.getExtraCredit()); account.setExtraCredit(BigDecimal.ZERO); account.setUpdateBy(username); @@ -143,13 +204,39 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi flow.setEffectiveDate(account.getExtraEffectiveDate()); flow.setExpireDate(account.getExtraExpireDate()); flow.setReason("临时额度到期自动失效"); - flow.setOperatorId(SecurityUtils.getUserId()); + flow.setOperatorId(userId); flow.setCreateBy(username); flow.setRemark("系统在访问账户时检测到临时额度已过失效日期,自动置零"); flow.setDelFlag("0"); customerMoneyMapper.insertCreditFlow(flow); } + /** 当前登录用户名;无登录态(定时任务)时返回 system */ + private String currentUsername() + { + try + { + return SecurityUtils.getUsername(); + } + catch (Exception e) + { + return "system"; + } + } + + /** 当前登录用户ID;无登录态(定时任务)时返回null */ + private Long currentUserId() + { + try + { + return SecurityUtils.getUserId(); + } + catch (Exception e) + { + return null; + } + } + /* ================= 授信额度占用 / 释放 ================= */ @Override diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/mapper/CustomerMoneyMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/mapper/CustomerMoneyMapper.java index ec07a8b6..6d300560 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/mapper/CustomerMoneyMapper.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/mapper/CustomerMoneyMapper.java @@ -28,6 +28,12 @@ public interface CustomerMoneyMapper /** 查询客户全部币种账户 */ List selectAccountListByCustomerId(@Param("customerId") Long customerId); + /** + * 扫描临时额度已到期的CNY账户客户ID(extra_credit>0 且失效日期早于今天;失效日当天仍有效)。 + * 供定时任务批量物化失效使用,结果集不加锁,逐户处理时再行锁。 + */ + List selectExpiredTempCreditCustomerIds(); + /** 新增账户 */ int insertAccount(CustomerAccount account); 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 new file mode 100644 index 00000000..172a8570 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/task/TempCreditExpireTask.java @@ -0,0 +1,62 @@ +package com.ruoyi.customer.customermoney.task; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import com.ruoyi.customer.customermoney.api.ICustomerAccountApi; + +/** + * 客户临时额度到期自动失效任务。 + * + *

每天 01:30 扫描临时额度已到期(extra_credit>0 且失效日期早于今天)的CNY账户, + * 逐户独立事务物化失效:额度清零 -> 回写客户档案 -> 写 EXPIRE 额度流水。 + * 失效日当天临时额度仍有效,故严格按「早于今天」判断。

+ * + *

除惰性触发(访问资金接口时即查即失效)外,本任务保证展示口径一致: + * 客户列表可用额度是账户表现算,不经过惰性入口,需由本任务把到期状态落库。 + * 应用启动后也会执行一次,避免停机期间错过调度。

+ * + *

到期不影响已用额度:已确认账单形成的授信占用仍然有效,客户回款核销后 + * used_credit 自然下降;可用额度可能短暂为负,新计费开单会被额度校验拦截。

+ * + * @author ruoyi + * @date 2026-09-17 + */ +@Component +public class TempCreditExpireTask implements ApplicationRunner +{ + private static final Logger log = LoggerFactory.getLogger(TempCreditExpireTask.class); + + @Autowired + private ICustomerAccountApi customerAccountApi; + + /** 每天 01:30 执行(cron: 秒 分 时 日 月 周) */ + @Scheduled(cron = "0 30 1 * * ?") + public void scheduled() + { + runExpire(); + } + + @Override + public void run(ApplicationArguments args) + { + runExpire(); + } + + private void runExpire() + { + try + { + int count = customerAccountApi.expireAllDueTempCredits(); + log.info("客户临时额度到期失效任务执行完成,失效账户数={}", count); + } + catch (Exception e) + { + log.error("客户临时额度到期失效任务执行异常", e); + } + } +} diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerMoneyMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerMoneyMapper.xml index 2bbb8aed..7eef3dc3 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerMoneyMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerMoneyMapper.xml @@ -49,6 +49,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where customer_id = #{customerId} and del_flag = '0' + + + insert into customer_account diff --git a/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue b/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue index caeade6c..d4b2e825 100644 --- a/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue +++ b/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue @@ -70,7 +70,10 @@
可用额度
-
{{ availableCredit }}
+
{{ availableCredit }}
+
+ 已超额,回款核销后恢复 +
@@ -1006,14 +1009,18 @@ const currentAccount = computed(() => { return accountList.value.find(a => a.currency === 'CNY') || {} }) -/** 可用额度 = 总额度 + 额外额度 - 已用额度 */ -const availableCredit = computed(() => { +/** 可用额度数值 = 总额度 + 临时额度 - 已用额度(可能为负,表示临时额度到期/占用超授信总额) */ +const availableCreditValue = computed(() => { const a = currentAccount.value const total = Number(a.totalCredit) || 0 const extra = Number(a.extraCredit) || 0 const used = Number(a.usedCredit) || 0 - return formatMoney(total + extra - used) + return total + extra - used }) +/** 可用额度展示文本 */ +const availableCredit = computed(() => formatMoney(availableCreditValue.value)) +/** 是否处于超额状态(已用额度超过授信总额,新计费开单将被拦截,需先回款核销) */ +const availableOverLimit = computed(() => availableCreditValue.value < 0) /** 临时额度有效期文案 */ const tempCreditPeriodText = computed(() => { diff --git a/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue b/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue index e8c0a5dd..4d09bb7d 100644 --- a/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue +++ b/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue @@ -157,7 +157,15 @@ - + + +