feat: 入账逻辑

pull/1121/head
梁宇奇 2025-04-10 14:22:44 +08:00
parent 5b8127d109
commit 8ed8d0fc51
10 changed files with 235 additions and 61 deletions

View File

@ -202,6 +202,8 @@ public class Constants
*/
public static final Integer ORDER_NUM_1 = 1;
public static final String VERSION_LOCK_ERROR_COMMON_MSG = "系统繁忙,请稍后再试";

View File

@ -1,5 +1,6 @@
package com.ruoyi.xkt.domain;
import com.baomidou.mybatisplus.annotation.Version;
import com.ruoyi.common.core.domain.SimpleEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -54,4 +55,9 @@ public class ExternalAccountTransDetail extends SimpleEntity {
*
*/
private String remark;
/**
*
*/
@Version
private Long version;
}

View File

@ -1,5 +1,6 @@
package com.ruoyi.xkt.domain;
import com.baomidou.mybatisplus.annotation.Version;
import com.ruoyi.common.core.domain.SimpleEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -58,4 +59,9 @@ public class InternalAccountTransDetail extends SimpleEntity {
*
*/
private String remark;
/**
*
*/
@Version
private Long version;
}

View File

@ -73,4 +73,8 @@ public class ExternalAccountTransDetailDTO {
*
*/
private Date updateTime;
/**
*
*/
private Long version;
}

View File

@ -77,4 +77,8 @@ public class InternalAccountTransDetailDTO {
*
*/
private Date updateTime;
/**
*
*/
private Long version;
}

View File

@ -3,6 +3,7 @@ package com.ruoyi.xkt.service;
import com.ruoyi.xkt.domain.ExternalAccount;
import com.ruoyi.xkt.dto.finance.TransInfo;
import com.ruoyi.xkt.enums.EEntryStatus;
import com.ruoyi.xkt.enums.EFinBillType;
import com.ruoyi.xkt.enums.ELoanDirection;
/**
@ -30,4 +31,12 @@ public interface IExternalAccountService {
int addTransDetail(Long externalAccountId, TransInfo transInfo, ELoanDirection loanDirection,
EEntryStatus entryStatus);
/**
*
*
* @param srcBillId
* @param srcBillType
*/
void entryTransDetail(Long srcBillId, EFinBillType srcBillType);
}

View File

@ -3,45 +3,41 @@ package com.ruoyi.xkt.service;
import com.ruoyi.xkt.domain.InternalAccount;
import com.ruoyi.xkt.dto.finance.TransInfo;
import com.ruoyi.xkt.enums.EEntryStatus;
import com.ruoyi.xkt.enums.EFinBillType;
import com.ruoyi.xkt.enums.ELoanDirection;
import java.util.Collection;
import java.util.List;
/**
* @author liangyq
* @date 2025-04-08 21:14
*/
public interface IInternalAccountService {
/**
*
* ID
*
* @param id
* @return
*/
InternalAccount getWithLock(Long id);
/**
*
*
* @param ids
* @return
*/
List<InternalAccount> listWithLock(Collection<Long> ids);
InternalAccount getById(Long id);
/**
*
*
*
*
* @param internalAccount
* @param internalAccountId
* @param transInfo
* @param loanDirection
* @param entryStatus
* @return
*/
int addTransDetail(InternalAccount internalAccount, TransInfo transInfo, ELoanDirection loanDirection,
int addTransDetail(Long internalAccountId, TransInfo transInfo, ELoanDirection loanDirection,
EEntryStatus entryStatus);
/**
*
*
*
* @param srcBillId
* @param srcBillType
*/
void entryTransDetail(Long srcBillId, EFinBillType srcBillType);
}

View File

@ -1,7 +1,10 @@
package com.ruoyi.xkt.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.NumberUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.SimpleEntity;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.bean.BeanValidators;
import com.ruoyi.xkt.domain.ExternalAccount;
@ -9,21 +12,27 @@ import com.ruoyi.xkt.domain.ExternalAccountTransDetail;
import com.ruoyi.xkt.dto.finance.TransInfo;
import com.ruoyi.xkt.enums.EAccountStatus;
import com.ruoyi.xkt.enums.EEntryStatus;
import com.ruoyi.xkt.enums.EFinBillType;
import com.ruoyi.xkt.enums.ELoanDirection;
import com.ruoyi.xkt.mapper.ExternalAccountMapper;
import com.ruoyi.xkt.mapper.ExternalAccountTransDetailMapper;
import com.ruoyi.xkt.service.IExternalAccountService;
import io.jsonwebtoken.lang.Assert;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author liangyq
* @date 2025-04-08 21:14
*/
@Slf4j
@Service
public class ExternalAccountServiceImpl implements IExternalAccountService {
@ -42,13 +51,9 @@ public class ExternalAccountServiceImpl implements IExternalAccountService {
@Override
public int addTransDetail(Long externalAccountId, TransInfo transInfo, ELoanDirection loanDirection,
EEntryStatus entryStatus) {
ExternalAccount account = getById(externalAccountId);
if (!BeanValidators.exists(account)) {
throw new ServiceException("外部账户[" + externalAccountId + "]不存在");
}
if (!EAccountStatus.NORMAL.getValue().equals(account.getAccountStatus())) {
throw new ServiceException("外部账户[" + externalAccountId + "]已冻结");
}
Assert.notNull(externalAccountId);
ExternalAccount account = externalAccountMapper.selectById(externalAccountId);
checkAccountStatus(account);
ExternalAccountTransDetail transDetail = new ExternalAccountTransDetail();
transDetail.setExternalAccountId(externalAccountId);
transDetail.setSrcBillId(transInfo.getSrcBillId());
@ -62,8 +67,62 @@ public class ExternalAccountServiceImpl implements IExternalAccountService {
transDetail.setHandlerId(transInfo.getHandlerId());
transDetail.setEntryStatus(entryStatus.getValue());
transDetail.setRemark(transInfo.getRemark());
transDetail.setVersion(0L);
transDetail.setDelFlag(Constants.UNDELETED);
return externalAccountTransDetailMapper.insert(transDetail);
}
@Transactional
@Override
public void entryTransDetail(Long srcBillId, EFinBillType srcBillType) {
Assert.notNull(srcBillId);
Assert.notNull(srcBillType);
List<ExternalAccountTransDetail> transDetails = externalAccountTransDetailMapper.selectList(Wrappers
.lambdaQuery(ExternalAccountTransDetail.class)
.eq(ExternalAccountTransDetail::getSrcBillId, srcBillId)
.eq(ExternalAccountTransDetail::getSrcBillType, srcBillType.getValue())
.eq(SimpleEntity::getDelFlag, Constants.UNDELETED));
if (CollUtil.isEmpty(transDetails)) {
log.warn("单据没有关联的交易明细:{}{}", srcBillType.getLabel(), srcBillId);
return;
}
List<Long> accountIds = transDetails.stream().map(ExternalAccountTransDetail::getExternalAccountId).distinct()
.collect(Collectors.toList());
Assert.notEmpty(accountIds);
Map<Long, ExternalAccount> accountMap = externalAccountMapper.selectByIds(accountIds)
.stream()
.collect(Collectors.toMap(SimpleEntity::getId, o -> o));
//开始入账
for (ExternalAccountTransDetail transDetail : transDetails) {
ExternalAccount account = accountMap.get(transDetail.getExternalAccountId());
checkAccountStatus(account);
if (EEntryStatus.WAITING.getValue().equals(transDetail.getEntryStatus())) {
//明细未入账
transDetail.setEntryStatus(EEntryStatus.FINISH.getValue());
//更新交易明细
int r = externalAccountTransDetailMapper.updateById(transDetail);
if (r == 0) {
throw new ServiceException(Constants.VERSION_LOCK_ERROR_COMMON_MSG);
}
} else {
log.warn("单据重复调用入账方法:{}{}", srcBillType.getLabel(), srcBillId);
}
}
}
/**
*
*
* @param externalAccount
*/
private void checkAccountStatus(ExternalAccount externalAccount) {
Assert.notNull(externalAccount, "账户不存在");
if (!BeanValidators.exists(externalAccount)) {
throw new ServiceException("账户[" + externalAccount.getId() + "]不存在");
}
if (!EAccountStatus.NORMAL.getValue().equals(externalAccount.getAccountStatus())) {
throw new ServiceException("账户[" + externalAccount.getId() + "]已冻结");
}
}
}

View File

@ -4,7 +4,6 @@ import cn.hutool.core.util.IdUtil;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.xkt.domain.FinanceBill;
import com.ruoyi.xkt.domain.FinanceBillDetail;
import com.ruoyi.xkt.domain.InternalAccount;
import com.ruoyi.xkt.domain.StoreOrderDetail;
import com.ruoyi.xkt.dto.finance.FinanceBillInfo;
import com.ruoyi.xkt.dto.finance.TransInfo;
@ -48,7 +47,6 @@ public class FinanceBillServiceImpl implements IFinanceBillService {
Assert.notNull(payId);
Assert.notNull(payChannel);
//获取平台内部账户并加锁
InternalAccount ia = internalAccountService.getWithLock(Constants.PLATFORM_INTERNAL_ACCOUNT_ID);
FinanceBill bill = new FinanceBill();
bill.setBillNo(generateBillNo(EFinBillType.COLLECTION));
bill.setBillType(EFinBillType.COLLECTION.getValue());
@ -61,7 +59,7 @@ public class FinanceBillServiceImpl implements IFinanceBillService {
//支付渠道+回调ID构成业务唯一键防止重复创建收款单
String businessUniqueKey = payChannel.getPrefix() + payId;
bill.setBusinessUniqueKey(businessUniqueKey);
bill.setInputInternalAccountId(ia.getId());
bill.setInputInternalAccountId(Constants.PLATFORM_INTERNAL_ACCOUNT_ID);
bill.setInputExternalAccountId(payChannel.getPlatformExternalAccountId());
bill.setBusinessAmount(orderInfo.getOrder().getTotalAmount());
bill.setTransAmount(orderInfo.getOrder().getRealTotalAmount());
@ -89,7 +87,8 @@ public class FinanceBillServiceImpl implements IFinanceBillService {
.remark("订单支付完成")
.build();
//内部账户
internalAccountService.addTransDetail(ia, transInfo, ELoanDirection.DEBIT, EEntryStatus.FINISH);
internalAccountService.addTransDetail(Constants.PLATFORM_INTERNAL_ACCOUNT_ID, transInfo,
ELoanDirection.DEBIT, EEntryStatus.FINISH);
//外部账户
externalAccountService.addTransDetail(payChannel.getPlatformExternalAccountId(), transInfo,
ELoanDirection.DEBIT, EEntryStatus.FINISH);

View File

@ -1,31 +1,36 @@
package com.ruoyi.xkt.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.NumberUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.SimpleEntity;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.bean.BeanValidators;
import com.ruoyi.xkt.domain.InternalAccount;
import com.ruoyi.xkt.domain.InternalAccountTransDetail;
import com.ruoyi.xkt.dto.finance.TransInfo;
import com.ruoyi.xkt.enums.EAccountStatus;
import com.ruoyi.xkt.enums.EEntryExecuted;
import com.ruoyi.xkt.enums.EEntryStatus;
import com.ruoyi.xkt.enums.ELoanDirection;
import com.ruoyi.xkt.enums.*;
import com.ruoyi.xkt.mapper.InternalAccountMapper;
import com.ruoyi.xkt.mapper.InternalAccountTransDetailMapper;
import com.ruoyi.xkt.service.IInternalAccountService;
import io.jsonwebtoken.lang.Assert;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author liangyq
* @date 2025-04-08 21:14
*/
@Slf4j
@Service
public class InternalAccountServiceImpl implements IInternalAccountService {
@ -34,29 +39,19 @@ public class InternalAccountServiceImpl implements IInternalAccountService {
@Autowired
private InternalAccountTransDetailMapper internalAccountTransDetailMapper;
@Transactional
@Override
public InternalAccount getWithLock(Long id) {
return internalAccountMapper.getForUpdate(id);
public InternalAccount getById(Long id) {
Assert.notNull(id);
return internalAccountMapper.selectById(id);
}
@Transactional
@Override
public List<InternalAccount> listWithLock(Collection<Long> ids) {
return internalAccountMapper.listForUpdate(ids);
}
@Transactional
@Override
public int addTransDetail(InternalAccount internalAccount, TransInfo transInfo, ELoanDirection loanDirection,
public int addTransDetail(Long internalAccountId, TransInfo transInfo, ELoanDirection loanDirection,
EEntryStatus entryStatus) {
Assert.notNull(internalAccount, "内部账户不存在");
if (!BeanValidators.exists(internalAccount)) {
throw new ServiceException("内部账户[" + internalAccount.getId() + "]不存在");
}
if (!EAccountStatus.NORMAL.getValue().equals(internalAccount.getAccountStatus())) {
throw new ServiceException("内部账户[" + internalAccount.getId() + "]已冻结");
}
Assert.notNull(internalAccountId);
InternalAccount internalAccount = internalAccountMapper.getForUpdate(internalAccountId);
checkAccountStatus(internalAccount);
InternalAccountTransDetail transDetail = new InternalAccountTransDetail();
transDetail.setInternalAccountId(internalAccount.getId());
transDetail.setSrcBillId(transInfo.getSrcBillId());
@ -73,11 +68,13 @@ public class InternalAccountServiceImpl implements IInternalAccountService {
transDetail.setEntryExecuted(EEntryStatus.FINISH == entryStatus ?
EEntryExecuted.FINISH.getValue() : EEntryExecuted.WAITING.getValue());
transDetail.setRemark(transInfo.getRemark());
transDetail.setVersion(0L);
transDetail.setDelFlag(Constants.UNDELETED);
switch (loanDirection) {
case DEBIT:
//借
if (EEntryExecuted.FINISH.getValue().equals(transDetail.getEntryExecuted())) {
//已执行
if (EEntryStatus.FINISH.getValue().equals(transDetail.getEntryStatus())) {
//已入账&已执行
//余额 = 余额 + 交易金额
internalAccount.setBalance(internalAccount.getBalance()
.add(transDetail.getTransAmount()));
@ -86,7 +83,7 @@ public class InternalAccountServiceImpl implements IInternalAccountService {
.add(transDetail.getTransAmount()));
//支付中金额不变
} else {
//未执行
//未入账
//余额不变
//可用余额不变
//支付中金额不变
@ -94,8 +91,8 @@ public class InternalAccountServiceImpl implements IInternalAccountService {
break;
case CREDIT:
//贷
if (EEntryExecuted.FINISH.getValue().equals(transDetail.getEntryExecuted())) {
//已执行
if (EEntryStatus.FINISH.getValue().equals(transDetail.getEntryStatus())) {
//已入账&已执行
//余额 = 余额 - 交易金额
internalAccount.setBalance(internalAccount.getBalance()
.subtract(transDetail.getTransAmount()));
@ -104,7 +101,7 @@ public class InternalAccountServiceImpl implements IInternalAccountService {
.subtract(transDetail.getTransAmount()));
//支付中金额不变
} else {
//未执行
//未入账
//余额不变
//可用余额 = 可用余额 - 交易金额
internalAccount.setUsableBalance(internalAccount.getUsableBalance()
@ -118,6 +115,102 @@ public class InternalAccountServiceImpl implements IInternalAccountService {
throw new ServiceException("未知借贷方向");
}
//检查账户余额,不允许出现负数
checkAccountAmount(internalAccount);
//更新账户余额
internalAccountMapper.updateById(internalAccount);
//交易明细
return internalAccountTransDetailMapper.insert(transDetail);
}
@Transactional
@Override
public void entryTransDetail(Long srcBillId, EFinBillType srcBillType) {
Assert.notNull(srcBillId);
Assert.notNull(srcBillType);
List<InternalAccountTransDetail> transDetails = internalAccountTransDetailMapper.selectList(Wrappers
.lambdaQuery(InternalAccountTransDetail.class)
.eq(InternalAccountTransDetail::getSrcBillId, srcBillId)
.eq(InternalAccountTransDetail::getSrcBillType, srcBillType.getValue())
.eq(SimpleEntity::getDelFlag, Constants.UNDELETED));
if (CollUtil.isEmpty(transDetails)) {
log.warn("单据没有关联的交易明细:{}{}", srcBillType.getLabel(), srcBillId);
return;
}
List<Long> accountIds = transDetails.stream().map(InternalAccountTransDetail::getInternalAccountId).distinct()
.collect(Collectors.toList());
//获取账户并加锁
List<InternalAccount> accounts = internalAccountMapper.listForUpdate(accountIds);
Map<Long, InternalAccount> accountMap = accounts.stream().collect(Collectors.toMap(SimpleEntity::getId, o -> o));
//开始入账
for (InternalAccountTransDetail transDetail : transDetails) {
InternalAccount internalAccount = accountMap.get(transDetail.getInternalAccountId());
checkAccountStatus(internalAccount);
if (EEntryStatus.WAITING.getValue().equals(transDetail.getEntryStatus())) {
//明细未入账
transDetail.setEntryStatus(EEntryStatus.FINISH.getValue());
//不考虑热点账户问题,已入账 => 已执行
transDetail.setEntryExecuted(EEntryExecuted.FINISH.getValue());
switch (Objects.requireNonNull(ELoanDirection.of(transDetail.getLoanDirection()))) {
case DEBIT:
//借
//余额 = 余额 + 交易金额
internalAccount.setBalance(internalAccount.getBalance()
.add(transDetail.getTransAmount()));
//可用余额 = 可用余额 + 交易金额
internalAccount.setUsableBalance(internalAccount.getUsableBalance()
.add(transDetail.getTransAmount()));
//支付中金额不变
break;
case CREDIT:
//贷
//余额 = 余额 - 交易金额
internalAccount.setBalance(internalAccount.getBalance()
.subtract(transDetail.getTransAmount()));
//可用余额不变
//支付中金额 = 支付中金额 - 交易金额
internalAccount.setPaymentAmount(internalAccount.getPaymentAmount()
.subtract(transDetail.getTransAmount()));
break;
default:
throw new ServiceException("未知借贷方向");
}
//检查账户余额,不允许出现负数
checkAccountAmount(internalAccount);
//更新账户余额
internalAccountMapper.updateById(internalAccount);
//更新交易明细
int r = internalAccountTransDetailMapper.updateById(transDetail);
if (r == 0) {
throw new ServiceException(Constants.VERSION_LOCK_ERROR_COMMON_MSG);
}
} else {
log.warn("单据重复调用入账方法:{}{}", srcBillType.getLabel(), srcBillId);
}
}
}
/**
*
*
* @param internalAccount
*/
private void checkAccountStatus(InternalAccount internalAccount) {
Assert.notNull(internalAccount, "账户不存在");
if (!BeanValidators.exists(internalAccount)) {
throw new ServiceException("账户[" + internalAccount.getId() + "]已删除");
}
if (!EAccountStatus.NORMAL.getValue().equals(internalAccount.getAccountStatus())) {
throw new ServiceException("账户[" + internalAccount.getId() + "]已冻结");
}
}
/**
*
*
* @param internalAccount
*/
private void checkAccountAmount(InternalAccount internalAccount) {
if (less0(internalAccount.getBalance())) {
throw new ServiceException("账户[" + internalAccount.getId() + "]余额不足");
}
@ -127,10 +220,6 @@ public class InternalAccountServiceImpl implements IInternalAccountService {
if (less0(internalAccount.getPaymentAmount())) {
throw new ServiceException("账户[" + internalAccount.getId() + "]支付中金额异常");
}
//更新账户余额
internalAccountMapper.updateById(internalAccount);
//交易明细
return internalAccountTransDetailMapper.insert(transDetail);
}
/**