粉丝数构造

This commit is contained in:
2026-07-29 17:47:29 +08:00
parent 1a7fff68c0
commit 74abf74ec8
4 changed files with 267 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
package com.rj.controller; package com.rj.controller;
import com.rj.dto.LbFanRecursiveFetchRequest;
import com.rj.dto.LbFanSimulateLoginRequest; import com.rj.dto.LbFanSimulateLoginRequest;
import com.rj.entity.LbFanManagement; import com.rj.entity.LbFanManagement;
import com.rj.service.ILbFanManagementService; import com.rj.service.ILbFanManagementService;
@@ -52,7 +53,7 @@ public class LbFanManagementController {
@PostMapping("/simulate-login") @PostMapping("/simulate-login")
@Operation( @Operation(
summary = "批量模拟第三方登录", summary = "批量模拟第三方登录,并发现粉丝",
description = description =
"按 tenantId 从 lb_third_integration_config 读取 login_api_path 等配置," "按 tenantId 从 lb_third_integration_config 读取 login_api_path 等配置,"
+ "对每个手机号调用第三方登录接口并解析返回 JSON;" + "对每个手机号调用第三方登录接口并解析返回 JSON;"
@@ -64,6 +65,20 @@ public class LbFanManagementController {
return toResponse(result); return toResponse(result);
} }
@PostMapping("/recursive-fetch")
@Operation(
summary = "递归发现所有粉丝(包括粉丝的粉丝)",
description =
"按 tenantId 从 lb_third_integration_config 读取配置,"
+ "对每个手机号登录后拉取粉丝,再递归登录粉丝拉取粉丝的粉丝;"
+ "支持最大递归深度限制和跳过已存在粉丝")
public ResponseEntity<Map<String, Object>> recursiveFetchFans(
@Parameter(description = "递归发现粉丝请求", required = true)
@RequestBody LbFanRecursiveFetchRequest request) {
Map<String, Object> result = lbFanManagementService.recursiveFetchFans(request);
return toResponse(result);
}
@GetMapping("/list") @GetMapping("/list")
@Operation(summary = "分页查询粉丝") @Operation(summary = "分页查询粉丝")
public ResponseEntity<Map<String, Object>> list( public ResponseEntity<Map<String, Object>> list(

View File

@@ -0,0 +1,29 @@
package com.rj.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 粉丝管理:递归发现所有粉丝(包括粉丝的粉丝)。
*/
@Data
@Schema(description = "递归发现粉丝请求")
public class LbFanRecursiveFetchRequest {
@Schema(description = "租户 id,关联 lb_third_integration_config.tenant_id", requiredMode = Schema.RequiredMode.REQUIRED)
private String tenantId;
@Schema(description = "手机号列表(作为根节点开始递归发现)", requiredMode = Schema.RequiredMode.REQUIRED)
private List<String> mobiles;
@Schema(description = "登录密码;未传时默认为 123456")
private String password;
@Schema(description = "最大递归深度;不传或传 0 时不限深度")
private Integer maxDepth;
@Schema(description = "是否跳过已存在的粉丝;不传时默认为 true")
private Boolean skipExisting;
}

View File

@@ -1,6 +1,7 @@
package com.rj.service; package com.rj.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.rj.dto.LbFanRecursiveFetchRequest;
import com.rj.dto.LbFanSimulateLoginRequest; import com.rj.dto.LbFanSimulateLoginRequest;
import com.rj.entity.LbFanManagement; import com.rj.entity.LbFanManagement;
@@ -38,4 +39,11 @@ public interface ILbFanManagementService extends IService<LbFanManagement> {
* @return 树形结构数据 * @return 树形结构数据
*/ */
Map<String, Object> getFanTree(String tenantId, String parentMobile); Map<String, Object> getFanTree(String tenantId, String parentMobile);
/**
* 递归发现所有粉丝(包括粉丝的粉丝)
* @param request 请求参数
* @return 递归发现结果
*/
Map<String, Object> recursiveFetchFans(LbFanRecursiveFetchRequest request);
} }

View File

@@ -3,6 +3,7 @@ package com.rj.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rj.dto.LbFanRecursiveFetchRequest;
import com.rj.dto.LbFanSimulateLoginRequest; import com.rj.dto.LbFanSimulateLoginRequest;
import com.rj.dto.hxr.HxrFansApiContext; import com.rj.dto.hxr.HxrFansApiContext;
import com.rj.dto.hxr.HxrLbFansPageData; import com.rj.dto.hxr.HxrLbFansPageData;
@@ -850,4 +851,217 @@ public class LbFanManagementServiceImpl
} }
} }
} }
@Override
public Map<String, Object> recursiveFetchFans(LbFanRecursiveFetchRequest request) {
Map<String, Object> result = new HashMap<>();
try {
if (request == null) {
result.put("success", false);
result.put("message", "请求体不能为空");
return result;
}
if (request.getTenantId() == null || request.getTenantId().trim().isEmpty()) {
result.put("success", false);
result.put("message", "tenantId不能为空");
return result;
}
if (request.getMobiles() == null || request.getMobiles().isEmpty()) {
result.put("success", false);
result.put("message", "手机号列表不能为空");
return result;
}
String tenantId = request.getTenantId().trim();
String password = request.getPassword() != null && !request.getPassword().trim().isEmpty()
? request.getPassword().trim()
: DEFAULT_SIMULATE_LOGIN_PASSWORD;
int maxDepth = request.getMaxDepth() != null && request.getMaxDepth() > 0
? request.getMaxDepth()
: Integer.MAX_VALUE;
boolean skipExisting = request.getSkipExisting() == null || request.getSkipExisting();
Optional<HxrUserLoginApiContext> loginCtxOpt =
lbThirdIntegrationConfigService.resolveUserLoginApiContext(tenantId);
if (loginCtxOpt.isEmpty()) {
result.put("success", false);
result.put("message",
"未找到该租户的第三方集成配置,或配置未启用、URL 不完整(请检查 lb_third_integration_config)");
return result;
}
HxrUserLoginApiContext loginCtx = loginCtxOpt.get();
Map<String, Object> stats = new LinkedHashMap<>();
stats.put("totalLoginAttempts", 0);
stats.put("totalLoginSuccess", 0);
stats.put("totalFansSaved", 0);
stats.put("totalFansFetched", 0);
stats.put("maxDepthReached", 0);
Map<String, Map<String, Object>> processedMobiles = new LinkedHashMap<>();
List<String> rootMobiles = new ArrayList<>();
for (String mobile : request.getMobiles()) {
if (mobile != null && !mobile.trim().isEmpty()) {
rootMobiles.add(mobile.trim());
}
}
recursiveFetchLevel(rootMobiles, null, tenantId, password, loginCtx,
0, maxDepth, skipExisting, stats, processedMobiles);
result.put("success", true);
result.put("message", "递归发现粉丝完成");
result.put("stats", stats);
result.put("processedMobiles", processedMobiles);
return result;
} catch (Exception e) {
log.error("递归发现粉丝异常", e);
result.put("success", false);
result.put("message", "递归发现粉丝异常:" + e.getMessage());
return result;
}
}
private void recursiveFetchLevel(List<String> mobiles,
Map<String, Object> parentInfo,
String tenantId,
String password,
HxrUserLoginApiContext loginCtx,
int currentDepth,
int maxDepth,
boolean skipExisting,
Map<String, Object> stats,
Map<String, Map<String, Object>> processedMobiles) {
if (currentDepth >= maxDepth) {
return;
}
if (mobiles == null || mobiles.isEmpty()) {
return;
}
int depth = currentDepth + 1;
if (depth > (Integer) stats.get("maxDepthReached")) {
stats.put("maxDepthReached", depth);
}
List<String> nextLevelMobiles = new ArrayList<>();
for (String mobile : mobiles) {
if (mobile == null || mobile.trim().isEmpty()) {
continue;
}
String trimmedMobile = mobile.trim();
if (processedMobiles.containsKey(trimmedMobile)) {
continue;
}
Map<String, Object> mobileResult = new LinkedHashMap<>();
mobileResult.put("mobile", trimmedMobile);
mobileResult.put("depth", depth);
mobileResult.put("loginSuccess", false);
mobileResult.put("saved", false);
mobileResult.put("fansFetched", 0);
mobileResult.put("fansSaved", 0);
if (skipExisting) {
LbFanManagement existing = this.getOne(
new LambdaQueryWrapper<LbFanManagement>()
.eq(LbFanManagement::getTenantId, tenantId)
.eq(LbFanManagement::getMobile, trimmedMobile)
.last("LIMIT 1"));
if (existing != null) {
mobileResult.put("saved", true);
mobileResult.put("saveMsg", "已存在,跳过");
processedMobiles.put(trimmedMobile, mobileResult);
List<LbFanManagement> existingFans = this.list(
new LambdaQueryWrapper<LbFanManagement>()
.eq(LbFanManagement::getTenantId, tenantId)
.eq(LbFanManagement::getPid, existing.getId()));
for (LbFanManagement fan : existingFans) {
if (fan.getMobile() != null && !fan.getMobile().trim().isEmpty()) {
nextLevelMobiles.add(fan.getMobile().trim());
}
}
continue;
}
}
try {
stats.put("totalLoginAttempts", (Integer) stats.get("totalLoginAttempts") + 1);
HxrAdminUserLoginService.LoginApiResult loginResult =
hxrAdminUserLoginService.login(trimmedMobile, password, loginCtx);
mobileResult.put("loginHttpStatus", loginResult.httpStatus());
mobileResult.put("loginApiCode", loginResult.apiCode());
mobileResult.put("loginApiMsg", loginResult.apiMsg());
if (!loginResult.success()) {
processedMobiles.put(trimmedMobile, mobileResult);
continue;
}
mobileResult.put("loginSuccess", true);
stats.put("totalLoginSuccess", (Integer) stats.get("totalLoginSuccess") + 1);
Map<String, Object> userinfo = HxrAdminUserLoginService.extractUserinfo(loginResult.parsed());
if (userinfo == null) {
mobileResult.put("saved", false);
mobileResult.put("saveMsg", "响应中无 data.userinfo");
processedMobiles.put(trimmedMobile, mobileResult);
continue;
}
Map<String, Object> saveResult = saveFanFromLoginUserinfo(userinfo, tenantId);
mobileResult.put("saved", saveResult.get("saved"));
mobileResult.put("saveMsg", saveResult.get("saveMsg"));
if (Boolean.TRUE.equals(saveResult.get("saved"))) {
stats.put("totalFansSaved", (Integer) stats.get("totalFansSaved") + 1);
Map<String, Object> fetchResult = fetchAndSaveFansFromThirdParty(userinfo, tenantId);
mobileResult.put("fansFetchSuccess", fetchResult.get("fansFetchSuccess"));
mobileResult.put("fansFetchMsg", fetchResult.get("fansFetchMsg"));
Integer fansFetched = (Integer) fetchResult.get("fansFetched");
Integer fansSaved = (Integer) fetchResult.get("fansSaved");
if (fansFetched != null) {
mobileResult.put("fansFetched", fansFetched);
stats.put("totalFansFetched", (Integer) stats.get("totalFansFetched") + fansFetched);
}
if (fansSaved != null) {
mobileResult.put("fansSaved", fansSaved);
stats.put("totalFansSaved", (Integer) stats.get("totalFansSaved") + fansSaved);
}
Long parentId = parseLong(userinfo.get("id"));
if (parentId != null) {
List<LbFanManagement> fetchedFans = this.list(
new LambdaQueryWrapper<LbFanManagement>()
.eq(LbFanManagement::getTenantId, tenantId)
.eq(LbFanManagement::getPid, parentId));
for (LbFanManagement fan : fetchedFans) {
if (fan.getMobile() != null && !fan.getMobile().trim().isEmpty()) {
nextLevelMobiles.add(fan.getMobile().trim());
}
}
}
}
processedMobiles.put(trimmedMobile, mobileResult);
} catch (Exception e) {
log.warn("递归发现粉丝异常 depth={} mobile={} tenantId={}", depth, trimmedMobile, tenantId, e);
mobileResult.put("error", e.getMessage());
processedMobiles.put(trimmedMobile, mobileResult);
}
}
if (!nextLevelMobiles.isEmpty()) {
recursiveFetchLevel(nextLevelMobiles, null, tenantId, password, loginCtx,
depth, maxDepth, skipExisting, stats, processedMobiles);
}
}
} }