feat: 记账逻辑

pull/1121/head
梁宇奇 2025-04-10 01:46:36 +08:00
parent b41e6c5297
commit 3b2d3cdacb
13 changed files with 492 additions and 7 deletions

View File

@ -192,6 +192,10 @@ public class Constants
* ID
*/
public static final Long PLATFORM_INTERNAL_ACCOUNT_ID = 1L;
/**
* ID-
*/
public static final Long PLATFORM_ALIPAY_EXTERNAL_ACCOUNT_ID = 1L;
/**
* 1

View File

@ -0,0 +1,62 @@
package com.ruoyi.xkt.dto.finance;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Date;
/**
* @author liangyq
* @date 2025-04-09 23:47
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TransInfo {
// /**
// * 账户ID
// */
// private Long accountId;
/**
* ID
*/
private Long srcBillId;
/**
*
*
* @see com.ruoyi.xkt.enums.EFinBillType
*/
private Integer srcBillType;
// /**
// * 借贷方向
// *
// * @see com.ruoyi.xkt.enums.ELoanDirection
// */
// private Integer loanDirection;
/**
*
*/
private BigDecimal transAmount;
/**
*
*/
private Date transTime;
/**
* ID
*/
private Long handlerId;
// /**
// * 入账状态
// *
// * @see com.ruoyi.xkt.enums.EEntryStatus
// */
// private Integer entryStatus;
/**
*
*/
private String remark;
}

View File

@ -0,0 +1,28 @@
package com.ruoyi.xkt.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author liangyq
* @date 2025-04-02 23:42
*/
@Getter
@AllArgsConstructor
public enum EAccountStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结");
private final Integer value;
private final String label;
public static EAccountStatus of(Integer value) {
for (EAccountStatus e : EAccountStatus.values()) {
if (e.getValue().equals(value)) {
return e;
}
}
return null;
}
}

View File

@ -0,0 +1,28 @@
package com.ruoyi.xkt.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author liangyq
* @date 2025-04-02 23:42
*/
@Getter
@AllArgsConstructor
public enum EEntryExecuted {
FINISH(1, "已执行"),
WAITING(2, "未执行");
private final Integer value;
private final String label;
public static EEntryExecuted of(Integer value) {
for (EEntryExecuted e : EEntryExecuted.values()) {
if (e.getValue().equals(value)) {
return e;
}
}
return null;
}
}

View File

@ -0,0 +1,28 @@
package com.ruoyi.xkt.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author liangyq
* @date 2025-04-02 23:42
*/
@Getter
@AllArgsConstructor
public enum EEntryStatus {
FINISH(1, "已入账"),
WAITING(2, "待入账");
private final Integer value;
private final String label;
public static EEntryStatus of(Integer value) {
for (EEntryStatus e : EEntryStatus.values()) {
if (e.getValue().equals(value)) {
return e;
}
}
return null;
}
}

View File

@ -0,0 +1,29 @@
package com.ruoyi.xkt.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author liangyq
* @date 2025-04-02 23:42
*/
@Getter
@AllArgsConstructor
public enum ELoanDirection {
DEBIT(1, "借", "D"),
CREDIT(2, "贷", "C");
private final Integer value;
private final String label;
private final String code;
public static ELoanDirection of(Integer value) {
for (ELoanDirection e : ELoanDirection.values()) {
if (e.getValue().equals(value)) {
return e;
}
}
return null;
}
}

View File

@ -1,5 +1,6 @@
package com.ruoyi.xkt.enums;
import com.ruoyi.common.constant.Constants;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -11,10 +12,12 @@ import lombok.Getter;
@AllArgsConstructor
public enum EPayChannel {
ALI_PAY(1, "支付宝");
ALI_PAY(1, "支付宝", "ALIPAY_", Constants.PLATFORM_ALIPAY_EXTERNAL_ACCOUNT_ID);
private final Integer value;
private final String label;
private final String prefix;
private final Long platformExternalAccountId;
public static EPayChannel of(Integer value) {
for (EPayChannel e : EPayChannel.values()) {

View File

@ -0,0 +1,33 @@
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.ELoanDirection;
/**
* @author liangyq
* @date 2025-04-08 21:14
*/
public interface IExternalAccountService {
/**
* ID
*
* @param id
* @return
*/
ExternalAccount getById(Long id);
/**
*
*
* @param externalAccountId
* @param transInfo
* @param loanDirection
* @param entryStatus
* @return
*/
int addTransDetail(Long externalAccountId, TransInfo transInfo, ELoanDirection loanDirection,
EEntryStatus entryStatus);
}

View File

@ -2,6 +2,7 @@ package com.ruoyi.xkt.service;
import com.ruoyi.xkt.dto.finance.FinanceBillInfo;
import com.ruoyi.xkt.dto.order.StoreOrderInfo;
import com.ruoyi.xkt.enums.EPayChannel;
/**
* @author liangyq
@ -12,8 +13,9 @@ public interface IFinanceBillService {
*
*
* @param orderInfo
* @param srcId
* @param payId
* @param payChannel
* @return
*/
FinanceBillInfo createCollectionBillAfterOrderPaid(StoreOrderInfo orderInfo, String srcId);
FinanceBillInfo createCollectionBillAfterOrderPaid(StoreOrderInfo orderInfo, Long payId, EPayChannel payChannel);
}

View File

@ -1,6 +1,9 @@
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.ELoanDirection;
import java.util.Collection;
import java.util.List;
@ -27,4 +30,18 @@ public interface IInternalAccountService {
*/
List<InternalAccount> listWithLock(Collection<Long> ids);
/**
*
*
*
*
* @param internalAccount
* @param transInfo
* @param loanDirection
* @param entryStatus
* @return
*/
int addTransDetail(InternalAccount internalAccount, TransInfo transInfo, ELoanDirection loanDirection,
EEntryStatus entryStatus);
}

View File

@ -0,0 +1,69 @@
package com.ruoyi.xkt.service.impl;
import cn.hutool.core.util.NumberUtil;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.bean.BeanValidators;
import com.ruoyi.xkt.domain.ExternalAccount;
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.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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
/**
* @author liangyq
* @date 2025-04-08 21:14
*/
@Service
public class ExternalAccountServiceImpl implements IExternalAccountService {
@Autowired
private ExternalAccountMapper externalAccountMapper;
@Autowired
private ExternalAccountTransDetailMapper externalAccountTransDetailMapper;
@Override
public ExternalAccount getById(Long id) {
Assert.notNull(id);
return externalAccountMapper.selectById(id);
}
@Transactional
@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 + "]已冻结");
}
ExternalAccountTransDetail transDetail = new ExternalAccountTransDetail();
transDetail.setExternalAccountId(externalAccountId);
transDetail.setSrcBillId(transInfo.getSrcBillId());
transDetail.setSrcBillType(transInfo.getSrcBillType());
transDetail.setLoanDirection(loanDirection.getValue());
if (NumberUtil.isLess(transInfo.getTransAmount(), BigDecimal.ZERO)) {
throw new ServiceException("交易金额异常");
}
transDetail.setTransAmount(transInfo.getTransAmount());
transDetail.setTransTime(transInfo.getTransTime());
transDetail.setHandlerId(transInfo.getHandlerId());
transDetail.setEntryStatus(entryStatus.getValue());
transDetail.setRemark(transInfo.getRemark());
transDetail.setDelFlag(Constants.UNDELETED);
return externalAccountTransDetailMapper.insert(transDetail);
}
}

View File

@ -1,17 +1,29 @@
package com.ruoyi.xkt.service.impl;
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;
import com.ruoyi.xkt.dto.order.StoreOrderInfo;
import com.ruoyi.xkt.enums.*;
import com.ruoyi.xkt.mapper.FinanceBillDetailMapper;
import com.ruoyi.xkt.mapper.FinanceBillMapper;
import com.ruoyi.xkt.service.IExternalAccountService;
import com.ruoyi.xkt.service.IFinanceBillService;
import com.ruoyi.xkt.service.IInternalAccountService;
import io.jsonwebtoken.lang.Assert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @author liangyq
* @date 2025-04-08 21:14
@ -25,12 +37,74 @@ public class FinanceBillServiceImpl implements IFinanceBillService {
private FinanceBillDetailMapper financeBillDetailMapper;
@Autowired
private IInternalAccountService internalAccountService;
@Autowired
private IExternalAccountService externalAccountService;
@Transactional
@Override
public FinanceBillInfo createCollectionBillAfterOrderPaid(StoreOrderInfo orderInfo, String srcId) {
//获取平台账户并加锁
InternalAccount ca = internalAccountService.getWithLock(Constants.PLATFORM_INTERNAL_ACCOUNT_ID);
return null;
public FinanceBillInfo createCollectionBillAfterOrderPaid(StoreOrderInfo orderInfo, Long payId,
EPayChannel payChannel) {
Assert.notNull(orderInfo);
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());
//直接标记成功
bill.setBillStatus(EFinBillStatus.SUCCESS.getValue());
bill.setSrcType(EFinBillSrcType.STORE_ORDER_PAID.getValue());
bill.setSrcId(payId);
bill.setRelType(EFinBillRelType.STORE_ORDER.getValue());
bill.setRelId(orderInfo.getOrder().getId());
//支付渠道+回调ID构成业务唯一键防止重复创建收款单
String businessUniqueKey = payChannel.getPrefix() + payId;
bill.setBusinessUniqueKey(businessUniqueKey);
bill.setInputInternalAccountId(ia.getId());
bill.setInputExternalAccountId(payChannel.getPlatformExternalAccountId());
bill.setBusinessAmount(orderInfo.getOrder().getTotalAmount());
bill.setTransAmount(orderInfo.getOrder().getRealTotalAmount());
bill.setVersion(0L);
bill.setDelFlag(Constants.UNDELETED);
financeBillMapper.insert(bill);
List<FinanceBillDetail> billDetails = new ArrayList<>(orderInfo.getOrderDetails().size());
for (StoreOrderDetail orderDetail : orderInfo.getOrderDetails()) {
FinanceBillDetail billDetail = new FinanceBillDetail();
billDetail.setFinanceBillId(bill.getId());
billDetail.setRelType(EFinBillDetailRelType.STORE_ORDER_DETAIL.getValue());
billDetail.setRelId(orderDetail.getId());
billDetail.setBusinessAmount(orderDetail.getTotalAmount());
billDetail.setTransAmount(orderDetail.getRealTotalAmount());
billDetail.setDelFlag(Constants.UNDELETED);
financeBillDetailMapper.insert(billDetail);
billDetails.add(billDetail);
}
TransInfo transInfo = TransInfo.builder()
.srcBillId(bill.getId())
.srcBillType(bill.getBillType())
.transAmount(bill.getTransAmount())
.transTime(new Date())
.handlerId(null)
.remark("订单支付完成")
.build();
//内部账户
internalAccountService.addTransDetail(ia, transInfo, ELoanDirection.DEBIT, EEntryStatus.FINISH);
//外部账户
externalAccountService.addTransDetail(payChannel.getPlatformExternalAccountId(), transInfo,
ELoanDirection.DEBIT, EEntryStatus.FINISH);
return new FinanceBillInfo(bill, billDetails);
}
/**
*
*
* @param billType
* @return
*/
public String generateBillNo(EFinBillType billType) {
//未确定规则暂时用UUID代替
return IdUtil.simpleUUID();
}
}

View File

@ -1,13 +1,24 @@
package com.ruoyi.xkt.service.impl;
import cn.hutool.core.util.NumberUtil;
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.mapper.InternalAccountMapper;
import com.ruoyi.xkt.mapper.InternalAccountTransDetailMapper;
import com.ruoyi.xkt.service.IInternalAccountService;
import io.jsonwebtoken.lang.Assert;
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;
@ -34,4 +45,101 @@ public class InternalAccountServiceImpl implements IInternalAccountService {
public List<InternalAccount> listWithLock(Collection<Long> ids) {
return internalAccountMapper.listForUpdate(ids);
}
@Transactional
@Override
public int addTransDetail(InternalAccount internalAccount, 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() + "]已冻结");
}
InternalAccountTransDetail transDetail = new InternalAccountTransDetail();
transDetail.setInternalAccountId(internalAccount.getId());
transDetail.setSrcBillId(transInfo.getSrcBillId());
transDetail.setSrcBillType(transInfo.getSrcBillType());
transDetail.setLoanDirection(loanDirection.getValue());
if (less0(transInfo.getTransAmount())) {
throw new ServiceException("交易金额异常");
}
transDetail.setTransAmount(transInfo.getTransAmount());
transDetail.setTransTime(transInfo.getTransTime());
transDetail.setHandlerId(transInfo.getHandlerId());
transDetail.setEntryStatus(entryStatus.getValue());
//不考虑热点账户问题,已入账 => 已执行
transDetail.setEntryExecuted(EEntryStatus.FINISH == entryStatus ?
EEntryExecuted.FINISH.getValue() : EEntryExecuted.WAITING.getValue());
transDetail.setRemark(transInfo.getRemark());
switch (loanDirection) {
case DEBIT:
//借
if (EEntryExecuted.FINISH.getValue().equals(transDetail.getEntryExecuted())) {
//已执行
//余额 = 余额 + 交易金额
internalAccount.setBalance(internalAccount.getBalance()
.add(transDetail.getTransAmount()));
//可用余额 = 可用余额 + 交易金额
internalAccount.setUsableBalance(internalAccount.getUsableBalance()
.add(transDetail.getTransAmount()));
//支付中金额不变
} else {
//未执行
//余额不变
//可用余额不变
//支付中金额不变
}
break;
case CREDIT:
//贷
if (EEntryExecuted.FINISH.getValue().equals(transDetail.getEntryExecuted())) {
//已执行
//余额 = 余额 - 交易金额
internalAccount.setBalance(internalAccount.getBalance()
.subtract(transDetail.getTransAmount()));
//可用余额 = 可用余额 - 交易金额
internalAccount.setUsableBalance(internalAccount.getUsableBalance()
.subtract(transDetail.getTransAmount()));
//支付中金额不变
} else {
//未执行
//余额不变
//可用余额 = 可用余额 - 交易金额
internalAccount.setUsableBalance(internalAccount.getUsableBalance()
.subtract(transDetail.getTransAmount()));
//支付中金额 = 支付中金额 + 交易金额
internalAccount.setPaymentAmount(internalAccount.getPaymentAmount()
.add(transDetail.getTransAmount()));
}
break;
default:
throw new ServiceException("未知借贷方向");
}
//检查账户余额,不允许出现负数
if (less0(internalAccount.getBalance())) {
throw new ServiceException("账户[" + internalAccount.getId() + "]余额不足");
}
if (less0(internalAccount.getUsableBalance())) {
throw new ServiceException("账户[" + internalAccount.getId() + "]可用余额不足");
}
if (less0(internalAccount.getPaymentAmount())) {
throw new ServiceException("账户[" + internalAccount.getId() + "]支付中金额异常");
}
//更新账户余额
internalAccountMapper.updateById(internalAccount);
//交易明细
return internalAccountTransDetailMapper.insert(transDetail);
}
/**
* 0
*
* @param num
* @return
*/
private boolean less0(BigDecimal num) {
return NumberUtil.isLess(num, BigDecimal.ZERO);
}
}