统一修改ruoyi->bms
This commit is contained in:
+63
@@ -0,0 +1,63 @@
|
||||
package com.bms.customer.customermoney.api;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 客户账户余额变动命令(扣减/回补余额,写余额流水;可选联动已用额度)。
|
||||
*
|
||||
* <p>典型场景:</p>
|
||||
* <ul>
|
||||
* <li>余额核销账单/订单扣款 -> {@link ICustomerAccountApi#deductBalance}(余额减少,默认同步释放已用额度)</li>
|
||||
* <li>收款审核入账/反核销回补/退款 -> {@link ICustomerAccountApi#refundBalance}(余额增加)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-17
|
||||
*/
|
||||
@Data
|
||||
public class BalanceChangeCommand
|
||||
{
|
||||
/** 客户ID(必填) */
|
||||
private Long customerId;
|
||||
|
||||
/** 币种(流水原币口径;为空按本位币 CNY 处理) */
|
||||
private String currency;
|
||||
|
||||
/**
|
||||
* 实际加锁/变动的账户币种(为空时取 {@link #currency})。
|
||||
* 外币收款折算入账场景:账户只有CNY,需锁CNY账户按 {@link #baseAmount} 变动余额,
|
||||
* 但流水币种仍保留收款原币 {@link #currency}。
|
||||
*/
|
||||
private String accountCurrency;
|
||||
|
||||
/** 变动金额,原币口径(必须大于0,恒为正数) */
|
||||
private BigDecimal amount;
|
||||
|
||||
/** 对CNY汇率(1原币=?CNY),为空按1处理 */
|
||||
private BigDecimal exchangeRate;
|
||||
|
||||
/** 本位币变动金额(实际参与余额变动,为空时取 {@link #amount}) */
|
||||
private BigDecimal baseAmount;
|
||||
|
||||
/** 业务类型,取 {@link CustomerFundBizType} 常量(必填) */
|
||||
private String bizType;
|
||||
|
||||
/** 关联业务单据ID */
|
||||
private Long bizId;
|
||||
|
||||
/** 关联业务单据号 */
|
||||
private String bizNo;
|
||||
|
||||
/** 流水备注 */
|
||||
private String remark;
|
||||
|
||||
/** 扣减余额时是否同步释放等额已用额度(口径变更 2026-09-23 后已忽略,保留兼容历史调用) */
|
||||
private Boolean releaseUsedCredit = Boolean.TRUE;
|
||||
|
||||
/** 回补余额时是否同步重新占用等额已用额度(口径变更 2026-09-23 后已忽略,保留兼容历史调用) */
|
||||
private Boolean reoccupyUsedCredit = Boolean.FALSE;
|
||||
|
||||
/** 扣减时是否校验余额充足(不足抛异常),默认true */
|
||||
private Boolean checkBalanceEnough = Boolean.TRUE;
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.bms.customer.customermoney.api;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 授信额度占用/释放命令(变动客户账户已用额度 used_credit,并写额度流水)。
|
||||
*
|
||||
* <p>典型场景:</p>
|
||||
* <ul>
|
||||
* <li>计费订单开单/应收账单确认 -> {@link ICustomerAccountApi#occupyCredit}(已用额度增加)</li>
|
||||
* <li>账单作废/计费变动核减 -> {@link ICustomerAccountApi#releaseCredit}(已用额度减少)</li>
|
||||
* </ul>
|
||||
* 客户标识 customerId / customerCode 二选一,传一个即可。
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-17
|
||||
*/
|
||||
@Data
|
||||
public class CreditUsageCommand
|
||||
{
|
||||
/**
|
||||
* 客户ID,与 {@link #customerCode} 二选一(至少传一个)。
|
||||
* 两者同时传值时以 customerId 为准。
|
||||
*/
|
||||
private Long customerId;
|
||||
|
||||
/**
|
||||
* 客户编码,与 {@link #customerId} 二选一(至少传一个)。
|
||||
* 未传 customerId 时由公共服务按编码解析客户档案(del_flag=0),
|
||||
* 编码不存在或匹配多条时抛 ServiceException。
|
||||
*/
|
||||
private String customerCode;
|
||||
|
||||
/** 币种(为空按本位币 CNY 处理) */
|
||||
private String currency;
|
||||
|
||||
/** 变动金额(必须大于0,恒为正数) */
|
||||
private BigDecimal amount;
|
||||
|
||||
/** 业务来源类型,取 {@link CustomerFundBizType} 常量(必填) */
|
||||
private String bizType;
|
||||
|
||||
/** 关联业务单据ID(如账单ID/订单ID) */
|
||||
private Long bizId;
|
||||
|
||||
/** 关联业务单据号(如账单号/订单号,写入流水便于追溯) */
|
||||
private String bizNo;
|
||||
|
||||
/** 流水原因(为空按业务类型+单据号自动生成) */
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* 字段保留兼容历史调用,当前计费策略(2026-09-20 起)已不再硬拦截:
|
||||
* 超额客户计费继续占用信用额度,允许可用额度为负,后续改由超额提醒机制处理。
|
||||
*/
|
||||
@Deprecated
|
||||
private Boolean checkAvailable = Boolean.TRUE;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.bms.customer.customermoney.api;
|
||||
|
||||
/**
|
||||
* 客户资金变动业务来源类型(写入余额流水 biz_type / 额度流水 biz_type)。
|
||||
* 各业务模块调用 {@link ICustomerAccountApi} 时传本类常量,便于按来源单据反查流水。
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-17
|
||||
*/
|
||||
public final class CustomerFundBizType
|
||||
{
|
||||
/** 收款单(余额入账) */
|
||||
public static final String RECEIPT = "RECEIPT";
|
||||
|
||||
/** 应收账单(确认占用授信 / 作废释放授信) */
|
||||
public static final String AR_BILL = "AR_BILL";
|
||||
|
||||
/** 账单余额核销(余额扣款 / 反核销回补) */
|
||||
public static final String BILL_PAY = "BILL_PAY";
|
||||
|
||||
/** 计费订单(开单占用授信 / 计费变动核减释放) */
|
||||
public static final String ORDER = "ORDER";
|
||||
|
||||
private CustomerFundBizType()
|
||||
{
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package com.bms.customer.customermoney.api;
|
||||
|
||||
import com.bms.customer.customermoney.domain.CustomerAccount;
|
||||
import com.bms.customer.customermoney.domain.CustomerBalanceFlow;
|
||||
import com.bms.customer.customermoney.domain.CustomerCreditFlow;
|
||||
import com.bms.customer.customermoney.vo.CustomerAccountStatusVO;
|
||||
|
||||
/**
|
||||
* 客户资金公共服务:客户余额扣减/回补、授信额度占用/释放的统一入口。
|
||||
*
|
||||
* <p>客户余额、授信额度及其流水(客户模块资产)的所有变动都必须经过本接口,
|
||||
* 其他模块(应收账单、计费订单/计费变动等)不得直接更新 ctr_customer_account /
|
||||
* 直接插入流水表。每个方法在同一事务内完成:行锁账户 -> 乐观锁变动 -> 写流水,
|
||||
* 调用方自身的事务会与本操作共享事务(默认 REQUIRED),任一失败整体回滚。</p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>单据形成应收/计费开单:{@link #occupyCredit} 占用授信(used_credit 增加)</li>
|
||||
* <li>单据作废/计费核减:{@link #releaseCredit} 释放授信(used_credit 减少)</li>
|
||||
* <li>用账户余额支付/核销:{@link #deductBalance} 扣余额(可同步释放已用额度)</li>
|
||||
* <li>收款入账/退款/反核销回补:{@link #refundBalance} 加余额(可同步重新占用)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-17
|
||||
*/
|
||||
public interface ICustomerAccountApi
|
||||
{
|
||||
/**
|
||||
* 普通查询客户某币种账户(不加锁,不存在返回null),供业务模块判断余额/可用额度。
|
||||
*/
|
||||
CustomerAccount getAccount(Long customerId, String currency);
|
||||
|
||||
/**
|
||||
* 事务内行锁获取客户CNY账户,不存在则按客户档案额度自动初始化(不处理授信过期)。
|
||||
* 调用方拿到后如需最新额度,可再调 {@link #expireDueCreditsIfNeeded}。
|
||||
*/
|
||||
CustomerAccount lockCnyAccount(Long customerId);
|
||||
|
||||
/**
|
||||
* 授信(信用额度/临时额度)到期自动失效:过期项额度清零 + 释放角色额度池
|
||||
* + 回写客户档案 + 写 EXPIRE 额度流水。
|
||||
* 入参必须是同一事务内行锁查出的账户;均未过期时不做任何操作。
|
||||
*/
|
||||
void expireDueCreditsIfNeeded(CustomerAccount account);
|
||||
|
||||
/**
|
||||
* 扫描所有授信已到期(信用或临时额度>0 且失效日期早于今天)的CNY账户,
|
||||
* 逐户独立事务物化失效(行锁 -> 清零 -> 释放角色池 -> 回写档案 -> EXPIRE流水)。
|
||||
* 单户失败仅记录日志不影响其他户;定时任务每日调用,应用启动后也会执行一次。
|
||||
*
|
||||
* @return 实际发生失效的账户数
|
||||
*/
|
||||
int expireAllDueCredits();
|
||||
|
||||
/**
|
||||
* 新建客户时初始化默认CNY账户(余额0,信用额度默认0,档案有值按档案),
|
||||
* 写 INIT 额度流水;CNY账户已存在时不重复创建。
|
||||
*/
|
||||
int initDefaultCnyAccount(Long customerId);
|
||||
|
||||
/**
|
||||
* 计费占用:扣减账户余额(balance -= amount),写 -1 余额流水(口径变更 2026-09-23)。
|
||||
* 不再变动 used_credit;额度仅在调整/到期失效时变化。允许负余额(欠费)继续计费。
|
||||
* 命令中 customerId 与 customerCode 二选一;
|
||||
* CNY账户不存在自动初始化,外币账户不存在时静默跳过(返回null)。
|
||||
*
|
||||
* @return 口径变更后不再产生额度流水,固定返回 null;账户不存在跳过场景也返回null
|
||||
*/
|
||||
CustomerCreditFlow occupyCredit(CreditUsageCommand command);
|
||||
|
||||
/**
|
||||
* 计费释放:回补账户余额(balance += amount),写 +1 余额流水(口径变更 2026-09-23)。
|
||||
* 不再变动 used_credit。命令中 customerId 与 customerCode 二选一;
|
||||
* 账户不存在时静默跳过(返回null)。
|
||||
*
|
||||
* @return 口径变更后不再产生额度流水,固定返回 null;账户不存在跳过场景也返回null
|
||||
*/
|
||||
CustomerCreditFlow releaseCredit(CreditUsageCommand command);
|
||||
|
||||
/**
|
||||
* 扣减账户余额:balance -= amount,写 -1 余额流水。
|
||||
* 口径变更后不再联动 used_credit(releaseUsedCredit 字段已忽略)。
|
||||
* 账户不存在或余额不足(checkBalanceEnough=true)抛 ServiceException。
|
||||
*
|
||||
* @return 已落库的余额流水(含ID、账户ID)
|
||||
*/
|
||||
CustomerBalanceFlow deductBalance(BalanceChangeCommand command);
|
||||
|
||||
/**
|
||||
* 回补/入账账户余额:balance += amount,写 +1 余额流水。
|
||||
* 口径变更后不再联动 used_credit(reoccupyUsedCredit 字段已忽略)。
|
||||
*
|
||||
* @return 已落库的余额流水(含ID、账户ID)
|
||||
*/
|
||||
CustomerBalanceFlow refundBalance(BalanceChangeCommand command);
|
||||
|
||||
/**
|
||||
* 查询客户账号状态(收货扣件准入控制用)。支持 customerId / customerCode 二选一,只读轻量查询,
|
||||
* 不锁账户也不物化临时额度失效——临时额度已过期时按"视为0"计算可用额度,但 DB 保持原状,
|
||||
* 由定时任务或后续真实资金入口落库。
|
||||
*
|
||||
* <p>欠费定义:可用额度 < 0(账户余额 + 信用额度 + 有效临时额度 不足)。</p>
|
||||
*
|
||||
* @return 账号状态VO;客户不存在时返回 null
|
||||
*/
|
||||
CustomerAccountStatusVO getAccountStatus(Long customerId, String customerCode);
|
||||
}
|
||||
+833
@@ -0,0 +1,833 @@
|
||||
package com.bms.customer.customermoney.api.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
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.bms.common.exception.ServiceException;
|
||||
import com.bms.common.utils.SecurityUtils;
|
||||
import com.bms.customer.customerinfo.domain.BmsCustomer;
|
||||
import com.bms.customer.customerinfo.mapper.BmsCustomerMapper;
|
||||
import com.bms.customer.customermoney.api.BalanceChangeCommand;
|
||||
import com.bms.customer.customermoney.api.CreditUsageCommand;
|
||||
import com.bms.customer.customermoney.api.CustomerFundBizType;
|
||||
import com.bms.customer.customermoney.api.ICustomerAccountApi;
|
||||
import com.bms.customer.customermoney.domain.CustomerAccount;
|
||||
import com.bms.customer.customermoney.domain.CustomerBalanceFlow;
|
||||
import com.bms.customer.customermoney.vo.CustomerAccountStatusVO;
|
||||
import com.bms.customer.customermoney.domain.CustomerCreditFlow;
|
||||
import com.bms.customer.customermoney.mapper.CustomerMoneyMapper;
|
||||
import com.bms.system.domain.BmsExchangeRate;
|
||||
|
||||
/**
|
||||
* 客户资金公共服务实现:客户账户余额/授信额度变动与流水落库的唯一收口。
|
||||
* 加锁顺序统一为「业务单据(调用方持锁)-> 客户账户(本服务行锁)」。
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-17
|
||||
*/
|
||||
@Service
|
||||
public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(CustomerAccountApiImpl.class);
|
||||
|
||||
private static final DateTimeFormatter NO_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
|
||||
|
||||
/** 本位币币种 */
|
||||
private static final String BASE_CURRENCY = BmsExchangeRate.BASE_CURRENCY;
|
||||
/** 新客户默认总额度(CNY):默认0,信用额度一律通过「申请-审批」分配 */
|
||||
private static final BigDecimal DEFAULT_CREDIT = BigDecimal.ZERO;
|
||||
/** 金额保留位数 */
|
||||
private static final int MONEY_SCALE = 2;
|
||||
|
||||
@Autowired
|
||||
private CustomerMoneyMapper customerMoneyMapper;
|
||||
|
||||
@Autowired
|
||||
private BmsCustomerMapper bmsCustomerMapper;
|
||||
|
||||
/** 角色额度池台账:授信到期释放角色池 */
|
||||
@Autowired
|
||||
private com.bms.customer.customermoney.service.ICreditQuotaService creditQuotaService;
|
||||
|
||||
@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)
|
||||
{
|
||||
if (customerId == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return customerMoneyMapper.selectAccountByCustomerCurrency(customerId, resolveCurrency(currency));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomerAccount lockCnyAccount(Long customerId)
|
||||
{
|
||||
if (customerId == null)
|
||||
{
|
||||
throw new ServiceException("客户ID不能为空");
|
||||
}
|
||||
return getOrInitCnyAccount(customerId, SecurityUtils.getUsername());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int initDefaultCnyAccount(Long customerId)
|
||||
{
|
||||
if (customerId == null)
|
||||
{
|
||||
throw new ServiceException("客户ID不能为空");
|
||||
}
|
||||
String username = SecurityUtils.getUsername();
|
||||
CustomerAccount account = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY);
|
||||
if (account != null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
BmsCustomer customer = bmsCustomerMapper.selectRawCreditByCustomerId(customerId);
|
||||
if (customer == null)
|
||||
{
|
||||
throw new ServiceException("客户不存在");
|
||||
}
|
||||
BigDecimal initCredit = nz(customer.getCreditEd());
|
||||
if (initCredit.compareTo(BigDecimal.ZERO) < 0)
|
||||
{
|
||||
initCredit = 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, "新客户初始化账户,默认信用额度0(需通过额度申请审批分配)");
|
||||
if (initTemp.compareTo(BigDecimal.ZERO) > 0)
|
||||
{
|
||||
writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_TEMP,
|
||||
initTemp, null, null, username, "新客户初始化临时额度");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expireAllDueCredits()
|
||||
{
|
||||
List<Long> customerIds = customerMoneyMapper.selectExpiredCreditCustomerIds();
|
||||
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) && !isCreditExpired(locked)))
|
||||
{
|
||||
// 可能被其他节点/入口抢先处理
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
expireDueCreditsIfNeeded(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 expireDueCreditsIfNeeded(CustomerAccount 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 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)
|
||||
{
|
||||
throw new ServiceException("账户额度已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
account.setVersion(account.getVersion() + 1);
|
||||
bmsCustomerMapper.updateCustomerCredit(account.getCustomerId(),
|
||||
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(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(effectiveDate);
|
||||
flow.setExpireDate(expireDate);
|
||||
flow.setReason(reason);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 计费扣减 / 回补余额(口径变更:不再变动 used_credit) ================= */
|
||||
// 2026-09-23 口径变更:计费占用/释放改为扣减/回补账户余额,允许负余额(欠费)继续计费。
|
||||
// 额度(totalCredit/extraCredit)仅在额度调整或到期失效时变化,计费不再影响 used_credit。
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CustomerCreditFlow occupyCredit(CreditUsageCommand command)
|
||||
{
|
||||
validateUsageCommand(command);
|
||||
Long customerId = resolveCustomerId(command);
|
||||
String currency = resolveCurrency(command.getCurrency());
|
||||
String username = currentUsername();
|
||||
Long userId = currentUserId();
|
||||
CustomerAccount account = lockAccountForUsage(customerId, currency, username);
|
||||
if (account == null)
|
||||
{
|
||||
// 外币账户尚未开立时不自动占用,与历史账单口径保持一致(CNY账户会自动初始化)
|
||||
return null;
|
||||
}
|
||||
BigDecimal amount = money(command.getAmount());
|
||||
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("账户已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
// 写余额流水(计费扣减),不再写额度流水
|
||||
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
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CustomerCreditFlow releaseCredit(CreditUsageCommand command)
|
||||
{
|
||||
validateUsageCommand(command);
|
||||
Long customerId = resolveCustomerId(command);
|
||||
String currency = resolveCurrency(command.getCurrency());
|
||||
String username = currentUsername();
|
||||
Long userId = currentUserId();
|
||||
CustomerAccount account = lockAccountForUsage(customerId, currency, username);
|
||||
if (account == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
BigDecimal amount = money(command.getAmount());
|
||||
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("账户已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/* ================= 余额扣减 / 回补 ================= */
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CustomerBalanceFlow deductBalance(BalanceChangeCommand command)
|
||||
{
|
||||
validateBalanceCommand(command);
|
||||
String flowCurrency = resolveCurrency(command.getCurrency());
|
||||
String accountCurrency = StringUtils.hasText(command.getAccountCurrency())
|
||||
? resolveCurrency(command.getAccountCurrency()) : flowCurrency;
|
||||
String username = SecurityUtils.getUsername();
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
CustomerAccount account = lockAccountForChange(command.getCustomerId(), accountCurrency, username, "扣款");
|
||||
|
||||
BigDecimal originalAmount = money(command.getAmount());
|
||||
// 账户币种实际变动额:折算入账时取本位币金额,其余场景等于原币金额
|
||||
BigDecimal changeAmount = command.getBaseAmount() != null ? money(command.getBaseAmount()) : originalAmount;
|
||||
BigDecimal beforeBalance = nz(account.getBalance());
|
||||
if (!Boolean.FALSE.equals(command.getCheckBalanceEnough()) && beforeBalance.compareTo(changeAmount) < 0)
|
||||
{
|
||||
throw new ServiceException("账户余额不足,当前" + accountCurrency + "余额:" + beforeBalance.toPlainString()
|
||||
+ ",本次需扣减:" + changeAmount.toPlainString());
|
||||
}
|
||||
BigDecimal afterBalance = beforeBalance.subtract(changeAmount);
|
||||
BigDecimal frozenBalance = nz(account.getFrozenBalance());
|
||||
account.setBalance(afterBalance);
|
||||
account.setUpdateBy(username);
|
||||
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
||||
{
|
||||
throw new ServiceException("账户余额已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
|
||||
CustomerBalanceFlow flow = newBalanceFlow(account, command, flowCurrency, originalAmount, -1,
|
||||
beforeBalance, afterBalance, frozenBalance, frozenBalance, username, userId);
|
||||
customerMoneyMapper.insertFlow(flow);
|
||||
return flow;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CustomerBalanceFlow refundBalance(BalanceChangeCommand command)
|
||||
{
|
||||
validateBalanceCommand(command);
|
||||
String flowCurrency = resolveCurrency(command.getCurrency());
|
||||
String accountCurrency = StringUtils.hasText(command.getAccountCurrency())
|
||||
? resolveCurrency(command.getAccountCurrency()) : flowCurrency;
|
||||
String username = SecurityUtils.getUsername();
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
CustomerAccount account = lockAccountForChange(command.getCustomerId(), accountCurrency, username, "回补");
|
||||
|
||||
BigDecimal originalAmount = money(command.getAmount());
|
||||
// 账户币种实际变动额:折算入账时取本位币金额,其余场景等于原币金额
|
||||
BigDecimal changeAmount = command.getBaseAmount() != null ? money(command.getBaseAmount()) : originalAmount;
|
||||
BigDecimal beforeBalance = nz(account.getBalance());
|
||||
BigDecimal afterBalance = beforeBalance.add(changeAmount);
|
||||
BigDecimal frozenBalance = nz(account.getFrozenBalance());
|
||||
account.setBalance(afterBalance);
|
||||
account.setUpdateBy(username);
|
||||
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
||||
{
|
||||
throw new ServiceException("账户余额已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
|
||||
CustomerBalanceFlow flow = newBalanceFlow(account, command, flowCurrency, originalAmount, 1,
|
||||
beforeBalance, afterBalance, frozenBalance, frozenBalance, username, userId);
|
||||
customerMoneyMapper.insertFlow(flow);
|
||||
return flow;
|
||||
}
|
||||
|
||||
/* ================= 内部实现 ================= */
|
||||
|
||||
/** 额度占用命令 -> 余额变动命令(计费口径变更后,occupy/release 复用余额流水落库) */
|
||||
private BalanceChangeCommand toBalanceCommand(CreditUsageCommand source, String currency, String reason)
|
||||
{
|
||||
BalanceChangeCommand target = new BalanceChangeCommand();
|
||||
target.setCustomerId(source.getCustomerId());
|
||||
target.setCurrency(currency);
|
||||
target.setAmount(money(source.getAmount()));
|
||||
target.setBizType(source.getBizType());
|
||||
target.setBizId(source.getBizId());
|
||||
target.setBizNo(source.getBizNo());
|
||||
target.setRemark(reason);
|
||||
return target;
|
||||
}
|
||||
|
||||
/** 余额变动写余额流水(原币金额留痕,本位币金额参与余额变动) */
|
||||
private CustomerBalanceFlow newBalanceFlow(CustomerAccount account, BalanceChangeCommand command,
|
||||
String currency, BigDecimal amount, int direction,
|
||||
BigDecimal beforeBalance, BigDecimal afterBalance,
|
||||
BigDecimal beforeFrozen, BigDecimal afterFrozen,
|
||||
String username, Long userId)
|
||||
{
|
||||
BigDecimal rate = command.getExchangeRate() != null
|
||||
&& command.getExchangeRate().compareTo(BigDecimal.ZERO) > 0
|
||||
? command.getExchangeRate() : BigDecimal.ONE;
|
||||
BigDecimal baseAmount = command.getBaseAmount() != null ? command.getBaseAmount()
|
||||
: amount.multiply(rate).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
CustomerBalanceFlow flow = new CustomerBalanceFlow();
|
||||
flow.setFlowNo(generateNo("LS"));
|
||||
flow.setCustomerId(account.getCustomerId());
|
||||
flow.setAccountId(account.getId());
|
||||
flow.setCurrency(currency);
|
||||
flow.setExchangeRate(rate.setScale(8, RoundingMode.HALF_UP));
|
||||
flow.setBaseAmount(baseAmount);
|
||||
flow.setBizType(command.getBizType());
|
||||
flow.setBizId(command.getBizId());
|
||||
flow.setBizNo(command.getBizNo());
|
||||
flow.setDirection(direction);
|
||||
flow.setAmount(amount);
|
||||
flow.setBeforeBalance(beforeBalance);
|
||||
flow.setAfterBalance(afterBalance);
|
||||
flow.setBeforeFrozenBalance(beforeFrozen);
|
||||
flow.setAfterFrozenBalance(afterFrozen);
|
||||
flow.setOperatorId(userId);
|
||||
flow.setCreateBy(username);
|
||||
flow.setRemark(command.getRemark());
|
||||
return flow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 占用/释放场景锁账户:CNY 不存在自动初始化(先处理临时额度过期),
|
||||
* 外币账户不存在返回null(调用方静默跳过)。
|
||||
*/
|
||||
private CustomerAccount lockAccountForUsage(Long customerId, String currency, String username)
|
||||
{
|
||||
if (BASE_CURRENCY.equals(currency))
|
||||
{
|
||||
CustomerAccount account = getOrInitCnyAccount(customerId, username);
|
||||
expireDueCreditsIfNeeded(account);
|
||||
return account;
|
||||
}
|
||||
return customerMoneyMapper.selectAccountForUpdate(customerId, currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* 余额变动场景锁账户:账户不存在直接抛异常(余额扣款/回补必须有账户)。
|
||||
*/
|
||||
private CustomerAccount lockAccountForChange(Long customerId, String currency, String username, String action)
|
||||
{
|
||||
CustomerAccount account;
|
||||
if (BASE_CURRENCY.equals(currency))
|
||||
{
|
||||
account = getOrInitCnyAccount(customerId, username);
|
||||
expireDueCreditsIfNeeded(account);
|
||||
}
|
||||
else
|
||||
{
|
||||
account = customerMoneyMapper.selectAccountForUpdate(customerId, currency);
|
||||
}
|
||||
if (account == null)
|
||||
{
|
||||
throw new ServiceException("客户没有" + currency + "账户,无法" + action);
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
/** 行锁获取客户CNY账户,不存在则按客户档案当前额度初始化创建 */
|
||||
private CustomerAccount getOrInitCnyAccount(Long customerId, String username)
|
||||
{
|
||||
CustomerAccount account = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY);
|
||||
if (account != null)
|
||||
{
|
||||
return account;
|
||||
}
|
||||
BmsCustomer customer = bmsCustomerMapper.selectRawCreditByCustomerId(customerId);
|
||||
if (customer == null)
|
||||
{
|
||||
throw new ServiceException("客户不存在");
|
||||
}
|
||||
BigDecimal initCredit = nz(customer.getCreditEd());
|
||||
BigDecimal initTemp = nz(customer.getTempEd());
|
||||
try
|
||||
{
|
||||
account = createCnyAccount(customerId, initCredit, initTemp, username);
|
||||
writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_CREDIT,
|
||||
initCredit, null, null, username, "首次创建CNY账户,从客户档案同步初始额度");
|
||||
if (initTemp.compareTo(BigDecimal.ZERO) > 0)
|
||||
{
|
||||
writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_TEMP,
|
||||
initTemp, null, null, username, "首次创建CNY账户,从客户档案同步临时额度");
|
||||
}
|
||||
return account;
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
// Concurrent first-create: peer may have inserted; re-lock existing row.
|
||||
if (isAccountInitRace(e))
|
||||
{
|
||||
account = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY);
|
||||
if (account != null)
|
||||
{
|
||||
log.warn("CNY account created by peer, reuse customerId={}", customerId);
|
||||
return account;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** Deadlock / duplicate key while inserting ctr_customer_account. */
|
||||
private static boolean isAccountInitRace(Throwable e)
|
||||
{
|
||||
Throwable t = e;
|
||||
while (t != null)
|
||||
{
|
||||
String name = t.getClass().getName();
|
||||
if (name.indexOf("DeadlockLoser") >= 0
|
||||
|| name.indexOf("DuplicateKey") >= 0
|
||||
|| name.indexOf("MySQLTransactionRollback") >= 0
|
||||
|| name.indexOf("DataIntegrityViolation") >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
String msg = t.getMessage();
|
||||
if (msg != null)
|
||||
{
|
||||
String m = msg.toLowerCase();
|
||||
if (m.indexOf("deadlock") >= 0
|
||||
|| m.indexOf("duplicate entry") >= 0
|
||||
|| m.indexOf("try restarting transaction") >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
t = t.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 创建CNY账户(余额0,已用0) */
|
||||
private CustomerAccount createCnyAccount(Long customerId, BigDecimal totalCredit,
|
||||
BigDecimal extraCredit, String username)
|
||||
{
|
||||
CustomerAccount account = new CustomerAccount();
|
||||
account.setCustomerId(customerId);
|
||||
account.setCurrency(BASE_CURRENCY);
|
||||
account.setBalance(BigDecimal.ZERO);
|
||||
account.setFrozenBalance(BigDecimal.ZERO);
|
||||
account.setTotalCredit(totalCredit);
|
||||
account.setExtraCredit(extraCredit);
|
||||
account.setUsedCredit(BigDecimal.ZERO);
|
||||
account.setTax(BigDecimal.ZERO);
|
||||
account.setVersion(0);
|
||||
account.setCreateBy(username);
|
||||
customerMoneyMapper.insertAccount(account);
|
||||
return account;
|
||||
}
|
||||
|
||||
/** 账户首次初始化时写额度 INIT 流水(从客户档案额度同步) */
|
||||
private void writeInitFlow(CustomerAccount account, String creditType,
|
||||
BigDecimal initAmount, Date effectiveDate, Date expireDate,
|
||||
String username, String remark)
|
||||
{
|
||||
CustomerCreditFlow flow = new CustomerCreditFlow();
|
||||
flow.setFlowNo(generateNo("EDLS"));
|
||||
flow.setCustomerId(account.getCustomerId());
|
||||
flow.setAccountId(account.getId());
|
||||
flow.setCreditType(creditType);
|
||||
flow.setChangeType(CustomerCreditFlow.CHANGE_INIT);
|
||||
flow.setDirection(initAmount.compareTo(BigDecimal.ZERO) > 0 ? 1 : 0);
|
||||
flow.setBeforeAmount(BigDecimal.ZERO);
|
||||
flow.setChangeAmount(initAmount);
|
||||
flow.setAfterAmount(initAmount);
|
||||
flow.setUsedCredit(nz(account.getUsedCredit()));
|
||||
flow.setEffectiveDate(effectiveDate);
|
||||
flow.setExpireDate(expireDate);
|
||||
flow.setReason("账户初始化");
|
||||
// No login (open API / MQ / job): operatorId null, same as occupyCredit
|
||||
flow.setOperatorId(currentUserId());
|
||||
flow.setCreateBy(username);
|
||||
flow.setRemark(remark);
|
||||
flow.setDelFlag("0");
|
||||
customerMoneyMapper.insertCreditFlow(flow);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计费开单准入:客户档案必须存在且为启用状态(is_enable='0')。
|
||||
* 删除(del_flag='1',查询返回null)或停用(is_enable='1')均拒绝新增授信占用。
|
||||
*/
|
||||
private void assertCustomerActive(Long customerId)
|
||||
{
|
||||
BmsCustomer customer = bmsCustomerMapper.selectBmsCustomerByCustomerId(customerId);
|
||||
if (customer == null)
|
||||
{
|
||||
throw new ServiceException("客户不存在或已删除,不可计费开单(客户ID:" + customerId + ")");
|
||||
}
|
||||
if ("1".equals(customer.getIsEnable()))
|
||||
{
|
||||
throw new ServiceException("客户【" + customer.getCustomerName() + "】已停用,不可计费开单");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateUsageCommand(CreditUsageCommand command)
|
||||
{
|
||||
if (command == null || (command.getCustomerId() == null && !StringUtils.hasText(command.getCustomerCode())))
|
||||
{
|
||||
throw new ServiceException("客户ID与客户编码至少传一个");
|
||||
}
|
||||
if (!StringUtils.hasText(command.getBizType()))
|
||||
{
|
||||
throw new ServiceException("业务类型不能为空");
|
||||
}
|
||||
if (command.getAmount() == null || command.getAmount().compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
throw new ServiceException("额度变动金额必须大于0");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析额度命令的客户标识:customerId 优先;未传时按 customerCode 精确查询有效客户档案。
|
||||
* 编码不存在或匹配多条时抛异常,避免资金变动落到错误客户。
|
||||
*/
|
||||
private Long resolveCustomerId(CreditUsageCommand command)
|
||||
{
|
||||
if (command.getCustomerId() != null)
|
||||
{
|
||||
return command.getCustomerId();
|
||||
}
|
||||
Long customerId = resolveCustomerIdByCode(command.getCustomerCode());
|
||||
// 回填,便于调用方/流水排查时直接取到解析结果
|
||||
command.setCustomerId(customerId);
|
||||
return customerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按客户编码精确查询有效客户(selectBmsCustomerList 自带 del_flag='0' 过滤)。
|
||||
* 编码不存在或匹配多条时抛异常,避免资金/状态判定落到错误客户。
|
||||
*/
|
||||
Long resolveCustomerIdByCode(String customerCode)
|
||||
{
|
||||
if (!StringUtils.hasText(customerCode))
|
||||
{
|
||||
throw new ServiceException("客户编码不能为空");
|
||||
}
|
||||
String code = customerCode.trim();
|
||||
BmsCustomer query = new BmsCustomer();
|
||||
query.setCustomerCode(code);
|
||||
List<BmsCustomer> customers = bmsCustomerMapper.selectBmsCustomerList(query);
|
||||
if (customers == null || customers.isEmpty())
|
||||
{
|
||||
throw new ServiceException("客户编码不存在:" + code);
|
||||
}
|
||||
if (customers.size() > 1)
|
||||
{
|
||||
throw new ServiceException("客户编码匹配到多个客户:" + code);
|
||||
}
|
||||
return customers.get(0).getCustomerId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomerAccountStatusVO getAccountStatus(Long customerId, String customerCode)
|
||||
{
|
||||
if (customerId == null && !StringUtils.hasText(customerCode))
|
||||
{
|
||||
throw new ServiceException("客户ID与客户编码至少传一个");
|
||||
}
|
||||
Long cid = customerId != null ? customerId : resolveCustomerIdByCode(customerCode);
|
||||
|
||||
// 1. 读客户档案(不加锁)
|
||||
BmsCustomer cust = bmsCustomerMapper.selectBmsCustomerByCustomerId(cid);
|
||||
if (cust == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 读CNY账户(普通读不加锁;新客户尚未初始化账户时按0额度处理)
|
||||
CustomerAccount account = customerMoneyMapper.selectAccountByCustomerCurrency(cid, BASE_CURRENCY);
|
||||
|
||||
// 3. 组装状态VO
|
||||
CustomerAccountStatusVO vo = new CustomerAccountStatusVO();
|
||||
vo.setCustomerId(cid);
|
||||
vo.setCustomerCode(cust.getCustomerCode());
|
||||
vo.setCustomerName(cust.getCustomerName());
|
||||
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;
|
||||
|
||||
// 已过期的信用/临时额度视为0(仅内存计算,不物化DB;由定时任务/真实资金入口落库)
|
||||
boolean creditExpiredPending = isCreditExpired(account);
|
||||
boolean expiredPending = isTempCreditExpired(account);
|
||||
BigDecimal effectiveTotal = creditExpiredPending ? BigDecimal.ZERO : totalCredit;
|
||||
BigDecimal effectiveExtra = expiredPending ? BigDecimal.ZERO : rawExtra;
|
||||
|
||||
vo.setBalance(balance);
|
||||
vo.setFrozenBalance(account != null ? nz(account.getFrozenBalance()) : BigDecimal.ZERO);
|
||||
vo.setTotalCredit(totalCredit);
|
||||
vo.setEffectiveTotalCredit(effectiveTotal);
|
||||
vo.setExtraCredit(rawExtra);
|
||||
vo.setEffectiveExtraCredit(effectiveExtra);
|
||||
// 可用额度 = 账户余额 + 信用额度 + 有效临时额度(2026-09-23 口径:计费扣减余额,已用额度不再参与)
|
||||
BigDecimal available = balance.add(effectiveTotal).add(effectiveExtra);
|
||||
vo.setAvailableCredit(available);
|
||||
boolean overdue = available.compareTo(BigDecimal.ZERO) < 0;
|
||||
vo.setOverdue(overdue);
|
||||
vo.setExpiredTempCreditPending(expiredPending || creditExpiredPending);
|
||||
|
||||
boolean disabled = "1".equals(cust.getIsEnable());
|
||||
if (disabled && overdue)
|
||||
{
|
||||
vo.setAccountStatus(CustomerAccountStatusVO.STATUS_DISABLED_AND_OVERDUE);
|
||||
vo.setAccountStatusDesc("客户已停用且已超额(欠费),停用客户不可新计费");
|
||||
}
|
||||
else if (disabled)
|
||||
{
|
||||
vo.setAccountStatus(CustomerAccountStatusVO.STATUS_DISABLED);
|
||||
vo.setAccountStatusDesc("客户已停用,不可新计费");
|
||||
}
|
||||
else if (overdue)
|
||||
{
|
||||
vo.setAccountStatus(CustomerAccountStatusVO.STATUS_OVERDUE);
|
||||
vo.setAccountStatusDesc("客户已超额(欠费),可继续计费占用额度,请及时催收回款");
|
||||
}
|
||||
else
|
||||
{
|
||||
vo.setAccountStatus(CustomerAccountStatusVO.STATUS_NORMAL);
|
||||
vo.setAccountStatusDesc("客户状态正常,可正常下单");
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
private void validateBalanceCommand(BalanceChangeCommand command)
|
||||
{
|
||||
if (command == null || command.getCustomerId() == null)
|
||||
{
|
||||
throw new ServiceException("客户ID不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(command.getBizType()))
|
||||
{
|
||||
throw new ServiceException("业务类型不能为空");
|
||||
}
|
||||
if (command.getAmount() == null || command.getAmount().compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
throw new ServiceException("余额变动金额必须大于0");
|
||||
}
|
||||
}
|
||||
|
||||
/** 临时额度是否已过期:有失效日期且今天已晚于失效日(失效日当天仍有效) */
|
||||
private boolean isTempCreditExpired(CustomerAccount account)
|
||||
{
|
||||
return account != null
|
||||
&& nz(account.getExtraCredit()).compareTo(BigDecimal.ZERO) > 0
|
||||
&& account.getExtraExpireDate() != null
|
||||
&& LocalDate.now().isAfter(account.getExtraExpireDate().toInstant()
|
||||
.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;
|
||||
}
|
||||
|
||||
private BigDecimal money(BigDecimal value)
|
||||
{
|
||||
return nz(value).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/** 生成业务单号:前缀 + 时间戳(毫秒) + 3位随机数 */
|
||||
private String generateNo(String prefix)
|
||||
{
|
||||
return prefix + LocalDateTime.now().format(NO_FORMATTER)
|
||||
+ ThreadLocalRandom.current().nextInt(100, 1000);
|
||||
}
|
||||
|
||||
private BigDecimal nz(BigDecimal value)
|
||||
{
|
||||
return value == null ? BigDecimal.ZERO : value;
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package com.bms.customer.customermoney.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.bms.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 客户余额账户 ctr_customer_account
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-11
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class CustomerAccount extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 主键 */
|
||||
private Long id;
|
||||
|
||||
/** 客户ID */
|
||||
private Long customerId;
|
||||
|
||||
/** 币种,如 CNY/USD */
|
||||
private String currency;
|
||||
|
||||
/** 账户余额 */
|
||||
private BigDecimal balance;
|
||||
|
||||
/** 冻结余额 */
|
||||
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;
|
||||
|
||||
/** 临时额度生效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date extraEffectiveDate;
|
||||
|
||||
/** 临时额度失效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date extraExpireDate;
|
||||
|
||||
/** 已用额度 */
|
||||
private BigDecimal usedCredit;
|
||||
|
||||
/** 税金 */
|
||||
private BigDecimal tax;
|
||||
|
||||
/** 乐观锁版本号 */
|
||||
private Integer version;
|
||||
|
||||
/** 删除标志(0存在 1删除) */
|
||||
private String delFlag;
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package com.bms.customer.customermoney.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import com.bms.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 客户余额流水 ctr_customer_balance_flow
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-11
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class CustomerBalanceFlow extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 业务类型:收款 */
|
||||
public static final String BIZ_RECEIPT = "RECEIPT";
|
||||
/** 业务类型:账单支付 */
|
||||
public static final String BIZ_BILL_PAY = "BILL_PAY";
|
||||
/** 业务类型:退款 */
|
||||
public static final String BIZ_REFUND = "REFUND";
|
||||
/** 业务类型:手工调整 */
|
||||
public static final String BIZ_ADJUST = "ADJUST";
|
||||
/** 业务类型:冻结 */
|
||||
public static final String BIZ_FREEZE = "FREEZE";
|
||||
/** 业务类型:解冻 */
|
||||
public static final String BIZ_UNFREEZE = "UNFREEZE";
|
||||
|
||||
/** 主键 */
|
||||
private Long id;
|
||||
|
||||
/** 流水号 */
|
||||
private String flowNo;
|
||||
|
||||
/** 客户ID */
|
||||
private Long customerId;
|
||||
|
||||
/** 客户账户ID */
|
||||
private Long accountId;
|
||||
|
||||
/** 币种(业务原币) */
|
||||
private String currency;
|
||||
|
||||
/** 对CNY汇率(1原币=?CNY,CNY为1) */
|
||||
private BigDecimal exchangeRate;
|
||||
|
||||
/** 本位币金额(CNY,实际参与余额变动的金额) */
|
||||
private BigDecimal baseAmount;
|
||||
|
||||
/** 业务类型:RECEIPT/BILL_PAY/REFUND/ADJUST/FREEZE/UNFREEZE */
|
||||
private String bizType;
|
||||
|
||||
/** 关联业务ID */
|
||||
private Long bizId;
|
||||
|
||||
/** 关联业务单号 */
|
||||
private String bizNo;
|
||||
|
||||
/** 变动方向:1增加,-1减少 */
|
||||
private Integer direction;
|
||||
|
||||
/** 变动金额 */
|
||||
private BigDecimal amount;
|
||||
|
||||
/** 变动前余额 */
|
||||
private BigDecimal beforeBalance;
|
||||
|
||||
/** 变动后余额 */
|
||||
private BigDecimal afterBalance;
|
||||
|
||||
/** 变动前冻结余额 */
|
||||
private BigDecimal beforeFrozenBalance;
|
||||
|
||||
/** 变动后冻结余额 */
|
||||
private BigDecimal afterFrozenBalance;
|
||||
|
||||
/** 操作人ID */
|
||||
private Long operatorId;
|
||||
|
||||
/** 删除标志(0存在 1删除) */
|
||||
private String delFlag;
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package com.bms.customer.customermoney.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.bms.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 客户额度调整申请审批 ctr_customer_credit_apply
|
||||
* 信用额度/额外额度(临时额度)调整先申请,审批通过后才真正调整账户额度
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class CustomerCreditApply extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 待审批 */
|
||||
public static final String STATUS_PENDING = "PENDING";
|
||||
/** 已通过 */
|
||||
public static final String STATUS_APPROVED = "APPROVED";
|
||||
/** 已驳回 */
|
||||
public static final String STATUS_REJECTED = "REJECTED";
|
||||
/** 已撤销 */
|
||||
public static final String STATUS_CANCELLED = "CANCELLED";
|
||||
|
||||
/** 申请单ID */
|
||||
private Long applyId;
|
||||
|
||||
/** 申请单号(EDSQ前缀) */
|
||||
private String applyNo;
|
||||
|
||||
/** 客户ID */
|
||||
private Long customerId;
|
||||
|
||||
/** 客户账户ID */
|
||||
private Long accountId;
|
||||
|
||||
/** 额度类型:CREDIT信用额度 TEMP临时额度(额外额度) */
|
||||
private String creditType;
|
||||
|
||||
/** 调整方式:SET设置为 ADD增加 SUB减少 */
|
||||
private String adjustMode;
|
||||
|
||||
/** 调整金额(正数) */
|
||||
private java.math.BigDecimal changeAmount;
|
||||
|
||||
/** 临时额度生效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date effectiveDate;
|
||||
|
||||
/** 临时额度失效日期(空=长期) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date expireDate;
|
||||
|
||||
/** 调整原因 */
|
||||
private String reason;
|
||||
|
||||
/** 状态:PENDING/APPROVED/REJECTED/CANCELLED */
|
||||
private String status;
|
||||
|
||||
/** 申请人 */
|
||||
private String applyBy;
|
||||
|
||||
/** 申请人ID */
|
||||
private Long applyUserId;
|
||||
|
||||
/** 申请时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date applyTime;
|
||||
|
||||
/** 审批人 */
|
||||
private String approveBy;
|
||||
|
||||
/** 审批人ID */
|
||||
private Long approveUserId;
|
||||
|
||||
/** 审批占用的额度池角色ID(NULL=超管审批不占池;调减/到期释放时不消费池) */
|
||||
private Long approveRoleId;
|
||||
|
||||
/** 审批时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date approveTime;
|
||||
|
||||
/** 审批意见/驳回原因 */
|
||||
private String approveRemark;
|
||||
|
||||
/** 审批通过后生成的额度流水ID */
|
||||
private Long flowId;
|
||||
|
||||
/** 删除标志 */
|
||||
private String delFlag;
|
||||
|
||||
/* -------- 展示字段 -------- */
|
||||
|
||||
/** 客户编码 */
|
||||
private String customerCode;
|
||||
|
||||
/** 客户名称 */
|
||||
private String customerName;
|
||||
|
||||
/** 额度池角色名称(关联展示) */
|
||||
private String approveRoleName;
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package com.bms.customer.customermoney.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.bms.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 客户额度流水 ctr_customer_credit_flow
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class CustomerCreditFlow extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 额度类型:信用额度(长期) */
|
||||
public static final String CREDIT_TYPE_CREDIT = "CREDIT";
|
||||
/** 额度类型:临时额度 */
|
||||
public static final String CREDIT_TYPE_TEMP = "TEMP";
|
||||
|
||||
/** 变动类型:初始化 */
|
||||
public static final String CHANGE_INIT = "INIT";
|
||||
/** 变动类型:手工调整 */
|
||||
public static final String CHANGE_ADJUST = "ADJUST";
|
||||
/** 变动类型:到期自动失效 */
|
||||
public static final String CHANGE_EXPIRE = "EXPIRE";
|
||||
/** 变动类型:业务单据占用授信(已用额度增加,如账单确认/计费订单开单) */
|
||||
public static final String CHANGE_OCCUPY = "OCCUPY";
|
||||
/** 变动类型:业务单据释放授信(已用额度减少,如账单作废/计费变动核减) */
|
||||
public static final String CHANGE_RELEASE = "RELEASE";
|
||||
|
||||
/** 调整方式:设置为 */
|
||||
public static final String MODE_SET = "SET";
|
||||
/** 调整方式:增加 */
|
||||
public static final String MODE_ADD = "ADD";
|
||||
/** 调整方式:减少 */
|
||||
public static final String MODE_SUB = "SUB";
|
||||
|
||||
/** 主键 */
|
||||
private Long id;
|
||||
|
||||
/** 流水号 */
|
||||
private String flowNo;
|
||||
|
||||
/** 客户ID */
|
||||
private Long customerId;
|
||||
|
||||
/** 客户账户ID */
|
||||
private Long accountId;
|
||||
|
||||
/** 额度类型:CREDIT信用额度 TEMP临时额度 */
|
||||
private String creditType;
|
||||
|
||||
/** 变动类型:INIT初始化 ADJUST调整 EXPIRE到期失效 OCCUPY授信占用 RELEASE授信释放 */
|
||||
private String changeType;
|
||||
|
||||
/** 业务来源类型:AR_BILL应收账单 ORDER计费订单(仅占用/释放流水) */
|
||||
private String bizType;
|
||||
|
||||
/** 关联业务单据ID */
|
||||
private Long bizId;
|
||||
|
||||
/** 关联业务单据号 */
|
||||
private String bizNo;
|
||||
|
||||
/** 变动方向:1增加 -1减少 0持平 */
|
||||
private Integer direction;
|
||||
|
||||
/** 变动前额度 */
|
||||
private BigDecimal beforeAmount;
|
||||
|
||||
/** 变动额度(恒为非负数,方向看 direction) */
|
||||
private BigDecimal changeAmount;
|
||||
|
||||
/** 变动后额度 */
|
||||
private BigDecimal afterAmount;
|
||||
|
||||
/** 变动时已用额度快照 */
|
||||
private BigDecimal usedCredit;
|
||||
|
||||
/** 生效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date effectiveDate;
|
||||
|
||||
/** 失效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date expireDate;
|
||||
|
||||
/** 调整原因 */
|
||||
private String reason;
|
||||
|
||||
/** 调整方式(仅ADJUST流水有值):SET/ADD/SUB */
|
||||
private String adjustMode;
|
||||
|
||||
/** 操作人ID */
|
||||
private Long operatorId;
|
||||
|
||||
/** 额度池角色ID(ADJUST 审批调整时记录本次占用/释放的角色池;NULL=超管/系统) */
|
||||
private Long roleId;
|
||||
|
||||
/** 删除标志(0存在 1删除) */
|
||||
private String delFlag;
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package com.bms.customer.customermoney.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.bms.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 客户授信额度分配台账 ctr_customer_credit_grant
|
||||
* 审批通过的净增额度占一行(占用角色额度池);调减/到期按 FIFO 核销行余额。
|
||||
* 角色池已用 = SUM(remaining WHERE role_id=? AND status='0')
|
||||
*
|
||||
* @author bms
|
||||
* @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;
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package com.bms.customer.customermoney.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.bms.common.core.domain.BaseEntity;
|
||||
import com.bms.customer.customerinfo.domain.BmsCustomerAttachment;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 客户收款 ctr_customer_receipt
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-11
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class CustomerReceipt extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 状态:待确认 */
|
||||
public static final String STATUS_PENDING = "PENDING";
|
||||
/** 状态:已确认 */
|
||||
public static final String STATUS_CONFIRMED = "CONFIRMED";
|
||||
/** 状态:已取消 */
|
||||
public static final String STATUS_CANCELLED = "CANCELLED";
|
||||
|
||||
/** 收款类型:预充值 */
|
||||
public static final String TYPE_PREPAY = "PREPAY";
|
||||
/** 收款类型:还款核销 */
|
||||
public static final String TYPE_REPAY = "REPAY";
|
||||
/** 收款类型:混合 */
|
||||
public static final String TYPE_MIXED = "MIXED";
|
||||
|
||||
/** 汇率来源:系统默认(本币CNY 1:1) */
|
||||
public static final String RATE_SOURCE_SYSTEM = "SYSTEM";
|
||||
/** 汇率来源:汇率表(收款日即期汇率自动带出) */
|
||||
public static final String RATE_SOURCE_RATE_TABLE = "RATE_TABLE";
|
||||
/** 汇率来源:手工修改/手工录入 */
|
||||
public static final String RATE_SOURCE_MANUAL = "MANUAL";
|
||||
|
||||
/** 主键 */
|
||||
private Long id;
|
||||
|
||||
/** 收款单号 */
|
||||
private String receiptNo;
|
||||
|
||||
/** 客户ID */
|
||||
private Long customerId;
|
||||
|
||||
/** 客户账户ID(预留) */
|
||||
private Long accountId;
|
||||
|
||||
/** 收款类型:PREPAY预充值/REPAY还款核销/MIXED混合 */
|
||||
private String receiptType;
|
||||
|
||||
/** 收款金额 */
|
||||
private BigDecimal amount;
|
||||
|
||||
/** 已核销金额 */
|
||||
private BigDecimal allocatedAmount;
|
||||
|
||||
/** 待核销金额 */
|
||||
private BigDecimal unallocatedAmount;
|
||||
|
||||
/** 币种(收款原币) */
|
||||
private String currency;
|
||||
|
||||
/** 对CNY汇率(1原币=?CNY,CNY为1;默认取汇率表,可手工修改) */
|
||||
private BigDecimal exchangeRate;
|
||||
|
||||
/** 汇率来源:SYSTEM系统默认(本币1:1)/RATE_TABLE汇率表/MANUAL手工修改 */
|
||||
private String rateSource;
|
||||
|
||||
/** 汇率来源说明(汇率表来源及命中路径,或手工录入说明) */
|
||||
private String rateSourceDesc;
|
||||
|
||||
/** 本位币金额(CNY,原币金额×汇率) */
|
||||
private BigDecimal baseAmount;
|
||||
|
||||
/** 收款方式:BANK_TRANSFER/ALIPAY/WECHAT/CASH/OTHER */
|
||||
private String payMethod;
|
||||
|
||||
/** 客户银行账户ID(ar_bank_account) */
|
||||
private Long bankAccountId;
|
||||
|
||||
/** 客户银行账户户名(登记时快照) */
|
||||
private String bankAccountName;
|
||||
|
||||
/** 客户银行账号(登记时快照) */
|
||||
private String bankAccountNo;
|
||||
|
||||
/** 开户行名称(登记时快照) */
|
||||
private String bankName;
|
||||
|
||||
/** 银行流水号/支付平台流水号 */
|
||||
private String bankTransactionNo;
|
||||
|
||||
/** 收款日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date receivedAt;
|
||||
|
||||
/** 状态:PENDING待确认/CONFIRMED已确认/CANCELLED已取消 */
|
||||
private String status;
|
||||
|
||||
/** 确认时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date confirmedAt;
|
||||
|
||||
/** 确认人 */
|
||||
private Long confirmedBy;
|
||||
|
||||
/** 开票状态:NONE未开/PART部分/ALL全部 */
|
||||
private String invoiceStatus;
|
||||
|
||||
/** 乐观锁版本号 */
|
||||
private Integer version;
|
||||
|
||||
/** 删除标志(0存在 1删除) */
|
||||
private String delFlag;
|
||||
|
||||
/** 收款附件列表(存 ctr_customer_attachment,attachment_type='3',biz_id 关联本单) */
|
||||
private List<BmsCustomerAttachment> attachmentList;
|
||||
|
||||
/** 收款附件数量(列表查询统计) */
|
||||
private Integer attachmentCount;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.bms.customer.customermoney.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import com.bms.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 角色可分配授信额度池 sys_role_credit_quota
|
||||
* 一个角色一行;没有记录视为额度0(该角色用户不能审批任何增量授信)
|
||||
*
|
||||
* @author bms
|
||||
* @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.bms.customer.customermoney.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.customer.customermoney.domain.CustomerCreditGrant;
|
||||
import com.bms.customer.customermoney.domain.SysRoleCreditQuota;
|
||||
import com.bms.customer.customermoney.vo.RoleCreditQuotaVo;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 角色额度池 / 授信分配台账 Mapper
|
||||
*
|
||||
* @author bms
|
||||
* @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);
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.bms.customer.customermoney.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.customer.customermoney.domain.CustomerCreditApply;
|
||||
|
||||
/**
|
||||
* 客户额度调整申请审批 Mapper
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
public interface CustomerCreditApplyMapper
|
||||
{
|
||||
/** 新增申请 */
|
||||
int insertApply(CustomerCreditApply apply);
|
||||
|
||||
/** 按ID查申请(行锁,审批事务使用) */
|
||||
CustomerCreditApply selectApplyByIdForUpdate(@Param("applyId") Long applyId);
|
||||
|
||||
/** 按ID查申请 */
|
||||
CustomerCreditApply selectApplyById(@Param("applyId") Long applyId);
|
||||
|
||||
/** 查询申请列表(可按客户/状态/类型过滤) */
|
||||
List<CustomerCreditApply> selectApplyList(CustomerCreditApply query);
|
||||
|
||||
/** 审批通过:PENDING -> APPROVED,回写账户ID、流水ID、审批人/时间/意见 */
|
||||
int updateApproved(CustomerCreditApply apply);
|
||||
|
||||
/** 审批驳回:PENDING -> REJECTED */
|
||||
int updateRejected(CustomerCreditApply apply);
|
||||
|
||||
/** 申请人撤销:PENDING -> CANCELLED */
|
||||
int updateCancelled(@Param("applyId") Long applyId, @Param("updateBy") String updateBy);
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package com.bms.customer.customermoney.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.customer.customermoney.domain.CustomerAccount;
|
||||
import com.bms.customer.customermoney.domain.CustomerBalanceFlow;
|
||||
import com.bms.customer.customermoney.domain.CustomerCreditFlow;
|
||||
import com.bms.customer.customermoney.domain.CustomerReceipt;
|
||||
|
||||
/**
|
||||
* 客户余额/收款/流水 Mapper
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-11
|
||||
*/
|
||||
public interface CustomerMoneyMapper
|
||||
{
|
||||
/* ================= 客户余额账户 ================= */
|
||||
|
||||
/** 按客户+币种查询账户 */
|
||||
CustomerAccount selectAccountByCustomerCurrency(@Param("customerId") Long customerId,
|
||||
@Param("currency") String currency);
|
||||
|
||||
/** 按客户+币种查询账户(行锁,审核事务内使用) */
|
||||
CustomerAccount selectAccountForUpdate(@Param("customerId") Long customerId,
|
||||
@Param("currency") String currency);
|
||||
|
||||
/** 查询客户全部币种账户 */
|
||||
List<CustomerAccount> selectAccountListByCustomerId(@Param("customerId") Long customerId);
|
||||
|
||||
/**
|
||||
* 扫描授信已到期的CNY账户客户ID(信用或临时额度失效日早于今天;失效日当天仍有效)。
|
||||
* 供定时任务批量物化失效使用,结果集不加锁,逐户处理时再行锁。
|
||||
*/
|
||||
List<Long> selectExpiredCreditCustomerIds();
|
||||
|
||||
/** 新增账户 */
|
||||
int insertAccount(CustomerAccount account);
|
||||
|
||||
/** 乐观锁更新余额 */
|
||||
int updateAccountBalance(CustomerAccount account);
|
||||
|
||||
/** 乐观锁更新额度(信用额度/临时额度/有效期) */
|
||||
int updateAccountCredit(CustomerAccount account);
|
||||
|
||||
/* ================= 额度流水 ================= */
|
||||
|
||||
/** 查询额度流水 */
|
||||
List<CustomerCreditFlow> selectCreditFlowList(CustomerCreditFlow query);
|
||||
|
||||
/** 新增额度流水 */
|
||||
int insertCreditFlow(CustomerCreditFlow flow);
|
||||
|
||||
/* ================= 客户收款 ================= */
|
||||
|
||||
/** 查询收款单列表 */
|
||||
List<CustomerReceipt> selectReceiptList(CustomerReceipt query);
|
||||
|
||||
/** 按ID查询收款单 */
|
||||
CustomerReceipt selectReceiptById(@Param("id") Long id);
|
||||
|
||||
/** 新增收款单 */
|
||||
int insertReceipt(CustomerReceipt receipt);
|
||||
|
||||
/** 审核通过:PENDING -> CONFIRMED(带状态+乐观锁条件) */
|
||||
int confirmReceipt(CustomerReceipt receipt);
|
||||
|
||||
/** 取消收款单:PENDING -> CANCELLED */
|
||||
int cancelReceipt(@Param("id") Long id, @Param("updateBy") String updateBy);
|
||||
|
||||
/* ================= 余额流水 ================= */
|
||||
|
||||
/** 查询流水列表 */
|
||||
List<CustomerBalanceFlow> selectFlowList(CustomerBalanceFlow query);
|
||||
|
||||
/** 新增流水 */
|
||||
int insertFlow(CustomerBalanceFlow flow);
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.bms.customer.customermoney.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.bms.customer.customermoney.domain.CustomerAccount;
|
||||
import com.bms.customer.customermoney.domain.CustomerCreditGrant;
|
||||
import com.bms.customer.customermoney.vo.RoleCreditQuotaVo;
|
||||
|
||||
/**
|
||||
* 角色额度池 Service
|
||||
*
|
||||
* @author bms
|
||||
* @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);
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package com.bms.customer.customermoney.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.bms.customer.customermoney.domain.CustomerAccount;
|
||||
import com.bms.customer.customermoney.domain.CustomerBalanceFlow;
|
||||
import com.bms.customer.customermoney.domain.CustomerCreditApply;
|
||||
import com.bms.customer.customermoney.domain.CustomerCreditFlow;
|
||||
import com.bms.customer.customermoney.domain.CustomerReceipt;
|
||||
|
||||
/**
|
||||
* 客户余额/收款/流水 Service接口
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-11
|
||||
*/
|
||||
public interface ICustomerMoneyService
|
||||
{
|
||||
/** 查询客户各币种账户 */
|
||||
List<CustomerAccount> selectAccountList(Long customerId);
|
||||
|
||||
/**
|
||||
* 新建客户时初始化默认CNY账户(余额0,信用额度默认0),并写 INIT 额度流水。
|
||||
* 已存在CNY账户时不重复创建。
|
||||
*
|
||||
* @param customerId 客户ID
|
||||
* @return 1=已初始化或已存在
|
||||
*/
|
||||
int initDefaultCnyAccount(Long customerId);
|
||||
|
||||
/** 查询收款单列表 */
|
||||
List<CustomerReceipt> selectReceiptList(CustomerReceipt query);
|
||||
|
||||
/** 查询收款单详情(含附件) */
|
||||
CustomerReceipt selectReceiptById(Long id);
|
||||
|
||||
/** 登记收款(状态默认 PENDING,同时保存附件) */
|
||||
int insertReceipt(CustomerReceipt receipt);
|
||||
|
||||
/**
|
||||
* 审核通过收款单:PENDING -> CONFIRMED
|
||||
* 事务内:锁定账户(不存在则初始化)-> 变动余额/已用额度 -> 写余额流水
|
||||
*/
|
||||
int confirmReceipt(Long id);
|
||||
|
||||
/** 取消收款单(仅待确认可取消) */
|
||||
int cancelReceipt(Long id);
|
||||
|
||||
/** 查询余额流水列表 */
|
||||
List<CustomerBalanceFlow> selectFlowList(CustomerBalanceFlow query);
|
||||
|
||||
/** 查询额度流水列表 */
|
||||
List<CustomerCreditFlow> selectCreditFlowList(CustomerCreditFlow query);
|
||||
|
||||
/**
|
||||
* 提交额度调整申请(信用额度/临时额度)。申请仅落库为 PENDING,不立即变动账户额度;
|
||||
* 申请时会按账户当前额度做一次合法性校验(不能为负、不能低于已用额度),审批通过时会再次校验。
|
||||
*/
|
||||
int applyCreditAdjust(CustomerCreditApply apply);
|
||||
|
||||
/**
|
||||
* 审批通过额度调整申请:PENDING -> APPROVED。
|
||||
* 事务内重新锁定CNY账户并按审批时的实际额度计算/校验,通过后调整额度、回写客户档案、写额度流水。
|
||||
* 净增额度需占用 approveRoleId 对应角色额度池(超管传 null 不占池;净减/到期释放不占池)。
|
||||
*/
|
||||
int approveCreditApply(Long applyId, String approveRemark, Long approveRoleId);
|
||||
|
||||
/** 审批驳回:PENDING -> REJECTED,驳回原因必填 */
|
||||
int rejectCreditApply(Long applyId, String rejectReason);
|
||||
|
||||
/** 申请人撤销:PENDING -> CANCELLED */
|
||||
int cancelCreditApply(Long applyId);
|
||||
|
||||
/** 查询额度调整申请列表 */
|
||||
List<CustomerCreditApply> selectCreditApplyList(CustomerCreditApply query);
|
||||
|
||||
/** 查询额度调整申请详情 */
|
||||
CustomerCreditApply selectCreditApplyById(Long applyId);
|
||||
}
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
package com.bms.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.bms.common.core.domain.entity.SysRole;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.common.utils.SecurityUtils;
|
||||
import com.bms.customer.customermoney.domain.CustomerAccount;
|
||||
import com.bms.customer.customermoney.domain.CustomerCreditGrant;
|
||||
import com.bms.customer.customermoney.domain.SysRoleCreditQuota;
|
||||
import com.bms.customer.customermoney.mapper.CreditQuotaMapper;
|
||||
import com.bms.customer.customermoney.service.ICreditQuotaService;
|
||||
import com.bms.customer.customermoney.vo.RoleCreditQuotaVo;
|
||||
import com.bms.system.mapper.SysRoleMapper;
|
||||
|
||||
/**
|
||||
* 角色额度池 Service实现
|
||||
*
|
||||
* @author bms
|
||||
* @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("无法获取当前登录用户");
|
||||
}
|
||||
// 超管审批不占用任何角色额度池:直接返回空列表,前端不弹选池框(否则 LEFT JOIN 会带出
|
||||
// 超管角色未配置的 0 额度池,导致前端强制选池并被"额度池剩余不足"拦截)
|
||||
if (SecurityUtils.isAdmin(userId))
|
||||
{
|
||||
return java.util.Collections.emptyList();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
+830
@@ -0,0 +1,830 @@
|
||||
package com.bms.customer.customermoney.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.common.utils.SecurityUtils;
|
||||
import com.bms.ar.domain.ArBankAccount;
|
||||
import com.bms.ar.mapper.ArBankAccountMapper;
|
||||
import com.bms.customer.customerinfo.domain.BmsCustomer;
|
||||
import com.bms.customer.customerinfo.domain.BmsCustomerAttachment;
|
||||
import com.bms.customer.customerinfo.mapper.BmsCustomerMapper;
|
||||
import com.bms.customer.customermoney.api.BalanceChangeCommand;
|
||||
import com.bms.customer.customermoney.api.CustomerFundBizType;
|
||||
import com.bms.customer.customermoney.api.ICustomerAccountApi;
|
||||
import com.bms.customer.customermoney.domain.CustomerAccount;
|
||||
import com.bms.customer.customermoney.domain.CustomerBalanceFlow;
|
||||
import com.bms.customer.customermoney.domain.CustomerCreditApply;
|
||||
import com.bms.customer.customermoney.domain.CustomerCreditFlow;
|
||||
import com.bms.customer.customermoney.domain.CustomerReceipt;
|
||||
import com.bms.customer.customermoney.mapper.CustomerCreditApplyMapper;
|
||||
import com.bms.customer.customermoney.mapper.CustomerMoneyMapper;
|
||||
import com.bms.customer.customermoney.service.ICreditQuotaService;
|
||||
import com.bms.customer.customermoney.service.ICustomerMoneyService;
|
||||
import com.bms.price.service.IPrcOrderBillService;
|
||||
import com.bms.system.domain.BmsExchangeRate;
|
||||
import com.bms.system.service.IBmsExchangeRateService;
|
||||
|
||||
/**
|
||||
* 客户余额/收款/流水 Service实现
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-11
|
||||
*/
|
||||
@Service
|
||||
public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
{
|
||||
private static final DateTimeFormatter NO_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
|
||||
|
||||
/** 本位币币种 */
|
||||
private static final String BASE_CURRENCY = BmsExchangeRate.BASE_CURRENCY;
|
||||
/** 金额保留位数 */
|
||||
private static final int MONEY_SCALE = 2;
|
||||
|
||||
@Autowired
|
||||
private CustomerMoneyMapper customerMoneyMapper;
|
||||
|
||||
@Autowired
|
||||
private CustomerCreditApplyMapper creditApplyMapper;
|
||||
|
||||
@Autowired
|
||||
private BmsCustomerMapper bmsCustomerMapper;
|
||||
|
||||
@Autowired
|
||||
private IPrcOrderBillService orderBillService;
|
||||
|
||||
@Autowired
|
||||
private IBmsExchangeRateService exchangeRateService;
|
||||
|
||||
/** 客户资金公共服务:账户/余额/授信额度变动统一收口 */
|
||||
@Autowired
|
||||
private ICustomerAccountApi customerAccountApi;
|
||||
|
||||
/** 角色额度池:审批增量占用/调减与到期释放 */
|
||||
@Autowired
|
||||
private ICreditQuotaService creditQuotaService;
|
||||
|
||||
/** 公司收款银行账户(登记收款选择并快照) */
|
||||
@Autowired
|
||||
private ArBankAccountMapper arBankAccountMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<CustomerAccount> selectAccountList(Long customerId)
|
||||
{
|
||||
List<CustomerAccount> list = customerMoneyMapper.selectAccountListByCustomerId(customerId);
|
||||
// 查询时顺带处理授信到期自动失效(仅CNY账户),保证卡片展示为最新值
|
||||
for (int i = 0; i < list.size(); i++)
|
||||
{
|
||||
CustomerAccount account = list.get(i);
|
||||
if (BASE_CURRENCY.equals(account.getCurrency())
|
||||
&& (isTempCreditExpired(account) || isCreditExpired(account)))
|
||||
{
|
||||
CustomerAccount locked = customerAccountApi.lockCnyAccount(customerId);
|
||||
customerAccountApi.expireDueCreditsIfNeeded(locked);
|
||||
if (locked != null)
|
||||
{
|
||||
list.set(i, locked);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CustomerReceipt> selectReceiptList(CustomerReceipt query)
|
||||
{
|
||||
return customerMoneyMapper.selectReceiptList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomerReceipt selectReceiptById(Long id)
|
||||
{
|
||||
CustomerReceipt receipt = customerMoneyMapper.selectReceiptById(id);
|
||||
if (receipt != null)
|
||||
{
|
||||
BmsCustomerAttachment query = new BmsCustomerAttachment();
|
||||
query.setBizType(BmsCustomerAttachment.BIZ_TYPE_CUSTOMER_RECEIPT);
|
||||
query.setBizId(id);
|
||||
receipt.setAttachmentList(bmsCustomerMapper.selectBmsCustomerAttachmentList(query));
|
||||
}
|
||||
return receipt;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int insertReceipt(CustomerReceipt receipt)
|
||||
{
|
||||
if (receipt.getCustomerId() == null)
|
||||
{
|
||||
throw new ServiceException("客户ID不能为空");
|
||||
}
|
||||
if (receipt.getAmount() == null || receipt.getAmount().compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
throw new ServiceException("收款金额必须大于0");
|
||||
}
|
||||
if (!StringUtils.hasText(receipt.getReceiptType()))
|
||||
{
|
||||
receipt.setReceiptType(CustomerReceipt.TYPE_PREPAY);
|
||||
}
|
||||
if (!StringUtils.hasText(receipt.getCurrency()))
|
||||
{
|
||||
receipt.setCurrency(BASE_CURRENCY);
|
||||
}
|
||||
else
|
||||
{
|
||||
receipt.setCurrency(receipt.getCurrency().trim().toUpperCase());
|
||||
}
|
||||
// 收款银行账户:以库中启用账户为准回填快照(不信任前端传入的户名/开户行文本),
|
||||
// 账户后续停用或修改不影响历史单据展示。
|
||||
// 未选账户时,允许只手工录入客户银行账号文本(bankAccountNo),户名/开户行清空。
|
||||
if (receipt.getBankAccountId() != null)
|
||||
{
|
||||
ArBankAccount bankAccount = arBankAccountMapper.selectEnabledById(receipt.getBankAccountId());
|
||||
if (bankAccount == null)
|
||||
{
|
||||
throw new ServiceException("收款银行账户不存在或已停用,请重新选择");
|
||||
}
|
||||
receipt.setBankAccountName(bankAccount.getAccountName());
|
||||
receipt.setBankAccountNo(bankAccount.getAccountNo());
|
||||
receipt.setBankName(bankAccount.getBankName());
|
||||
}
|
||||
else
|
||||
{
|
||||
receipt.setBankAccountName(null);
|
||||
receipt.setBankName(null);
|
||||
receipt.setBankAccountNo(StringUtils.hasText(receipt.getBankAccountNo())
|
||||
? receipt.getBankAccountNo().trim() : null);
|
||||
}
|
||||
// 汇率与本位币金额:CNY 固定 1:1;外币以收款日即期汇率表为基准,允许页面手工修改。
|
||||
// 汇率来源由后端权威判定(不信任前端标记):与汇率表一致=RATE_TABLE,不一致/表未配置=MANUAL。
|
||||
BigDecimal rate = receipt.getExchangeRate();
|
||||
if (BASE_CURRENCY.equals(receipt.getCurrency()))
|
||||
{
|
||||
rate = BigDecimal.ONE;
|
||||
receipt.setRateSource(CustomerReceipt.RATE_SOURCE_SYSTEM);
|
||||
receipt.setRateSourceDesc("系统默认(本位币1:1)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Date bizDate = receipt.getReceivedAt() != null ? receipt.getReceivedAt() : new Date();
|
||||
BmsExchangeRate tableRate = null;
|
||||
try
|
||||
{
|
||||
tableRate = exchangeRateService.resolveRate(
|
||||
receipt.getCurrency(), BASE_CURRENCY, BmsExchangeRate.RATE_TYPE_SPOT, bizDate);
|
||||
}
|
||||
catch (ServiceException ex)
|
||||
{
|
||||
// 汇率表未配置:若页面手工录了有效汇率则放行,否则继续走下面的非空校验拦截
|
||||
tableRate = null;
|
||||
}
|
||||
|
||||
if (rate == null || rate.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
if (tableRate == null)
|
||||
{
|
||||
throw new ServiceException("汇率表未维护【" + receipt.getCurrency()
|
||||
+ " → CNY】收款日生效的即期汇率,请手工录入汇率");
|
||||
}
|
||||
rate = tableRate.getExchangeRate();
|
||||
}
|
||||
if (rate.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
throw new ServiceException("汇率必须大于0,请检查录入的汇率");
|
||||
}
|
||||
rate = rate.setScale(8, RoundingMode.HALF_UP);
|
||||
|
||||
// 与汇率表汇率一致(8位精度)即认定为汇率表来源,否则为手工修改
|
||||
if (tableRate != null
|
||||
&& rate.compareTo(tableRate.getExchangeRate().setScale(8, RoundingMode.HALF_UP)) == 0)
|
||||
{
|
||||
receipt.setRateSource(CustomerReceipt.RATE_SOURCE_RATE_TABLE);
|
||||
receipt.setRateSourceDesc(buildRateTableSourceDesc(tableRate));
|
||||
}
|
||||
else
|
||||
{
|
||||
receipt.setRateSource(CustomerReceipt.RATE_SOURCE_MANUAL);
|
||||
if (tableRate != null)
|
||||
{
|
||||
receipt.setRateSourceDesc("手工修改(汇率表即期汇率 "
|
||||
+ tableRate.getExchangeRate().setScale(8, RoundingMode.HALF_UP).toPlainString() + ")");
|
||||
}
|
||||
else
|
||||
{
|
||||
receipt.setRateSourceDesc("手工录入(汇率表未维护该币种收款日汇率)");
|
||||
}
|
||||
}
|
||||
}
|
||||
receipt.setExchangeRate(rate.setScale(8, RoundingMode.HALF_UP));
|
||||
BigDecimal baseAmount = receipt.getAmount().multiply(rate).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
receipt.setBaseAmount(baseAmount);
|
||||
|
||||
receipt.setReceiptNo(generateNo("SK"));
|
||||
receipt.setStatus(CustomerReceipt.STATUS_PENDING);
|
||||
receipt.setAllocatedAmount(BigDecimal.ZERO);
|
||||
// 核销口径统一为本位币 CNY
|
||||
receipt.setUnallocatedAmount(baseAmount);
|
||||
if (!StringUtils.hasText(receipt.getInvoiceStatus()))
|
||||
{
|
||||
receipt.setInvoiceStatus("NONE");
|
||||
}
|
||||
receipt.setVersion(0);
|
||||
customerMoneyMapper.insertReceipt(receipt);
|
||||
|
||||
// 收款附件:复用客户附件表,业务模块挂 customer_receipt,biz_id 关联收款单
|
||||
saveReceiptAttachments(receipt);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造汇率表来源说明:汇率来源(中国银行/央行等)+ 命中路径(正向/反向取倒数/经CNY交叉)。
|
||||
* resolveRate 返回对象的 remark 带 __RATE_PATH__ 前缀标记命中路径。
|
||||
*/
|
||||
private String buildRateTableSourceDesc(BmsExchangeRate tableRate)
|
||||
{
|
||||
String source = StringUtils.hasText(tableRate.getSource()) ? tableRate.getSource() : "汇率表";
|
||||
String path = "DIRECT";
|
||||
String remark = tableRate.getRemark();
|
||||
if (StringUtils.hasText(remark) && remark.startsWith("__RATE_PATH__"))
|
||||
{
|
||||
path = remark.substring("__RATE_PATH__".length());
|
||||
}
|
||||
String pathText;
|
||||
switch (path)
|
||||
{
|
||||
case "REVERSE":
|
||||
pathText = "反向配置取倒数";
|
||||
break;
|
||||
case "CROSS":
|
||||
pathText = "经CNY交叉换算";
|
||||
break;
|
||||
case "SAME":
|
||||
pathText = "同币种";
|
||||
break;
|
||||
case "DIRECT":
|
||||
default:
|
||||
pathText = "正向配置";
|
||||
break;
|
||||
}
|
||||
return "汇率表·" + source + "(" + pathText + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int confirmReceipt(Long id)
|
||||
{
|
||||
CustomerReceipt receipt = customerMoneyMapper.selectReceiptById(id);
|
||||
if (receipt == null)
|
||||
{
|
||||
throw new ServiceException("收款单不存在");
|
||||
}
|
||||
if (!CustomerReceipt.STATUS_PENDING.equals(receipt.getStatus()))
|
||||
{
|
||||
throw new ServiceException("仅待确认的收款单可以审核");
|
||||
}
|
||||
|
||||
String currency = StringUtils.hasText(receipt.getCurrency()) ? receipt.getCurrency().trim().toUpperCase()
|
||||
: BASE_CURRENCY;
|
||||
String username = SecurityUtils.getUsername();
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
|
||||
// 本位币入账金额(兼容历史数据:未折算时按原金额/汇率1处理)
|
||||
BigDecimal rate = receipt.getExchangeRate() != null && receipt.getExchangeRate().compareTo(BigDecimal.ZERO) > 0
|
||||
? receipt.getExchangeRate() : BigDecimal.ONE;
|
||||
BigDecimal originalAmount = nz(receipt.getAmount());
|
||||
BigDecimal baseAmount = receipt.getBaseAmount() != null
|
||||
? receipt.getBaseAmount()
|
||||
: originalAmount.multiply(rate).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
|
||||
// 1. 本位币入账(公共资金服务内完成:锁户/初始化 -> 余额变动 -> 余额流水)。
|
||||
// 额度占用统一以账单为准:收款审核只增加余额,核销账单时才释放已用额度
|
||||
BalanceChangeCommand fundCmd = new BalanceChangeCommand();
|
||||
fundCmd.setCustomerId(receipt.getCustomerId());
|
||||
// 流水保留收款原币;无论收什么币种,一律锁CNY账户折算入账
|
||||
fundCmd.setCurrency(currency);
|
||||
fundCmd.setAccountCurrency(BASE_CURRENCY);
|
||||
fundCmd.setAmount(originalAmount);
|
||||
fundCmd.setExchangeRate(rate);
|
||||
fundCmd.setBaseAmount(baseAmount);
|
||||
fundCmd.setBizType(CustomerFundBizType.RECEIPT);
|
||||
fundCmd.setBizId(receipt.getId());
|
||||
fundCmd.setBizNo(receipt.getReceiptNo());
|
||||
fundCmd.setReoccupyUsedCredit(false);
|
||||
fundCmd.setRemark("收款单审核通过:" + receipt.getReceiptNo());
|
||||
customerAccountApi.refundBalance(fundCmd);
|
||||
|
||||
// 余额入账后:按 FIFO 清除订单欠费状态(ARREARS -> NORMAL)
|
||||
BmsCustomer customer = bmsCustomerMapper.selectBmsCustomerByCustomerId(receipt.getCustomerId());
|
||||
if (customer != null && StringUtils.hasText(customer.getCustomerCode())
|
||||
&& baseAmount != null && baseAmount.compareTo(BigDecimal.ZERO) > 0)
|
||||
{
|
||||
orderBillService.clearCreditArrearsByAmount(customer.getCustomerCode().trim(), baseAmount);
|
||||
}
|
||||
|
||||
// 2. 收款单置为已确认
|
||||
receipt.setStatus(CustomerReceipt.STATUS_CONFIRMED);
|
||||
receipt.setConfirmedAt(new Date());
|
||||
receipt.setConfirmedBy(userId);
|
||||
receipt.setUpdateBy(username);
|
||||
int receiptRows = customerMoneyMapper.confirmReceipt(receipt);
|
||||
if (receiptRows == 0)
|
||||
{
|
||||
throw new ServiceException("收款单状态已变更,请刷新后重试");
|
||||
}
|
||||
return receiptRows;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int cancelReceipt(Long id)
|
||||
{
|
||||
CustomerReceipt receipt = customerMoneyMapper.selectReceiptById(id);
|
||||
if (receipt == null)
|
||||
{
|
||||
throw new ServiceException("收款单不存在");
|
||||
}
|
||||
if (!CustomerReceipt.STATUS_PENDING.equals(receipt.getStatus()))
|
||||
{
|
||||
throw new ServiceException("仅待确认的收款单可以取消");
|
||||
}
|
||||
return customerMoneyMapper.cancelReceipt(id, SecurityUtils.getUsername());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CustomerBalanceFlow> selectFlowList(CustomerBalanceFlow query)
|
||||
{
|
||||
return customerMoneyMapper.selectFlowList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CustomerCreditFlow> selectCreditFlowList(CustomerCreditFlow query)
|
||||
{
|
||||
return customerMoneyMapper.selectCreditFlowList(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行额度调整(额度申请审批通过后调用)。
|
||||
* 锁CNY账户 -> 按审批时实际额度计算并校验 -> 乐观锁更新 -> 回写客户档案 -> 写额度流水 -> 角色额度池占用/释放。
|
||||
* roleId 为本次审批选择的额度池角色(超管/净减时可为null);applyId 为审批申请单ID。
|
||||
* 返回已落库的额度流水(含ID)。
|
||||
*/
|
||||
private CustomerCreditFlow executeAdjustCredit(CustomerCreditFlow cmd, Long roleId, Long applyId)
|
||||
{
|
||||
String username = SecurityUtils.getUsername();
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
Long customerId = cmd.getCustomerId();
|
||||
String creditType = cmd.getCreditType();
|
||||
String adjustMode = cmd.getAdjustMode();
|
||||
String reason = cmd.getReason();
|
||||
boolean isTemp = CustomerCreditFlow.CREDIT_TYPE_TEMP.equals(creditType);
|
||||
|
||||
// 1. 参数/日期/结果合法性校验并锁定CNY账户(不存在则按客户档案额度初始化);先处理临时额度过期
|
||||
CustomerAccount account = validateAdjustCommand(cmd, username);
|
||||
BigDecimal input = nz(cmd.getChangeAmount());
|
||||
|
||||
// 2. 计算调整结果
|
||||
BigDecimal before = isTemp ? nz(account.getExtraCredit()) : nz(account.getTotalCredit());
|
||||
BigDecimal after;
|
||||
switch (adjustMode)
|
||||
{
|
||||
case CustomerCreditFlow.MODE_SET:
|
||||
after = input;
|
||||
break;
|
||||
case CustomerCreditFlow.MODE_ADD:
|
||||
after = before.add(input);
|
||||
break;
|
||||
default:
|
||||
after = before.subtract(input);
|
||||
break;
|
||||
}
|
||||
after = after.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
|
||||
// 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)
|
||||
{
|
||||
account.setExtraCredit(after);
|
||||
account.setExtraEffectiveDate(effectiveDate);
|
||||
account.setExtraExpireDate(expireDate);
|
||||
}
|
||||
else
|
||||
{
|
||||
account.setTotalCredit(after);
|
||||
account.setCreditEffectiveDate(effectiveDate);
|
||||
account.setCreditExpireDate(expireDate);
|
||||
}
|
||||
account.setUpdateBy(username);
|
||||
|
||||
// 4. 乐观锁更新额度
|
||||
int rows = customerMoneyMapper.updateAccountCredit(account);
|
||||
if (rows == 0)
|
||||
{
|
||||
throw new ServiceException("账户额度已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
account.setVersion(account.getVersion() + 1);
|
||||
|
||||
// 5. 回写客户档案额度字段(额度唯一数据源为账户,档案仅做同步展示)
|
||||
bmsCustomerMapper.updateCustomerCredit(customerId,
|
||||
nz(account.getTotalCredit()), nz(account.getExtraCredit()), username);
|
||||
|
||||
// 6. 写额度流水
|
||||
CustomerCreditFlow flow = new CustomerCreditFlow();
|
||||
flow.setFlowNo(generateNo("EDLS"));
|
||||
flow.setCustomerId(customerId);
|
||||
flow.setAccountId(account.getId());
|
||||
flow.setCreditType(creditType);
|
||||
flow.setChangeType(CustomerCreditFlow.CHANGE_ADJUST);
|
||||
int direction = after.compareTo(before) > 0 ? 1 : -1;
|
||||
flow.setDirection(direction);
|
||||
flow.setBeforeAmount(before);
|
||||
flow.setChangeAmount(after.subtract(before).abs());
|
||||
flow.setAfterAmount(after);
|
||||
flow.setUsedCredit(nz(account.getUsedCredit()));
|
||||
flow.setEffectiveDate(effectiveDate);
|
||||
flow.setExpireDate(expireDate);
|
||||
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;
|
||||
}
|
||||
|
||||
/* ================= 额度调整:申请 / 审批 ================= */
|
||||
|
||||
@Override
|
||||
public List<CustomerCreditApply> selectCreditApplyList(CustomerCreditApply query)
|
||||
{
|
||||
return creditApplyMapper.selectApplyList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomerCreditApply selectCreditApplyById(Long applyId)
|
||||
{
|
||||
return creditApplyMapper.selectApplyById(applyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int applyCreditAdjust(CustomerCreditApply apply)
|
||||
{
|
||||
if (apply == null || apply.getCustomerId() == null)
|
||||
{
|
||||
throw new ServiceException("客户ID不能为空");
|
||||
}
|
||||
// 构造内部调整命令,复用统一的参数/日期/额度合法性校验(只校验,不写入)
|
||||
CustomerCreditFlow cmd = new CustomerCreditFlow();
|
||||
cmd.setCustomerId(apply.getCustomerId());
|
||||
cmd.setCreditType(apply.getCreditType());
|
||||
cmd.setAdjustMode(apply.getAdjustMode());
|
||||
cmd.setChangeAmount(nz(apply.getChangeAmount()));
|
||||
cmd.setReason(apply.getReason());
|
||||
cmd.setRemark(apply.getRemark());
|
||||
cmd.setEffectiveDate(apply.getEffectiveDate());
|
||||
cmd.setExpireDate(apply.getExpireDate());
|
||||
|
||||
String username = SecurityUtils.getUsername();
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
|
||||
// 1. 参数校验 + 日期校验 + 按当前账户额度预演结果,提前拦截不可能通过的申请(不写入账户)
|
||||
validateAdjustCommand(cmd, username);
|
||||
apply.setChangeAmount(cmd.getChangeAmount());
|
||||
apply.setReason(cmd.getReason());
|
||||
|
||||
// 2. 待审批申请落库,不动账户额度
|
||||
apply.setApplyNo(generateNo("EDSQ"));
|
||||
apply.setStatus(CustomerCreditApply.STATUS_PENDING);
|
||||
apply.setApplyBy(username);
|
||||
apply.setApplyUserId(userId);
|
||||
apply.setApplyTime(new Date());
|
||||
apply.setCreateBy(username);
|
||||
return creditApplyMapper.insertApply(apply);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int approveCreditApply(Long applyId, String approveRemark, Long approveRoleId)
|
||||
{
|
||||
if (applyId == null)
|
||||
{
|
||||
throw new ServiceException("申请单ID不能为空");
|
||||
}
|
||||
CustomerCreditApply apply = creditApplyMapper.selectApplyByIdForUpdate(applyId);
|
||||
if (apply == null)
|
||||
{
|
||||
throw new ServiceException("额度调整申请不存在");
|
||||
}
|
||||
if (!CustomerCreditApply.STATUS_PENDING.equals(apply.getStatus()))
|
||||
{
|
||||
throw new ServiceException("该申请已处理,不能重复审批");
|
||||
}
|
||||
String username = SecurityUtils.getUsername();
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
|
||||
// 以审批时账户的实际额度重新计算并调整(申请期间额度可能已变动)
|
||||
CustomerCreditFlow cmd = new CustomerCreditFlow();
|
||||
cmd.setCustomerId(apply.getCustomerId());
|
||||
cmd.setCreditType(apply.getCreditType());
|
||||
cmd.setAdjustMode(apply.getAdjustMode());
|
||||
cmd.setChangeAmount(nz(apply.getChangeAmount()));
|
||||
cmd.setReason(apply.getReason());
|
||||
cmd.setRemark(apply.getRemark());
|
||||
cmd.setEffectiveDate(apply.getEffectiveDate());
|
||||
cmd.setExpireDate(apply.getExpireDate());
|
||||
CustomerCreditFlow flow = executeAdjustCredit(cmd, approveRoleId, applyId);
|
||||
|
||||
CustomerCreditApply update = new CustomerCreditApply();
|
||||
update.setApplyId(applyId);
|
||||
update.setAccountId(flow.getAccountId());
|
||||
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)
|
||||
{
|
||||
throw new ServiceException("申请状态已变化,审批失败");
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int rejectCreditApply(Long applyId, String rejectReason)
|
||||
{
|
||||
if (applyId == null)
|
||||
{
|
||||
throw new ServiceException("申请单ID不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(rejectReason))
|
||||
{
|
||||
throw new ServiceException("请填写驳回原因");
|
||||
}
|
||||
String reason = rejectReason.trim();
|
||||
if (reason.length() > 255)
|
||||
{
|
||||
throw new ServiceException("驳回原因不能超过255个字");
|
||||
}
|
||||
CustomerCreditApply apply = creditApplyMapper.selectApplyByIdForUpdate(applyId);
|
||||
if (apply == null)
|
||||
{
|
||||
throw new ServiceException("额度调整申请不存在");
|
||||
}
|
||||
if (!CustomerCreditApply.STATUS_PENDING.equals(apply.getStatus()))
|
||||
{
|
||||
throw new ServiceException("该申请已处理,不能重复审批");
|
||||
}
|
||||
CustomerCreditApply update = new CustomerCreditApply();
|
||||
update.setApplyId(applyId);
|
||||
update.setApproveBy(SecurityUtils.getUsername());
|
||||
update.setApproveUserId(SecurityUtils.getUserId());
|
||||
update.setApproveRemark(reason);
|
||||
int rows = creditApplyMapper.updateRejected(update);
|
||||
if (rows == 0)
|
||||
{
|
||||
throw new ServiceException("申请状态已变化,驳回失败");
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int cancelCreditApply(Long applyId)
|
||||
{
|
||||
if (applyId == null)
|
||||
{
|
||||
throw new ServiceException("申请单ID不能为空");
|
||||
}
|
||||
CustomerCreditApply apply = creditApplyMapper.selectApplyByIdForUpdate(applyId);
|
||||
if (apply == null)
|
||||
{
|
||||
throw new ServiceException("额度调整申请不存在");
|
||||
}
|
||||
if (!CustomerCreditApply.STATUS_PENDING.equals(apply.getStatus()))
|
||||
{
|
||||
throw new ServiceException("仅待审批的申请可以撤销");
|
||||
}
|
||||
int rows = creditApplyMapper.updateCancelled(applyId, SecurityUtils.getUsername());
|
||||
if (rows == 0)
|
||||
{
|
||||
throw new ServiceException("申请状态已变化,撤销失败");
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 额度调整参数/日期/结果合法性校验(申请与审批共用)。
|
||||
* 锁定CNY账户做预演但不写入,返回当前账户;规整后的金额/原因/日期回写到 cmd。
|
||||
*/
|
||||
private CustomerAccount validateAdjustCommand(CustomerCreditFlow cmd, String username)
|
||||
{
|
||||
if (cmd == null || cmd.getCustomerId() == null)
|
||||
{
|
||||
throw new ServiceException("客户ID不能为空");
|
||||
}
|
||||
String creditType = cmd.getCreditType();
|
||||
if (!CustomerCreditFlow.CREDIT_TYPE_CREDIT.equals(creditType)
|
||||
&& !CustomerCreditFlow.CREDIT_TYPE_TEMP.equals(creditType))
|
||||
{
|
||||
throw new ServiceException("额度类型不正确");
|
||||
}
|
||||
String adjustMode = cmd.getAdjustMode();
|
||||
if (!CustomerCreditFlow.MODE_SET.equals(adjustMode)
|
||||
&& !CustomerCreditFlow.MODE_ADD.equals(adjustMode)
|
||||
&& !CustomerCreditFlow.MODE_SUB.equals(adjustMode))
|
||||
{
|
||||
throw new ServiceException("调整方式不正确");
|
||||
}
|
||||
BigDecimal input = nz(cmd.getChangeAmount());
|
||||
if (input.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
throw new ServiceException("调整金额必须大于0");
|
||||
}
|
||||
if (!StringUtils.hasText(cmd.getReason()))
|
||||
{
|
||||
throw new ServiceException("请填写调整原因");
|
||||
}
|
||||
String reason = cmd.getReason().trim();
|
||||
if (reason.length() > 255)
|
||||
{
|
||||
throw new ServiceException("调整原因不能超过255个字");
|
||||
}
|
||||
cmd.setChangeAmount(input);
|
||||
cmd.setReason(reason);
|
||||
boolean isTemp = CustomerCreditFlow.CREDIT_TYPE_TEMP.equals(creditType);
|
||||
|
||||
CustomerAccount account = customerAccountApi.lockCnyAccount(cmd.getCustomerId());
|
||||
customerAccountApi.expireDueCreditsIfNeeded(account);
|
||||
|
||||
BigDecimal before = isTemp ? nz(account.getExtraCredit()) : nz(account.getTotalCredit());
|
||||
BigDecimal after;
|
||||
switch (adjustMode)
|
||||
{
|
||||
case CustomerCreditFlow.MODE_SET:
|
||||
after = input;
|
||||
break;
|
||||
case CustomerCreditFlow.MODE_ADD:
|
||||
after = before.add(input);
|
||||
break;
|
||||
default:
|
||||
after = before.subtract(input);
|
||||
break;
|
||||
}
|
||||
after = after.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
if (after.compareTo(BigDecimal.ZERO) < 0)
|
||||
{
|
||||
throw new ServiceException("调整后额度不能为负数");
|
||||
}
|
||||
// 口径变更(2026-09-23):计费不再变动 used_credit,额度调整无需校验已用额度
|
||||
if (after.compareTo(before) == 0)
|
||||
{
|
||||
throw new ServiceException("额度未发生变化,无需调整");
|
||||
}
|
||||
|
||||
if (!isTemp && cmd.getExpireDate() == null)
|
||||
{
|
||||
// 信用额度必须设置失效日期(到期自动清零);临时额度不填=长期
|
||||
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("失效日期必须晚于生效日期");
|
||||
}
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
/* ================= 额度私有方法 ================= */
|
||||
|
||||
/**
|
||||
* 新建客户时默认初始化CNY账户(委托公共资金服务统一收口)
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int initDefaultCnyAccount(Long customerId)
|
||||
{
|
||||
return customerAccountApi.initDefaultCnyAccount(customerId);
|
||||
}
|
||||
|
||||
/** 临时额度是否已过期:有失效日期且今天已晚于失效日(失效日当天仍有效)。仅只读判断,不落库 */
|
||||
private boolean isTempCreditExpired(CustomerAccount account)
|
||||
{
|
||||
return account != null
|
||||
&& nz(account.getExtraCredit()).compareTo(BigDecimal.ZERO) > 0
|
||||
&& account.getExtraExpireDate() != null
|
||||
&& 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();
|
||||
}
|
||||
|
||||
private Date toSqlDate(LocalDate localDate)
|
||||
{
|
||||
return java.sql.Date.valueOf(localDate);
|
||||
}
|
||||
|
||||
/* ================= 私有方法 ================= */
|
||||
|
||||
/** 保存收款附件到客户附件表 */
|
||||
private void saveReceiptAttachments(CustomerReceipt receipt)
|
||||
{
|
||||
if (CollectionUtils.isEmpty(receipt.getAttachmentList()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
int sort = 0;
|
||||
for (BmsCustomerAttachment attachment : receipt.getAttachmentList())
|
||||
{
|
||||
attachment.setAttachmentId(null);
|
||||
attachment.setCustomerId(receipt.getCustomerId());
|
||||
attachment.setBizId(receipt.getId());
|
||||
attachment.setBizType(BmsCustomerAttachment.BIZ_TYPE_CUSTOMER_RECEIPT);
|
||||
if (!StringUtils.hasText(attachment.getIsLogo()))
|
||||
{
|
||||
attachment.setIsLogo("0");
|
||||
}
|
||||
if (attachment.getSortOrder() == null)
|
||||
{
|
||||
attachment.setSortOrder(sort);
|
||||
}
|
||||
attachment.setDelFlag("0");
|
||||
bmsCustomerMapper.insertBmsCustomerAttachment(attachment);
|
||||
sort++;
|
||||
}
|
||||
}
|
||||
|
||||
/** 生成业务单号:前缀 + 时间戳(毫秒) + 3位随机数 */
|
||||
private String generateNo(String prefix)
|
||||
{
|
||||
return prefix + LocalDateTime.now().format(NO_FORMATTER)
|
||||
+ ThreadLocalRandom.current().nextInt(100, 1000);
|
||||
}
|
||||
|
||||
private BigDecimal nz(BigDecimal value)
|
||||
{
|
||||
return value == null ? BigDecimal.ZERO : value;
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.bms.customer.customermoney.task;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.bms.customer.customermoney.api.ICustomerAccountApi;
|
||||
|
||||
/**
|
||||
* 客户授信(信用额度/临时额度)到期自动失效任务。
|
||||
*
|
||||
* <p>扫描授信已到期(信用或临时额度>0 且失效日期早于今天)的CNY账户,
|
||||
* 逐户独立事务物化失效:额度清零 -> 释放角色额度池 -> 回写客户档案 -> 写 EXPIRE 额度流水。
|
||||
* 失效日当天额度仍有效,故严格按「早于今天」判断。</p>
|
||||
*
|
||||
* <p>本任务纳入 Bms 定时任务菜单(Quartz / sys_job)统一管理:
|
||||
* 调用目标为 {@code customerCreditTask.expireCredits()},cron、启停均在
|
||||
* 「系统监控 - 定时任务」中配置,不再使用 @Scheduled 硬编码。
|
||||
* 除定时调度外,资金接口访问时仍有惰性失效(即查即失效)兜底。</p>
|
||||
*
|
||||
* <p>到期不影响已用额度:已确认账单形成的授信占用仍然有效,客户回款核销后
|
||||
* used_credit 自然下降;可用额度可能短暂为负,新计费开单会被额度校验拦截。</p>
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-17
|
||||
*/
|
||||
@Component("customerCreditTask")
|
||||
public class TempCreditExpireTask
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(TempCreditExpireTask.class);
|
||||
|
||||
@Autowired
|
||||
private ICustomerAccountApi customerAccountApi;
|
||||
|
||||
/**
|
||||
* 授信到期失效(sys_job 调用目标:customerCreditTask.expireCredits())。
|
||||
* 异常直接抛出,由 Quartz 框架记录任务执行失败日志。
|
||||
*/
|
||||
public void expireCredits()
|
||||
{
|
||||
int count = customerAccountApi.expireAllDueCredits();
|
||||
log.info("客户授信到期失效任务执行完成,失效账户数={}", count);
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package com.bms.customer.customermoney.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 客户账号状态查询VO(收货扣件准入控制用)。
|
||||
*
|
||||
* <p>只读轻量,不锁账户也不物化临时额度失效:临时额度已过期时按"视为0"计算可用额度,
|
||||
* 但 DB 保持原状——由定时任务或后续真实资金入口落库。</p>
|
||||
*
|
||||
* <p>欠费定义:availableCredit < 0(可用额度 = 账户余额 + 信用额度 + 有效临时额度)。</p>
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-17
|
||||
*/
|
||||
public class CustomerAccountStatusVO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 综合状态:正常 */
|
||||
public static final String STATUS_NORMAL = "NORMAL";
|
||||
/** 综合状态:停用 */
|
||||
public static final String STATUS_DISABLED = "DISABLED";
|
||||
/** 综合状态:欠费(可用额度不足) */
|
||||
public static final String STATUS_OVERDUE = "OVERDUE";
|
||||
/** 综合状态:停用且欠费 */
|
||||
public static final String STATUS_DISABLED_AND_OVERDUE = "DISABLED_AND_OVERDUE";
|
||||
|
||||
/** 客户ID */
|
||||
private Long customerId;
|
||||
/** 客户编码 */
|
||||
private String customerCode;
|
||||
/** 客户名称 */
|
||||
private String customerName;
|
||||
/** 启用状态(0正常 1停用)—— 来自 ctr_customer.is_enable */
|
||||
private String isEnable;
|
||||
/** 启用状态文案(正常 / 停用) */
|
||||
private String isEnableLabel;
|
||||
|
||||
/** CNY账户余额 */
|
||||
private BigDecimal balance;
|
||||
/** CNY账户冻结余额 */
|
||||
private BigDecimal frozenBalance;
|
||||
/** 授信总额度(CNY) */
|
||||
private BigDecimal totalCredit;
|
||||
/** 授信总额度实际有效值(过期未物化时为0,接口计算得出,不物化DB) */
|
||||
private BigDecimal effectiveTotalCredit;
|
||||
/** 临时额度原值(CNY,DB 中 extra_credit 字段) */
|
||||
private BigDecimal extraCredit;
|
||||
/** 临时额度实际有效值(过期时为0,接口计算得出,不物化DB) */
|
||||
private BigDecimal effectiveExtraCredit;
|
||||
/** 可用额度(账户余额 + 授信总额度 + 有效临时额度;可能为负表示欠费) */
|
||||
private BigDecimal availableCredit;
|
||||
|
||||
/** 是否欠费(可用额度 < 0) */
|
||||
private boolean overdue;
|
||||
/** 是否存在过期未物化的临时额度(仅作提示,不影响判定) */
|
||||
private boolean expiredTempCreditPending;
|
||||
/** 综合状态:NORMAL / DISABLED / OVERDUE / DISABLED_AND_OVERDUE */
|
||||
private String accountStatus;
|
||||
/** 综合状态说明 */
|
||||
private String accountStatusDesc;
|
||||
|
||||
public Long getCustomerId() { return customerId; }
|
||||
public void setCustomerId(Long customerId) { this.customerId = customerId; }
|
||||
public String getCustomerCode() { return customerCode; }
|
||||
public void setCustomerCode(String customerCode) { this.customerCode = customerCode; }
|
||||
public String getCustomerName() { return customerName; }
|
||||
public void setCustomerName(String customerName) { this.customerName = customerName; }
|
||||
public String getIsEnable() { return isEnable; }
|
||||
public void setIsEnable(String isEnable) { this.isEnable = isEnable; }
|
||||
public String getIsEnableLabel() { return isEnableLabel; }
|
||||
public void setIsEnableLabel(String isEnableLabel) { this.isEnableLabel = isEnableLabel; }
|
||||
public BigDecimal getBalance() { return balance; }
|
||||
public void setBalance(BigDecimal balance) { this.balance = balance; }
|
||||
public BigDecimal getFrozenBalance() { return frozenBalance; }
|
||||
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; }
|
||||
public void setEffectiveExtraCredit(BigDecimal effectiveExtraCredit) { this.effectiveExtraCredit = effectiveExtraCredit; }
|
||||
public BigDecimal getAvailableCredit() { return availableCredit; }
|
||||
public void setAvailableCredit(BigDecimal availableCredit) { this.availableCredit = availableCredit; }
|
||||
public boolean isOverdue() { return overdue; }
|
||||
public void setOverdue(boolean overdue) { this.overdue = overdue; }
|
||||
public boolean isExpiredTempCreditPending() { return expiredTempCreditPending; }
|
||||
public void setExpiredTempCreditPending(boolean expiredTempCreditPending) { this.expiredTempCreditPending = expiredTempCreditPending; }
|
||||
public String getAccountStatus() { return accountStatus; }
|
||||
public void setAccountStatus(String accountStatus) { this.accountStatus = accountStatus; }
|
||||
public String getAccountStatusDesc() { return accountStatusDesc; }
|
||||
public void setAccountStatusDesc(String accountStatusDesc) { this.accountStatusDesc = accountStatusDesc; }
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.bms.customer.customermoney.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.bms.common.annotation.Excel;
|
||||
import com.bms.customer.customermoney.domain.CustomerBalanceFlow;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 客户余额流水导出 VO(列与页面余额流水 tab 对齐,枚举导出为中文文案)
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-21
|
||||
*/
|
||||
@Data
|
||||
public class CustomerBalanceFlowExportVO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Excel(name = "流水号", width = 22)
|
||||
private String flowNo;
|
||||
|
||||
@Excel(name = "时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
@Excel(name = "业务类型", width = 12)
|
||||
private String bizTypeLabel;
|
||||
|
||||
@Excel(name = "收支", width = 8)
|
||||
private String directionLabel;
|
||||
|
||||
@Excel(name = "原币金额", width = 16)
|
||||
private String amountText;
|
||||
|
||||
@Excel(name = "汇率", width = 12)
|
||||
private String exchangeRate;
|
||||
|
||||
@Excel(name = "本位币变动(CNY)", width = 18)
|
||||
private BigDecimal baseChangeAmount;
|
||||
|
||||
@Excel(name = "变动前余额", width = 14)
|
||||
private BigDecimal beforeBalance;
|
||||
|
||||
@Excel(name = "变动后余额", width = 14)
|
||||
private BigDecimal afterBalance;
|
||||
|
||||
@Excel(name = "关联单号", width = 22)
|
||||
private String bizNo;
|
||||
|
||||
@Excel(name = "操作人", width = 12)
|
||||
private String createBy;
|
||||
|
||||
@Excel(name = "备注", width = 30)
|
||||
private String remark;
|
||||
|
||||
public static CustomerBalanceFlowExportVO from(CustomerBalanceFlow f)
|
||||
{
|
||||
CustomerBalanceFlowExportVO vo = new CustomerBalanceFlowExportVO();
|
||||
vo.setFlowNo(f.getFlowNo());
|
||||
vo.setCreateTime(f.getCreateTime());
|
||||
vo.setBizTypeLabel(bizTypeLabel(f.getBizType()));
|
||||
boolean income = f.getDirection() != null && f.getDirection() == 1;
|
||||
vo.setDirectionLabel(income ? "收入" : "支出");
|
||||
String amt = f.getAmount() == null ? "" : f.getAmount().stripTrailingZeros().toPlainString();
|
||||
vo.setAmountText((f.getCurrency() == null ? "" : f.getCurrency() + " ") + amt);
|
||||
vo.setExchangeRate(f.getExchangeRate() == null ? null : f.getExchangeRate().stripTrailingZeros().toPlainString());
|
||||
BigDecimal base = f.getBaseAmount() != null ? f.getBaseAmount() : f.getAmount();
|
||||
// 支出导出为负数(配合“收支”列阅读,同时保持 Excel 数值类型)
|
||||
vo.setBaseChangeAmount(base == null ? null : (income ? base : base.negate()));
|
||||
vo.setBeforeBalance(f.getBeforeBalance());
|
||||
vo.setAfterBalance(f.getAfterBalance());
|
||||
vo.setBizNo(f.getBizNo());
|
||||
vo.setCreateBy(f.getCreateBy());
|
||||
vo.setRemark(f.getRemark());
|
||||
return vo;
|
||||
}
|
||||
|
||||
private static String bizTypeLabel(String v)
|
||||
{
|
||||
if ("RECEIPT".equals(v)) return "收款";
|
||||
if ("AR_BILL".equals(v)) return "应收账单";
|
||||
if ("BILL_PAY".equals(v)) return "账单核销";
|
||||
if ("ORDER".equals(v)) return "计费订单";
|
||||
if ("REFUND".equals(v)) return "退款";
|
||||
if ("ADJUST".equals(v)) return "手工调整";
|
||||
if ("FREEZE".equals(v)) return "冻结";
|
||||
if ("UNFREEZE".equals(v)) return "解冻";
|
||||
return v;
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package com.bms.customer.customermoney.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.bms.common.annotation.Excel;
|
||||
import com.bms.customer.customermoney.domain.CustomerCreditFlow;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 客户额度流水导出 VO(列与页面额度流水 tab 对齐,枚举导出为中文文案)
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-21
|
||||
*/
|
||||
@Data
|
||||
public class CustomerCreditFlowExportVO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Excel(name = "流水号", width = 22)
|
||||
private String flowNo;
|
||||
|
||||
@Excel(name = "时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
@Excel(name = "业务单号", width = 22)
|
||||
private String bizNo;
|
||||
|
||||
@Excel(name = "变动类型", width = 12)
|
||||
private String changeTypeLabel;
|
||||
|
||||
@Excel(name = "变动维度", width = 18)
|
||||
private String dimensionLabel;
|
||||
|
||||
@Excel(name = "调整方式", width = 10)
|
||||
private String adjustModeLabel;
|
||||
|
||||
@Excel(name = "方向", width = 8)
|
||||
private String directionLabel;
|
||||
|
||||
@Excel(name = "变动前", width = 12)
|
||||
private BigDecimal beforeAmount;
|
||||
|
||||
@Excel(name = "变动额", width = 12)
|
||||
private BigDecimal changeAmount;
|
||||
|
||||
@Excel(name = "变动后", width = 12)
|
||||
private BigDecimal afterAmount;
|
||||
|
||||
@Excel(name = "已用额度快照", width = 14)
|
||||
private BigDecimal usedCredit;
|
||||
|
||||
@Excel(name = "生效日期", width = 12, dateFormat = "yyyy-MM-dd")
|
||||
private Date effectiveDate;
|
||||
|
||||
@Excel(name = "失效日期", width = 12)
|
||||
private String expireDateText;
|
||||
|
||||
@Excel(name = "调整原因", width = 30)
|
||||
private String reason;
|
||||
|
||||
@Excel(name = "操作人", width = 12)
|
||||
private String createBy;
|
||||
|
||||
@Excel(name = "备注", width = 30)
|
||||
private String remark;
|
||||
|
||||
public static CustomerCreditFlowExportVO from(CustomerCreditFlow f)
|
||||
{
|
||||
CustomerCreditFlowExportVO vo = new CustomerCreditFlowExportVO();
|
||||
vo.setFlowNo(f.getFlowNo());
|
||||
vo.setCreateTime(f.getCreateTime());
|
||||
vo.setBizNo(f.getBizNo());
|
||||
vo.setChangeTypeLabel(changeTypeLabel(f.getChangeType()));
|
||||
boolean usageFlow = "OCCUPY".equals(f.getChangeType()) || "RELEASE".equals(f.getChangeType());
|
||||
vo.setDimensionLabel(usageFlow ? "授信占用(已用额度)" : "授信额度");
|
||||
vo.setAdjustModeLabel(adjustModeLabel(f.getAdjustMode()));
|
||||
Integer dir = f.getDirection();
|
||||
vo.setDirectionLabel(dir == null ? "-" : (dir == 1 ? "增加" : "减少"));
|
||||
vo.setBeforeAmount(f.getBeforeAmount());
|
||||
// 减少导出为负数(配合“方向”列阅读,同时保持 Excel 数值类型)
|
||||
BigDecimal change = f.getChangeAmount();
|
||||
if (change != null && dir != null && dir == -1)
|
||||
{
|
||||
change = change.negate();
|
||||
}
|
||||
vo.setChangeAmount(change);
|
||||
vo.setAfterAmount(f.getAfterAmount());
|
||||
// 占用/释放流水的已用额度快照在页面上不展示
|
||||
vo.setUsedCredit(usageFlow ? null : f.getUsedCredit());
|
||||
vo.setEffectiveDate(f.getEffectiveDate());
|
||||
vo.setExpireDateText(f.getExpireDate() == null ? "长期" : new java.text.SimpleDateFormat("yyyy-MM-dd").format(f.getExpireDate()));
|
||||
vo.setReason(f.getReason());
|
||||
vo.setCreateBy(f.getCreateBy());
|
||||
vo.setRemark(f.getRemark());
|
||||
return vo;
|
||||
}
|
||||
|
||||
private static String changeTypeLabel(String v)
|
||||
{
|
||||
if ("INIT".equals(v)) return "初始化";
|
||||
if ("ADJUST".equals(v)) return "手工调整";
|
||||
if ("EXPIRE".equals(v)) return "到期失效";
|
||||
if ("OCCUPY".equals(v)) return "授信占用";
|
||||
if ("RELEASE".equals(v)) return "授信释放";
|
||||
return v;
|
||||
}
|
||||
|
||||
private static String adjustModeLabel(String v)
|
||||
{
|
||||
if ("SET".equals(v)) return "设置为";
|
||||
if ("ADD".equals(v)) return "增加";
|
||||
if ("SUB".equals(v)) return "减少";
|
||||
return v;
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package com.bms.customer.customermoney.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.bms.common.annotation.Excel;
|
||||
import com.bms.customer.customermoney.domain.CustomerReceipt;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 客户收款记录导出 VO(列与页面收款记录 tab 对齐,枚举导出为中文文案)
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-21
|
||||
*/
|
||||
@Data
|
||||
public class CustomerReceiptExportVO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Excel(name = "收款单号", width = 22)
|
||||
private String receiptNo;
|
||||
|
||||
@Excel(name = "收款日期", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date receivedAt;
|
||||
|
||||
@Excel(name = "收款类型", width = 12)
|
||||
private String receiptTypeLabel;
|
||||
|
||||
@Excel(name = "币种", width = 8)
|
||||
private String currency;
|
||||
|
||||
@Excel(name = "原币金额", width = 14)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Excel(name = "汇率", width = 12)
|
||||
private String exchangeRate;
|
||||
|
||||
@Excel(name = "汇率来源", width = 10)
|
||||
private String rateSourceLabel;
|
||||
|
||||
@Excel(name = "本位币金额(CNY)", width = 16)
|
||||
private BigDecimal baseAmount;
|
||||
|
||||
@Excel(name = "收款方式", width = 12)
|
||||
private String payMethodLabel;
|
||||
|
||||
@Excel(name = "客户银行账号", width = 24)
|
||||
private String bankAccountNo;
|
||||
|
||||
@Excel(name = "银行/平台流水号", width = 22)
|
||||
private String bankTransactionNo;
|
||||
|
||||
@Excel(name = "状态", width = 10)
|
||||
private String statusLabel;
|
||||
|
||||
@Excel(name = "登记人", width = 12)
|
||||
private String createBy;
|
||||
|
||||
@Excel(name = "备注", width = 30)
|
||||
private String remark;
|
||||
|
||||
public static CustomerReceiptExportVO from(CustomerReceipt r)
|
||||
{
|
||||
CustomerReceiptExportVO vo = new CustomerReceiptExportVO();
|
||||
vo.setReceiptNo(r.getReceiptNo());
|
||||
vo.setReceivedAt(r.getReceivedAt());
|
||||
vo.setReceiptTypeLabel(receiptTypeLabel(r.getReceiptType()));
|
||||
vo.setCurrency(r.getCurrency());
|
||||
vo.setAmount(r.getAmount());
|
||||
vo.setExchangeRate(r.getExchangeRate() == null ? null : r.getExchangeRate().stripTrailingZeros().toPlainString());
|
||||
vo.setRateSourceLabel(rateSourceLabel(r.getRateSource()));
|
||||
vo.setBaseAmount(r.getBaseAmount());
|
||||
vo.setPayMethodLabel(payMethodLabel(r.getPayMethod()));
|
||||
vo.setBankAccountNo(r.getBankAccountNo());
|
||||
vo.setBankTransactionNo(r.getBankTransactionNo());
|
||||
vo.setStatusLabel(statusLabel(r.getStatus()));
|
||||
vo.setCreateBy(r.getCreateBy());
|
||||
vo.setRemark(r.getRemark());
|
||||
return vo;
|
||||
}
|
||||
|
||||
private static String receiptTypeLabel(String v)
|
||||
{
|
||||
if ("PREPAY".equals(v)) return "预充值";
|
||||
if ("REPAY".equals(v)) return "还款核销";
|
||||
if ("MIXED".equals(v)) return "混合";
|
||||
return v;
|
||||
}
|
||||
|
||||
private static String statusLabel(String v)
|
||||
{
|
||||
if ("PENDING".equals(v)) return "待确认";
|
||||
if ("CONFIRMED".equals(v)) return "已确认";
|
||||
if ("CANCELLED".equals(v)) return "已取消";
|
||||
return v;
|
||||
}
|
||||
|
||||
private static String payMethodLabel(String v)
|
||||
{
|
||||
if ("BANK_TRANSFER".equals(v)) return "银行转账";
|
||||
if ("ALIPAY".equals(v)) return "支付宝";
|
||||
if ("WECHAT".equals(v)) return "微信";
|
||||
if ("CASH".equals(v)) return "现金";
|
||||
if ("OTHER".equals(v)) return "其他";
|
||||
return v;
|
||||
}
|
||||
|
||||
private static String rateSourceLabel(String v)
|
||||
{
|
||||
if ("RATE_TABLE".equals(v)) return "汇率表";
|
||||
if ("MANUAL".equals(v)) return "手工修改";
|
||||
if ("SYSTEM".equals(v)) return "系统默认";
|
||||
return v;
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.bms.customer.customermoney.vo;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 角色额度池视图(角色管理/审批选池用)
|
||||
*
|
||||
* @author bms
|
||||
* @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