package com.yami.trading.service.loan;
|
|
import cn.hutool.core.util.IdUtil;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.yami.trading.bean.constans.WalletConstants;
|
import com.yami.trading.bean.loan.LoanConfig;
|
import com.yami.trading.bean.loan.LoanOrder;
|
import com.yami.trading.common.constants.TipConstants;
|
import com.yami.trading.common.exception.YamiShopBindException;
|
import com.yami.trading.common.util.Arith;
|
import com.yami.trading.dao.loan.LoanOrderMapper;
|
import com.yami.trading.service.WalletService;
|
import com.yami.trading.service.system.TipService;
|
import lombok.extern.slf4j.Slf4j;
|
import org.apache.commons.lang3.StringUtils;
|
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.Calendar;
|
import java.util.Date;
|
import java.util.List;
|
import java.util.Map;
|
|
@Service
|
@Slf4j
|
public class LoanOrderServiceImpl extends ServiceImpl<LoanOrderMapper, LoanOrder> implements LoanOrderService {
|
|
@Autowired
|
private LoanConfigService loanConfigService;
|
@Autowired
|
private WalletService walletService;
|
@Autowired
|
private TipService tipService;
|
|
@Override
|
@Transactional
|
public LoanOrder apply(String userId, String loanConfigId, double quota, String symbol,
|
String frontImg, String backImg, String handheldImg) {
|
if (StringUtils.isBlank(frontImg) || StringUtils.isBlank(backImg)) {
|
throw new YamiShopBindException("Please upload ID card front and back photos");
|
}
|
LoanConfig config = resolveConfig(loanConfigId, quota);
|
long pending = count(new LambdaQueryWrapper<LoanOrder>()
|
.eq(LoanOrder::getUserId, userId)
|
.eq(LoanOrder::getState, LoanOrder.STATE_PENDING));
|
if (pending > 0) {
|
throw new YamiShopBindException("You already have a pending loan application");
|
}
|
|
LoanOrder order = new LoanOrder();
|
order.setOrderNo("LN" + System.currentTimeMillis() + IdUtil.randomUUID().substring(0, 6));
|
order.setUserId(userId);
|
order.setLoanConfigId(config.getUuid());
|
order.setSymbol(StringUtils.defaultIfBlank(symbol, "USDT").toUpperCase());
|
order.setQuota(quota > 0 ? quota : config.getMaxQuota());
|
order.setTerm(config.getTerm());
|
order.setDailyRate(config.getDailyRate());
|
order.setTotalInterest(calcInterest(order.getQuota(), order.getDailyRate(), order.getTerm()));
|
order.setRepayCycle(config.getRepayCycle() != null ? config.getRepayCycle() : config.getTerm());
|
order.setRepayment(config.getRepayment());
|
order.setLendingInstitution(config.getLendingInstitution());
|
order.setLendingName(config.getLendingName());
|
order.setState(LoanOrder.STATE_PENDING);
|
order.setIdFrontImg(stripImageHost(frontImg));
|
order.setIdBackImg(stripImageHost(backImg));
|
order.setHandheldImg(stripImageHost(handheldImg));
|
order.setCreateTime(new Date());
|
save(order);
|
tipService.saveTip(order.getUuid(), TipConstants.LOAN);
|
return order;
|
}
|
|
@Override
|
public List<LoanOrder> listByUser(String userId) {
|
return list(new LambdaQueryWrapper<LoanOrder>()
|
.eq(LoanOrder::getUserId, userId)
|
.orderByDesc(LoanOrder::getCreateTime));
|
}
|
|
@Override
|
public LoanOrder findById(String id) {
|
return getById(id);
|
}
|
|
@Override
|
public LoanOrder findByOrderNo(String orderNo) {
|
return getOne(new LambdaQueryWrapper<LoanOrder>().eq(LoanOrder::getOrderNo, orderNo).last("limit 1"));
|
}
|
|
@Override
|
@Transactional
|
public String approve(String orderId, String operator) {
|
LoanOrder order = getById(orderId);
|
if (order == null) {
|
return "Order not found";
|
}
|
if (order.getState() != LoanOrder.STATE_PENDING) {
|
return "Only pending orders can be approved";
|
}
|
Date now = new Date();
|
order.setState(LoanOrder.STATE_ACTIVE);
|
order.setApproveTime(now);
|
order.setDueTime(addDays(now, order.getTerm()));
|
order.setTotalInterest(calcInterest(order.getQuota(), order.getDailyRate(), order.getTerm()));
|
updateById(order);
|
|
String coin = order.getSymbol().toLowerCase();
|
walletService.updateMoney(
|
coin,
|
order.getUserId(),
|
BigDecimal.valueOf(order.getQuota()),
|
BigDecimal.ZERO,
|
WalletConstants.MONEYLOG_CATEGORY_LOAN,
|
WalletConstants.WALLET_USDT,
|
WalletConstants.MONEYLOG_CONTENT_LOAN_ADD,
|
"Loan disbursement, order: " + order.getOrderNo()
|
);
|
tipService.deleteTip(order.getUuid());
|
log.info("Loan approved by {}, orderNo={}", operator, order.getOrderNo());
|
return null;
|
}
|
|
@Override
|
@Transactional
|
public String reject(String orderId, String reason, String operator) {
|
LoanOrder order = getById(orderId);
|
if (order == null) {
|
return "Order not found";
|
}
|
if (order.getState() != LoanOrder.STATE_PENDING) {
|
return "Only pending orders can be rejected";
|
}
|
order.setState(LoanOrder.STATE_REJECTED);
|
order.setReason(reason);
|
updateById(order);
|
tipService.deleteTip(order.getUuid());
|
log.info("Loan rejected by {}, orderNo={}", operator, order.getOrderNo());
|
return null;
|
}
|
|
@Override
|
@Transactional
|
public String manualRepay(String orderId, String operator) {
|
LoanOrder order = getById(orderId);
|
if (order == null) {
|
return "Order not found";
|
}
|
if (order.getState() != LoanOrder.STATE_ACTIVE && order.getState() != LoanOrder.STATE_OVERDUE) {
|
return "Order is not in repayment status";
|
}
|
return doRepay(order, operator);
|
}
|
|
@Override
|
@Transactional
|
public String autoRepayDueOrder(LoanOrder order) {
|
if (order.getState() != LoanOrder.STATE_ACTIVE && order.getState() != LoanOrder.STATE_OVERDUE) {
|
return "Skip";
|
}
|
if (order.getDueTime() == null || order.getDueTime().after(new Date())) {
|
return "Not due yet";
|
}
|
return doRepay(order, "system");
|
}
|
|
private String doRepay(LoanOrder order, String operator) {
|
double repayAmount = Arith.add(order.getQuota(), order.getTotalInterest());
|
String coin = order.getSymbol().toLowerCase();
|
try {
|
walletService.updateMoney(
|
coin,
|
order.getUserId(),
|
BigDecimal.valueOf(-repayAmount),
|
BigDecimal.ZERO,
|
WalletConstants.MONEYLOG_CATEGORY_LOAN,
|
WalletConstants.WALLET_USDT,
|
WalletConstants.MONEYLOG_CONTENT_LOAN_REPAY,
|
"Loan repayment, order: " + order.getOrderNo()
|
);
|
} catch (Exception e) {
|
order.setState(LoanOrder.STATE_OVERDUE);
|
updateById(order);
|
log.warn("Loan auto repay failed, orderNo={}, err={}", order.getOrderNo(), e.getMessage());
|
return e.getMessage();
|
}
|
order.setState(LoanOrder.STATE_REPAID);
|
order.setRepaidTime(new Date());
|
updateById(order);
|
log.info("Loan repaid by {}, orderNo={}", operator, order.getOrderNo());
|
return null;
|
}
|
|
@Override
|
@Transactional
|
public void modifyOrder(LoanOrder order) {
|
LoanOrder db = getById(order.getUuid());
|
if (db == null) {
|
throw new YamiShopBindException("Order not found");
|
}
|
if (db.getState() == LoanOrder.STATE_REPAID) {
|
throw new YamiShopBindException("Repaid orders cannot be modified");
|
}
|
if (order.getQuota() > 0) {
|
db.setQuota(order.getQuota());
|
}
|
if (StringUtils.isNotBlank(order.getSymbol())) {
|
db.setSymbol(order.getSymbol().toUpperCase());
|
}
|
if (StringUtils.isNotBlank(order.getIdFrontImg())) {
|
db.setIdFrontImg(stripImageHost(order.getIdFrontImg()));
|
}
|
if (StringUtils.isNotBlank(order.getIdBackImg())) {
|
db.setIdBackImg(stripImageHost(order.getIdBackImg()));
|
}
|
if (StringUtils.isNotBlank(order.getHandheldImg())) {
|
db.setHandheldImg(stripImageHost(order.getHandheldImg()));
|
}
|
db.setTotalInterest(calcInterest(db.getQuota(), db.getDailyRate(), db.getTerm()));
|
updateById(db);
|
}
|
|
@Override
|
public Page<Map<String, Object>> adminPagedQuery(int pageNo, int pageSize, String status,
|
String userName, String orderNo, String rolename,
|
List<String> children) {
|
Page<Map<String, Object>> page = new Page<>(pageNo, pageSize);
|
return baseMapper.adminPagedQuery(page, status, userName, orderNo, rolename, children);
|
}
|
|
@Override
|
public List<LoanOrder> findDueOrders() {
|
return list(new LambdaQueryWrapper<LoanOrder>()
|
.in(LoanOrder::getState, LoanOrder.STATE_ACTIVE, LoanOrder.STATE_OVERDUE)
|
.le(LoanOrder::getDueTime, new Date()));
|
}
|
|
private LoanConfig resolveConfig(String loanConfigId, double quota) {
|
LoanConfig config = null;
|
if (StringUtils.isNotBlank(loanConfigId)) {
|
config = loanConfigService.findById(loanConfigId);
|
}
|
if (config == null) {
|
List<LoanConfig> configs = loanConfigService.listEnabled();
|
if (configs.isEmpty()) {
|
throw new YamiShopBindException("Loan product not configured");
|
}
|
config = configs.get(0);
|
}
|
if (config.getState() != 1) {
|
throw new YamiShopBindException("Loan product is disabled");
|
}
|
return config;
|
}
|
|
private double calcInterest(double quota, double dailyRate, int term) {
|
return Arith.mul(Arith.mul(quota, dailyRate), term);
|
}
|
|
private Date addDays(Date date, int days) {
|
Calendar cal = Calendar.getInstance();
|
cal.setTime(date);
|
cal.add(Calendar.DAY_OF_MONTH, days);
|
return cal.getTime();
|
}
|
|
private String stripImageHost(String url) {
|
if (StringUtils.isBlank(url)) {
|
return url;
|
}
|
int idx = url.indexOf("/upload/");
|
if (idx >= 0) {
|
return url.substring(idx);
|
}
|
return url;
|
}
|
}
|