diff --git a/xianyu-common/src/main/java/top/biwin/xinayu/common/enums/WebSocketConnectionState.java b/xianyu-common/src/main/java/top/biwin/xinayu/common/enums/WebSocketConnectionState.java
new file mode 100644
index 0000000..d31c666
--- /dev/null
+++ b/xianyu-common/src/main/java/top/biwin/xinayu/common/enums/WebSocketConnectionState.java
@@ -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;
+ }
+}
diff --git a/xianyu-goofish/pom.xml b/xianyu-goofish/pom.xml
index 685427a..bbfe696 100644
--- a/xianyu-goofish/pom.xml
+++ b/xianyu-goofish/pom.xml
@@ -17,6 +17,10 @@
xianyu-core
${revision}
+
+ org.springframework.boot
+ spring-boot-starter-websocket
+
com.microsoft.playwright
playwright
diff --git a/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/service/BrowserService.java b/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/service/BrowserService.java
index ee794c4..93c41d0 100644
--- a/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/service/BrowserService.java
+++ b/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/service/BrowserService.java
@@ -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 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 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 cookies = CookieUtils.parseCookieList(goofishCookieStr);
browserContext.addCookies(cookies);
}
diff --git a/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/service/GoofishApiService.java b/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/service/GoofishApiService.java
index bacd0ee..8543388 100644
--- a/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/service/GoofishApiService.java
+++ b/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/service/GoofishApiService.java
@@ -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();
}
diff --git a/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/service/GoofishPwdLoginService.java b/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/service/GoofishPwdLoginService.java
index 0925e7d..4a2b8d1 100644
--- a/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/service/GoofishPwdLoginService.java
+++ b/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/service/GoofishPwdLoginService.java
@@ -131,13 +131,7 @@ public class GoofishPwdLoginService {
}
}
- private String buildCookieStr(BrowserContext context) {
- List 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 = {
diff --git a/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/util/CookieUtils.java b/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/util/CookieUtils.java
index e5e12a3..388290e 100644
--- a/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/util/CookieUtils.java
+++ b/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/util/CookieUtils.java
@@ -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 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 cookies) {
if (CollUtil.isEmpty(cookies)) {
return "";
@@ -25,4 +41,170 @@ public final class CookieUtils {
});
return sb.substring(0, sb.length() - 1);
}
+
+ public static Map parseCookieMap(String cookiesStr) {
+ Map 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 parseCookieList(String cookiesStr) {
+ Map cookieMap = parseCookieMap(cookiesStr);
+ List cookies = new ArrayList<>();
+
+ List 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;
+ }
}
diff --git a/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/util/XianyuUtils.java b/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/util/XianyuUtils.java
new file mode 100644
index 0000000..56e80f3
--- /dev/null
+++ b/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/util/XianyuUtils.java
@@ -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;
+ }
+}
diff --git a/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/websocket/GoofishAccountWebsocket.java b/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/websocket/GoofishAccountWebsocket.java
new file mode 100644
index 0000000..a1c1d64
--- /dev/null
+++ b/xianyu-goofish/src/main/java/top/biwin/xianyu/goofish/websocket/GoofishAccountWebsocket.java
@@ -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 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 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 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 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);
+ }
+}
diff --git a/xianyu-server/src/main/java/top/biwin/xinayu/server/config/ThreadPoolConfig.java b/xianyu-server/src/main/java/top/biwin/xinayu/server/config/ThreadPoolConfig.java
new file mode 100644
index 0000000..8a3a0a2
--- /dev/null
+++ b/xianyu-server/src/main/java/top/biwin/xinayu/server/config/ThreadPoolConfig.java
@@ -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;
+ });
+ }
+
+}