Преглед изворни кода

feat:agent用户列表新增登陆ip检索; 登陆风控322支持通过登陆IP及设备应用列表判断设备唯一性

hidewnd пре 1 месец
родитељ
комит
1cbc66592f

+ 7 - 0
yt-common/src/main/java/com/ytpm/app/param/LoginRiskYtDyzUserParam.java

@@ -21,6 +21,12 @@ public class LoginRiskYtDyzUserParam implements Serializable {
     @ApiModelProperty("最新登录IP")
     private String lastLoginIp;
 
+    @ApiModelProperty("设备信息")
+    private String phoneJson;
+
+    @ApiModelProperty("风控322 限制天数内")
+    private Integer limitDays322;
+
     @ApiModelProperty("最新登陆IP解析地址")
     private String lastLoginIpAddress;
 
@@ -32,4 +38,5 @@ public class LoginRiskYtDyzUserParam implements Serializable {
 
     @ApiModelProperty("风控334 限制小时数")
     private Integer limitHour335;
+
 }

+ 5 - 0
yt-common/src/main/java/com/ytpm/app/param/YtAppUserListParam.java

@@ -38,4 +38,9 @@ public class YtAppUserListParam extends PageMeta {
     private String appIds;
     @ApiModelProperty("应用类型")
     private String appType;
+
+    @ApiModelProperty("最新登陆IP")
+    private String lastLoginIp;
+    @ApiModelProperty("同IP激励视频数提醒阈值")
+    private Integer videoThreshold;
 }

+ 22 - 0
yt-common/src/main/java/com/ytpm/app/view/IpAdRecordView.java

@@ -0,0 +1,22 @@
+package com.ytpm.app.view;
+
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author lih
+ * @date 2025-12-26 17:14
+ */
+@Data
+public class IpAdRecordView implements Serializable {
+
+    private String iosId;
+
+    private String userId;
+
+    private Long adCount;
+
+
+}

+ 3 - 0
yt-common/src/main/java/com/ytpm/app/view/RiskYtDyzUserView.java

@@ -23,4 +23,7 @@ public class RiskYtDyzUserView implements Serializable {
 
     @ApiModelProperty("同地址登陆用户数")
     private Integer cityUserCount;
+
+    @ApiModelProperty("基于IP和应用列表识别的相同设备数")
+    private Long sameDeviceCount;
 }

+ 3 - 0
yt-common/src/main/java/com/ytpm/app/view/YtAppUserListView.java

@@ -99,6 +99,9 @@ public class YtAppUserListView extends PageMeta {
     @ApiModelProperty("今日答题数")
     private Integer todayAnswer;
 
+    @ApiModelProperty("该IP下是否存在任务已完成的用户")
+    private Integer maybeDone;
+
     /** 登录历史记录 */
     @ApiModelProperty("登录历史记录")
     private List<YtDyzLoginRecord> loginRecordList;

+ 39 - 5
yt-ios-lemon/lemon-ios-service/src/main/java/com/ytpm/lemonios/controller/UserController.java

@@ -3,6 +3,7 @@ package com.ytpm.lemonios.controller;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.DateTime;
 import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.toolkit.StringUtils;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
@@ -24,6 +25,7 @@ import com.ytpm.app.param.AppUserTodayBannedParam;
 import com.ytpm.app.param.LoginRiskYtDyzUserParam;
 import com.ytpm.app.param.YtAppUserListParam;
 import com.ytpm.app.view.HourCountView;
+import com.ytpm.app.view.IpAdRecordView;
 import com.ytpm.app.view.RiskYtDyzUserView;
 import com.ytpm.app.view.YtAppUserListView;
 import com.ytpm.general.RepMessage;
@@ -55,6 +57,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -119,6 +122,13 @@ public class UserController {
         // 今日答题记录
         Map<String, Long> todayAnswerMap = answerRecords.stream().collect(
                 Collectors.groupingBy(YtDyzAnswerRecord::getUserId, Collectors.counting()));
+        Map<String, Long> ipAdRecordMap = new HashMap<>();
+        // 查询该IP是否有激励视频已达要求的用户
+        if (StrUtil.isNotEmpty(param.getLastLoginIp()) && param.getVideoThreshold() != null) {
+            ipAdRecordMap = adRecordMapper.queryGroupByIpSourceType(param)
+                    .stream().collect(Collectors.toMap(IpAdRecordView::getUserId, IpAdRecordView::getAdCount,
+                            (o1, o2) -> o2));
+        }
         for (YtAppUserListView user : result) {
             if (loginMap.containsKey(user.getUserId())) {
                 user.setLoginRecordList(loginMap.get(user.getUserId()));
@@ -138,7 +148,12 @@ public class UserController {
                     user.setNearlyIncome(user.getNearlyIncome() == null ? nearlyRevenue : user.getNearlyIncome().add(nearlyRevenue));
                 }
             }
+            user.setMaybeDone(0);
             user.setTodayAnswer(Math.toIntExact(todayAnswerMap.getOrDefault(user.getUserId(), 0L)));
+            if (StrUtil.equals(param.getLastLoginIp(), user.getLastLoginIp()) && param.getVideoThreshold() != null
+                    && ipAdRecordMap.get(user.getUserId()) != null && param.getVideoThreshold() <= ipAdRecordMap.get(user.getUserId())) {
+                user.setMaybeDone(1);
+            }
         }
     }
 
@@ -385,10 +400,11 @@ public class UserController {
     @PostMapping("/queryUserByParam")
     public RiskYtDyzUserView queryUserByParam(@RequestBody LoginRiskYtDyzUserParam param) {
         RiskYtDyzUserView riskYtDyzUserView = new RiskYtDyzUserView();
+        Calendar instance = Calendar.getInstance();
+        Date currentDate = new Date();
         // 查询指定时间内同ip登陆用户数
         if (StringUtils.isNotEmpty(param.getLastLoginIp()) && param.getLimitHour() != null) {
-            Calendar instance = Calendar.getInstance();
-            instance.setTime(new Date());
+            instance.setTime(currentDate);
             instance.add(Calendar.HOUR_OF_DAY, -param.getLimitHour());
             Date startTime = instance.getTime();
             Long userCount = appUserMapper.queryCountByIpTime(param.getLastLoginIp(), startTime);
@@ -401,14 +417,32 @@ public class UserController {
         }
         // 查询指定地区的的所有登陆用户
         if (StringUtils.isNotEmpty(param.getLimitCity()) && param.getLimitHour335() != null) {
-            List<String> cityList = Arrays.asList(paramx.getLimitCity().split(","));
-            Calendar instance = Calendar.getInstance();
-            instance.setTime(new Date());
+            List<String> cityList = Arrays.asList(param.getLimitCity().split(","));
+            instance.setTime(currentDate);
             instance.add(Calendar.HOUR_OF_DAY, -param.getLimitHour());
             Date startTime = instance.getTime();
             Long userCount = appUserMapper.queryCountByIpRegion(cityList, startTime);
             riskYtDyzUserView.setCityUserCount(Math.toIntExact(userCount));
         }
+        // 查询三天内 该渠道 相同IP 相同设备应用列表的用户数
+        if (StrUtil.isNotEmpty(param.getLastLoginIp()) && StrUtil.isNotEmpty(param.getPhoneJson())) {
+            JSONObject phoneJson = JSONObject.parseObject(param.getPhoneJson());
+            String appList = phoneJson.get("appList") == null ? null : phoneJson.getString("appList");
+            if ("[]".equals(appList)) {
+                appList = "";
+            }
+            if (StrUtil.isNotEmpty(appList)) {
+                String lastLoginIp = param.getLastLoginIp();
+                Date startTime = null;
+                if (param.getLimitDays322() != null) {
+                    instance.setTime(currentDate);
+                    instance.add(Calendar.DAY_OF_MONTH, -param.getLimitDays322());
+                    startTime = instance.getTime();
+                }
+                Long count = appUserMapper.countUserByLastLoginIpAndAppList(lastLoginIp, appList, startTime);
+                riskYtDyzUserView.setSameDeviceCount(count);
+            }
+        }
         return riskYtDyzUserView;
     }
 

+ 20 - 0
yt-ios-lemon/lemon-ios-service/src/main/java/com/ytpm/lemonios/controller/WxController.java

@@ -5,6 +5,7 @@ import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.http.HttpUtil;
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.ytpm.agent.enums.AdRecordEnum;
 import com.ytpm.agent.enums.UserStatusEnum;
 import com.ytpm.agent.model.YtDitch;
@@ -51,9 +52,11 @@ import java.math.BigDecimal;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 
 @Slf4j
@@ -261,6 +264,22 @@ public class WxController {
             }
             redisService.del(redisKey);
         }
+        // phoneJson 处理
+        if (StrUtil.isNotEmpty(param.getPhoneJson())) {
+            JSONObject jsonObject = JSONObject.parseObject(param.getPhoneJson());
+            List<String> appList = null;
+            if (jsonObject.get("appList") != null) {
+                appList = jsonObject.getJSONArray("appList").toJavaList(String.class);
+            }
+            if (jsonObject.get("app_list") != null) {
+                appList = jsonObject.getJSONArray("app_list").toJavaList(String.class);
+            }
+            if (CollUtil.isNotEmpty(appList)) {
+                appList = appList.stream().map(String::toLowerCase).sorted().collect(Collectors.toList());
+                jsonObject.put("appList", appList);
+                param.setPhoneJson(JSONObject.toJSONString(jsonObject));
+            }
+        }
         IosUserInfo userInfo = setIosUserInfo(param);
         param.setLoginIp(getClientIp(request));
         // 获取或注册用户
@@ -286,6 +305,7 @@ public class WxController {
         newUser.setUserStatus(UserStatusEnum.NORMAL.getCode());
         appUserMapper.updateUser(newUser);
         //调用风控服务校验默认风控配置
+        dyzUser.setPhoneJson(param.getPhoneJson());
         dyzUser.setLoginType(LoginType.IOS);
         dyzUser.setRiskCode("313");
         Result<?> result = riskFeign.checkRisk(dyzUser);

+ 3 - 0
yt-ios-lemon/lemon-ios-service/src/main/java/com/ytpm/lemonios/dao/AdRecordMapper.java

@@ -3,6 +3,7 @@ package com.ytpm.lemonios.dao;
 import com.ytpm.agent.param.AdRecordListParam;
 import com.ytpm.app.model.YtDyzAdRecord;
 import com.ytpm.app.param.YtAppUserListParam;
+import com.ytpm.app.view.IpAdRecordView;
 import com.ytpm.middle.view.AppRevenueHourVO;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
@@ -86,4 +87,6 @@ public interface AdRecordMapper {
     void batchAdd(@Param("adRecords") List<YtDyzAdRecord> saveList);
 
     long getAdCountByLoadId(@Param("iosId") String iosId, @Param("loadId") String loadId);
+
+    List<IpAdRecordView> queryGroupByIpSourceType(@Param("param") YtAppUserListParam param);
 }

+ 3 - 0
yt-ios-lemon/lemon-ios-service/src/main/java/com/ytpm/lemonios/dao/AppUserMapper.java

@@ -239,4 +239,7 @@ public interface AppUserMapper {
     Long queryCountByIpTime(@Param("lastLoginIp") String lastLoginIp, @Param("date") Date startTime);
 
     Long queryCountByIpRegion(@Param("cityList") List<String> cityList,@Param("date") Date startTime);
+
+    Long countUserByLastLoginIpAndAppList(@Param("lastLoginIp") String lastLoginIp, @Param("appList") String appList,
+                                          @Param("startTime") Date startTime);
 }

+ 1 - 1
yt-ios-lemon/lemon-ios-service/src/main/resources/bootstrap.yml

@@ -4,7 +4,7 @@ spring:
   main:
     allow-bean-definition-overriding: true
   application:
-    name: lemonios-service
+    name: typingios-service
     name-zh: '青柠檬IOS'
   profiles:
     active: local

+ 27 - 0
yt-ios-lemon/lemon-ios-service/src/main/resources/mapper/AdRecordMapper.xml

@@ -373,4 +373,31 @@
         </if>
         where record_id in <foreach collection="ids" item="id" open="(" close=")" separator=",">#{id}</foreach>
     </select>
+    <select id="queryGroupByIpSourceType" resultType="com.ytpm.app.view.IpAdRecordView">
+        select ydar.ios_id, ydu.user_id, count(ydar.record_id) as ad_count
+        from yt_dyz_ad_record ydar
+        left join yt_dyz_user ydu on ydu.ios_id = ydar.ios_id
+        where ydar.ad_source_type = 1 and ydu.last_login_ip = #{param.lastLoginIp}
+        <if test="param.ditchId != null">
+            and ydu.ditch_id = #{param.ditchId}
+        </if>
+        <if test="param.userStatus != null">
+            and ydu.user_status = #{param.userStatus}
+        </if>
+        <if test="param.appIds != null and param.appIds != ''">
+            and ydu.app_id in
+            <foreach collection="param.appIds.split(',')" separator="," item="item" open="(" close=")">
+                #{item}
+            </foreach>
+        </if>
+        <if test="param.registryTimeBegin != null ">
+            and DATE_FORMAT(ydu.registry_time,'%Y-%m-%d') <![CDATA[>=]]> #{param.registryTimeBegin}
+        </if>
+        <if test="param.registryTimeEnd != null ">
+            and DATE_FORMAT(ydu.registry_time,'%Y-%m-%d') <![CDATA[<=]]> #{param.registryTimeEnd}
+        </if>
+        group by ydu.app_id, ydar.user_id
+        having ad_count <![CDATA[>=]]>  #{param.videoThreshold}
+        order by ydu.last_login_time
+    </select>
 </mapper>

+ 50 - 28
yt-ios-lemon/lemon-ios-service/src/main/resources/mapper/AppUserMapper.xml

@@ -334,37 +334,41 @@
         sign_days, user_status, risk_reason, wx_open_id, ios_id, platform_id,
         nearly_income, nearly_begin_time
         from yt_dyz_user
-        where 1 = 1
-        <if test="userId != null and userId !=''">
-            and user_id = #{userId}
-        </if>
-        <if test="nickName != null and nickName !=''">
-            and nick_name like concat('%',#{nickName},'%')
-        </if>
-        <if test="ditchId != null">
-            and ditch_id = #{ditchId}
-        </if>
-        <if test="userStatus != null">
-            and user_status = #{userStatus}
-        </if>
-        <if test="registryTimeBegin != null">
-            and DATE_FORMAT(registry_time, '%Y-%m-%d') <![CDATA[>=]]> DATE_FORMAT(#{registryTimeBegin}, '%Y-%m-%d')
-        </if>
-        <if test="registryTimeEnd != null">
-            and DATE_FORMAT(registry_time, '%Y-%m-%d') <![CDATA[<=]]> DATE_FORMAT(#{registryTimeEnd}, '%Y-%m-%d')
-        </if>
-        <if test="lastLoginTimeBegin != null">
-            and DATE_FORMAT(last_login_time, '%Y-%m-%d') <![CDATA[>=]]> DATE_FORMAT(#{lastLoginTimeBegin}, '%Y-%m-%d')
-        </if>
-        <if test="lastLoginTimeEnd != null">
-            and DATE_FORMAT(last_login_time, '%Y-%m-%d') <![CDATA[<=]]> DATE_FORMAT(#{lastLoginTimeEnd}, '%Y-%m-%d')
-        </if>
-        <if test="appIds != null and appIds != ''">
-            and app_id in
+        <where>
+            <if test="userId != null and userId !=''">
+                and user_id = #{userId}
+            </if>
+            <if test="nickName != null and nickName !=''">
+                and nick_name like concat('%',#{nickName},'%')
+            </if>
+            <if test="lastLoginIp != null and lastLoginIp !=''">
+                and last_login_ip like concat(#{lastLoginIp},'%')
+            </if>
+            <if test="ditchId != null">
+                and ditch_id = #{ditchId}
+            </if>
+            <if test="userStatus != null">
+                and user_status = #{userStatus}
+            </if>
+            <if test="registryTimeBegin != null">
+                and DATE_FORMAT(registry_time, '%Y-%m-%d') <![CDATA[>=]]> DATE_FORMAT(#{registryTimeBegin}, '%Y-%m-%d')
+            </if>
+            <if test="registryTimeEnd != null">
+                and DATE_FORMAT(registry_time, '%Y-%m-%d') <![CDATA[<=]]> DATE_FORMAT(#{registryTimeEnd}, '%Y-%m-%d')
+            </if>
+            <if test="lastLoginTimeBegin != null">
+                and DATE_FORMAT(last_login_time, '%Y-%m-%d') <![CDATA[>=]]> DATE_FORMAT(#{lastLoginTimeBegin}, '%Y-%m-%d')
+            </if>
+            <if test="lastLoginTimeEnd != null">
+                and DATE_FORMAT(last_login_time, '%Y-%m-%d') <![CDATA[<=]]> DATE_FORMAT(#{lastLoginTimeEnd}, '%Y-%m-%d')
+            </if>
+            <if test="appIds != null and appIds != ''">
+                and app_id in
                 <foreach collection="appIds.split(',')" separator="," item="item" open="(" close=")">
                     #{item}
                 </foreach>
-        </if>
+            </if>
+        </where>
         order by user_id desc
     </select>
     <select id="selectPrimaryKey" resultType="com.ytpm.app.model.YtDyzUser">
@@ -1102,6 +1106,24 @@
             </foreach>
         )
     </select>
+    <select id="countUserByLastLoginIpAndAppList" resultType="java.lang.Long">
+        SELECT
+            COUNT(DISTINCT u.user_id) AS user_count
+        FROM yt_dyz_user u
+        INNER JOIN yt_dyz_login_record lr ON u.user_id = lr.user_id
+        WHERE
+          u.last_login_ip = #{lastLoginIp}
+          AND lr.phone_json IS NOT NULL
+          AND JSON_CONTAINS_PATH(CAST(lr.phone_json AS JSON), 'one', '$.app_list') = 1
+          AND JSON_CONTAINS(
+              JSON_EXTRACT(CAST(lr.phone_json AS JSON), '$.app_list'),
+              CAST(#{appList} AS JSON),
+              '$'
+          ) = 1
+          <if test="startTime != null">
+              AND u.last_login_time <![CDATA[>=]]> #{startTime}
+          </if>
+    </select>
     <update id="unlockUser">
         update yt_dyz_user
         set user_status = 1

+ 16 - 3
yt-risk/risk-manage/src/main/java/com/ytpm/service/impl/RiskServiceImpl.java

@@ -419,14 +419,16 @@ public class RiskServiceImpl implements RiskService {
         List<YtDyzUser> dyzUsers = null;
         Integer userCount = null;
         Integer cityUserCount = null;
+        Long sameDeviceCount = null;
         YtApp ytApp = appMapper.selectRiskApp(dyzUser.getAppId());
         RiskYtDyzUserView riskYtDyzUserView = queryParamByUnified(dyzUser, ytApp);
         if (riskYtDyzUserView != null) {
             dyzUsers = riskYtDyzUserView.getUserList();
+            sameDeviceCount = riskYtDyzUserView.getSameDeviceCount();
             userCount = riskYtDyzUserView.getUserCount();
             cityUserCount = riskYtDyzUserView.getCityUserCount();
         }
-        checkRisk322(dyzUser, dyzUsers);
+        checkRisk322(dyzUser, dyzUsers, sameDeviceCount);
         if (ytApp.getAppType() == 2) {
             checkRisk334(dyzUser, userCount);
             checkRisk335(dyzUser, cityUserCount);
@@ -450,6 +452,16 @@ public class RiskServiceImpl implements RiskService {
                 LoginRiskYtDyzUserParam param = new LoginRiskYtDyzUserParam();
                 param.setIosId(dyzUser.getIosId());
                 param.setAppId(dyzUser.getAppId());
+                param.setPhoneJson(dyzUser.getPhoneJson());
+                // 传递322风控限制参数
+                if (riskTempView322 != null && riskTempView322.getEnabled() == 1) {
+                    List<RiskConfigView> configList = riskTempView322.getConfigList();
+                    Map<String, String> configMap = configList.stream().collect(
+                            Collectors.toMap(RiskConfigView::getFieldName, RiskConfigView::getConfigVal));
+                    int days = Integer.parseInt(configMap.get("days"));
+                    param.setLimitDays322(days);
+                }
+                // 传递332风控限制参数
                 if (riskTempView334 != null && riskTempView334.getEnabled() == 1) {
                     param.setLastLoginIp(dyzUser.getLastLoginIp());
                     List<RiskConfigView> configList = riskTempView334.getConfigList();
@@ -458,6 +470,7 @@ public class RiskServiceImpl implements RiskService {
                     int limitHour = Integer.parseInt(configMap.get("limitHour"));
                     param.setLimitHour(limitHour);
                 }
+                // 传递335风控限制参数
                 if (riskTempView335 != null && riskTempView335.getEnabled() == 1) {
                     List<RiskConfigView> configList = riskTempView335.getConfigList();
                     Map<String, String> configMap = configList.stream().collect(
@@ -580,7 +593,7 @@ public class RiskServiceImpl implements RiskService {
     /**
      * 校验风控规则322
      */
-    private void checkRisk322(YtDyzUser dyzUser, List<YtDyzUser> dyzUsers) {
+    private void checkRisk322(YtDyzUser dyzUser, List<YtDyzUser> dyzUsers, Long sameDeviceCount) {
         // ip白名单用户直接放行
         if (checkDyzUserIp(dyzUser)) {
             return;
@@ -609,7 +622,7 @@ public class RiskServiceImpl implements RiskService {
                 s -> (days > DateUtil.between(new Date(), s.getRegistryTime(), DateUnit.DAY))
         ).count();
         //三天内注册的渠道数小于预设的渠道数通过校验,否则风控锁定用户
-        if (ditchCount < uidCount) return;
+        if (ditchCount < uidCount  && (sameDeviceCount == null || sameDeviceCount <= 1)) return;
         String tips = getTipsMsg();
         if (dyzUser.getLoginType() != null && LoginType.IOS == dyzUser.getLoginType() || StrUtil.isNotEmpty(dyzUser.getIosId())) {
             tips = StrUtil.isNotEmpty(iosDithTip) ? iosDithTip : tips;