From 74abf74ec8be474721a3fb6db41c957422b00765 Mon Sep 17 00:00:00 2001 From: cst61 Date: Wed, 29 Jul 2026 17:47:29 +0800 Subject: [PATCH] =?UTF-8?q?=E7=B2=89=E4=B8=9D=E6=95=B0=E6=9E=84=E9=80=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/LbFanManagementController.java | 17 +- .../rj/dto/LbFanRecursiveFetchRequest.java | 29 +++ .../rj/service/ILbFanManagementService.java | 8 + .../impl/LbFanManagementServiceImpl.java | 214 ++++++++++++++++++ 4 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/rj/dto/LbFanRecursiveFetchRequest.java diff --git a/src/main/java/com/rj/controller/LbFanManagementController.java b/src/main/java/com/rj/controller/LbFanManagementController.java index 53b2ade..26245b1 100644 --- a/src/main/java/com/rj/controller/LbFanManagementController.java +++ b/src/main/java/com/rj/controller/LbFanManagementController.java @@ -1,5 +1,6 @@ package com.rj.controller; +import com.rj.dto.LbFanRecursiveFetchRequest; import com.rj.dto.LbFanSimulateLoginRequest; import com.rj.entity.LbFanManagement; import com.rj.service.ILbFanManagementService; @@ -52,7 +53,7 @@ public class LbFanManagementController { @PostMapping("/simulate-login") @Operation( - summary = "批量模拟第三方登录", + summary = "批量模拟第三方登录,并发现粉丝", description = "按 tenantId 从 lb_third_integration_config 读取 login_api_path 等配置," + "对每个手机号调用第三方登录接口并解析返回 JSON;" @@ -64,6 +65,20 @@ public class LbFanManagementController { return toResponse(result); } + @PostMapping("/recursive-fetch") + @Operation( + summary = "递归发现所有粉丝(包括粉丝的粉丝)", + description = + "按 tenantId 从 lb_third_integration_config 读取配置," + + "对每个手机号登录后拉取粉丝,再递归登录粉丝拉取粉丝的粉丝;" + + "支持最大递归深度限制和跳过已存在粉丝") + public ResponseEntity> recursiveFetchFans( + @Parameter(description = "递归发现粉丝请求", required = true) + @RequestBody LbFanRecursiveFetchRequest request) { + Map result = lbFanManagementService.recursiveFetchFans(request); + return toResponse(result); + } + @GetMapping("/list") @Operation(summary = "分页查询粉丝") public ResponseEntity> list( diff --git a/src/main/java/com/rj/dto/LbFanRecursiveFetchRequest.java b/src/main/java/com/rj/dto/LbFanRecursiveFetchRequest.java new file mode 100644 index 0000000..bc54738 --- /dev/null +++ b/src/main/java/com/rj/dto/LbFanRecursiveFetchRequest.java @@ -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 mobiles; + + @Schema(description = "登录密码;未传时默认为 123456") + private String password; + + @Schema(description = "最大递归深度;不传或传 0 时不限深度") + private Integer maxDepth; + + @Schema(description = "是否跳过已存在的粉丝;不传时默认为 true") + private Boolean skipExisting; +} \ No newline at end of file diff --git a/src/main/java/com/rj/service/ILbFanManagementService.java b/src/main/java/com/rj/service/ILbFanManagementService.java index 136b7b7..50aa2be 100644 --- a/src/main/java/com/rj/service/ILbFanManagementService.java +++ b/src/main/java/com/rj/service/ILbFanManagementService.java @@ -1,6 +1,7 @@ package com.rj.service; import com.baomidou.mybatisplus.extension.service.IService; +import com.rj.dto.LbFanRecursiveFetchRequest; import com.rj.dto.LbFanSimulateLoginRequest; import com.rj.entity.LbFanManagement; @@ -38,4 +39,11 @@ public interface ILbFanManagementService extends IService { * @return 树形结构数据 */ Map getFanTree(String tenantId, String parentMobile); + + /** + * 递归发现所有粉丝(包括粉丝的粉丝) + * @param request 请求参数 + * @return 递归发现结果 + */ + Map recursiveFetchFans(LbFanRecursiveFetchRequest request); } diff --git a/src/main/java/com/rj/service/impl/LbFanManagementServiceImpl.java b/src/main/java/com/rj/service/impl/LbFanManagementServiceImpl.java index 41a13e2..1d68076 100644 --- a/src/main/java/com/rj/service/impl/LbFanManagementServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbFanManagementServiceImpl.java @@ -3,6 +3,7 @@ package com.rj.service.impl; 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.rj.dto.LbFanRecursiveFetchRequest; import com.rj.dto.LbFanSimulateLoginRequest; import com.rj.dto.hxr.HxrFansApiContext; import com.rj.dto.hxr.HxrLbFansPageData; @@ -850,4 +851,217 @@ public class LbFanManagementServiceImpl } } } + + @Override + public Map recursiveFetchFans(LbFanRecursiveFetchRequest request) { + Map 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 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 stats = new LinkedHashMap<>(); + stats.put("totalLoginAttempts", 0); + stats.put("totalLoginSuccess", 0); + stats.put("totalFansSaved", 0); + stats.put("totalFansFetched", 0); + stats.put("maxDepthReached", 0); + + Map> processedMobiles = new LinkedHashMap<>(); + List 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 mobiles, + Map parentInfo, + String tenantId, + String password, + HxrUserLoginApiContext loginCtx, + int currentDepth, + int maxDepth, + boolean skipExisting, + Map stats, + Map> 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 nextLevelMobiles = new ArrayList<>(); + + for (String mobile : mobiles) { + if (mobile == null || mobile.trim().isEmpty()) { + continue; + } + String trimmedMobile = mobile.trim(); + + if (processedMobiles.containsKey(trimmedMobile)) { + continue; + } + + Map 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() + .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 existingFans = this.list( + new LambdaQueryWrapper() + .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 userinfo = HxrAdminUserLoginService.extractUserinfo(loginResult.parsed()); + if (userinfo == null) { + mobileResult.put("saved", false); + mobileResult.put("saveMsg", "响应中无 data.userinfo"); + processedMobiles.put(trimmedMobile, mobileResult); + continue; + } + + Map 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 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 fetchedFans = this.list( + new LambdaQueryWrapper() + .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); + } + } }