init
This commit is contained in:
parent
0dc64cba1d
commit
7fc7cc26a7
@ -20,7 +20,7 @@ public abstract class GoofishAbstractApi<T> implements GoofishApi<T> {
|
||||
private String apiHostUrl;
|
||||
|
||||
protected final String buildApiUrl() {
|
||||
return apiHostUrl + getApi() + "/" + getVersion() + "/";
|
||||
return apiHostUrl + getApi() + "/" + getVersion() + "/?";
|
||||
}
|
||||
|
||||
protected Map<String, String> buildQueryParams(String cookieStr, String dataStr) {
|
||||
|
||||
@ -40,19 +40,25 @@ public class GetAccountApi extends GoofishAbstractApi<String> {
|
||||
@Override
|
||||
public String call(String goofishId, String cookieStr, String dataStr) {
|
||||
String apiUrl = buildApiUrl() + HttpUtil.toParams(buildQueryParams(cookieStr, dataStr));
|
||||
log.debug("【{}】获取账号名 ApiUrl: {}", goofishId, apiUrl);
|
||||
log.debug("【{}】获取账号名时使用的 Cookie 为: {}", goofishId, cookieStr);
|
||||
|
||||
try (HttpResponse response = HttpRequest.post(apiUrl)
|
||||
.header("Cookie", cookieStr)
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.header("priority", "u=1, i")
|
||||
.body("data=" + URLEncoder.encode(dataStr, StandardCharsets.UTF_8))
|
||||
.execute()) {
|
||||
String body = response.body();
|
||||
log.info("【{}】Auto confirm response: {}", goofishId, body);
|
||||
log.info("【{}】获取账号名时,服务端返回的完整响应为: {}", goofishId, body);
|
||||
|
||||
JSONObject json = JSONUtil.parseObj(body);
|
||||
if (json.containsKey("ret")) {
|
||||
JSONArray ret = json.getJSONArray("ret");
|
||||
if (ret != null && !ret.isEmpty() && ret.getStr(0).startsWith("SUCCESS")) {
|
||||
return json.getJSONObject("data").getJSONObject("module").getJSONObject("base").getStr("displayNick");
|
||||
String account = json.getJSONObject("data").getJSONObject("module").getJSONObject("base").getStr("displayNick");
|
||||
log.debug("【{}】获取到的账号名为: {}", goofishId, account);
|
||||
return account;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
package top.biwin.xianyu.goofish.service;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.microsoft.playwright.BrowserContext;
|
||||
import com.microsoft.playwright.ElementHandle;
|
||||
import com.microsoft.playwright.Frame;
|
||||
import com.microsoft.playwright.Page;
|
||||
import com.microsoft.playwright.options.Cookie;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.biwin.xianyu.goofish.util.CookieUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@ -28,8 +29,9 @@ public class GoofishPwdLoginService {
|
||||
|
||||
@Autowired
|
||||
private BrowserService browserService;
|
||||
@Autowired
|
||||
@Autowired
|
||||
private GoofishApiService goofishApiService;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param account
|
||||
@ -68,13 +70,14 @@ public class GoofishPwdLoginService {
|
||||
log.debug("【{}】正在检查协议复选框,并勾选......", account);
|
||||
agreement.click();
|
||||
}
|
||||
log.info("【{}】单击登录按钮...", account);
|
||||
log.debug("【{}】单击登录按钮...", account);
|
||||
loginFrame.click("button.fm-button.fm-submit.password-login");
|
||||
|
||||
// TODO 这里点击后,可能并不能成功,会出现滑块
|
||||
if (Objects.nonNull(findLoginFrame(page, account))) {
|
||||
boolean b = attemptSolveSlider(loginFrame, account);
|
||||
log.debug("滑块结果: {}", b);
|
||||
// TODO 内部应该要有重试处理滑块的逻辑
|
||||
boolean resolveResult = attemptSolveSlider(loginFrame, account);
|
||||
log.debug("【{}】滑块结果: {}!", account, resolveResult ? "成功" : "失败");
|
||||
}
|
||||
|
||||
} else {
|
||||
@ -83,18 +86,25 @@ public class GoofishPwdLoginService {
|
||||
} else {
|
||||
// 可能是快捷登录按钮页。等待 10s 后,查看右上角有没有登录信息
|
||||
page.waitForTimeout(10000);
|
||||
checkLoggedIn(context, account);
|
||||
}
|
||||
|
||||
// TODO 处理登录成功后的账号 cookie,并存库,更新persistentData 目录名称
|
||||
if (checkLoggedIn(context, account)) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
log.debug("....");
|
||||
}
|
||||
|
||||
private boolean checkLoggedIn(BrowserContext context,String account) {
|
||||
List<Cookie> cookies = context.cookies();
|
||||
private boolean checkLoggedIn(BrowserContext context, String account) {
|
||||
List<Cookie> cookies = context.cookies().stream().filter(i -> Objects.equals(i.domain, ".goofish.com")).toList();
|
||||
if (CollUtil.isEmpty(cookies)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
goofishApiService.getAccount(account, "");
|
||||
return false;
|
||||
String remoteAccount = goofishApiService.getAccount(account, CookieUtils.buildCookieStr(cookies));
|
||||
return Objects.equals(remoteAccount, account);
|
||||
}
|
||||
|
||||
private Frame findLoginFrame(Page page, String account) {
|
||||
|
||||
@ -4,52 +4,185 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* 浏览器轨迹生成工具类
|
||||
* 用于生成模拟人类滑动行为的鼠标轨迹
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-28
|
||||
*/
|
||||
public class BrowserTrajectoryUtils {
|
||||
|
||||
public static class TrajectoryPoint {
|
||||
public double x;
|
||||
public double y;
|
||||
public double delay;
|
||||
private static final Random random = new Random();
|
||||
|
||||
public TrajectoryPoint(double x, double y, double delay) {
|
||||
/**
|
||||
* 轨迹点,包含坐标
|
||||
*/
|
||||
public static class TrajectoryPoint {
|
||||
/** X坐标偏移量 */
|
||||
public double x;
|
||||
/** Y坐标偏移量(微小抖动) */
|
||||
public double y;
|
||||
|
||||
public TrajectoryPoint(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.delay = delay;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Port of _generate_physics_trajectory from Python
|
||||
* Based on physics acceleration model - Fast Mode
|
||||
* 生成丝滑轨迹 - 快速加速一滑到底
|
||||
*
|
||||
* 核心理念:高密度轨迹点 + 加速曲线 = 丝滑!
|
||||
*
|
||||
* 特点:
|
||||
* - 大量轨迹点(50-80个),让滑动超级丝滑
|
||||
* - 加速度曲线:先快速加速,然后匀速冲刺
|
||||
* - 微小Y轴抖动模拟人手
|
||||
* - 微小超调后回调
|
||||
*
|
||||
* @param distance 需要滑动的总距离(像素)
|
||||
* @return 轨迹点列表
|
||||
*/
|
||||
public static List<TrajectoryPoint> generatePhysicsTrajectory(double distance) {
|
||||
Random random = new Random();
|
||||
public static List<TrajectoryPoint> generateSmoothTrajectory(double distance) {
|
||||
List<TrajectoryPoint> trajectory = new ArrayList<>();
|
||||
|
||||
// Ensure overshoot 100-110%
|
||||
double targetDistance = distance * (2.0 + random.nextDouble() * 0.1);
|
||||
// 高密度轨迹点:50-80个点,让滑动丝滑流畅
|
||||
int steps = 50 + random.nextInt(31);
|
||||
|
||||
// Minimal steps (5-8 steps)
|
||||
int steps = 5 + random.nextInt(4); // 5 to 8
|
||||
// 微小超调(2-5像素),然后回调
|
||||
double overshoot = 2 + random.nextDouble() * 3;
|
||||
double targetWithOvershoot = distance + overshoot;
|
||||
|
||||
// Fast time interval (0.2ms - 0.5ms) - roughly converted to use in sleep
|
||||
double baseDelay = 0.0002 + random.nextDouble() * 0.0003;
|
||||
// 主滑动阶段(90%的步数用于主滑动)
|
||||
int mainSteps = (int) (steps * 0.9);
|
||||
|
||||
for (int i = 0; i < steps; i++) {
|
||||
double progress = (double)(i + 1) / steps;
|
||||
// 使用加速曲线生成主轨迹
|
||||
for (int i = 1; i <= mainSteps; i++) {
|
||||
double progress = (double) i / mainSteps;
|
||||
|
||||
// Calculate current position (Square acceleration curve)
|
||||
double x = targetDistance * Math.pow(progress, 1.5);
|
||||
// 加速曲线:ease-out (1 - (1-t)^2),快速起步然后平稳
|
||||
// 这会让开始阶段加速很快,后面逐渐平稳
|
||||
double easedProgress = 1 - Math.pow(1 - progress, 2.5);
|
||||
|
||||
// Minimal Y jitter
|
||||
double y = random.nextDouble() * 2;
|
||||
double x = targetWithOvershoot * easedProgress;
|
||||
|
||||
// Short delay
|
||||
double delay = baseDelay * (0.9 + random.nextDouble() * 0.2); // 0.9 to 1.1 factor
|
||||
// Y轴微小随机抖动(±2像素),模拟人手不稳
|
||||
double y = (random.nextDouble() - 0.5) * 4;
|
||||
|
||||
trajectory.add(new TrajectoryPoint(x, y, delay));
|
||||
trajectory.add(new TrajectoryPoint(x, y));
|
||||
}
|
||||
|
||||
// 回调阶段(10%的步数用于回调到精确位置)
|
||||
int returnSteps = steps - mainSteps;
|
||||
double currentX = targetWithOvershoot;
|
||||
|
||||
for (int i = 1; i <= returnSteps; i++) {
|
||||
double progress = (double) i / returnSteps;
|
||||
|
||||
// 线性回调到目标位置
|
||||
double x = currentX - overshoot * progress;
|
||||
|
||||
// 回调时Y轴抖动更小
|
||||
double y = (random.nextDouble() - 0.5) * 1.5;
|
||||
|
||||
trajectory.add(new TrajectoryPoint(x, y));
|
||||
}
|
||||
|
||||
// 最后确保精确到达目标(±1像素内)
|
||||
trajectory.add(new TrajectoryPoint(
|
||||
distance + (random.nextDouble() - 0.5) * 2,
|
||||
(random.nextDouble() - 0.5) * 1
|
||||
));
|
||||
|
||||
return trajectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成极速轨迹 - 最快速度一滑到底(备选方案)
|
||||
*
|
||||
* 如果丝滑版通过率不高,可以尝试这个更激进的版本
|
||||
* 特点:更少的点,更快的速度,更像"甩"过去
|
||||
*
|
||||
* @param distance 需要滑动的总距离(像素)
|
||||
* @return 轨迹点列表
|
||||
*/
|
||||
public static List<TrajectoryPoint> generateFastTrajectory(double distance) {
|
||||
List<TrajectoryPoint> trajectory = new ArrayList<>();
|
||||
|
||||
// 较少的点(20-30个),速度更快
|
||||
int steps = 20 + random.nextInt(11);
|
||||
|
||||
// 激进超调(5-10像素)
|
||||
double overshoot = 5 + random.nextDouble() * 5;
|
||||
double target = distance + overshoot;
|
||||
|
||||
for (int i = 1; i <= steps; i++) {
|
||||
double progress = (double) i / steps;
|
||||
|
||||
// 更激进的加速曲线:几乎立即达到最大速度
|
||||
double easedProgress = 1 - Math.pow(1 - progress, 3);
|
||||
|
||||
double x = target * easedProgress;
|
||||
double y = (random.nextDouble() - 0.5) * 3;
|
||||
|
||||
trajectory.add(new TrajectoryPoint(x, y));
|
||||
}
|
||||
|
||||
// 快速回调(2-3步)
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
double x = target - overshoot * ((double) i / 3);
|
||||
double y = (random.nextDouble() - 0.5) * 2;
|
||||
trajectory.add(new TrajectoryPoint(x, y));
|
||||
}
|
||||
|
||||
return trajectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成贝塞尔曲线轨迹(备选方案)
|
||||
* 使用三次贝塞尔曲线生成更平滑的轨迹
|
||||
*
|
||||
* @param distance 需要滑动的总距离(像素)
|
||||
* @return 轨迹点列表
|
||||
*/
|
||||
public static List<TrajectoryPoint> generateBezierTrajectory(double distance) {
|
||||
List<TrajectoryPoint> trajectory = new ArrayList<>();
|
||||
|
||||
// 控制点定义曲线形状
|
||||
double p0x = 0, p0y = 0; // 起点
|
||||
double p1x = distance * 0.6, p1y = random.nextDouble() * 8 - 4; // 控制点1:快速加速
|
||||
double p2x = distance * 0.9, p2y = random.nextDouble() * 6 - 3; // 控制点2:接近终点
|
||||
double p3x = distance, p3y = 0; // 终点
|
||||
|
||||
int steps = 40 + random.nextInt(20);
|
||||
|
||||
for (int i = 1; i <= steps; i++) {
|
||||
double t = (double) i / steps;
|
||||
|
||||
// 三次贝塞尔曲线公式
|
||||
double x = cubicBezier(t, p0x, p1x, p2x, p3x);
|
||||
double y = cubicBezier(t, p0y, p1y, p2y, p3y);
|
||||
|
||||
// 添加微小随机抖动
|
||||
x += (random.nextDouble() - 0.5) * 1.5;
|
||||
y += (random.nextDouble() - 0.5) * 2;
|
||||
|
||||
trajectory.add(new TrajectoryPoint(x, y));
|
||||
}
|
||||
|
||||
return trajectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* 三次贝塞尔曲线计算
|
||||
* B(t) = (1-t)^3 * P0 + 3(1-t)^2 * t * P1 + 3(1-t) * t^2 * P2 + t^3 * P3
|
||||
*/
|
||||
private static double cubicBezier(double t, double p0, double p1, double p2, double p3) {
|
||||
double oneMinusT = 1 - t;
|
||||
return oneMinusT * oneMinusT * oneMinusT * p0 +
|
||||
3 * oneMinusT * oneMinusT * t * p1 +
|
||||
3 * oneMinusT * t * t * p2 +
|
||||
t * t * t * p3;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package top.biwin.xianyu.goofish.util;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.microsoft.playwright.options.Cookie;
|
||||
|
||||
import java.util.List;
|
||||
@ -11,9 +12,17 @@ import java.util.List;
|
||||
* @since 2026-01-28 23:02
|
||||
*/
|
||||
public final class CookieUtils {
|
||||
private CookieUtils(){}
|
||||
private CookieUtils() {
|
||||
}
|
||||
|
||||
public String buildCookieStr(List<Cookie> cookies) {
|
||||
return "";
|
||||
public static String buildCookieStr(List<Cookie> cookies) {
|
||||
if (CollUtil.isEmpty(cookies)) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
cookies.forEach(i -> {
|
||||
sb.append(i.name).append("=").append(i.value).append(";");
|
||||
});
|
||||
return sb.substring(0, sb.length() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,23 +7,34 @@ import com.microsoft.playwright.options.BoundingBox;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* 滑块处理
|
||||
* 滑块验证码处理工具类
|
||||
* 模拟人类操作行为解决滑块验证
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-28 23:16
|
||||
*/
|
||||
@Slf4j
|
||||
public final class SliderUtils {
|
||||
|
||||
private SliderUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试解决滑块验证码
|
||||
*
|
||||
* @param frame 包含滑块的Frame
|
||||
* @param goofishId 闲鱼账号ID(用于日志)
|
||||
* @return 是否成功解决滑块
|
||||
*/
|
||||
public static boolean attemptSolveSlider(Frame frame, String goofishId) {
|
||||
try {
|
||||
String[] sliderSelectors = {"#nc_1_n1z", ".nc-container", ".nc_scale", ".nc-wrapper"};
|
||||
ElementHandle sliderButton = null;
|
||||
|
||||
// 检查当前Frame是否包含滑块容器
|
||||
boolean containerFound = false;
|
||||
for (String s : sliderSelectors) {
|
||||
if (frame.querySelector(s) != null && frame.isVisible(s)) {
|
||||
@ -32,6 +43,7 @@ public final class SliderUtils {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果当前Frame没有滑块,递归检查子Frame
|
||||
if (!containerFound && CollUtil.isNotEmpty(frame.childFrames())) {
|
||||
for (Frame childFrame : frame.childFrames()) {
|
||||
return attemptSolveSlider(childFrame, goofishId);
|
||||
@ -40,50 +52,100 @@ public final class SliderUtils {
|
||||
|
||||
if (!containerFound) return false;
|
||||
|
||||
// 定位滑块按钮
|
||||
sliderButton = frame.querySelector("#nc_1_n1z");
|
||||
if (sliderButton == null) sliderButton = frame.querySelector(".nc_iconfont");
|
||||
|
||||
if (sliderButton != null && sliderButton.isVisible()) {
|
||||
log.info("【Login Task】Detected slider in frame: {}", frame.url());
|
||||
log.info("【Login Task】检测到滑块验证,Frame: {}", frame.url());
|
||||
BoundingBox box = sliderButton.boundingBox();
|
||||
if (box == null) return false;
|
||||
|
||||
// 定位滑块轨道
|
||||
ElementHandle track = frame.querySelector("#nc_1_n1t");
|
||||
if (track == null) track = frame.querySelector(".nc_scale");
|
||||
if (track == null) return false;
|
||||
|
||||
BoundingBox trackBox = track.boundingBox();
|
||||
double distance = trackBox.width - box.width;
|
||||
log.info("【Login Task】Solving Slider: distance={}", distance);
|
||||
log.info("【Login Task】准备解决滑块: 距离={}px", distance);
|
||||
|
||||
List<BrowserTrajectoryUtils.TrajectoryPoint> trajectory =
|
||||
BrowserTrajectoryUtils.generatePhysicsTrajectory(distance);
|
||||
// 执行人类模拟滑动(丝滑版)
|
||||
boolean success = performSmoothSlide(frame, box, distance);
|
||||
|
||||
double startX = box.x + box.width / 2;
|
||||
double startY = box.y + box.height / 2;
|
||||
// 等待验证结果
|
||||
Thread.sleep(800 + ThreadLocalRandom.current().nextInt(400));
|
||||
|
||||
// PlaywrightUtils.moveMouseInFrame(frame, startX, startY);
|
||||
frame.page().mouse().move(startX, startY);
|
||||
frame.page().mouse().down();
|
||||
|
||||
for (BrowserTrajectoryUtils.TrajectoryPoint p : trajectory) {
|
||||
// PlaywrightUtils.moveMouseInFrame(frame, startX + p.x, startY + p.y);
|
||||
frame.page().mouse().move(startX + p.x, startY + p.y);
|
||||
}
|
||||
frame.page().mouse().up();
|
||||
|
||||
Thread.sleep(1000);
|
||||
// 检查滑块是否消失(成功标志)
|
||||
if (!sliderButton.isVisible()) {
|
||||
log.info("【Login Task】Slider solved (button disappeared)!");
|
||||
log.info("【Login Task】滑块验证成功!(按钮已消失)");
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
return success;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("【Login Task】Error solving slider: {}", e.getMessage());
|
||||
log.warn("【Login Task】解决滑块时出错: {}", e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行丝滑滑动操作
|
||||
* <p>
|
||||
* 关键理念:快速一滑到底,中间不停顿!
|
||||
* <p>
|
||||
* 人类快速滑动特征:
|
||||
* 1. 按下鼠标前有短暂停顿(视觉定位)
|
||||
* 2. 按下后立即快速连续滑动(一气呵成,不停顿)
|
||||
* 3. 到达终点后释放
|
||||
*
|
||||
* @param frame 目标Frame
|
||||
* @param buttonBox 滑块按钮的边界框
|
||||
* @param distance 需要滑动的距离
|
||||
* @return 是否执行成功
|
||||
*/
|
||||
private static boolean performSmoothSlide(Frame frame, BoundingBox buttonBox, double distance) {
|
||||
try {
|
||||
ThreadLocalRandom random = ThreadLocalRandom.current();
|
||||
|
||||
// 计算滑块起始位置(微小随机偏移)
|
||||
double startX = buttonBox.x + buttonBox.width / 2 + (random.nextDouble() - 0.5) * 4;
|
||||
double startY = buttonBox.y + buttonBox.height / 2 + (random.nextDouble() - 0.5) * 3;
|
||||
|
||||
// 步骤1:移动鼠标到滑块位置
|
||||
frame.page().mouse().move(startX, startY);
|
||||
|
||||
// 步骤2:短暂停顿(人类反应时间:100-250ms)
|
||||
Thread.sleep(100 + random.nextInt(150));
|
||||
|
||||
// 步骤3:按下鼠标
|
||||
frame.page().mouse().down();
|
||||
|
||||
// 步骤4:按下后极短延迟(30-60ms)
|
||||
Thread.sleep(30 + random.nextInt(30));
|
||||
|
||||
// 步骤5:生成丝滑轨迹
|
||||
List<BrowserTrajectoryUtils.TrajectoryPoint> trajectory =
|
||||
BrowserTrajectoryUtils.generateFastTrajectory(distance);
|
||||
|
||||
log.debug("【Login Task】生成轨迹点数: {}", trajectory.size());
|
||||
|
||||
// 步骤6:一气呵成快速滑动!!不加任何sleep!!
|
||||
for (BrowserTrajectoryUtils.TrajectoryPoint point : trajectory) {
|
||||
frame.page().mouse().move(startX + point.x, startY + point.y);
|
||||
// 不加sleep!让Playwright自己处理速度!
|
||||
}
|
||||
|
||||
// 步骤7:释放鼠标
|
||||
frame.page().mouse().up();
|
||||
|
||||
log.info("【Login Task】滑动执行完成,等待验证结果...");
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("【Login Task】执行滑动时出错: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user