This commit is contained in:
磷叶 2024-10-21 14:19:15 +08:00
parent fb736ec07a
commit 72c8a68c39
27 changed files with 380 additions and 91 deletions

View File

@ -152,17 +152,20 @@ public class SmUser extends BaseEntity
@Excel(name = "真实姓名")
@ApiModelProperty("真实姓名")
@JsonView(JsonViewProfile.AppMch.class)
@JsonView(JsonViewProfile.App.class)
@Sensitive(desensitizedType = DesensitizedType.USERNAME)
private String realName;
@Excel(name = "身份证号")
@ApiModelProperty("身份证号")
@Sensitive(desensitizedType = DesensitizedType.ID_CARD)
@JsonView(JsonViewProfile.App.class)
private String realIdCard;
@Excel(name = "实名认证手机号")
@ApiModelProperty("实名认证手机号")
@Sensitive(desensitizedType = DesensitizedType.PHONE)
@JsonView(JsonViewProfile.App.class)
private String realPhone;
@ApiModelProperty("是否已经实名认证")

View File

@ -14,8 +14,10 @@ import lombok.Getter;
public enum ServiceCode {
DEVICE_RECHARGE_IOT_ERROR(101001, "物联网设备充值失败"),
DEVICE_RECHARGE_ERROR(101002, "设备充值失败");
DEVICE_RECHARGE_ERROR(101002, "设备充值失败"),
RISK_WITHDRAW(102001, "提现风险"),
RISK_REFUND(102002, "退款风险");
private final Integer code;
private final String msg;
}
}

View File

@ -49,4 +49,8 @@ public class RealName extends BaseEntity
@ApiModelProperty("人脸图像")
private String faceImage;
@Excel(name = "认证类型", readConverterExp = "1=普通认证,2=风控认证")
@ApiModelProperty("认证类型")
private String type;
}

View File

@ -0,0 +1,20 @@
package com.ruoyi.ss.realName.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
/**
* @author wjh
* 2024/10/21
*/
@Getter
@AllArgsConstructor
public enum RealNameType {
NORMAL("1", "普通实名"),
RISK("2", "风控实名");
private final String type;
private final String msg;
}

View File

@ -16,6 +16,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
srn.create_time,
srn.score,
srn.face_image,
srn.type,
su.user_name as user_name
from ss_real_name srn
left join sm_user su on su.user_id = srn.user_id
@ -28,6 +29,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="query.idCard != null and query.idCard != ''"> and srn.id_card like concat('%', #{query.idCard}, '%')</if>
<if test="query.mobile != null and query.mobile != ''"> and srn.mobile like concat('%', #{query.mobile}, '%')</if>
<if test="query.userName != null and query.userName != ''"> and su.user_name like concat('%', #{query.userName}, '%')</if>
<if test="query.type != null and query.type != ''"> and type = #{query.type}</if>
</sql>
<select id="selectRealNameList" parameterType="RealNameQuery" resultMap="RealNameResult">
@ -43,15 +45,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<insert id="insertRealName" parameterType="RealName" useGeneratedKeys="true" keyProperty="id">
<selectKey keyProperty="id" order="AFTER" resultType="Long">
SELECT LAST_INSERT_ID();
</selectKey>
insert into ss_real_name
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="userId != null">user_id,</if>
<if test="name != null">name,</if>
<if test="name != null">`name`,</if>
<if test="idCard != null">id_card,</if>
<if test="mobile != null">mobile,</if>
<if test="createTime != null">create_time,</if>
<if test="score != null">score,</if>
<if test="faceImage != null">face_image,</if>
<if test="type != null and type != ''">type,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="userId != null">#{userId},</if>
@ -61,6 +67,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="createTime != null">#{createTime},</if>
<if test="score != null">#{score},</if>
<if test="faceImage != null">#{faceImage},</if>
<if test="type != null and type != ''">#{type},</if>
</trim>
</insert>
@ -74,12 +81,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<sql id="updateColumns">
<if test="data.userId != null">user_id = #{data.userId},</if>
<if test="data.name != null">name = #{data.name},</if>
<if test="data.name != null">`name` = #{data.name},</if>
<if test="data.idCard != null">id_card = #{data.idCard},</if>
<if test="data.mobile != null">mobile = #{data.mobile},</if>
<if test="data.createTime != null">create_time = #{data.createTime},</if>
<if test="data.score != null">score = #{data.score},</if>
<if test="data.faceImage != null">face_image = #{data.faceImage},</if>
<if test="data.type != null and data.type != ''">type = #{data.type},</if>
</sql>
<delete id="deleteRealNameById" parameterType="Long">

View File

@ -1,6 +1,10 @@
package com.ruoyi.ss.risk.domain;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ -28,7 +32,7 @@ public class Risk extends BaseEntity
@Excel(name = "类型")
@ApiModelProperty("类型")
private String type;
private List<String> type;
@Excel(name = "风控原因")
@ApiModelProperty("风控原因")
@ -37,7 +41,7 @@ public class Risk extends BaseEntity
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("结束时间")
private Date endTime;
private LocalDateTime endTime;
@Excel(name = "实名认证ID")
@ApiModelProperty("实名认证ID")

View File

@ -1,11 +1,17 @@
package com.ruoyi.ss.risk.domain;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
/**
* @author wjh
* 2024/10/18
*/
@Data
public class RiskQuery extends RiskVO{
public class RiskQuery extends RiskVO {
@ApiModelProperty("包含类型")
private String containsType;
}

View File

@ -1,5 +1,6 @@
package com.ruoyi.ss.risk.domain;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
@ -8,4 +9,11 @@ import lombok.Data;
*/
@Data
public class RiskVO extends Risk{
@ApiModelProperty("是否结束")
private Boolean isFinished;
@ApiModelProperty("用户名")
private String userName;
}

View File

@ -0,0 +1,19 @@
package com.ruoyi.ss.risk.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author wjh
* 2024/10/21
*/
@Getter
@AllArgsConstructor
public enum RiskType {
WITHDRAW("1", "限制提现"),
REFUND("2", "限制退款");
private final String type;
private final String msg;
}

View File

@ -1,5 +1,6 @@
package com.ruoyi.ss.risk.mapper;
import java.time.LocalDateTime;
import java.util.List;
import com.ruoyi.ss.risk.domain.Risk;
import com.ruoyi.ss.risk.domain.RiskVO;
@ -61,4 +62,14 @@ public interface RiskMapper
* @return 结果
*/
public int deleteRiskByRiskIds(Long[] riskIds);
/**
* 批量修改结束时间
* @param riskIds ID列表
* @param endTime 结束时间
* @return
*/
int batchFinish(@Param("riskIds") List<Long> riskIds,
@Param("endTime") LocalDateTime endTime,
@Param("realNameId") Long realNameId);
}

View File

@ -4,7 +4,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.ss.risk.mapper.RiskMapper">
<resultMap type="RiskVO" id="RiskResult" autoMapping="true"/>
<resultMap type="RiskVO" id="RiskResult" autoMapping="true">
<result column="type" property="type" typeHandler="com.ruoyi.system.mapper.typehandler.StringSplitListTypeHandler"/>
</resultMap>
<sql id="isFinished">
(sr.end_time is not null and sr.end_time &lt;= now())
</sql>
<sql id="selectRiskVo">
select
@ -14,16 +20,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
sr.reason,
sr.end_time,
sr.create_time,
sr.real_name_id
sr.real_name_id,
<include refid="isFinished"/> as is_finished,
su.user_name as user_name
from ss_risk sr
left join sm_user su on su.user_id = sr.user_id
</sql>
<sql id="searchCondition">
<if test="query.riskId != null "> and sr.risk_id = #{query.riskId}</if>
<if test="query.userId != null "> and sr.user_id = #{query.userId}</if>
<if test="query.type != null and query.type != ''"> and sr.type = #{query.type}</if>
<if test="query.containsType != null and query.containsType != ''">
and find_in_set(#{query.containsType}, sr.type)
</if>
<if test="query.reason != null and query.reason != ''"> and sr.reason like concat('%', #{query.reason}, '%')</if>
<if test="query.userName != null and query.userName != ''"> and su.user_name like concat('%', #{query.userName}, '%')</if>
<if test="query.realNameId != null "> and sr.real_name_id = #{query.realNameId}</if>
<if test="query.isFinished != null">
<if test="query.isFinished">and <include refid="isFinished"/></if>
<if test="!query.isFinished">and !<include refid="isFinished"/></if>
</if>
</sql>
<select id="selectRiskList" parameterType="RiskQuery" resultMap="RiskResult">
@ -42,7 +59,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
insert into ss_risk
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="userId != null">user_id,</if>
<if test="type != null and type != ''">type,</if>
<if test="type != null and type.size() > 0">type,</if>
<if test="reason != null">reason,</if>
<if test="endTime != null">end_time,</if>
<if test="createTime != null">create_time,</if>
@ -50,7 +67,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="userId != null">#{userId},</if>
<if test="type != null and type != ''">#{type},</if>
<if test="type != null and type.size() > 0">#{type,typeHandler=com.ruoyi.system.mapper.typehandler.StringSplitListTypeHandler},</if>
<if test="reason != null">#{reason},</if>
<if test="endTime != null">#{endTime},</if>
<if test="createTime != null">#{createTime},</if>
@ -66,9 +83,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where risk_id = #{data.riskId}
</update>
<update id="batchFinish">
update ss_risk
set end_time = #{endTime},
real_name_id = #{realNameId}
where risk_id in
<foreach item="item" collection="riskIds" open="(" separator="," close=")">
#{item}
</foreach>
</update>
<sql id="updateColumns">
<if test="data.userId != null">user_id = #{data.userId},</if>
<if test="data.type != null and data.type != ''">type = #{data.type},</if>
<if test="data.type != null and data.type.size() > 0">type = #{data.type,typeHandler=com.ruoyi.system.mapper.typehandler.StringSplitListTypeHandler},</if>
<if test="data.reason != null">reason = #{data.reason},</if>
<if test="data.endTime != null">end_time = #{data.endTime},</if>
<if test="data.createTime != null">create_time = #{data.createTime},</if>

View File

@ -60,4 +60,21 @@ public interface IRiskService
* @return 结果
*/
public int deleteRiskByRiskId(Long riskId);
/**
* 查询未结束的风控列表
*/
List<RiskVO> selectNotFinishedList(RiskQuery query);
/**
* 批量结束
*/
int batchFinish(List<Long> riskIds, Long realNameId);
/**
* 限制提现
* @param userId 用户ID
* @param reason 限制原因
*/
int limitWithdraw(Long userId, String reason);
}

View File

@ -1,7 +1,11 @@
package com.ruoyi.ss.risk.service.impl;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.collection.CollectionUtils;
import com.ruoyi.ss.risk.domain.enums.RiskType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.ss.risk.mapper.RiskMapper;
@ -94,4 +98,27 @@ public class RiskServiceImpl implements IRiskService
{
return riskMapper.deleteRiskByRiskId(riskId);
}
@Override
public List<RiskVO> selectNotFinishedList(RiskQuery query) {
query.setIsFinished(false);
return this.selectRiskList(query);
}
@Override
public int batchFinish(List<Long> riskIds, Long realNameId) {
if (CollectionUtils.isEmptyElement(riskIds)) {
return 0;
}
return riskMapper.batchFinish(riskIds, LocalDateTime.now(), realNameId);
}
@Override
public int limitWithdraw(Long userId, String reason) {
Risk risk = new Risk();
risk.setUserId(userId);
risk.setType(Collections.singletonList(RiskType.WITHDRAW.getType()));
risk.setReason(reason);
return this.insertRisk(risk);
}
}

View File

@ -125,6 +125,10 @@ public class TransactionBillQuery extends TransactionBill {
@ApiModelProperty("创建时间(结束)")
private LocalDateTime createTimeEnd;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("创建时间(起始)")
private LocalDateTime createTimeStart;
@ApiModelProperty("收入人ID")
private Long incomeUserId;
}

View File

@ -197,6 +197,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="query.suitFeeType != null and query.suitFeeType != ''"> and stb.suit_fee_type = #{query.suitFeeType}</if>
<if test="query.suitEnableLowPowerClose != null "> and stb.suit_enable_low_power_close = #{query.suitEnableLowPowerClose}</if>
<if test="query.createTimeEnd != null "> and stb.create_time &lt;= #{query.createTimeEnd}</if>
<if test="query.createTimeStart != null "> and stb.create_time >= #{query.createTimeStart}</if>
<if test="query.deviceProductId != null and query.deviceProductId != ''"> and stb.device_product_id = #{query.deviceProductId}</if>
<if test="query.deviceServiceMode != null and query.deviceServiceMode != ''"> and stb.device_service_mode = #{query.deviceServiceMode}</if>
<if test="query.version != null "> and stb.version = #{query.version}</if>

View File

@ -5,6 +5,7 @@ import com.ruoyi.ss.transactionBill.domain.bo.RechargeBO;
import com.ruoyi.ss.transactionBill.domain.bo.RechargePayDepositBO;
import com.ruoyi.ss.transactionBill.domain.dto.RechargePayBO;
import com.ruoyi.ss.transactionBill.domain.vo.TransactionBillVO;
import com.ruoyi.ss.user.domain.SmUserVo;
/**
* 2024/4/22
@ -59,4 +60,10 @@ public interface TransactionBillValidator {
* 判断用户是否是订单的代理商
*/
boolean isAgent(TransactionBillVO bill, Long userId);
/**
* 校验退款风控
* @param user 用户
*/
void checkRefundRisk(SmUserVo user);
}

View File

@ -1,7 +1,7 @@
package com.ruoyi.ss.transactionBill.service;
import com.ruoyi.common.core.domain.ValidateResult;
import com.ruoyi.ss.transactionBill.domain.bo.WithdrawBO;
import com.ruoyi.ss.user.domain.SmUserVo;
/**
* @author wjh
@ -14,4 +14,9 @@ public interface WithdrawValidator {
*/
boolean hasRisk(WithdrawBO bo);
/**
* 校验实名认证风控
* @param user 用户
*/
void checkWithdrawRisk(SmUserVo user);
}

View File

@ -5,6 +5,7 @@ import com.github.pagehelper.PageHelper;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.redis.RedisLock;
import com.ruoyi.common.core.redis.enums.RedisLockKey;
import com.ruoyi.common.enums.ServiceCode;
import com.ruoyi.common.enums.WithdrawServiceType;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.*;
@ -39,6 +40,7 @@ import com.ruoyi.ss.record.time.domain.enums.RecordTimeType;
import com.ruoyi.ss.record.time.service.IRecordTimeService;
import com.ruoyi.ss.record.time.service.RecordTimeConverter;
import com.ruoyi.ss.recordBalance.domain.enums.RecordBalanceBstType;
import com.ruoyi.ss.risk.service.IRiskService;
import com.ruoyi.ss.store.domain.StoreVo;
import com.ruoyi.ss.suit.domain.SuitVO;
import com.ruoyi.ss.suit.domain.enums.SuitFeeMode;
@ -163,6 +165,9 @@ public class TransactionBillServiceImpl implements TransactionBillService, After
@Autowired
private ReceiveBillService receiveBillService;
@Autowired
private IRiskService riskService;
/**
* 查询充值记录
*
@ -565,18 +570,10 @@ public class TransactionBillServiceImpl implements TransactionBillService, After
ServiceUtil.assertion(user.getIsReal() == null || !user.getIsReal(), "用户未实名认证,无法提现");
// 判断用户是否被限制提现
boolean limitWithdraw = user.getLimitWithdraw() != null && user.getLimitWithdraw();
if (limitWithdraw) {
LocalDateTime limitWithdrawTime = user.getLimitWithdrawTime();
if (limitWithdrawTime == null) {
throw new ServiceException("您被永久限制提现:" + user.getLimitWithdrawReason());
} else if (limitWithdrawTime.isAfter(LocalDateTime.now())){
throw new ServiceException("您被限制提现至" + DateUtils.format(limitWithdrawTime, DateUtils.YYYY_MM_DD_HH_MM_SS) + ":" + user.getLimitWithdrawReason() );
}
}
withdrawValidator.checkWithdrawRisk(user);
// 风控规则判断用户是否有风险
boolean enabled = sysConfigService.getBoolean(ConfigKey.RISK_WITHDRAW_ENABLED);
boolean enabled = sysConfigService.getBoolean(ConfigKey.RISK_WITHDRAW_ENABLED); // 是否开启提现风控
if (enabled) {
boolean hasRisk = withdrawValidator.hasRisk(bo);
if (hasRisk) {
@ -585,14 +582,13 @@ public class TransactionBillServiceImpl implements TransactionBillService, After
// 若用户风险次数已达到阈值将用户标记为提现风险
int riskWithdrawCount = sysConfigService.getInt(ConfigKey.RISK_WITHDRAW_COUNT);
if (user.getRiskCount() + 1 >= riskWithdrawCount) {
userService.limitWithdraw(userId, LocalDateTime.now().plusDays(1), "根据风控规则判断,您的提现具有风险", "风险客户");
riskService.limitWithdraw(userId, "根据风控规则判断,您的提现具有风险");
// 返回错误
throw new ServiceException("提现具有风险,无法提现");
throw new ServiceException("提现具有风险,无法提现", ServiceCode.RISK_WITHDRAW.getCode());
}
}
}
// 判断今天提现成功和正在审核中的提现是否超过限额
String dailyLimitStr = sysConfigService.selectConfigByKey(ConfigKey.DAILY_WITHDRAW_AMOUNT.getKey());
String dailyLimitCountStr = sysConfigService.selectConfigByKey(ConfigKey.DAILY_WITHDRAW_COUNT.getKey());

View File

@ -2,12 +2,20 @@ package com.ruoyi.ss.transactionBill.service.impl;
import com.ruoyi.common.core.domain.BaseValidator;
import com.ruoyi.common.core.domain.ValidateResult;
import com.ruoyi.common.enums.ServiceCode;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.ServiceUtil;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.collection.CollectionUtils;
import com.ruoyi.ss.channel.domain.ChannelVO;
import com.ruoyi.ss.device.domain.vo.DeviceVO;
import com.ruoyi.ss.device.service.DeviceService;
import com.ruoyi.ss.model.domain.enums.ModelTag;
import com.ruoyi.ss.risk.domain.RiskQuery;
import com.ruoyi.ss.risk.domain.RiskVO;
import com.ruoyi.ss.risk.domain.enums.RiskType;
import com.ruoyi.ss.risk.service.IRiskService;
import com.ruoyi.ss.store.domain.StoreVo;
import com.ruoyi.ss.store.service.StoreValidator;
import com.ruoyi.ss.suit.domain.SuitVO;
@ -54,6 +62,9 @@ public class TransactionBillValidatorImpl extends BaseValidator implements Trans
@Autowired
private TransactionBillService transactionBillService;
@Autowired
private IRiskService riskService;
@Override
public ValidateResult preAddOrder(RechargeBO bo) {
if (bo == null) {
@ -320,4 +331,28 @@ public class TransactionBillValidatorImpl extends BaseValidator implements Trans
public boolean isAgent(TransactionBillVO bill, Long userId) {
return bill != null && userId != null && Objects.equals(bill.getAgentId(), userId);
}
@Override
public void checkRefundRisk(SmUserVo user) {
if (user == null || user.getUserId() == null) {
return;
}
// 查询未结束的风控列表
RiskQuery query = new RiskQuery();
query.setUserId(user.getUserId());
query.setContainsType(RiskType.REFUND.getType());
List<RiskVO> riskList = riskService.selectNotFinishedList(query);
if (CollectionUtils.isNotEmptyElement(riskList)) {
RiskVO risk = riskList.get(0);
ServiceUtil.assertion(risk == null, "风控信息异常");
LocalDateTime endTime = risk.getEndTime();
if (endTime == null) {
throw new ServiceException("您被限制退款:" + risk.getReason(), ServiceCode.RISK_REFUND.getCode());
} else if (endTime.isAfter(LocalDateTime.now())){
throw new ServiceException("您被限制退款至" + DateUtils.format(endTime, DateUtils.YYYY_MM_DD_HH_MM_SS) + ":" + risk.getReason(), ServiceCode.RISK_REFUND.getCode());
}
}
}
}

View File

@ -2,7 +2,15 @@ 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.enums.ServiceCode;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.ServiceUtil;
import com.ruoyi.common.utils.collection.CollectionUtils;
import com.ruoyi.ss.risk.domain.RiskQuery;
import com.ruoyi.ss.risk.domain.RiskVO;
import com.ruoyi.ss.risk.domain.enums.RiskType;
import com.ruoyi.ss.risk.service.IRiskService;
import com.ruoyi.ss.transactionBill.domain.TransactionBillQuery;
import com.ruoyi.ss.transactionBill.domain.bo.WithdrawBO;
import com.ruoyi.ss.transactionBill.domain.dto.WithdrawDTO;
@ -17,8 +25,10 @@ import com.ruoyi.system.service.ISysConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
/**
* @author wjh
@ -32,6 +42,10 @@ public class WithdrawValidatorImpl extends BaseValidator implements WithdrawVali
@Autowired
private ISysConfigService sysConfigService;
@Autowired
private IRiskService riskService;
@Autowired
private RedisCache redisCache;
@ -48,27 +62,47 @@ public class WithdrawValidatorImpl extends BaseValidator implements WithdrawVali
SmUserVo user = bo.getUser();
WithdrawDTO dto = bo.getDto();
// 查询最近一次的订单
// 查询时间相近的充值订单
int riskWithdrawTime = sysConfigService.getInt(ConfigKey.RISK_WITHDRAW_TIME); // 最低允许相隔时长分钟
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) {
query.setCreateTimeStart(LocalDateTime.now().plusMinutes(-riskWithdrawTime));
List<TransactionBillVO> list = transactionBillService.selectSmTransactionBillList(query);
// 计算总金额
BigDecimal rechargeAmount = list.stream().map(TransactionBillVO::getArrivalAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
if (rechargeAmount.compareTo(BigDecimal.ZERO) == 0) {
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;
// 判断订单金额是否和提现一致若一致则判定为风险
// FIXME 可以考虑使用一个范围在这个误差范围内均为风险
return dto.getMoney() != null && rechargeAmount.compareTo(dto.getMoney()) == 0;
}
@Override
public void checkWithdrawRisk(SmUserVo user) {
if (user == null || user.getUserId() == null) {
return;
}
return false;
// 查询未结束的风控列表
RiskQuery query = new RiskQuery();
query.setUserId(user.getUserId());
query.setContainsType(RiskType.WITHDRAW.getType());
List<RiskVO> riskList = riskService.selectNotFinishedList(query);
if (CollectionUtils.isNotEmptyElement(riskList)) {
RiskVO risk = riskList.get(0);
ServiceUtil.assertion(risk == null, "风控信息异常");
LocalDateTime endTime = risk.getEndTime();
if (endTime == null) {
throw new ServiceException("您被限制提现:" + risk.getReason(), ServiceCode.RISK_WITHDRAW.getCode());
} else if (endTime.isAfter(LocalDateTime.now())){
throw new ServiceException("您被限制提现至" + DateUtils.format(endTime, DateUtils.YYYY_MM_DD_HH_MM_SS) + ":" + risk.getReason(), ServiceCode.RISK_WITHDRAW.getCode());
}
}
}
}

View File

@ -12,6 +12,7 @@ public class UserFaceDTO {
private String token;
private Long userId;
private String type;
private String name;
private String idCard;
private String mobile;

View File

@ -1,10 +1,13 @@
package com.ruoyi.ss.user.domain.dto;
import com.ruoyi.common.utils.RegexpUtils;
import com.ruoyi.common.valid.EnumValid;
import com.ruoyi.ss.realName.domain.enums.RealNameType;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
@ -18,6 +21,9 @@ public class UserRealNameDTO {
@ApiModelProperty("用户ID")
private Long userId;
@ApiModelProperty("实名认证类型")
private String type;
@ApiModelProperty("姓名")
@NotBlank(message = "姓名不允许为空")
@Size(max = 20, message = "姓名长度不能超过20")

View File

@ -218,16 +218,6 @@ public interface ISmUserService
*/
int readMchLicence(Long userId);
/**
* 限制提现
* @param userId
* @param limitTime
* @param reason
* @param remark
* @return
*/
int limitWithdraw(Long userId, LocalDateTime limitTime, String reason, String remark);
/**
* 添加一次风险次数
*/

View File

@ -21,14 +21,18 @@ import com.ruoyi.common.valid.liveness.LivenessResponseQueryData;
import com.ruoyi.common.valid.liveness.LivenessResponseTokenData;
import com.ruoyi.common.valid.liveness.LivenessUtils;
import com.ruoyi.common.valid.liveness.enums.LivenessQueryResult;
import com.ruoyi.common.valid.realName.RealNameValidUtils;
import com.ruoyi.ss.device.domain.DeviceQuery;
import com.ruoyi.ss.device.domain.vo.DeviceVO;
import com.ruoyi.ss.device.service.DeviceService;
import com.ruoyi.ss.realName.domain.RealName;
import com.ruoyi.ss.realName.domain.enums.RealNameType;
import com.ruoyi.ss.realName.service.IRealNameService;
import com.ruoyi.ss.recordBalance.domain.enums.RecordBalanceBstType;
import com.ruoyi.ss.recordBalance.service.RecordBalanceService;
import com.ruoyi.ss.risk.domain.Risk;
import com.ruoyi.ss.risk.domain.RiskQuery;
import com.ruoyi.ss.risk.domain.RiskVO;
import com.ruoyi.ss.risk.service.IRiskService;
import com.ruoyi.ss.store.domain.StoreQuery;
import com.ruoyi.ss.store.domain.StoreVo;
import com.ruoyi.ss.store.service.StoreService;
@ -51,7 +55,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -105,6 +108,9 @@ public class SmUserServiceImpl implements ISmUserService
@Autowired
private WxAuthService wxAuthService;
@Autowired
private IRiskService riskService;
/**
* 查询普通用户信息
*
@ -348,12 +354,21 @@ public class SmUserServiceImpl implements ISmUserService
public UserRealNameVO realName(UserRealNameDTO dto) {
SmUserVo user = selectSmUserByUserId(dto.getUserId());
ServiceUtil.assertion(user == null, "用户信息不存在");
// ServiceUtil.assertion(user.getIsReal() != null && user.getIsReal(), "您已进行过实名认证,无需再次认证");
// 调用第三方API三要素验证
// NOTE:无需三要素只需要姓名身份证即可
// boolean check = RealNameValidUtils.validMobile3Info(dto.getRealIdCard(), dto.getRealPhone(), dto.getRealName());
// ServiceUtil.assertion(!check, "请输入正确的身份信息:姓名、身份证、手机号需要一致");
// 若用户已经实名则需要用户填写一样的实名信息
if (user.getIsReal()) {
if (user.getRealName() != null) {
dto.setRealName(user.getRealName());
}
if (user.getRealPhone() != null) {
dto.setRealPhone(user.getRealPhone());
}
if (user.getRealIdCard() != null) {
dto.setRealIdCard(user.getRealIdCard());
}
}
ServiceUtil.assertion(StringUtils.isBlank(dto.getRealName()), "用户姓名不允许为空");
ServiceUtil.assertion(StringUtils.isBlank(dto.getRealIdCard()), "用户身份证不允许为空");
// 获取活体检测token
LivenessResponseTokenData tokenData = LivenessUtils.getToken(LIVENESS_RETURN_URL, null, null);
@ -367,6 +382,7 @@ public class SmUserServiceImpl implements ISmUserService
faceDto.setName(dto.getRealName());
faceDto.setMobile(dto.getRealPhone());
faceDto.setIdCard(dto.getRealIdCard());
faceDto.setType(dto.getType());
redisCache.setCacheObject(cacheKey, faceDto, 10, TimeUnit.MINUTES);
// 返回数据
@ -385,20 +401,6 @@ public class SmUserServiceImpl implements ISmUserService
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);
@ -435,15 +437,7 @@ public class SmUserServiceImpl implements ISmUserService
ServiceUtil.assertion(!FaceIncorrect.isSuccess(faceIdb.getIncorrect()), "人证比对未通过:" + faceIdb.getMsg());
ServiceUtil.assertion(!FaceScoreResult.isSame(faceIdb.getScore()), "人证比对未通过,分数:" + faceIdb.getScore());
// 通过修改实名认证信息用户表实名认证表
// 构造用户信息
SmUser data = new SmUserQuery();
data.setUserId(face.getUserId());
data.setRealName(face.getName());
data.setRealIdCard(face.getIdCard());
data.setRealPhone(face.getMobile());
data.setIsReal(true);
// 构造实名认证表
// 通过构造实名认证表
RealName realName = new RealName();
realName.setUserId(userId);
realName.setName(face.getName());
@ -451,6 +445,63 @@ public class SmUserServiceImpl implements ISmUserService
realName.setMobile(face.getMobile());
realName.setScore(faceIdb.getScore());
realName.setFaceImage(imageUrl);
realName.setType(face.getType());
// 普通实名
if (RealNameType.NORMAL.getType().equals(face.getType())) {
return this.handleNormalRealName(realName, cacheKey);
}
// 风控实名修改用户风控信息
else {
return this.handleRiskRealName(realName, cacheKey);
}
}
/**
* 处理风控实名认证成功
* @param realName 实名认证
* @param cacheKey 缓存key
*/
private int handleRiskRealName(RealName realName, String cacheKey) {
// 查询用户是否有风控信息
RiskQuery query = new RiskQuery();
query.setUserId(realName.getUserId());
List<RiskVO> riskList = riskService.selectNotFinishedList(query);
Integer result = transactionTemplate.execute(status -> {
// 插入实名认证信息
int insert = realNameService.insertRealName(realName);
ServiceUtil.assertion(insert != 1, "新增实名认证失败");
// 修改风控信息
if (CollectionUtils.isNotEmptyElement(riskList)) {
int update = riskService.batchFinish(CollectionUtils.map(riskList, Risk::getRiskId), realName.getId());
ServiceUtil.assertion(update != riskList.size(), "修改风控信息失败");
}
// 删除缓存
redisCache.deleteObject(cacheKey);
return insert;
});
return result == null ? 0 : result;
}
/**
* 处理普通实名认证成功
* @param realName 实名认证
* @param cacheKey 缓存key
*/
private int handleNormalRealName(RealName realName, String cacheKey) {
// 修改实名认证信息用户表
SmUser data = new SmUserQuery();
data.setUserId(realName.getUserId());
data.setRealName(realName.getName());
data.setRealIdCard(realName.getIdCard());
data.setRealPhone(realName.getMobile());
data.setIsReal(true);
// 数据库操作
Integer result = transactionTemplate.execute(status -> {
// 修改用户实名信息

View File

@ -253,15 +253,7 @@ public class AppTransactionBillController extends BaseController
// 判断是否限制退款
SmUserVo user = userService.selectSmUserByUserId(userId);
boolean limitRefund = user.getLimitRefund() != null && user.getLimitRefund();
if (limitRefund) {
LocalDateTime limitRefundTime = user.getLimitRefundTime();
if (limitRefundTime == null) {
throw new ServiceException("您被永久限制退款:" + user.getLimitRefundReason());
} else if (limitRefundTime.isAfter(LocalDateTime.now())){
throw new ServiceException("您被限制退款至" + DateUtils.format(limitRefundTime, DateUtils.YYYY_MM_DD_HH_MM_SS) + ":" + user.getLimitRefundReason() );
}
}
transactionBillValidator.checkRefundRisk(user);
LoginUser loginUser = getLoginUser();
dto.setUserName(loginUser.getUsername());

View File

@ -12,6 +12,7 @@ import com.ruoyi.common.utils.ServiceUtil;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.SysPasswordService;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.ss.realName.domain.enums.RealNameType;
import com.ruoyi.ss.user.domain.SmUserVo;
import com.ruoyi.ss.user.domain.dto.UserChangeMobileDTO;
import com.ruoyi.ss.user.domain.dto.UserRealNameDTO;
@ -101,6 +102,16 @@ public class AppUserController extends BaseController {
@ApiOperation("用户实名认证")
@PutMapping("/realName")
public AjaxResult realName(@RequestBody @Validated UserRealNameDTO dto) {
dto.setType(RealNameType.NORMAL.getType());
dto.setUserId(getUserId());
return success(userService.realName(dto));
}
@ApiOperation("用户风控实名认证")
@PutMapping("/riskRealName")
public AjaxResult riskRealName() {
UserRealNameDTO dto = new UserRealNameDTO();
dto.setType(RealNameType.RISK.getType());
dto.setUserId(getUserId());
return success(userService.realName(dto));
}

View File

@ -134,4 +134,4 @@ tm:
# 活体检测跳转地址
liveness:
returnUrl: http://192.168.2.106:3001/liveness
returnUrl: http://192.168.2.15:3001/liveness