init
This commit is contained in:
parent
9eb9223bd8
commit
e77f642628
@ -0,0 +1,28 @@
|
||||
package top.biwin.xinayu.common.enums;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-30 22:21
|
||||
*/
|
||||
public enum WebSocketConnectionState {
|
||||
INIT("init"),
|
||||
DISCONNECTED("disconnected"),
|
||||
CONNECTING("connecting"),
|
||||
CONNECTED("connected"),
|
||||
REGISTER("register"),
|
||||
RECONNECTING("reconnecting"),
|
||||
FAILED("failed"),
|
||||
CLOSED("closed");
|
||||
|
||||
private final String value;
|
||||
|
||||
WebSocketConnectionState(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,10 @@
|
||||
<artifactId>xianyu-core</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.playwright</groupId>
|
||||
<artifactId>playwright</artifactId>
|
||||
|
||||
@ -10,10 +10,12 @@ import com.microsoft.playwright.options.Cookie;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import top.biwin.xianyu.goofish.util.CookieUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@ -27,6 +29,9 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static top.biwin.xianyu.goofish.BrowserConstant.STEALTH_SCRIPT;
|
||||
import static top.biwin.xianyu.goofish.util.CookieUtils.buildCookieStr;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
@ -44,6 +49,8 @@ public class BrowserService {
|
||||
private String browserLocation;
|
||||
@Value("${browser.ua}")
|
||||
private String ua;
|
||||
@Autowired
|
||||
private GoofishApiService goofishApiService;
|
||||
// 为每个账号维护持久化浏览器上下文(用于Cookie刷新)
|
||||
private final Map<String, BrowserContext> persistentContexts = new ConcurrentHashMap<>();
|
||||
private Playwright playwright;
|
||||
@ -277,6 +284,9 @@ public class BrowserService {
|
||||
Path sourceUserDataPath = Paths.get(userDataDir);
|
||||
Path targetUserDataPath = sourceUserDataPath.resolveSibling("user_" + targetId);
|
||||
try {
|
||||
if (Files.exists(targetUserDataPath)) {
|
||||
Files.delete(targetUserDataPath);
|
||||
}
|
||||
Files.move(sourceUserDataPath, targetUserDataPath, StandardCopyOption.ATOMIC_MOVE);
|
||||
log.debug("【{}-renamePersistentContext】重命名持久化浏览器数据成功,原目录: {},目标目录: {}", sourceId, sourceUserDataPath, targetUserDataPath);
|
||||
|
||||
@ -289,10 +299,35 @@ public class BrowserService {
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("【{}-renamePersistentContext】重命名持久化浏览器数据异常,原目录: {},目标目录: {},异常信息: {}", sourceId, sourceUserDataPath, targetUserDataPath, e.getMessage(), e);
|
||||
try {
|
||||
Files.deleteIfExists(sourceUserDataPath);
|
||||
} catch (IOException ex) {
|
||||
log.error("【{}-renamePersistentContext】删除原目录失败,异常信息: {}", sourceId, ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public String refreshGoofishAccountCookie(String goofishId, Boolean showBrowser, Double slowMo, String goofishCookieStr) {
|
||||
BrowserContext browserContext = loadPersistentContext(goofishId, showBrowser, slowMo, goofishCookieStr);
|
||||
addPersistentContextCookie(goofishId, goofishCookieStr);
|
||||
|
||||
// TODO 刷新账号信息,并返回新的 cookie
|
||||
Page page = browserContext.pages().isEmpty() ? browserContext.newPage() : browserContext.pages().get(0);
|
||||
page.addInitScript(STEALTH_SCRIPT);
|
||||
log.debug("【{}】正在导航至登录页... url: http://www.goofish.com/im", goofishId);
|
||||
page.navigate("https://www.goofish.com/im");
|
||||
log.debug("【{}】等待页面加载,查找登录框... url: http://www.goofish.com/im", goofishId);
|
||||
// 确保页面加载完成
|
||||
page.getByText("登录后可以更懂你,推荐你喜欢的商品!");
|
||||
String cookieStr = buildCookieStr(browserContext);
|
||||
Long goofishUserId = goofishApiService.getUserId(goofishId, cookieStr);
|
||||
if (goofishUserId > 0L) {
|
||||
return cookieStr;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public void closePersistentContext(String goofishId) {
|
||||
BrowserContext browserContext = persistentContexts.get(goofishId);
|
||||
if (Objects.isNull(browserContext)) {
|
||||
@ -304,14 +339,15 @@ public class BrowserService {
|
||||
/**
|
||||
* 该方法一般是在 loadPersistentContext 函数后面调用,设置 cookie
|
||||
*/
|
||||
public void addPersistentContextCookie(String goofishId, List<Cookie> cookies) {
|
||||
public void addPersistentContextCookie(String goofishId, String goofishCookieStr) {
|
||||
BrowserContext browserContext = persistentContexts.get(goofishId);
|
||||
if (Objects.isNull(browserContext)) {
|
||||
throw new IllegalStateException("未找到" + goofishId + "的浏览器持久化数据");
|
||||
}
|
||||
if (CollectionUtils.isEmpty(cookies)) {
|
||||
if (!StringUtils.hasText(goofishCookieStr)) {
|
||||
return;
|
||||
}
|
||||
List<Cookie> cookies = CookieUtils.parseCookieList(goofishCookieStr);
|
||||
browserContext.addCookies(cookies);
|
||||
}
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ public class GoofishApiService {
|
||||
public String getAccount(String goofishId, @Nullable String cookieStr) {
|
||||
GoofishApi<?> goofishApi = getApi("getAccount");
|
||||
if (!StringUtils.hasText(cookieStr)) {
|
||||
cookieStr = goofishAccountRepository.findByUsername(goofishId)
|
||||
cookieStr = goofishAccountRepository.findById(goofishId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("无法获取闲鱼用户名,缺少 Cookie 信息"))
|
||||
.getCookie();
|
||||
}
|
||||
|
||||
@ -131,13 +131,7 @@ public class GoofishPwdLoginService {
|
||||
}
|
||||
}
|
||||
|
||||
private String buildCookieStr(BrowserContext context) {
|
||||
List<Cookie> cookies = context.cookies().stream().filter(i -> Objects.equals(i.domain, ".goofish.com")).toList();
|
||||
if (CollUtil.isEmpty(cookies)) {
|
||||
return null;
|
||||
}
|
||||
return CookieUtils.buildCookieStr(cookies);
|
||||
}
|
||||
|
||||
|
||||
private Frame findLoginFrame(Page page, String account) {
|
||||
String[] selectors = {
|
||||
|
||||
@ -1,9 +1,17 @@
|
||||
package top.biwin.xianyu.goofish.util;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.microsoft.playwright.BrowserContext;
|
||||
import com.microsoft.playwright.options.Cookie;
|
||||
import com.microsoft.playwright.options.SameSiteAttribute;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
@ -15,6 +23,14 @@ public final class CookieUtils {
|
||||
private CookieUtils() {
|
||||
}
|
||||
|
||||
public static String buildCookieStr(BrowserContext context) {
|
||||
List<Cookie> cookies = context.cookies().stream().filter(i -> Objects.equals(i.domain, ".goofish.com")).toList();
|
||||
if (CollUtil.isEmpty(cookies)) {
|
||||
return null;
|
||||
}
|
||||
return buildCookieStr(cookies);
|
||||
}
|
||||
|
||||
public static String buildCookieStr(List<Cookie> cookies) {
|
||||
if (CollUtil.isEmpty(cookies)) {
|
||||
return "";
|
||||
@ -25,4 +41,170 @@ public final class CookieUtils {
|
||||
});
|
||||
return sb.substring(0, sb.length() - 1);
|
||||
}
|
||||
|
||||
public static Map<String, String> parseCookieMap(String cookiesStr) {
|
||||
Map<String, String> cookieMap = new HashMap<>();
|
||||
if (StrUtil.isBlank(cookiesStr)) {
|
||||
return cookieMap;
|
||||
}
|
||||
|
||||
String[] parts = cookiesStr.split("; ");
|
||||
for (String part : parts) {
|
||||
if (part.contains("=")) {
|
||||
String[] kv = part.split("=", 2);
|
||||
cookieMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
return cookieMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* [ {
|
||||
* "name" : "mtop_partitioned_detect",
|
||||
* "value" : "1",
|
||||
* "domain" : ".goofish.com",
|
||||
* "path" : "/",
|
||||
* "expires" : 1.769793740505499E9,
|
||||
* "httpOnly" : false,
|
||||
* "secure" : true,
|
||||
* "sameSite" : "NONE"
|
||||
* }, {
|
||||
* "name" : "_m_h5_tk",
|
||||
* "value" : "77506c33a992091889447f9e230f16a6_1769796621003",
|
||||
* "domain" : ".goofish.com",
|
||||
* "path" : "/",
|
||||
* "expires" : 1.769793740505528E9,
|
||||
* "httpOnly" : false,
|
||||
* "secure" : true,
|
||||
* "sameSite" : "NONE"
|
||||
* }, {
|
||||
* "name" : "_m_h5_tk_enc",
|
||||
* "value" : "c28c39dad575a9c69126cbd712cf29af",
|
||||
* "domain" : ".goofish.com",
|
||||
* "path" : "/",
|
||||
* "expires" : 1.76979374050555E9,
|
||||
* "httpOnly" : false,
|
||||
* "secure" : true,
|
||||
* "sameSite" : "NONE"
|
||||
* }, {
|
||||
* "name" : "cna",
|
||||
* "value" : "tcEDIuxw9H8CAXWw8c5fFeXx",
|
||||
* "domain" : ".goofish.com",
|
||||
* "path" : "/",
|
||||
* "expires" : 1.804348340644934E9,
|
||||
* "httpOnly" : false,
|
||||
* "secure" : true,
|
||||
* "sameSite" : "NONE"
|
||||
* }, {
|
||||
* "name" : "_samesite_flag_",
|
||||
* "value" : "true",
|
||||
* "domain" : ".goofish.com",
|
||||
* "path" : "/",
|
||||
* "expires" : -1,
|
||||
* "httpOnly" : true,
|
||||
* "secure" : true,
|
||||
* "sameSite" : "NONE"
|
||||
* }, {
|
||||
* "name" : "cookie2",
|
||||
* "value" : "1cac0f4e3ec7d9106ba61c242c77ac3d",
|
||||
* "domain" : ".goofish.com",
|
||||
* "path" : "/",
|
||||
* "expires" : -1,
|
||||
* "httpOnly" : true,
|
||||
* "secure" : true,
|
||||
* "sameSite" : "NONE"
|
||||
* }, {
|
||||
* "name" : "t",
|
||||
* "value" : "30d3981c3f2e9def630bbfb7732a9c0a",
|
||||
* "domain" : ".goofish.com",
|
||||
* "path" : "/",
|
||||
* "expires" : 1.777593140747377E9,
|
||||
* "httpOnly" : false,
|
||||
* "secure" : true,
|
||||
* "sameSite" : "NONE"
|
||||
* }, {
|
||||
* "name" : "_tb_token_",
|
||||
* "value" : "ad33e777ae76",
|
||||
* "domain" : ".goofish.com",
|
||||
* "path" : "/",
|
||||
* "expires" : -1,
|
||||
* "httpOnly" : false,
|
||||
* "secure" : true,
|
||||
* "sameSite" : "NONE"
|
||||
* }, {
|
||||
* "name" : "xlly_s",
|
||||
* "value" : "1",
|
||||
* "domain" : ".goofish.com",
|
||||
* "path" : "/",
|
||||
* "expires" : 1.77004754E9,
|
||||
* "httpOnly" : false,
|
||||
* "secure" : true,
|
||||
* "sameSite" : "NONE"
|
||||
* }, {
|
||||
* "name" : "tracknick",
|
||||
* "value" : "%E5%8D%8A%E5%A4%8F%E8%90%BD%E5%9C%B0",
|
||||
* "domain" : ".goofish.com",
|
||||
* "path" : "/",
|
||||
* "expires" : 1.801353161098281E9,
|
||||
* "httpOnly" : false,
|
||||
* "secure" : true,
|
||||
* "sameSite" : "NONE"
|
||||
* }, {
|
||||
* "name" : "unb",
|
||||
* "value" : "2045669855",
|
||||
* "domain" : ".goofish.com",
|
||||
* "path" : "/",
|
||||
* "expires" : 1.770076361098333E9,
|
||||
* "httpOnly" : false,
|
||||
* "secure" : true,
|
||||
* "sameSite" : "NONE"
|
||||
* }, {
|
||||
* "name" : "sgcookie",
|
||||
* "value" : "E100I7t2q%2Bjh2Ba0MMqWIxCXbauG%2FGhYHOChQo2y8mldHquuVvn8f3oniyWiwk5xDfilIeWkgQswlKG%2FDl94ZJpfwFPe0hbEuszls7JPcNyQRLo%3D",
|
||||
* "domain" : ".goofish.com",
|
||||
* "path" : "/",
|
||||
* "expires" : 1.801353161098246E9,
|
||||
* "httpOnly" : true,
|
||||
* "secure" : true,
|
||||
* "sameSite" : "NONE"
|
||||
* }, {
|
||||
* "name" : "csg",
|
||||
* "value" : "267bda7a",
|
||||
* "domain" : ".goofish.com",
|
||||
* "path" : "/",
|
||||
* "expires" : -1,
|
||||
* "httpOnly" : false,
|
||||
* "secure" : true,
|
||||
* "sameSite" : "NONE"
|
||||
* }, {
|
||||
* "name" : "tfstk",
|
||||
* "value" : "gofnChsUwhib7Emai0RQCCNMO329pBOWpghJ2QKzQh-svDhdz0jlbiQK9_Qyqgj9fwpdZaGkzaS7R3UQBw_BNQrvqoFARwNRMblYXQzP7QLlZMSsZw_BNVgt4-BNR7bsFCTeagRwQEYHaURrLPmwuhlezXRybPYWbQlETvuZbUT-Uv-F4NzMPh-yapSy_PYWb3Ryaf30YH5P91rDHPTW-E-csevH0w2jabfiMpxV8hcz01YHVn7ejYky0scLUaSzqbdVhGfk3gNo1BQcQC8h4Sre_Z6OYFjzmYxNn61kH6qmhH5BViYcx83WxO7A9HQmuf7Mn15pOOqmMnW1z_vNRoDXWT7A9TjLfb-dnNSMH1ntMC7VICjrKAk42Zc7SDvjFY9e5hxjrINzm2F1SzUgSxxWLFTeDP4iF7Je5Fj_SPDqgp86--C..",
|
||||
* "domain" : ".goofish.com",
|
||||
* "path" : "/",
|
||||
* "expires" : 1.785340361E9,
|
||||
* "httpOnly" : false,
|
||||
* "secure" : true,
|
||||
* "sameSite" : "NONE"
|
||||
* } ]
|
||||
* @param cookiesStr
|
||||
* @return
|
||||
*/
|
||||
public static List<Cookie> parseCookieList(String cookiesStr) {
|
||||
Map<String, String> cookieMap = parseCookieMap(cookiesStr);
|
||||
List<Cookie> cookies = new ArrayList<>();
|
||||
|
||||
List<String> httpOnlyKeys = Arrays.asList("_samesite_flag_", "cookie2", "sgcookie");
|
||||
cookieMap.forEach((k, v) -> {
|
||||
Cookie cookie = new Cookie(k, v);
|
||||
cookie.setDomain(".goofish.com");
|
||||
cookie.setPath("/");
|
||||
cookie.setExpires(-1);
|
||||
|
||||
cookie.setHttpOnly(httpOnlyKeys.contains(k));
|
||||
cookie.setSecure(true);
|
||||
cookie.setSameSite(SameSiteAttribute.NONE);
|
||||
cookies.add(cookie);
|
||||
});
|
||||
return cookies;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
package top.biwin.xianyu.goofish.util;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-30 22:43
|
||||
*/
|
||||
public class XianyuUtils {
|
||||
private XianyuUtils() {
|
||||
}
|
||||
|
||||
public static String generateDeviceId(String userId) {
|
||||
String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
StringBuilder result = new StringBuilder();
|
||||
Random random = new Random();
|
||||
|
||||
for (int i = 0; i < 36; i++) {
|
||||
if (i == 8 || i == 13 || i == 18 || i == 23) {
|
||||
result.append("-");
|
||||
} else if (i == 14) {
|
||||
result.append("4");
|
||||
} else {
|
||||
if (i == 19) {
|
||||
int randVal = (int) (16 * random.nextDouble());
|
||||
result.append(chars.charAt((randVal & 0x3) | 0x8));
|
||||
} else {
|
||||
int randVal = (int) (16 * random.nextDouble());
|
||||
result.append(chars.charAt(randVal));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toString() + "-" + userId;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,369 @@
|
||||
package top.biwin.xianyu.goofish.websocket;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.microsoft.playwright.BrowserContext;
|
||||
import jakarta.websocket.ContainerProvider;
|
||||
import jakarta.websocket.WebSocketContainer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.concurrent.ListenableFuture;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.WebSocketHttpHeaders;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.client.WebSocketClient;
|
||||
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
import top.biwin.xianyu.core.entity.GoofishAccountEntity;
|
||||
import top.biwin.xianyu.core.repository.GoofishAccountRepository;
|
||||
import top.biwin.xianyu.goofish.service.BrowserService;
|
||||
import top.biwin.xianyu.goofish.service.GoofishApiService;
|
||||
import top.biwin.xianyu.goofish.util.CookieUtils;
|
||||
import top.biwin.xianyu.goofish.util.XianyuUtils;
|
||||
import top.biwin.xinayu.common.enums.WebSocketConnectionState;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-30 22:18
|
||||
*/
|
||||
@Slf4j
|
||||
public class GoofishAccountWebsocket extends TextWebSocketHandler {
|
||||
private static final String WEBSOCKET_URL = "wss://wss-goofish.dingtalk.com/";
|
||||
private final AtomicBoolean connected = new AtomicBoolean(false);
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
private volatile WebSocketConnectionState connectionState = WebSocketConnectionState.DISCONNECTED;
|
||||
private final AtomicLong lastStateChangeTime = new AtomicLong(System.currentTimeMillis());
|
||||
private final AtomicInteger connectionFailures = new AtomicInteger(0);
|
||||
private static final int MAX_CONNECTION_FAILURES = 3;
|
||||
private WebSocketSession webSocketSession;
|
||||
private String cookiesStr;
|
||||
private Long userId;
|
||||
private Map<String, String> cookies;
|
||||
private String myId;
|
||||
private String deviceId;
|
||||
|
||||
private final String goofishId;
|
||||
private final GoofishAccountRepository goofishAccountRepository;
|
||||
private final BrowserService browserService;
|
||||
private final GoofishApiService goofishApiService;
|
||||
private final ExecutorService scheduledExecutor;
|
||||
|
||||
public GoofishAccountWebsocket(String goofishId,
|
||||
GoofishAccountRepository goofishAccountRepository,
|
||||
BrowserService browserService,
|
||||
GoofishApiService goofishApiService,
|
||||
@Qualifier("goofishAccountWebSocketExecutor") ExecutorService scheduledExecutor) {
|
||||
super();
|
||||
this.goofishId = goofishId;
|
||||
this.goofishAccountRepository = goofishAccountRepository;
|
||||
this.browserService = browserService;
|
||||
this.goofishApiService = goofishApiService;
|
||||
this.scheduledExecutor = scheduledExecutor;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (running.get()) {
|
||||
log.warn("【{}】客户端已在运行中", goofishId);
|
||||
return;
|
||||
}
|
||||
|
||||
running.set(true);
|
||||
log.info("【{}】开始启动 GoofishAccountWebsocket...", goofishId);
|
||||
|
||||
// 加载Cookie
|
||||
if (!loadGoofishAccount()) {
|
||||
log.error("【{}】加载闲鱼账号失败,无法启动 GoofishAccountWebsocket。", goofishId);
|
||||
running.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 启动WebSocket连接循环
|
||||
CompletableFuture.runAsync(this::connecting, scheduledExecutor);
|
||||
}
|
||||
|
||||
private boolean loadGoofishAccount() {
|
||||
try {
|
||||
log.info("【{}】开始加载闲鱼账号...", goofishId);
|
||||
Optional<GoofishAccountEntity> accountOpt = goofishAccountRepository.findById(goofishId);
|
||||
if (accountOpt.isEmpty()) {
|
||||
log.error("【{}】闲鱼账号不存在", goofishId);
|
||||
return false;
|
||||
}
|
||||
|
||||
GoofishAccountEntity account = accountOpt.get();
|
||||
this.cookiesStr = account.getCookie();
|
||||
this.userId = account.getUserId();
|
||||
|
||||
if (StrUtil.isBlank(cookiesStr)) {
|
||||
log.error("【{}】Cookie值为空", goofishId);
|
||||
return false;
|
||||
}
|
||||
|
||||
String accountName = goofishApiService.getAccount(account.getId(), cookiesStr);
|
||||
if(!StringUtils.hasText(accountName)) {
|
||||
// 说明 cookie 失效,尝试重新登录
|
||||
this.cookiesStr = browserService.refreshGoofishAccountCookie(account.getId(), account.getShowBrowser() == 1, 1000D, cookiesStr);
|
||||
}
|
||||
|
||||
// 解析Cookie
|
||||
this.cookies = CookieUtils.parseCookieMap(cookiesStr);
|
||||
log.info("【{}】Cookie解析完成,包含字段: {}", goofishId, cookies.keySet());
|
||||
|
||||
// 获取unb字段
|
||||
String unb = cookies.get("unb");
|
||||
if (StrUtil.isBlank(unb)) {
|
||||
log.error("【{}】Cookie中缺少必需的'unb'字段", goofishId);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.myId = unb;
|
||||
this.deviceId = XianyuUtils.generateDeviceId(myId);
|
||||
|
||||
log.info("【{}】用户ID: {}, 设备ID: {}", goofishId, myId, deviceId);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("【{}】加载闲鱼账号异常: {}", goofishId, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查账号是否启用
|
||||
*/
|
||||
private boolean isAccountEnabled() {
|
||||
try {
|
||||
Optional<GoofishAccountEntity> accountOpt = goofishAccountRepository.findById(goofishId);
|
||||
return accountOpt.isPresent() && Boolean.TRUE.equals(accountOpt.get().getEnabled());
|
||||
} catch (Exception e) {
|
||||
log.error("【{}】检查账号状态失败", goofishId, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 可中断的Sleep
|
||||
*/
|
||||
private void sleepWithInterruptCheck(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("【{}】Sleep被中断", goofishId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待WebSocket连接断开
|
||||
*/
|
||||
private void waitForDisconnection() {
|
||||
log.info("【{}】WebSocket连接已建立,等待连接断开...", goofishId);
|
||||
while (connected.get() && running.get()) {
|
||||
sleepWithInterruptCheck(1000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void stop() {
|
||||
if (!running.get()) {
|
||||
log.warn("【{}】客户端未运行", goofishId);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("【{}】开始停止 GoofishAccountWebsocket...", goofishId);
|
||||
running.set(false);
|
||||
connected.set(false);
|
||||
|
||||
// 取消所有后台任务
|
||||
// cancelAllBackgroundTasks();
|
||||
|
||||
// 关闭WebSocket连接
|
||||
// closeWebSocket();
|
||||
|
||||
// 清理实例缓存
|
||||
// cleanupInstanceCaches();
|
||||
|
||||
// 从全局字典中注销实例
|
||||
// unregisterInstance();
|
||||
|
||||
log.info("【{}】GoofishAccountWebsocket 已停止", goofishId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接状态并记录日志
|
||||
* 对应Python的_set_connection_state()方法
|
||||
*/
|
||||
private void setConnectionState(WebSocketConnectionState newState, String reason) {
|
||||
if (connectionState != newState) {
|
||||
WebSocketConnectionState oldState = connectionState;
|
||||
connectionState = newState;
|
||||
lastStateChangeTime.set(System.currentTimeMillis());
|
||||
|
||||
// 记录状态转换
|
||||
String stateMsg = String.format("【%s】连接状态: %s → %s", goofishId, oldState.getValue(), newState.getValue());
|
||||
if (StrUtil.isNotBlank(reason)) {
|
||||
stateMsg += " (" + reason + ")";
|
||||
}
|
||||
|
||||
// 根据状态严重程度选择日志级别
|
||||
switch (newState) {
|
||||
case FAILED:
|
||||
log.error(stateMsg);
|
||||
break;
|
||||
case RECONNECTING:
|
||||
log.warn(stateMsg);
|
||||
break;
|
||||
case CONNECTED:
|
||||
log.info(stateMsg); // 成功状态用info级别
|
||||
break;
|
||||
default:
|
||||
log.info(stateMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理连接错误
|
||||
* 对应Python的handleConnectionError()方法(隐式)
|
||||
*/
|
||||
private void handleConnectionError(Exception e) {
|
||||
connectionFailures.incrementAndGet();
|
||||
log.error("【{}】WebSocket连接错误(失败次数: {})", goofishId, connectionFailures.get(), e);
|
||||
|
||||
if (connectionFailures.get() >= MAX_CONNECTION_FAILURES) {
|
||||
log.error("【{}】连接失败次数过多,停止重连", goofishId);
|
||||
setConnectionState(WebSocketConnectionState.FAILED, "连接失败次数过多");
|
||||
running.set(false);
|
||||
} else {
|
||||
setConnectionState(WebSocketConnectionState.RECONNECTING, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算重试延迟(秒)
|
||||
* 对应Python的_calculate_retry_delay()方法
|
||||
*/
|
||||
private int calculateRetryDelay(int failures) {
|
||||
// 根据失败次数计算延迟:3秒 * 失败次数,最多30秒
|
||||
return Math.min(3 * failures, 30);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建WebSocket请求头
|
||||
*/
|
||||
private WebSocketHttpHeaders buildWebSocketHeaders() {
|
||||
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
|
||||
headers.add("Accept-Encoding", "gzip, deflate, br, zstd");
|
||||
headers.add("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
|
||||
headers.add("Cache-Control", "no-cache");
|
||||
headers.add("Connection", "Upgrade");
|
||||
headers.add("Host", "wss-goofish.dingtalk.com");
|
||||
headers.add("Origin", "https://www.goofish.com");
|
||||
headers.add("Pragma", "no-cache");
|
||||
headers.add("Sec-websocket-extensions", "permessage-deflate; client_max_window_bits");
|
||||
headers.add("sec-websocket-key", "Q5ejXOphWkfkyDZTTSrU2A==");
|
||||
headers.add("sec-websocket-version", "13");
|
||||
headers.add("upgrade", "websocket");
|
||||
headers.add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0");
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建WebSocket连接 - 重构版本,纯粹的单次连接尝试
|
||||
* 失败直接抛异常,由 connectionLoop() 统一处理重试
|
||||
*/
|
||||
private void connectWebSocket() throws Exception {
|
||||
log.info("【{}】开始建立WebSocket连接...", goofishId);
|
||||
|
||||
// 配置WebSocket容器,设置缓冲区大小为10MB(解决1009错误:消息过大)
|
||||
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
|
||||
container.setDefaultMaxTextMessageBufferSize(10 * 1024 * 1024); // 10MB
|
||||
container.setDefaultMaxBinaryMessageBufferSize(10 * 1024 * 1024); // 10MB
|
||||
|
||||
// 使用配置好的容器创建WebSocket客户端
|
||||
WebSocketClient client = new StandardWebSocketClient(container);
|
||||
|
||||
// 准备请求头
|
||||
WebSocketHttpHeaders headers = buildWebSocketHeaders();
|
||||
|
||||
try {
|
||||
// 发起WebSocket握手
|
||||
ListenableFuture<WebSocketSession> future =
|
||||
client.doHandshake(this, headers, URI.create(WEBSOCKET_URL));
|
||||
|
||||
// 等待连接完成(超时30秒)
|
||||
// 注意:由于 afterConnectionEstablished 已异步化,这个超时仅用于 WebSocket 握手本身
|
||||
this.webSocketSession = future.get(30, TimeUnit.SECONDS);
|
||||
log.info("【{}】WebSocket连接建立成功", goofishId);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new Exception("WebSocket连接被中断", e);
|
||||
} catch (ExecutionException e) {
|
||||
throw new Exception("WebSocket连接执行失败: " + e.getMessage(), e);
|
||||
} catch (TimeoutException e) {
|
||||
throw new Exception("WebSocket连接超时", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void connecting() {
|
||||
while (running.get()) {
|
||||
try {
|
||||
// 检查账号是否启用
|
||||
if (!isAccountEnabled()) {
|
||||
log.info("【{}】账号已禁用,停止连接循环", goofishId);
|
||||
break;
|
||||
}
|
||||
|
||||
// 更新连接状态
|
||||
setConnectionState(WebSocketConnectionState.INIT, "准备建立WebSocket连接");
|
||||
log.info("【{}】WebSocket目标地址: {}", goofishId, WEBSOCKET_URL);
|
||||
|
||||
// 单次连接尝试
|
||||
connectWebSocket();
|
||||
|
||||
// 连接成功后,进入等待循环,直到连接断开
|
||||
waitForDisconnection();
|
||||
|
||||
log.info("【{}】WebSocket连接已断开", goofishId);
|
||||
|
||||
} catch (Exception e) {
|
||||
// 统一处理连接错误
|
||||
handleConnectionError(e);
|
||||
}
|
||||
|
||||
// 计算并执行重连延迟
|
||||
if (running.get()) {
|
||||
int retryDelay = calculateRetryDelay(connectionFailures.get());
|
||||
log.info("【{}】{}秒后尝试重连...", goofishId, retryDelay);
|
||||
sleepWithInterruptCheck(retryDelay * 1000L);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("【{}】WebSocket 连接循环已退出", goofishId);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||
super.afterConnectionEstablished(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
|
||||
super.afterConnectionClosed(session, status);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package top.biwin.xinayu.server.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-30 22:48
|
||||
*/
|
||||
@Configuration
|
||||
public class ThreadPoolConfig {
|
||||
|
||||
@Bean(name = "goofishAccountWebSocketExecutor")
|
||||
public ExecutorService executor() {
|
||||
// 创建定时任务线程池
|
||||
return Executors.newScheduledThreadPool(5, r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setName("GoofishAccountWebSocket-" + t.getId());
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user