This commit is contained in:
2025-09-24 11:10:42 +08:00
parent a72b60be98
commit 05b923b1ac
348 changed files with 611 additions and 8472 deletions

View File

@@ -1,394 +0,0 @@
package com.ruoyi.web.controller.common;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import java.time.Duration;
import java.util.List;
import java.util.Random;
/**
* 高级滑块验证码处理器
* 支持多种滑块类型和重试机制
*/
public class AdvancedSliderCaptchaHandler {
private final WebDriver driver;
private final WebDriverWait wait;
private final Actions actions;
private final Random random;
private static final int MAX_RETRY_ATTEMPTS = 3;
private static final Duration WAIT_TIMEOUT = Duration.ofSeconds(15);
// 人类行为参数
private static final int MIN_CLICK_HOLD_DURATION = 300;
private static final int MAX_CLICK_HOLD_DURATION = 800;
private static final int MIN_MOVE_DURATION = 1000;
private static final int MAX_MOVE_DURATION = 2000;
private static final double OVERSHOOT_FACTOR = 1.05; // 5%的过冲概率
public AdvancedSliderCaptchaHandler(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, WAIT_TIMEOUT.toSeconds());
this.actions = new Actions(driver);
this.random = new Random();
}
public boolean handleAnyCaptcha() {
for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
System.out.println("" + attempt + " 次尝试处理验证码");
CaptchaType captchaType = detectCaptchaType();
if (captchaType == CaptchaType.NONE) {
System.out.println("未检测到验证码");
return true;
}
boolean success = processCaptchaByType(captchaType);
if (success) {
System.out.println("验证码处理成功");
return true;
}
if (attempt < MAX_RETRY_ATTEMPTS) {
waitBeforeRetry();
}
}
System.err.println("验证码处理失败,已达到最大重试次数");
return false;
}
/**
* 检测验证码类型
*/
private CaptchaType detectCaptchaType() {
try {
if (isElementPresent(By.id("nc_1_n1z"))) {
return CaptchaType.ALIBABA_SLIDER;
}
// 检测通用滑块
if (isElementPresent(By.className("btn_slide")) ||
isElementPresent(By.className("slider-btn"))) {
return CaptchaType.GENERIC_SLIDER;
}
// 检测其他常见滑块选择器
String[] commonSelectors = {
".captcha-slider-btn",
".slide-verify-slider-mask-item",
".slider_bg .slider_btn"
};
for (String selector : commonSelectors) {
if (isElementPresent(By.cssSelector(selector))) {
return CaptchaType.GENERIC_SLIDER;
}
}
return CaptchaType.NONE;
} catch (Exception e) {
System.err.println("检测验证码类型时发生错误: " + e.getMessage());
return CaptchaType.NONE;
}
}
/**
* 根据验证码类型进行处理
*/
private boolean processCaptchaByType(CaptchaType type) {
switch (type) {
case ALIBABA_SLIDER:
return handleAlibabaSlider();
case GENERIC_SLIDER:
return handleGenericSlider();
default:
return false;
}
}
/**
* 处理阿里云滑块验证码
*/
private boolean handleAlibabaSlider() {
try {
WebElement sliderButton = wait.until(
ExpectedConditions.elementToBeClickable(By.id("nc_1_n1z"))
);
WebElement sliderTrack = driver.findElement(By.id("nc_1_n1t"));
int moveDistance = calculateMoveDistance(sliderTrack, sliderButton);
return performSmartSlide(sliderButton, moveDistance);
} catch (Exception e) {
System.err.println("处理阿里云滑块失败: " + e.getMessage());
return false;
}
}
/**
* 处理通用滑块验证码
*/
private boolean handleGenericSlider() {
try {
List<WebElement> sliderButtons = driver.findElements(By.className("btn_slide"));
if (sliderButtons.isEmpty()) {
sliderButtons = driver.findElements(By.className("slider-btn"));
}
if (sliderButtons.isEmpty()) {
return false;
}
WebElement sliderButton = sliderButtons.get(0);
WebElement sliderTrack = findSliderTrack(sliderButton);
if (sliderTrack == null) {
return false;
}
int moveDistance = calculateMoveDistance(sliderTrack, sliderButton);
System.out.println("处理通用滑块失败: ");
return performSmartSlide(sliderButton, moveDistance);
} catch (Exception e) {
System.err.println("处理通用滑块失败: " + e.getMessage());
return false;
}
}
/**
* 智能滑动算法
*/
// private boolean performSmartSlide(WebElement sliderButton, int totalDistance) {
// try {
// actions.clickAndHold(sliderButton).perform();
// Thread.sleep(50 + random.nextInt(200));
//
// int moved = 0;
// int segments = 20 + random.nextInt(5);
//
// for (int i = 0; i < segments && moved < totalDistance; i++) {
// double progress = (double) i / segments;
// int stepSize = (int) (totalDistance * getBezierValue(progress) - moved);
//
// if (stepSize > 0 && moved + stepSize <= totalDistance) {
// int yOffset = (int) (Math.sin(progress * Math.PI * 2) * 2);
// actions.moveByOffset(stepSize, yOffset).perform();
// moved += stepSize;
//
// }
// }
// if (moved < totalDistance) {
// actions.moveByOffset(totalDistance - moved, 0).perform();
// }
//
// Thread.sleep(50 + random.nextInt(200));
// actions.release().perform();
//
// return waitForVerificationResult();
//
// } catch (Exception e) {
// System.err.println("智能滑动失败: " + e.getMessage());
// return false;
// }
// }
/**
* 贝塞尔曲线值计算
*/
private double getBezierValue(double t) {
return t * t * (3.0 - 2.0 * t);
}
/**
* 计算移动距离
*/
private int calculateMoveDistance(WebElement track, WebElement button) {
int trackWidth = track.getSize().getWidth() + 100;
int buttonWidth = button.getSize().getWidth();
return Math.max(trackWidth - buttonWidth - 5, 0);
}
/**
* 查找滑块轨道
*/
private WebElement findSliderTrack(WebElement button) {
try {
return button.findElement(By.xpath("./parent::*"));
} catch (Exception e) {
return null;
}
}
/**
* 等待验证结果
*/
private boolean waitForVerificationResult() {
try {
Thread.sleep(2000);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 检查元素是否存在
*/
private boolean isElementPresent(By locator) {
try {
driver.findElement(locator);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 重试前等待
*/
private void waitBeforeRetry() {
try {
Thread.sleep(2000 + random.nextInt(3000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* 验证码类型枚举
*/
private enum CaptchaType {
NONE,
ALIBABA_SLIDER,
GENERIC_SLIDER
}
private boolean performSmartSlide(WebElement sliderButton, int totalDistance) {
try {
// 模拟人类点击前的短暂思考时间
randomSleep(MIN_CLICK_HOLD_DURATION, MAX_CLICK_HOLD_DURATION);
// 点击并按住滑块
actions.clickAndHold(sliderButton).perform();
// 生成更自然的移动轨迹
List<MoveStep> moveSteps = generateHumanLikeTrajectory(totalDistance);
// 执行移动步骤
long startTime = System.currentTimeMillis();
long currentTime = startTime;
long endTime = startTime + randomLong(MIN_MOVE_DURATION, MAX_MOVE_DURATION);
for (MoveStep step : moveSteps) {
// 根据时间进度执行步骤,使整体移动速度更自然
while (currentTime < endTime * step.timeProgress) {
actions.moveByOffset(step.xOffset, step.yOffset).perform();
currentTime = System.currentTimeMillis();
// 微小的随机停顿
if (random.nextDouble() < 0.1) {
Thread.sleep(random.nextInt(50));
}
}
}
// 随机过冲然后回退,更像人类操作
if (random.nextDouble() < OVERSHOOT_FACTOR) {
int overshoot = random.nextInt(5) + 5;
actions.moveByOffset(overshoot, 0).perform();
Thread.sleep(random.nextInt(100) + 50);
actions.moveByOffset(-overshoot, 0).perform();
}
// 释放前的随机延迟
randomSleep(100, 300);
actions.release().perform();
// 模拟验证过程中的等待
randomSleep(500, 1500);
return waitForVerificationResult();
} catch (Exception e) {
System.err.println("智能滑动失败: " + e.getMessage());
return false;
}
}
/**
* 生成更接近人类行为的滑动轨迹
*/
private List<MoveStep> generateHumanLikeTrajectory(int totalDistance) {
java.util.List<MoveStep> steps = new java.util.ArrayList<>();
// 总步数 - 随机但合理的范围
int totalSteps = random.nextInt(15) + 25;
// 计算每个时间点的进度0-1之间
double[] timeProgress = new double[totalSteps];
for (int i = 0; i < totalSteps; i++) {
timeProgress[i] = (double) i / totalSteps;
}
// 生成更自然的S型速度曲线开始和结束较慢中间较快
double[] distanceProgress = new double[totalSteps];
for (int i = 0; i < totalSteps; i++) {
double t = timeProgress[i];
// 使用改进的S型曲线函数
distanceProgress[i] = t * t * t * (t * (t * 6 - 15) + 10);
}
// 计算每一步的偏移量
for (int i = 0; i < totalSteps; i++) {
double currentDistance = i == 0 ?
distanceProgress[i] * totalDistance :
(distanceProgress[i] - distanceProgress[i-1]) * totalDistance;
// 添加小幅度的垂直偏移,模拟人类手部颤抖
int yOffset = random.nextInt(3) - 1; // -1, 0, 1
steps.add(new MoveStep(
(int) Math.round(currentDistance),
yOffset,
timeProgress[i]
));
}
return steps;
}
/**
* 随机睡眠一段时间
*/
private void randomSleep(int min, int max) throws InterruptedException {
Thread.sleep(random.nextInt(max - min + 1) + min);
}
/**
* 生成随机长整型数
*/
private long randomLong(long min, long max) {
return min + (long) (random.nextDouble() * (max - min));
}
/**
* 移动步骤类 - 封装每一步的移动信息
*/
private static class MoveStep {
final int xOffset;
final int yOffset;
final double timeProgress;
MoveStep(int xOffset, int yOffset, double timeProgress) {
this.xOffset = xOffset;
this.yOffset = yOffset;
this.timeProgress = timeProgress;
}
}
}

View File

@@ -198,7 +198,6 @@ public class ClientAccountController extends BaseController {
Map<String, Object> claims = Jwts.parser().setSigningKey(jwtRsaKeyService.getPublicKey()).parseClaimsJws(token).getBody();
String username = (String) claims.getOrDefault("sub", claims.get("subject"));
String tokenClientId = (String) claims.get("clientId");
if (username == null || tokenClientId == null || !tokenClientId.equals(clientId)) {
throw new RuntimeException("会话不匹配");
}

View File

@@ -1,10 +1,6 @@
package com.ruoyi.web.controller.monitor;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import com.ruoyi.common.annotation.Anonymous;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@@ -12,8 +8,8 @@ import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.system.domain.*;
import com.ruoyi.web.service.IClientMonitorService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.web.service.IClientMonitorService;
/**
* 客户端监控控制器
@@ -111,18 +107,7 @@ public class ClientMonitorController extends BaseController {
}
}
/**
* 客户端心跳API
*/
@PostMapping("/api/heartbeat")
public AjaxResult clientHeartbeat(@RequestBody Map<String, Object> heartbeatData) {
try {
clientMonitorService.recordHeartbeat(heartbeatData);
return AjaxResult.success();
} catch (Exception e) {
return AjaxResult.error(e.getMessage());
}
}
/**
* 客户端错误上报API
@@ -167,67 +152,7 @@ public class ClientMonitorController extends BaseController {
}
/**
* 获取客户端日志内容
*/
@GetMapping("/logs/{clientId}")
public AjaxResult getClientLogs(@PathVariable String clientId) {
try {
return AjaxResult.success(clientMonitorService.getClientLogs(clientId));
} catch (Exception e) {
return AjaxResult.error("获取客户端日志失败: " + e.getMessage());
}
}
/**
* 下载客户端日志文件
*/
@PreAuthorize("@ss.hasPermi('monitor:client:list')")
@GetMapping("/logs/{clientId}/download")
public void downloadClientLogs(@PathVariable String clientId, HttpServletResponse response) {
try {
clientMonitorService.downloadClientLogs(clientId, response);
} catch (Exception e) {
logger.error("下载客户端日志失败: {}", e.getMessage());
}
}
/**
* 客户端日志上传接口
*/
@PostMapping("/logs/upload")
public AjaxResult uploadClientLogs(@RequestBody Map<String, Object> logData) {
try {
clientMonitorService.saveClientLogs(logData);
return AjaxResult.success();
} catch (Exception e) {
return AjaxResult.error("保存客户端日志失败: " + e.getMessage());
}
}
/**
* 客户端日志批量上传接口
*/
@PostMapping("/logs/batchUpload")
public AjaxResult batchUploadClientLogs(@RequestBody Map<String, Object> batchLogData) {
try {
String clientId = (String) batchLogData.get("clientId");
List<String> logEntries = (List<String>) batchLogData.get("logEntries");
if (clientId == null || logEntries == null || logEntries.isEmpty()) {
return AjaxResult.error("无效的批量日志数据");
}
clientMonitorService.saveBatchClientLogs(clientId, logEntries);
return AjaxResult.success();
} catch (Exception e) {
return AjaxResult.error("批量保存客户端日志失败: " + e.getMessage());
}
}
/**

View File

@@ -2,158 +2,127 @@ package com.ruoyi.web.service;
import java.util.List;
import java.util.Map;
import com.ruoyi.system.domain.ClientInfo;
import com.ruoyi.system.domain.ClientErrorReport;
import com.ruoyi.system.domain.ClientDataReport;
import com.ruoyi.system.domain.ClientEventLog;
import com.ruoyi.system.domain.*;
/**
* 客户端监控服务接口
*
*
* @author ruoyi
*/
public interface IClientMonitorService {
/**
* 查询客户端信息列表
* 查询客户端设备列表
*/
public List<ClientInfo> selectClientInfoList(ClientInfo clientInfo);
List<ClientDevice> selectClientDeviceList(String username);
/**
* 查询客户端错误报告列表
*/
public List<Map<String, Object>> selectClientErrorList(ClientErrorReport clientErrorReport);
List<Map<String, Object>> selectClientErrorList(ClientErrorReport clientErrorReport);
/**
* 查询客户端事件日志列表
*/
public List<ClientEventLog> selectClientEventLogList(ClientEventLog clientEventLog);
List<ClientEventLog> selectClientEventLogList(ClientEventLog clientEventLog);
/**
* 查询客户端数据采集报告列表
*/
public List<ClientDataReport> selectClientDataReportList(ClientDataReport clientDataReport);
List<ClientDataReport> selectClientDataReportList(ClientDataReport clientDataReport);
/**
* 查询在线客户端数量
*/
public int selectOnlineClientCount();
int selectOnlineClientCount();
/**
* 检查客户端是否在线
*/
boolean isClientOnline(String clientId);
/**
* 获取在线客户端ID列表
*/
List<String> getOnlineClientIds();
/**
* 查询客户端总数
*/
public int selectTotalClientCount();
/**
* 新增客户端信息
*/
public int insertClientInfo(ClientInfo clientInfo);
/**
* 新增客户端错误报告
*/
public int insertClientError(ClientErrorReport clientErrorReport);
/**
* 新增客户端事件日志
*/
public int insertClientEventLog(ClientEventLog clientEventLog);
/**
* 新增客户端数据采集报告
*/
public int insertDataReport(ClientDataReport clientDataReport);
int selectTotalClientCount();
/**
* 获取客户端统计数据
*/
public Map<String, Object> getClientStatistics();
Map<String, Object> getClientStatistics();
/**
* 获取客户端活跃趋势
*/
public Map<String, Object> getClientActiveTrend();
Map<String, Object> getClientActiveTrend();
/**
* 获取数据采集类型分布
*/
public Map<String, Object> getDataTypeDistribution();
Map<String, Object> getDataTypeDistribution();
/**
* 获取近7天在线客户端趋势
* 获取在线客户端趋势
*/
public Map<String, Object> getOnlineClientTrend();
Map<String, Object> getOnlineClientTrend();
/**
* 客户端认证
*/
public Map<String, Object> authenticateClient(String authKey, Map<String, Object> clientInfo);
Map<String, Object> authenticateClient(String authKey, Map<String, Object> clientInfo);
/**
* 记录客户端心跳
* 记录错误报告
*/
public void recordHeartbeat(Map<String, Object> heartbeatData);
void recordErrorReport(Map<String, Object> errorData);
/**
* 记录客户端错误
* 记录数据报告
*/
public void recordErrorReport(Map<String, Object> errorData);
/**
* 记录客户端数据采集报告
*/
public void recordDataReport(Map<String, Object> dataReport);
void recordDataReport(Map<String, Object> dataReport);
/**
* 获取客户端详细信息
*/
public Map<String, Object> getClientDetail(String clientId);
Map<String, Object> getClientDetail(String clientId);
/**
* 获取客户端版本分布
* 获取版本分布
*/
public List<Map<String, Object>> getVersionDistribution();
List<Map<String, Object>> getVersionDistribution();
/**
* 记录1688风控监控数据
* 插入客户端错误报告
*/
public void recordAlibaba1688MonitorData(Map<String, Object> monitorData);
int insertClientError(ClientErrorReport clientErrorReport);
/**
* 查询1688风控监控数据列表
* 插入客户端设备
*/
public List<Map<String, Object>> selectAlibaba1688MonitorList(Map<String, Object> params);
int insertClientDevice(ClientDevice clientDevice);
/**
* 获取1688风控数据统计
* 插入客户端事件日志
*/
public Map<String, Object> getAlibaba1688Statistics();
int insertClientEventLog(ClientEventLog clientEventLog);
/**
* 获取客户端日志内容
* 插入数据报告
*/
public Map<String, Object> getClientLogs(String clientId);
int insertDataReport(ClientDataReport clientDataReport);
/**
* 下载客户端日志文件
* 查询客户端信息列表
*/
public void downloadClientLogs(String clientId, javax.servlet.http.HttpServletResponse response);
/**
* 保存客户端日志
*/
public void saveClientLogs(Map<String, Object> logData);
/**
* 批量保存客户端日志
*
* @param clientId 客户端ID
* @param logEntries 日志条目列表
*/
public void saveBatchClientLogs(String clientId, List<String> logEntries);
List<ClientInfo> selectClientInfoList(ClientInfo clientInfo);
/**
* 清理过期数据
*/
public void cleanExpiredData();
void cleanExpiredData();
}

View File

@@ -4,7 +4,6 @@ import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import com.ruoyi.system.domain.*;
import com.ruoyi.common.core.redis.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.scheduling.annotation.Async;
@@ -21,10 +20,11 @@ import org.slf4j.LoggerFactory;
import com.ruoyi.web.service.IClientAccountService;
import com.ruoyi.web.service.IClientMonitorService;
import com.ruoyi.system.mapper.ClientMonitorMapper;
import com.ruoyi.system.mapper.ClientDeviceMapper;
/**
* 客户端监控服务实现
*
*
* @author ruoyi
*/
@Service
@@ -34,12 +34,13 @@ public class ClientMonitorServiceImpl implements IClientMonitorService {
@Autowired
private ClientMonitorMapper clientMonitorMapper;
@Autowired
private ClientDeviceMapper clientDeviceMapper;
@Autowired
private IClientAccountService clientAccountService;
@Autowired
private RedisCache redisCache;
// 线程池用于异步处理日志记录
private final ExecutorService logExecutor = new ThreadPoolExecutor(
@@ -52,21 +53,16 @@ public class ClientMonitorServiceImpl implements IClientMonitorService {
private final AtomicLong apiCallCounter = new AtomicLong(0);
/**
* 查询客户端信息列表 - 增强Redis在线状态
* 查询设备列表 - 基于ClientDevice表
*/
@Override
public List<ClientInfo> selectClientInfoList(ClientInfo clientInfo) {
logApiCallAsync("selectClientInfoList", null);
List<ClientInfo> clientList = clientMonitorMapper.selectClientInfoList(clientInfo);
// 使用Redis检查并更新在线状态
if (clientList != null) {
for (ClientInfo client : clientList) {
boolean isOnline = isClientOnline(client.getClientId());
client.setOnline(isOnline ? "1" : "0");
}
public List<ClientDevice> selectClientDeviceList(String username) {
logApiCallAsync("selectClientDeviceList", null);
if (username != null && !username.isEmpty()) {
return clientDeviceMapper.selectByUsername(username);
} else {
return clientMonitorMapper.selectOnlineDevices();
}
return clientList;
}
/**
@@ -97,36 +93,25 @@ public class ClientMonitorServiceImpl implements IClientMonitorService {
}
/**
* 查询在线客户端数量 - 基于Redis TTL
* 查询在线客户端数量 - 基于数据库
*/
@Override
public int selectOnlineClientCount() {
logApiCallAsync("selectOnlineClientCount", null);
try {
// 通过Redis检查所有心跳key
Collection<String> heartbeatKeys = redisCache.keys("client:heartbeat:*");
int onlineCount = heartbeatKeys != null ? heartbeatKeys.size() : 0;
System.out.println("Redis在线客户端数量: " + onlineCount + ", keys: " + heartbeatKeys);
return onlineCount;
} catch (Exception e) {
System.err.println("Redis查询在线客户端失败: " + e.getMessage());
// Redis查询失败时使用数据库备用方案
return clientMonitorMapper.selectOnlineClientCount();
}
return clientMonitorMapper.selectOnlineClientCount();
}
/**
* 检查客户端是否在线 - 基于Redis TTL
* 检查客户端是否在线 - 基于数据库
*/
@Override
public boolean isClientOnline(String clientId) {
if (clientId == null || clientId.isEmpty()) {
return false;
}
try {
String heartbeatKey = "client:heartbeat:" + clientId;
boolean isOnline = redisCache.hasKey(heartbeatKey);
System.out.println("检查客户端在线状态: " + clientId + " -> " + isOnline);
return isOnline;
ClientInfo clientInfo = clientMonitorMapper.selectClientInfoByClientId(clientId);
return clientInfo != null && "1".equals(clientInfo.getOnline());
} catch (Exception e) {
System.err.println("检查客户端在线状态失败: " + e.getMessage());
return false;
@@ -134,17 +119,17 @@ public class ClientMonitorServiceImpl implements IClientMonitorService {
}
/**
* 获取在线客户端列表 - 基于Redis TTL
* 获取在线客户端列表 - 基于数据库
*/
@Override
public List<String> getOnlineClientIds() {
try {
Collection<String> heartbeatKeys = redisCache.keys("client:heartbeat:*");
ClientInfo queryParam = new ClientInfo();
queryParam.setOnline("1");
List<ClientInfo> onlineClients = clientMonitorMapper.selectClientInfoList(queryParam);
List<String> onlineClientIds = new ArrayList<>();
if (heartbeatKeys != null) {
for (String key : heartbeatKeys) {
String clientId = key.replace("client:heartbeat:", "");
onlineClientIds.add(clientId);
}
for (ClientInfo client : onlineClients) {
onlineClientIds.add(client.getClientId());
}
return onlineClientIds;
} catch (Exception e) {
@@ -443,40 +428,6 @@ public class ClientMonitorServiceImpl implements IClientMonitorService {
private void recordClientAuth(String username, String authKey, String clientId, Map<String, Object> clientInfo, String accessToken) {
}
/**
* 记录客户端心跳 - 使用Redis TTL机制
*/
@Override
public void recordHeartbeat(Map<String, Object> heartbeatData) {
try {
String clientId = (String) heartbeatData.get("clientId");
if (clientId == null || clientId.isEmpty()) {
System.err.println("心跳记录失败: clientId为空");
return;
}
// 使用Redis TTL记录心跳600秒TTL10分钟
String heartbeatKey = "client:heartbeat:" + clientId;
Map<String, Object> clientData = new HashMap<>();
clientData.put("clientId", clientId);
clientData.put("timestamp", System.currentTimeMillis());
clientData.put("cpuUsage", parseDouble(heartbeatData.get("cpuUsage"), 0.0));
clientData.put("memoryUsage", parseDouble(heartbeatData.get("memoryUsage"), 0.0));
clientData.put("diskUsage", parseDouble(heartbeatData.get("diskUsage"), 0.0));
clientData.put("networkStatus", heartbeatData.getOrDefault("networkStatus", "normal"));
// 设置Redis keyTTL为600秒
redisCache.setCacheObject(heartbeatKey, clientData, 600, TimeUnit.SECONDS);
System.out.println("心跳记录成功: " + heartbeatKey + ", TTL: 600秒");
// 只在数据库中更新基本信息(减少频率)
clientMonitorMapper.updateClientOnlineStatus(clientId, "1");
} catch (Exception e) {
System.err.println("记录心跳失败: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 安全解析Double值
*/
@@ -662,27 +613,6 @@ public class ClientMonitorServiceImpl implements IClientMonitorService {
}
}
/**
* 清理过期客户端数据和日志
*/
public void cleanExpiredData() {
try {
// 删除7天前的心跳记录将离线状态的客户端设为离线
clientMonitorMapper.updateExpiredClientsOffline();
// 删除30天前的错误报告
clientMonitorMapper.deleteExpiredErrorReports();
// 删除7天前的事件日志
clientMonitorMapper.deleteExpiredEventLogs();
logger.info("✅ 清理过期数据完成");
} catch (Exception e) {
logger.error("清理过期数据失败: {}", e.getMessage(), e);
}
}
/**
* 优雅关闭线程池
@@ -726,304 +656,11 @@ public class ClientMonitorServiceImpl implements IClientMonitorService {
return distribution;
}
/**
* 记录1688爬取风控监控数据
*/
@Override
public void recordAlibaba1688MonitorData(Map<String, Object> monitorData) {
// 记录API调用日志
logApiCall("recordAlibaba1688MonitorData", "记录1688爬取风控数据");
try {
// 获取客户端ID
String clientId = (String) monitorData.get("clientId");
// 如果没有客户端ID则使用系统用户名生成
if (clientId == null || clientId.isEmpty()) {
clientId = "user";
monitorData.put("clientId", clientId);
}
// 尝试查找已有客户端信息,如果没有则不进行严格验证
ClientInfo authInfo = null;
try {
authInfo = clientMonitorMapper.selectClientInfoByClientId(clientId);
} catch (Exception e) {
// 查询失败时忽略,允许匿名上报
}
// 如果客户端不存在,创建一个简单的客户端记录
if (authInfo == null) {
ClientInfo newClientInfo = new ClientInfo();
newClientInfo.setClientId(clientId);
newClientInfo.setUsername((String) monitorData.getOrDefault("username", "user"));
newClientInfo.setIpAddress((String) monitorData.get("ipAddress"));
newClientInfo.setHostname((String) monitorData.get("hostname"));
newClientInfo.setStatus("0");
newClientInfo.setAuthTime(DateUtils.getNowDate());
newClientInfo.setLastActiveTime(DateUtils.getNowDate());
newClientInfo.setOnline("1");
// 添加系统信息
newClientInfo.setOsName((String) monitorData.get("osName"));
newClientInfo.setOsVersion((String) monitorData.get("osVersion"));
newClientInfo.setJavaVersion((String) monitorData.get("javaVersion"));
newClientInfo.setAppVersion((String) monitorData.get("appVersion"));
try {
clientMonitorMapper.insertClientInfo(newClientInfo);
} catch (Exception e) {
// 插入失败时忽略,可能是因为主键冲突(客户端已存在)
logger.warn("插入客户端信息失败,可能客户端已存在: {}", e.getMessage());
}
} else {
// 更新现有客户端的活跃状态
try {
authInfo.setLastActiveTime(DateUtils.getNowDate());
authInfo.setOnline("1");
if (monitorData.get("ipAddress") != null) {
authInfo.setIpAddress((String) monitorData.get("ipAddress"));
}
// 更新系统信息
if (monitorData.get("osName") != null) {
authInfo.setOsName((String) monitorData.get("osName"));
}
if (monitorData.get("osVersion") != null) {
authInfo.setOsVersion((String) monitorData.get("osVersion"));
}
if (monitorData.get("javaVersion") != null) {
authInfo.setJavaVersion((String) monitorData.get("javaVersion"));
}
if (monitorData.get("appVersion") != null) {
authInfo.setAppVersion((String) monitorData.get("appVersion"));
}
clientMonitorMapper.updateClientInfo(authInfo);
} catch (Exception e) {
logger.warn("更新客户端信息失败: {}", e.getMessage());
}
}
// 设置当前时间
monitorData.put("createTime", DateUtils.getNowDate());
// 插入监控数据
clientMonitorMapper.insertAlibaba1688MonitorData(monitorData);
} catch (Exception e) {
throw new RuntimeException("处理1688爬取风控监控数据失败: " + e.getMessage());
}
}
/**
* 查询1688爬取风控监控数据列表
*/
@Override
public List<Map<String, Object>> selectAlibaba1688MonitorList(Map<String, Object> params) {
// 记录API调用日志
logApiCall("selectAlibaba1688MonitorList", null);
return clientMonitorMapper.selectAlibaba1688MonitorList(params);
}
/**
* 获取客户端日志内容
*/
@Override
public Map<String, Object> getClientLogs(String clientId) {
Map<String, Object> result = new HashMap<>();
try {
// 构建日志文件路径
String logDir = System.getProperty("user.dir") + "/logs/clients/";
String logFilePath = logDir + clientId + ".txt";
File logFile = new File(logFilePath);
if (logFile.exists()) {
// 读取日志文件内容
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new FileReader(logFile, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
}
result.put("content", content.toString());
result.put("lastUpdate", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(logFile.lastModified())));
} else {
result.put("content", "暂无日志文件");
result.put("lastUpdate", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
// 记录API调用日志
logApiCall("getClientLogs", "客户端ID: " + clientId);
} catch (Exception e) {
result.put("content", "读取日志文件失败: " + e.getMessage());
result.put("lastUpdate", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
return result;
}
/**
* 下载客户端日志文件
*/
@Override
public void downloadClientLogs(String clientId, HttpServletResponse response) {
try {
// 构建日志文件路径
String logDir = System.getProperty("user.dir") + "/logs/clients/";
String logFilePath = logDir + clientId + ".txt";
File logFile = new File(logFilePath);
if (!logFile.exists()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 设置响应头
response.setContentType("text/plain;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment; filename=\"client_" + clientId + "_logs.txt\"");
response.setContentLength((int) logFile.length());
// 输出文件内容
try (FileInputStream fis = new FileInputStream(logFile);
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.flush();
}
// 记录API调用日志
logApiCall("downloadClientLogs", "客户端ID: " + clientId);
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
/**
* 保存客户端日志
*/
@Override
public void saveClientLogs(Map<String, Object> logData) {
try {
String clientId = (String) logData.get("clientId");
String logContent = (String) logData.get("logContent");
if (clientId == null || logContent == null) {
throw new RuntimeException("缺少必要的日志参数");
}
// 创建日志目录
String logDir = System.getProperty("user.dir") + "/logs/clients/";
File dir = new File(logDir);
if (!dir.exists()) {
dir.mkdirs();
}
// 写入日志文件
String logFilePath = logDir + clientId + ".txt";
try (FileWriter writer = new FileWriter(logFilePath, StandardCharsets.UTF_8, true)) {
writer.write(logContent);
writer.flush();
}
// 记录API调用日志
logApiCall("saveClientLogs", "客户端ID: " + clientId + ", 日志长度: " + logContent.length());
} catch (Exception e) {
throw new RuntimeException("保存客户端日志失败: " + e.getMessage());
}
}
/**
* 批量保存客户端日志
*/
@Override
public void saveBatchClientLogs(String clientId, List<String> logEntries) {
try {
if (clientId == null || logEntries == null || logEntries.isEmpty()) {
throw new RuntimeException("缺少必要的日志参数");
}
// 创建日志目录
String logDir = System.getProperty("user.dir") + "/logs/clients/";
File dir = new File(logDir);
if (!dir.exists()) {
dir.mkdirs();
}
// 写入日志文件
String logFilePath = logDir + clientId + ".txt";
try (FileWriter writer = new FileWriter(logFilePath, StandardCharsets.UTF_8, true)) {
for (String logEntry : logEntries) {
if (logEntry != null && !logEntry.isEmpty()) {
writer.write(logEntry);
writer.write(System.lineSeparator());
}
}
writer.flush();
}
// 记录API调用日志
logApiCall("saveBatchClientLogs", "客户端ID: " + clientId + ", 日志条目数: " + logEntries.size());
} catch (Exception e) {
throw new RuntimeException("批量保存客户端日志失败: " + e.getMessage());
}
}
/**
* 获取1688爬取风控数据统计
*/
@Override
public Map<String, Object> getAlibaba1688Statistics() {
// 记录API调用日志
logApiCall("getAlibaba1688Statistics", null);
// 获取统计数据
Map<String, Object> stats = clientMonitorMapper.selectAlibaba1688Statistics();
if (stats == null) {
stats = new HashMap<>();
}
// 如果没有数据,设置默认值
if (!stats.containsKey("totalEvents")) {
stats.put("totalEvents", 0);
}
if (!stats.containsKey("mobileBlockedCount")) {
stats.put("mobileBlockedCount", 0);
}
if (!stats.containsKey("desktopBlockedCount")) {
stats.put("desktopBlockedCount", 0);
}
if (!stats.containsKey("avgMobileDuration")) {
stats.put("avgMobileDuration", 0);
}
if (!stats.containsKey("avgDesktopDuration")) {
stats.put("avgDesktopDuration", 0);
}
if (!stats.containsKey("uniqueIpCount")) {
stats.put("uniqueIpCount", 0);
}
return stats;
}
/**
* 从客户端信息中获取IP地址
*/
private String getClientIp(Map<String, Object> clientInfo) {
return clientInfo != null && clientInfo.containsKey("ipAddress")
? (String) clientInfo.get("ipAddress")
: "";
}
/**
* 生成会话令牌
*/
@@ -1043,8 +680,8 @@ public class ClientMonitorServiceImpl implements IClientMonitorService {
* 新增客户端信息
*/
@Override
public int insertClientInfo(ClientInfo clientInfo) {
return clientMonitorMapper.insertClientInfo(clientInfo);
public int insertClientDevice(ClientDevice clientDevice) {
return clientDeviceMapper.insert(clientDevice);
}
/**
@@ -1062,4 +699,37 @@ public class ClientMonitorServiceImpl implements IClientMonitorService {
public int insertDataReport(ClientDataReport clientDataReport) {
return clientMonitorMapper.insertDataReport(clientDataReport);
}
/**
* 查询客户端信息列表
*/
@Override
public List<ClientInfo> selectClientInfoList(ClientInfo clientInfo) {
return clientMonitorMapper.selectClientInfoList(clientInfo);
}
/**
* 清理过期数据
*/
@Override
public void cleanExpiredData() {
try {
// 清理过期的客户端(设置为离线状态)
clientMonitorMapper.updateExpiredClientsOffline();
// 清理过期的设备(设置为离线状态)
clientMonitorMapper.updateExpiredDevicesOffline();
// 删除过期的错误报告
clientMonitorMapper.deleteExpiredErrorReports();
// 删除过期的事件日志
clientMonitorMapper.deleteExpiredEventLogs();
logger.info("过期数据清理完成");
} catch (Exception e) {
logger.error("清理过期数据失败: {}", e.getMessage(), e);
throw new RuntimeException("清理过期数据失败", e);
}
}
}