提现风控

This commit is contained in:
墨大叔 2024-09-21 19:28:59 +08:00
parent 7ff418ef44
commit f37bc4779a
13 changed files with 221 additions and 5 deletions

View File

@ -68,7 +68,7 @@ public class IotConstants {
public static final String COMMAND_RECHARGE = "time"; public static final String COMMAND_RECHARGE = "time";
/** /**
* 命令 设置断电方式 * 命令 设置断电方式
*/ */
public static final String COMMAND_OUTAGE_WAY = "set"; public static final String COMMAND_OUTAGE_WAY = "set";

View File

@ -1,6 +1,7 @@
package com.ruoyi.common.core.domain.entity; package com.ruoyi.common.core.domain.entity;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date; import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.annotation.JsonView;
@ -186,4 +187,14 @@ public class SmUser extends BaseEntity
@Excel(name = "限制退款原因") @Excel(name = "限制退款原因")
@ApiModelProperty("限制退款原因") @ApiModelProperty("限制退款原因")
private String limitRefundReason; private String limitRefundReason;
@Excel(name = "风险提现次数")
@ApiModelProperty("风险提现次数")
private Integer riskCount;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "限制提现时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("限制提现时间")
private LocalDateTime limitWithdrawTime;
} }

View File

@ -5,6 +5,7 @@ import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.*; import java.time.*;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.Temporal;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import org.apache.commons.lang3.time.DateFormatUtils; import org.apache.commons.lang3.time.DateFormatUtils;
@ -316,4 +317,12 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils
public static LocalDateTime toLocalDate(String timeStr, String format) { public static LocalDateTime toLocalDate(String timeStr, String format) {
return LocalDateTime.parse(timeStr, DateTimeFormatter.ofPattern(format)); return LocalDateTime.parse(timeStr, DateTimeFormatter.ofPattern(format));
} }
public static LocalDateTime toLocalDateTime(Date date) {
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
}
public static String format(LocalDateTime time, String format) {
return time.format(DateTimeFormatter.ofPattern(format));
}
} }

View File

@ -21,7 +21,10 @@ public enum ConfigKey {
DAILY_WITHDRAW_COUNT("daily.withdraw.count", "单日单用户提现次数(次)"), DAILY_WITHDRAW_COUNT("daily.withdraw.count", "单日单用户提现次数(次)"),
NOVERIFY_WITHDRAW_SINGLE("noverify.withdraw.single", "提现单笔免审核额度(元)"), NOVERIFY_WITHDRAW_SINGLE("noverify.withdraw.single", "提现单笔免审核额度(元)"),
RECHARGE_MIN_SERVICE("recharge.min.service","充值最低服务费(元)"), RECHARGE_MIN_SERVICE("recharge.min.service","充值最低服务费(元)"),
ORDER_AUTO_CLOSE_CD("order.auto.close.cd", "订单自动关闭冷却时间(分)"); ORDER_AUTO_CLOSE_CD("order.auto.close.cd", "订单自动关闭冷却时间(分)"),
RISK_WITHDRAW_TIME("risk.withdraw.time", "风控订单和提现相隔时长(分钟)"),
RISK_WITHDRAW_COUNT("risk.withdraw.count", "累计风险次数"),
RISK_WITHDRAW_ENABLED("risk.withdraw.enabled", "是否开启提现风控");
private final String key; private final String key;
private final String msg; private final String msg;

View File

@ -14,8 +14,8 @@ import java.util.Objects;
@Getter @Getter
public enum DeviceOutageWay { public enum DeviceOutageWay {
NOT_OUTAGE("0", "到时不断电"), NOT_OUTAGE("0", ""),
IMMEDIATE("1", "到时立即断电"); IMMEDIATE("1", "");
private final String value; private final String value;
private final String msg; private final String msg;

View File

@ -299,4 +299,9 @@ public interface TransactionBillService
* 开启/关闭订单设备 * 开启/关闭订单设备
*/ */
int switchDevice(TransactionBillVO bill, boolean open); int switchDevice(TransactionBillVO bill, boolean open);
/**
* 查询最后一个
*/
TransactionBillVO selectLastOne(TransactionBillQuery query);
} }

View File

@ -0,0 +1,17 @@
package com.ruoyi.ss.transactionBill.service;
import com.ruoyi.common.core.domain.ValidateResult;
import com.ruoyi.ss.transactionBill.domain.bo.WithdrawBO;
/**
* @author wjh
* 2024/9/21
*/
public interface WithdrawValidator {
/**
* 判断提现是否有风险
*/
boolean hasRisk(WithdrawBO bo);
}

View File

@ -2,6 +2,7 @@ package com.ruoyi.ss.transactionBill.service.impl;
import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageHelper;
import com.ruoyi.common.constant.Constants; import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.ValidateResult;
import com.ruoyi.common.core.redis.RedisLock; import com.ruoyi.common.core.redis.RedisLock;
import com.ruoyi.common.core.redis.enums.RedisLockKey; import com.ruoyi.common.core.redis.enums.RedisLockKey;
import com.ruoyi.common.enums.WithdrawServiceType; import com.ruoyi.common.enums.WithdrawServiceType;
@ -50,6 +51,7 @@ import com.ruoyi.ss.transactionBill.mapper.TransactionBillMapper;
import com.ruoyi.ss.transactionBill.service.TransactionBillConverter; import com.ruoyi.ss.transactionBill.service.TransactionBillConverter;
import com.ruoyi.ss.transactionBill.service.TransactionBillService; import com.ruoyi.ss.transactionBill.service.TransactionBillService;
import com.ruoyi.ss.transactionBill.service.TransactionBillValidator; import com.ruoyi.ss.transactionBill.service.TransactionBillValidator;
import com.ruoyi.ss.transactionBill.service.WithdrawValidator;
import com.ruoyi.ss.transfer.domain.TransferVO; import com.ruoyi.ss.transfer.domain.TransferVO;
import com.ruoyi.ss.transfer.interfaces.AfterTransfer; import com.ruoyi.ss.transfer.interfaces.AfterTransfer;
import com.ruoyi.ss.transfer.service.TransferConverter; import com.ruoyi.ss.transfer.service.TransferConverter;
@ -155,6 +157,9 @@ public class TransactionBillServiceImpl implements TransactionBillService, After
@Autowired @Autowired
private TransactionBillConverter transactionBillConverter; private TransactionBillConverter transactionBillConverter;
@Autowired
private WithdrawValidator withdrawValidator;
/** /**
* 查询充值记录 * 查询充值记录
* *
@ -414,7 +419,35 @@ public class TransactionBillServiceImpl implements TransactionBillService, After
ServiceUtil.assertion(user == null, "用户不存在"); ServiceUtil.assertion(user == null, "用户不存在");
ServiceUtil.assertion(user.getIsReal() == null || !user.getIsReal(), "用户未实名认证,无法提现"); ServiceUtil.assertion(user.getIsReal() == null || !user.getIsReal(), "用户未实名认证,无法提现");
ServiceUtil.assertion(user.getLimitWithdraw() != null && user.getLimitWithdraw(), "您被限制提现:" + user.getLimitWithdrawReason());
// 判断用户是否被限制提现
boolean limitWithdraw = user.getLimitWithdraw() != null && user.getLimitWithdraw();
if (limitWithdraw) {
LocalDateTime limitWithdrawTime = user.getLimitWithdrawTime();
if (limitWithdrawTime == null) {
throw new ServiceException("您被永久限制提现:" + user.getLimitWithdrawReason());
} else {
throw new ServiceException("您被限制提现至" + DateUtils.format(limitWithdrawTime, DateUtils.YYYY_MM_DD_HH_MM_SS) + ":" + user.getLimitWithdrawReason() );
}
}
// 风控规则判断用户是否有风险
boolean enabled = sysConfigService.getBoolean(ConfigKey.RISK_WITHDRAW_ENABLED);
if (enabled) {
boolean hasRisk = withdrawValidator.hasRisk(bo);
if (hasRisk) {
// 累计一次风险次数
userService.addRiskCount(userId, 1);
// 若用户风险次数已达到阈值将用户标记为提现风险
int riskWithdrawCount = sysConfigService.getInt(ConfigKey.RISK_WITHDRAW_COUNT);
if (user.getRiskCount() + 1 >= riskWithdrawCount) {
userService.limitWithdraw(userId, LocalDateTime.now().plusDays(1), "根据风控规则判断,您的提现具有风险", "风险客户");
// 返回错误
throw new ServiceException("提现具有风险,无法提现");
}
}
}
// 判断今天提现成功和正在审核中的提现是否超过限额 // 判断今天提现成功和正在审核中的提现是否超过限额
String dailyLimitStr = sysConfigService.selectConfigByKey(ConfigKey.DAILY_WITHDRAW_AMOUNT.getKey()); String dailyLimitStr = sysConfigService.selectConfigByKey(ConfigKey.DAILY_WITHDRAW_AMOUNT.getKey());
@ -1649,6 +1682,12 @@ public class TransactionBillServiceImpl implements TransactionBillService, After
} }
} }
@Override
public TransactionBillVO selectLastOne(TransactionBillQuery query) {
PageHelper.orderBy("stb.bill_id desc");
return selectOne(query);
}
@Override @Override
public UserWithdrawServiceVO getUserWithdrawService(Long userId, Long channelId) { public UserWithdrawServiceVO getUserWithdrawService(Long userId, Long channelId) {
SmUserVo user = userService.selectSmUserByUserId(userId); SmUserVo user = userService.selectSmUserByUserId(userId);

View File

@ -0,0 +1,74 @@
package com.ruoyi.ss.transactionBill.service.impl;
import com.ruoyi.common.core.domain.BaseValidator;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.ss.transactionBill.domain.TransactionBillQuery;
import com.ruoyi.ss.transactionBill.domain.bo.WithdrawBO;
import com.ruoyi.ss.transactionBill.domain.dto.WithdrawDTO;
import com.ruoyi.ss.transactionBill.domain.enums.TransactionBillStatus;
import com.ruoyi.ss.transactionBill.domain.enums.TransactionBillType;
import com.ruoyi.ss.transactionBill.domain.vo.TransactionBillVO;
import com.ruoyi.ss.transactionBill.service.TransactionBillService;
import com.ruoyi.ss.transactionBill.service.WithdrawValidator;
import com.ruoyi.ss.user.domain.SmUserVo;
import com.ruoyi.system.domain.enums.config.ConfigKey;
import com.ruoyi.system.service.ISysConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.LocalDateTime;
/**
* @author wjh
* 2024/9/21
*/
@Service
public class WithdrawValidatorImpl extends BaseValidator implements WithdrawValidator {
@Autowired
private TransactionBillService transactionBillService;
@Autowired
private ISysConfigService sysConfigService;
@Autowired
private RedisCache redisCache;
/**
* 判断提现是否有风险
*
* @param bo
*/
@Override
public boolean hasRisk(WithdrawBO bo) {
if (bo == null || bo.getUser() == null || bo.getDto() == null) {
return false;
}
SmUserVo user = bo.getUser();
WithdrawDTO dto = bo.getDto();
// 查询最近一次的订单
TransactionBillQuery query = new TransactionBillQuery();
query.setUserId(user.getUserId());
query.setType(TransactionBillType.RECHARGE.getType());
query.setStatus(TransactionBillStatus.SUCCESS.getStatus());
TransactionBillVO recharge = transactionBillService.selectLastOne(query);
if (recharge == null || recharge.getPayTime() == null) {
return false;
}
// 判断订单金额是否和提现一致订单时间与提现时间是否相差较近
// 金额一致
boolean equalsMoney = recharge.getArrivalAmount() != null && dto.getMoney() != null && recharge.getArrivalAmount().compareTo(dto.getMoney()) == 0;
// 提现时间相近
int riskWithdrawTime = sysConfigService.getInt(ConfigKey.RISK_WITHDRAW_TIME); // 最低允许相隔时长分钟
Duration between = Duration.between(DateUtils.toLocalDateTime(recharge.getPayTime()), LocalDateTime.now());
boolean timeLimit = between.toMinutes() < riskWithdrawTime;
if (equalsMoney && timeLimit) {
return true;
}
return false;
}
}

View File

@ -109,4 +109,12 @@ public interface SmUserMapper
* 查询用户余额 * 查询用户余额
*/ */
BigDecimal selectSumOfBalance(SmUserQuery query); BigDecimal selectSumOfBalance(SmUserQuery query);
/**
* 增加一次风险次数
* @param userId
* @param count
* @return
*/
int addRiskCount(@Param("userId") Long userId,@Param("count") int count);
} }

View File

@ -49,6 +49,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
su.limit_withdraw_reason, su.limit_withdraw_reason,
su.limit_refund, su.limit_refund,
su.limit_refund_reason, su.limit_refund_reason,
su.risk_count,
su.limit_withdraw_time,
(select sum(stb.money) from sm_transaction_bill stb where stb.user_id = su.user_id and stb.type = '1' and stb.status = '2') as recharge_amount, (select sum(stb.money) from sm_transaction_bill stb where stb.user_id = su.user_id and stb.type = '1' and stb.status = '2') as recharge_amount,
(select sum(stb.arrival_amount) from sm_transaction_bill stb where stb.user_id = su.user_id and stb.type = '2' and stb.status = '14') as with_drawl_amount, (select sum(stb.arrival_amount) from sm_transaction_bill stb where stb.user_id = su.user_id and stb.type = '2' and stb.status = '14') as with_drawl_amount,
(select sum(stb.arrival_amount) from sm_transaction_bill stb where stb.mch_id = su.user_id and stb.type = '1' and stb.status = '2') as total_income (select sum(stb.arrival_amount) from sm_transaction_bill stb where stb.mch_id = su.user_id and stb.type = '1' and stb.status = '2') as total_income
@ -169,6 +171,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="limitWithdrawReason != null">limit_withdraw_reason,</if> <if test="limitWithdrawReason != null">limit_withdraw_reason,</if>
<if test="limitRefund != null">limit_refund,</if> <if test="limitRefund != null">limit_refund,</if>
<if test="limitRefundReason != null">limit_refund_reason,</if> <if test="limitRefundReason != null">limit_refund_reason,</if>
<if test="riskCount != null">risk_count,</if>
<if test="limitWithdrawTime != null">limit_withdraw_time,</if>
</trim> </trim>
<trim prefix="values (" suffix=")" suffixOverrides=","> <trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="userName != null and userName != ''">#{userName},</if> <if test="userName != null and userName != ''">#{userName},</if>
@ -206,9 +210,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="limitWithdrawReason != null">#{limitWithdrawReason},</if> <if test="limitWithdrawReason != null">#{limitWithdrawReason},</if>
<if test="limitRefund != null">#{limitRefund},</if> <if test="limitRefund != null">#{limitRefund},</if>
<if test="limitRefundReason != null">#{limitRefundReason},</if> <if test="limitRefundReason != null">#{limitRefundReason},</if>
<if test="riskCount != null">#{riskCount},</if>
<if test="limitWithdrawTime != null">#{limitWithdrawTime},</if>
</trim> </trim>
</insert> </insert>
<insert id="addRiskCount">
update sm_user
set risk_count = risk_count + 1
where user_id = #{userId} and del_flag = '0'
</insert>
<update id="addBalance"> <update id="addBalance">
update sm_user update sm_user
set balance = balance + #{amount} set balance = balance + #{amount}
@ -253,6 +265,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="limitWithdrawReason != null">limit_withdraw_reason = #{limitWithdrawReason},</if> <if test="limitWithdrawReason != null">limit_withdraw_reason = #{limitWithdrawReason},</if>
<if test="limitRefund != null">limit_refund = #{limitRefund},</if> <if test="limitRefund != null">limit_refund = #{limitRefund},</if>
<if test="limitRefundReason != null">limit_refund_reason = #{limitRefundReason},</if> <if test="limitRefundReason != null">limit_refund_reason = #{limitRefundReason},</if>
<if test="riskCount != null">risk_count = #{riskCount},</if>
<if test="limitWithdrawTime != null">limit_withdraw_time = #{limitWithdrawTime},</if>
</trim> </trim>
where user_id = #{userId} where user_id = #{userId}
</update> </update>

View File

@ -8,6 +8,7 @@ import com.ruoyi.ss.user.domain.SmUserVo;
import com.ruoyi.ss.user.domain.dto.UserRealNameDTO; import com.ruoyi.ss.user.domain.dto.UserRealNameDTO;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
/** /**
@ -209,4 +210,19 @@ public interface ISmUserService
* 实名认证 * 实名认证
*/ */
int realName(UserRealNameDTO dto); int realName(UserRealNameDTO dto);
/**
* 限制提现
* @param userId
* @param limitTime
* @param reason
* @param remark
* @return
*/
int limitWithdraw(Long userId, LocalDateTime limitTime, String reason, String remark);
/**
* 添加一次风险次数
*/
int addRiskCount(Long userId, int count);
} }

View File

@ -26,6 +26,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -297,6 +298,25 @@ public class SmUserServiceImpl implements ISmUserService
return smUserMapper.updateSmUser(data); return smUserMapper.updateSmUser(data);
} }
@Override
public int limitWithdraw(Long userId, LocalDateTime limitTime, String reason, String remark) {
if (userId == null) {
return 0;
}
SmUser data = new SmUser();
data.setUserId(userId);
data.setLimitWithdraw(true);
data.setLimitWithdrawTime(limitTime);
data.setLimitWithdrawReason(reason);
data.setRemark(remark);
return smUserMapper.updateSmUser(data);
}
@Override
public int addRiskCount(Long userId, int count) {
return smUserMapper.addRiskCount(userId, count);
}
/** /**
* 逻辑删除前校验 * 逻辑删除前校验
* @param userIds * @param userIds