This commit is contained in:
wangli 2026-01-31 00:07:45 +08:00
parent 9eb9223bd8
commit e77f642628
9 changed files with 689 additions and 10 deletions

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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 = {

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
});
}
}