init
This commit is contained in:
parent
3e1e1cfc13
commit
5bec08604e
@ -45,7 +45,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.34</version>
|
<version>1.18.30</version>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -76,7 +76,7 @@
|
|||||||
<artifactId>okhttp</artifactId>
|
<artifactId>okhttp</artifactId>
|
||||||
<version>4.12.0</version>
|
<version>4.12.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- ZXing for QR Code generation -->
|
<!-- ZXing for QR Code generation -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.zxing</groupId>
|
<groupId>com.google.zxing</groupId>
|
||||||
@ -99,17 +99,33 @@
|
|||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<excludes>
|
<source>17</source>
|
||||||
<exclude>
|
<target>17</target>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
</exclude>
|
<version>1.18.30</version>
|
||||||
</excludes>
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<!-- <plugin>-->
|
||||||
|
<!-- <groupId>org.springframework.boot</groupId>-->
|
||||||
|
<!-- <artifactId>spring-boot-maven-plugin</artifactId>-->
|
||||||
|
<!-- <configuration>-->
|
||||||
|
<!-- <excludes>-->
|
||||||
|
<!-- <exclude>-->
|
||||||
|
<!-- <groupId>org.projectlombok</groupId>-->
|
||||||
|
<!-- <artifactId>lombok</artifactId>-->
|
||||||
|
<!-- </exclude>-->
|
||||||
|
<!-- </excludes>-->
|
||||||
|
<!-- </configuration>-->
|
||||||
|
<!-- </plugin>-->
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import jakarta.annotation.PreDestroy;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.servlet.resource.ResourceUrlProvider;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
@ -25,18 +26,20 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
public class BrowserService {
|
public class BrowserService {
|
||||||
|
|
||||||
private final CookieRepository cookieRepository;
|
private final CookieRepository cookieRepository;
|
||||||
|
private final ResourceUrlProvider resourceUrlProvider;
|
||||||
private Playwright playwright;
|
private Playwright playwright;
|
||||||
private Browser browser;
|
private Browser browser;
|
||||||
|
|
||||||
// 为每个账号维护持久化浏览器上下文(用于Cookie刷新)
|
// 为每个账号维护持久化浏览器上下文(用于Cookie刷新)
|
||||||
private final Map<String, BrowserContext> persistentContexts = new ConcurrentHashMap<>();
|
private final Map<String, BrowserContext> persistentContexts = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// 为每个账号维护同步锁,防止并发创建持久化上下文
|
// 为每个账号维护同步锁,防止并发创建持久化上下文
|
||||||
private final Map<String, Object> contextLocks = new ConcurrentHashMap<>();
|
private final Map<String, Object> contextLocks = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public BrowserService(CookieRepository cookieRepository) {
|
public BrowserService(CookieRepository cookieRepository, ResourceUrlProvider resourceUrlProvider) {
|
||||||
this.cookieRepository = cookieRepository;
|
this.cookieRepository = cookieRepository;
|
||||||
|
this.resourceUrlProvider = resourceUrlProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
@ -523,6 +526,54 @@ public class BrowserService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean attemptQuickLogin(Frame frame) {
|
||||||
|
boolean containerFound = false;
|
||||||
|
if (Objects.isNull(frame)) return containerFound;
|
||||||
|
ElementHandle elementHandle = frame.querySelector("#alibaba-login-box");
|
||||||
|
if (Objects.isNull(elementHandle)) return containerFound;
|
||||||
|
Frame quickLoginFrame = elementHandle.contentFrame();
|
||||||
|
if (Objects.isNull(quickLoginFrame)) return containerFound;
|
||||||
|
ElementHandle loginButton = quickLoginFrame.querySelector(".fm-button.fm-submit");
|
||||||
|
if (Objects.isNull(loginButton)) return containerFound;
|
||||||
|
if (loginButton.isVisible()) {
|
||||||
|
loginButton.click();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean attemptQuickLoginV2(Frame frame) {
|
||||||
|
try {
|
||||||
|
String[] loginButtonSelectors = {".has-login", ".cm-has-login", ".fm-btn", ".fm-button", ".fm-submit"};
|
||||||
|
|
||||||
|
boolean containerFound = false;
|
||||||
|
for (String s : loginButtonSelectors) {
|
||||||
|
if (frame.querySelector(s) != null && frame.isVisible(s)) {
|
||||||
|
containerFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!containerFound) return false;
|
||||||
|
|
||||||
|
ElementHandle loginButtonDialog = frame.querySelector(".has-login");
|
||||||
|
if (loginButtonDialog == null) loginButtonDialog = frame.querySelector(".cm-has-login");
|
||||||
|
|
||||||
|
if (loginButtonDialog != null && loginButtonDialog.isVisible()) {
|
||||||
|
log.info("【Login Task】Detected quick login in frame: {}", frame.url());
|
||||||
|
|
||||||
|
ElementHandle loginButton = frame.querySelector(".fm-button");
|
||||||
|
if (loginButton == null) loginButton = frame.querySelector(".fm-submit");
|
||||||
|
if (loginButton == null) return false;
|
||||||
|
loginButton.click();
|
||||||
|
log.info("【Login Task】quick login success!");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("【Login Task】quick login fail : {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新Cookie - 使用持久化浏览器上下文
|
* 刷新Cookie - 使用持久化浏览器上下文
|
||||||
* Cookie会自动保存到UserData目录,类似真实浏览器行为
|
* Cookie会自动保存到UserData目录,类似真实浏览器行为
|
||||||
@ -574,10 +625,18 @@ public class BrowserService {
|
|||||||
|
|
||||||
// 3. 等待页面加载
|
// 3. 等待页面加载
|
||||||
try {
|
try {
|
||||||
Thread.sleep(3000);
|
Thread.sleep(5000);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 判断是否有快捷登陆iframe
|
||||||
|
for (Frame frame : page.frames()) {
|
||||||
|
if (attemptQuickLogin(frame)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 4. 重新加载页面以触发Cookie刷新
|
// 4. 重新加载页面以触发Cookie刷新
|
||||||
log.info("【{}-Cookie Refresh】重新加载页面...", cookieId);
|
log.info("【{}-Cookie Refresh】重新加载页面...", cookieId);
|
||||||
try {
|
try {
|
||||||
@ -618,6 +677,7 @@ public class BrowserService {
|
|||||||
// 9. 更新数据库
|
// 9. 更新数据库
|
||||||
if (!newCookieStr.equals(cookie.getValue())) {
|
if (!newCookieStr.equals(cookie.getValue())) {
|
||||||
cookie.setValue(newCookieStr);
|
cookie.setValue(newCookieStr);
|
||||||
|
log.debug("【{}】🤖刷新浏览器后获取到的 cookie 为: {}", cookieId, newCookieStr);
|
||||||
cookieRepository.save(cookie);
|
cookieRepository.save(cookie);
|
||||||
log.info("【{}-Cookie Refresh】✅ Cookie已更新并保存到数据库: {}", cookieId, cookieId);
|
log.info("【{}-Cookie Refresh】✅ Cookie已更新并保存到数据库: {}", cookieId, cookieId);
|
||||||
} else {
|
} else {
|
||||||
@ -652,7 +712,7 @@ public class BrowserService {
|
|||||||
log.info("【QR Login】Verifying cookies for account: {}", accountId);
|
log.info("【QR Login】Verifying cookies for account: {}", accountId);
|
||||||
|
|
||||||
try (BrowserContext context = browser.newContext(new Browser.NewContextOptions()
|
try (BrowserContext context = browser.newContext(new Browser.NewContextOptions()
|
||||||
.setUserAgent("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")
|
.setUserAgent("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")
|
||||||
// .setViewportSize(1920, 1080)
|
// .setViewportSize(1920, 1080)
|
||||||
)) {
|
)) {
|
||||||
|
|
||||||
@ -719,13 +779,13 @@ public class BrowserService {
|
|||||||
private BrowserContext getPersistentContext(String cookieId) {
|
private BrowserContext getPersistentContext(String cookieId) {
|
||||||
// 获取或创建该账号的同步锁
|
// 获取或创建该账号的同步锁
|
||||||
Object lock = contextLocks.computeIfAbsent(cookieId, k -> new Object());
|
Object lock = contextLocks.computeIfAbsent(cookieId, k -> new Object());
|
||||||
|
|
||||||
// 使用同步锁防止并发创建同一个上下文
|
// 使用同步锁防止并发创建同一个上下文
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
return getPersistentContextInternal(cookieId);
|
return getPersistentContextInternal(cookieId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 内部方法:实际执行获取或创建上下文的逻辑
|
* 内部方法:实际执行获取或创建上下文的逻辑
|
||||||
*/
|
*/
|
||||||
@ -829,7 +889,7 @@ public class BrowserService {
|
|||||||
log.warn("【{}-Cookie Refresh】关闭失效上下文时出错: {}", cookieId, cookieId, e);
|
log.warn("【{}-Cookie Refresh】关闭失效上下文时出错: {}", cookieId, cookieId, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除整个 UserData 目录,包括 SingletonLock 文件
|
// 删除整个 UserData 目录,包括 SingletonLock 文件
|
||||||
try {
|
try {
|
||||||
String userDataDir = "browser_data/cookie_refresh/" + cookieId;
|
String userDataDir = "browser_data/cookie_refresh/" + cookieId;
|
||||||
@ -842,7 +902,7 @@ public class BrowserService {
|
|||||||
log.warn("【{}-Cookie Refresh】删除UserData目录失败: {}", cookieId, e.getMessage());
|
log.warn("【{}-Cookie Refresh】删除UserData目录失败: {}", cookieId, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 递归删除目录
|
* 递归删除目录
|
||||||
*/
|
*/
|
||||||
@ -850,13 +910,13 @@ public class BrowserService {
|
|||||||
if (java.nio.file.Files.isDirectory(path)) {
|
if (java.nio.file.Files.isDirectory(path)) {
|
||||||
try (java.util.stream.Stream<java.nio.file.Path> stream = java.nio.file.Files.walk(path)) {
|
try (java.util.stream.Stream<java.nio.file.Path> stream = java.nio.file.Files.walk(path)) {
|
||||||
stream.sorted(java.util.Comparator.reverseOrder())
|
stream.sorted(java.util.Comparator.reverseOrder())
|
||||||
.forEach(p -> {
|
.forEach(p -> {
|
||||||
try {
|
try {
|
||||||
java.nio.file.Files.delete(p);
|
java.nio.file.Files.delete(p);
|
||||||
} catch (java.io.IOException e) {
|
} catch (java.io.IOException e) {
|
||||||
log.warn("删除文件失败: {}", p, e);
|
log.warn("删除文件失败: {}", p, e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -356,14 +356,14 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebSocket连接循环 - 对应Python的main方法中的while True循环
|
* WebSocket连接循环 - 重构版本,去除嵌套
|
||||||
|
* 核心逻辑:外层循环保持运行,内部单次连接尝试,失败后延迟重试
|
||||||
*/
|
*/
|
||||||
private void connectionLoop() {
|
private void connectionLoop() {
|
||||||
while (running.get()) {
|
while (running.get()) {
|
||||||
try {
|
try {
|
||||||
// 检查账号是否启用
|
// 检查账号是否启用
|
||||||
Optional<Cookie> cookieOpt = cookieRepository.findById(cookieId);
|
if (!isAccountEnabled()) {
|
||||||
if (cookieOpt.isEmpty() || !Boolean.TRUE.equals(cookieOpt.get().getEnabled())) {
|
|
||||||
log.info("【{}】账号已禁用,停止连接循环", cookieId);
|
log.info("【{}】账号已禁用,停止连接循环", cookieId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -372,59 +372,108 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
setConnectionState(ConnectionState.CONNECTING, "准备建立WebSocket连接");
|
setConnectionState(ConnectionState.CONNECTING, "准备建立WebSocket连接");
|
||||||
log.info("【{}】WebSocket目标地址: {}", cookieId, WEBSOCKET_URL);
|
log.info("【{}】WebSocket目标地址: {}", cookieId, WEBSOCKET_URL);
|
||||||
|
|
||||||
// 创建WebSocket连接
|
// 单次连接尝试
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
|
|
||||||
// 连接成功后,等待连接断开
|
// 连接成功后,进入等待循环,直到连接断开
|
||||||
// WebSocket会在另一个线程中运行,这里需要阻塞等待
|
waitForDisconnection();
|
||||||
log.info("【{}】WebSocket连接已建立,等待连接断开...", cookieId);
|
|
||||||
while (connected.get() && running.get()) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(1000); // 每秒检查一次连接状态
|
|
||||||
} catch (InterruptedException ie) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("【{}】WebSocket连接已断开", cookieId);
|
log.info("【{}】WebSocket连接已断开", cookieId);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
// 统一处理连接错误
|
||||||
handleConnectionError(e);
|
handleConnectionError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重连延迟
|
// 计算并执行重连延迟
|
||||||
if (running.get()) {
|
if (running.get()) {
|
||||||
int retryDelay = calculateRetryDelay(connectionFailures.get());
|
int retryDelay = calculateRetryDelay(connectionFailures.get());
|
||||||
log.info("【{}】{}秒后尝试重连...", cookieId, retryDelay);
|
log.info("【{}】{}秒后尝试重连...", cookieId, retryDelay);
|
||||||
try {
|
sleepWithInterruptCheck(retryDelay * 1000L);
|
||||||
Thread.sleep(retryDelay * 1000L);
|
|
||||||
} catch (InterruptedException ie) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("【{}】WebSocket 连接循环已退出", cookieId);
|
log.info("【{}】WebSocket 连接循环已退出", cookieId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查账号是否启用
|
||||||
|
*/
|
||||||
|
private boolean isAccountEnabled() {
|
||||||
|
try {
|
||||||
|
Optional<Cookie> cookieOpt = cookieRepository.findById(cookieId);
|
||||||
|
return cookieOpt.isPresent() && Boolean.TRUE.equals(cookieOpt.get().getEnabled());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("【{}】检查账号状态失败", cookieId, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建WebSocket连接
|
* 等待WebSocket连接断开
|
||||||
|
*/
|
||||||
|
private void waitForDisconnection() {
|
||||||
|
log.info("【{}】WebSocket连接已建立,等待连接断开...", cookieId);
|
||||||
|
while (connected.get() && running.get()) {
|
||||||
|
sleepWithInterruptCheck(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可中断的Sleep
|
||||||
|
*/
|
||||||
|
private void sleepWithInterruptCheck(long millis) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(millis);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
log.warn("【{}】Sleep被中断", cookieId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建WebSocket连接 - 重构版本,纯粹的单次连接尝试
|
||||||
|
* 失败直接抛异常,由 connectionLoop() 统一处理重试
|
||||||
*/
|
*/
|
||||||
private void connectWebSocket() throws Exception {
|
private void connectWebSocket() throws Exception {
|
||||||
|
log.info("【{}】开始建立WebSocket连接...", cookieId);
|
||||||
|
|
||||||
// 配置WebSocket容器,设置缓冲区大小为10MB(解决1009错误:消息过大)
|
// 配置WebSocket容器,设置缓冲区大小为10MB(解决1009错误:消息过大)
|
||||||
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
|
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
|
||||||
container.setDefaultMaxTextMessageBufferSize(10 * 1024 * 1024); // 10MB
|
container.setDefaultMaxTextMessageBufferSize(10 * 1024 * 1024); // 10MB
|
||||||
container.setDefaultMaxBinaryMessageBufferSize(10 * 1024 * 1024); // 10MB
|
container.setDefaultMaxBinaryMessageBufferSize(10 * 1024 * 1024); // 10MB
|
||||||
|
|
||||||
// 使用配置好的容器创建WebSocket客户端
|
// 使用配置好的容器创建WebSocket客户端
|
||||||
WebSocketClient client = new StandardWebSocketClient(container);
|
WebSocketClient client = new StandardWebSocketClient(container);
|
||||||
|
|
||||||
// 准备请求头 - 使用WebSocketHttpHeaders,添加所有必要的headers
|
// 准备请求头
|
||||||
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
|
WebSocketHttpHeaders headers = buildWebSocketHeaders();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 发起WebSocket握手
|
||||||
|
ListenableFuture<WebSocketSession> future =
|
||||||
|
client.doHandshake(this, headers, URI.create(WEBSOCKET_URL));
|
||||||
|
|
||||||
|
// 等待连接完成(超时10秒)
|
||||||
|
this.webSocketSession = future.get(10, TimeUnit.SECONDS);
|
||||||
|
log.info("【{}】WebSocket连接建立成功", cookieId);
|
||||||
|
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new Exception("WebSocket连接被中断", e);
|
||||||
|
} catch (java.util.concurrent.ExecutionException e) {
|
||||||
|
throw new Exception("WebSocket连接执行失败: " + e.getMessage(), e);
|
||||||
|
} catch (java.util.concurrent.TimeoutException e) {
|
||||||
|
throw new Exception("WebSocket连接超时", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建WebSocket请求头
|
||||||
|
*/
|
||||||
|
private WebSocketHttpHeaders buildWebSocketHeaders() {
|
||||||
|
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
|
||||||
headers.add("Accept-Encoding", "gzip, deflate, br, zstd");
|
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("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("Cache-Control", "no-cache");
|
||||||
@ -437,24 +486,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
headers.add("sec-websocket-version", "13");
|
headers.add("sec-websocket-version", "13");
|
||||||
headers.add("upgrade", "websocket");
|
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");
|
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;
|
||||||
try {
|
|
||||||
// doHandshake参数: WebSocketHandler, WebSocketHttpHeaders, URI
|
|
||||||
ListenableFuture<WebSocketSession> future =
|
|
||||||
client.doHandshake(this, headers, URI.create(WEBSOCKET_URL));
|
|
||||||
|
|
||||||
// 等待连接完成
|
|
||||||
this.webSocketSession = future.get(10, TimeUnit.SECONDS);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
throw new Exception("WebSocket连接被中断", e);
|
|
||||||
} catch (java.util.concurrent.ExecutionException e) {
|
|
||||||
throw new Exception("WebSocket连接执行失败: " + e.getMessage(), e);
|
|
||||||
} catch (java.util.concurrent.TimeoutException e) {
|
|
||||||
throw new Exception("WebSocket连接超时", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("【{}】WebSocket连接建立成功", cookieId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -590,7 +622,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
|
|
||||||
JSONObject regHeaders = new JSONObject();
|
JSONObject regHeaders = new JSONObject();
|
||||||
regHeaders.put("cache-header", "app-key token ua wv");
|
regHeaders.put("cache-header", "app-key token ua wv");
|
||||||
regHeaders.put("app-key",WEBSOCKET_APP_KEY);
|
regHeaders.put("app-key", WEBSOCKET_APP_KEY);
|
||||||
regHeaders.put("token", currentToken);
|
regHeaders.put("token", currentToken);
|
||||||
regHeaders.put("ua", "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");
|
regHeaders.put("ua", "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");
|
||||||
regHeaders.put("dt", "j");
|
regHeaders.put("dt", "j");
|
||||||
@ -650,124 +682,109 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新Token - 对应Python的refresh_token()方法
|
* 刷新Token - 重构版本,去除嵌套循环
|
||||||
* 添加自动降级机制:Token获取失败时自动刷新Cookie
|
* 策略:尝试获取Token,失败则刷新Cookie后抛异常,由上层决定是否重试
|
||||||
*/
|
*/
|
||||||
private String refreshToken() {
|
private String refreshToken() {
|
||||||
int maxRetries = 3;
|
lastTokenRefreshStatus = "started";
|
||||||
int retryCount = 0;
|
log.info("【{}】开始刷新token...", cookieId);
|
||||||
|
|
||||||
while (retryCount < maxRetries) {
|
// 检查是否在消息冷却期内
|
||||||
try {
|
long currentTime = System.currentTimeMillis();
|
||||||
if (retryCount > 0) {
|
long timeSinceLastMessage = currentTime - lastMessageReceivedTime.get();
|
||||||
log.info("【{}】Token获取失败,第 {} 次重试...", cookieId, retryCount);
|
if (lastMessageReceivedTime.get() > 0 && timeSinceLastMessage < MESSAGE_COOLDOWN * 1000L) {
|
||||||
// 添加重试延迟,避免过快重试导致资源竞争
|
long remainingTime = MESSAGE_COOLDOWN * 1000L - timeSinceLastMessage;
|
||||||
try {
|
log.info("【{}】收到消息后冷却中,放弃本次token刷新,还需等待 {} 秒",
|
||||||
Thread.sleep(2000 * retryCount); // 指数退避:2s, 4s, 6s
|
cookieId, remainingTime / 1000);
|
||||||
} catch (InterruptedException ie) {
|
lastTokenRefreshStatus = "skipped_cooldown";
|
||||||
Thread.currentThread().interrupt();
|
return null;
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.info("【{}】开始刷新token...", cookieId);
|
|
||||||
}
|
|
||||||
lastTokenRefreshStatus = "started";
|
|
||||||
|
|
||||||
// 检查是否在消息冷却期内
|
|
||||||
long currentTime = System.currentTimeMillis();
|
|
||||||
long timeSinceLastMessage = currentTime - lastMessageReceivedTime.get();
|
|
||||||
if (lastMessageReceivedTime.get() > 0 && timeSinceLastMessage < MESSAGE_COOLDOWN * 1000L) {
|
|
||||||
long remainingTime = MESSAGE_COOLDOWN * 1000L - timeSinceLastMessage;
|
|
||||||
log.info("【{}】收到消息后冷却中,放弃本次token刷新,还需等待 {} 秒",
|
|
||||||
cookieId, remainingTime / 1000);
|
|
||||||
lastTokenRefreshStatus = "skipped_cooldown";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从数据库重新加载Cookie(可能已被浏览器刷新更新)
|
|
||||||
try {
|
|
||||||
Optional<Cookie> cookieOpt = cookieRepository.findById(cookieId);
|
|
||||||
if (cookieOpt.isPresent()) {
|
|
||||||
String newCookiesStr = cookieOpt.get().getValue();
|
|
||||||
if (!newCookiesStr.equals(this.cookiesStr)) {
|
|
||||||
log.info("【{}】检测到数据库中的cookie已更新,重新加载cookie", cookieId);
|
|
||||||
this.cookiesStr = newCookiesStr;
|
|
||||||
this.cookies = parseCookies(this.cookiesStr);
|
|
||||||
log.warn("【{}】Cookie已从数据库重新加载", cookieId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("【{}】从数据库重新加载cookie失败,继续使用当前cookie: {}", cookieId, e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试获取Token
|
|
||||||
log.debug("【{}】🤖准备调用官方API获取Token...", cookieId);
|
|
||||||
String token = attemptGetToken();
|
|
||||||
log.debug("【{}】🤖准备调用官方API获取Token为: {}", cookieId, token);
|
|
||||||
|
|
||||||
if (token != null) {
|
|
||||||
// Token获取成功
|
|
||||||
this.currentToken = token;
|
|
||||||
this.lastTokenRefreshTime.set(System.currentTimeMillis());
|
|
||||||
this.lastMessageReceivedTime.set(0); // 重置消息接收时间
|
|
||||||
log.warn("【{}】✅ Token刷新成功", cookieId);
|
|
||||||
lastTokenRefreshStatus = "success";
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Token获取失败,尝试刷新Cookie
|
|
||||||
log.warn("【{}】⚠️ Token获取失败,尝试通过浏览器刷新Cookie...", cookieId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Map<String, String> newCookies = browserService.refreshCookies(cookieId);
|
|
||||||
|
|
||||||
if (newCookies != null && !newCookies.isEmpty()) {
|
|
||||||
log.info("【{}】✅ Cookie刷新成功,重新加载...", cookieId);
|
|
||||||
// 重新加载Cookie
|
|
||||||
loadCookies();
|
|
||||||
retryCount++;
|
|
||||||
// 继续下一轮重试
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
log.error("【{}】❌ Cookie刷新失败,尝试强制重建持久化上下文", cookieId);
|
|
||||||
// 强制关闭持久化上下文,下次重试时会重新创建
|
|
||||||
try {
|
|
||||||
browserService.closePersistentContext(cookieId);
|
|
||||||
Thread.sleep(3000); // 等待3秒确保资源完全释放
|
|
||||||
|
|
||||||
// 再次尝试刷新Cookie
|
|
||||||
newCookies = browserService.refreshCookies(cookieId);
|
|
||||||
if (newCookies != null && !newCookies.isEmpty()) {
|
|
||||||
log.info("【{}】重建上下文后Cookie刷新成功", cookieId);
|
|
||||||
loadCookies();
|
|
||||||
retryCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} catch (Exception retryEx) {
|
|
||||||
log.error("【{}】重建上下文后仍然失败: {}", cookieId, retryEx.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
log.error("【{}】❌ Cookie刷新最终失败,无法继续", cookieId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("【{}】❌ Cookie刷新异常: {}", cookieId, e.getMessage());
|
|
||||||
// 异常时也尝试关闭上下文
|
|
||||||
try {
|
|
||||||
browserService.closePersistentContext(cookieId);
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("【{}】Token刷新过程异常", cookieId, e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.error("【{}】❌ Token刷新最终失败,已重试 {} 次", cookieId, retryCount);
|
// 从数据库重新加载Cookie(可能已被浏览器刷新更新)
|
||||||
lastTokenRefreshStatus = "failed";
|
reloadCookieFromDatabase();
|
||||||
return null;
|
|
||||||
|
// 尝试获取Token
|
||||||
|
log.debug("【{}】🤖准备调用官方API获取Token...", cookieId);
|
||||||
|
String token = attemptGetToken();
|
||||||
|
log.debug("【{}】🤖调用官方API获取Token结果: {}", cookieId, token != null ? "成功" : "失败");
|
||||||
|
|
||||||
|
if (token != null) {
|
||||||
|
// Token获取成功
|
||||||
|
this.currentToken = token;
|
||||||
|
this.lastTokenRefreshTime.set(System.currentTimeMillis());
|
||||||
|
this.lastMessageReceivedTime.set(0);
|
||||||
|
log.warn("【{}】✅ Token刷新成功", cookieId);
|
||||||
|
lastTokenRefreshStatus = "success";
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token获取失败,尝试刷新Cookie然后返回null
|
||||||
|
// 不在这里重试,失败后让connectionLoop处理重连
|
||||||
|
log.warn("【{}】⚠️ Token获取失败,尝试通过浏览器刷新Cookie...", cookieId);
|
||||||
|
refreshCookieViaBrowser();
|
||||||
|
|
||||||
|
// 尝试获取Token
|
||||||
|
log.debug("【{}】🤖刷新Cookie后调用官方API获取Token...", cookieId);
|
||||||
|
token = attemptGetToken();
|
||||||
|
log.debug("【{}】🤖刷新Cookie后调用官方API获取Token结果: {}", cookieId, token != null ? "成功" : "失败");
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据库重新加载Cookie(避免重复代码)
|
||||||
|
*/
|
||||||
|
private void reloadCookieFromDatabase() {
|
||||||
|
try {
|
||||||
|
Optional<Cookie> cookieOpt = cookieRepository.findById(cookieId);
|
||||||
|
if (cookieOpt.isPresent()) {
|
||||||
|
String newCookiesStr = cookieOpt.get().getValue();
|
||||||
|
if (!newCookiesStr.equals(this.cookiesStr)) {
|
||||||
|
log.info("【{}】检测到数据库中的cookie已更新,重新加载cookie", cookieId);
|
||||||
|
this.cookiesStr = newCookiesStr;
|
||||||
|
this.cookies = parseCookies(this.cookiesStr);
|
||||||
|
log.warn("【{}】Cookie已从数据库重新加载", cookieId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("【{}】从数据库重新加载cookie失败,继续使用当前cookie: {}", cookieId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过浏览器刷新Cookie(避免重复代码)
|
||||||
|
*/
|
||||||
|
private void refreshCookieViaBrowser() {
|
||||||
|
try {
|
||||||
|
Map<String, String> newCookies = browserService.refreshCookies(cookieId);
|
||||||
|
|
||||||
|
if (newCookies != null && !newCookies.isEmpty()) {
|
||||||
|
log.info("【{}】✅ Cookie刷新成功,重新加载...", cookieId);
|
||||||
|
loadCookies();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首次失败,尝试重建上下文
|
||||||
|
log.warn("【{}】Cookie刷新失败,尝试强制重建持久化上下文", cookieId);
|
||||||
|
browserService.closePersistentContext(cookieId);
|
||||||
|
Thread.sleep(3000); // 等待资源释放
|
||||||
|
|
||||||
|
// 再次尝试
|
||||||
|
newCookies = browserService.refreshCookies(cookieId);
|
||||||
|
if (newCookies != null && !newCookies.isEmpty()) {
|
||||||
|
log.info("【{}】重建上下文后Cookie刷新成功", cookieId);
|
||||||
|
loadCookies();
|
||||||
|
} else {
|
||||||
|
log.error("【{}】❌ Cookie刷新最终失败", cookieId);
|
||||||
|
}
|
||||||
|
lastTokenRefreshStatus = "success";
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("【{}】❌ Cookie刷新异常: {}", cookieId, e.getMessage());
|
||||||
|
try {
|
||||||
|
browserService.closePersistentContext(cookieId);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
lastTokenRefreshStatus = "failed";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -811,9 +828,10 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
log.info("【{}】API端点: {}", cookieId, url);
|
log.info("【{}】API端点: {}", cookieId, url);
|
||||||
log.info("【{}】timestamp: {}", cookieId, timestamp);
|
log.info("【{}】timestamp: {}", cookieId, timestamp);
|
||||||
log.info("【{}】sign: {}", cookieId, sign);
|
log.info("【{}】sign: {}", cookieId, sign);
|
||||||
|
log.info("【{}】cookies: {}", cookieId, cookiesStr);
|
||||||
|
|
||||||
// 发送POST请求
|
// 发送POST请求
|
||||||
HttpRequest request = cn.hutool.http.HttpRequest.post(url);
|
HttpRequest request = HttpRequest.post(url);
|
||||||
request.form("data", dataVal);
|
request.form("data", dataVal);
|
||||||
params.forEach((k, v) -> request.form(k, v.toString()));
|
params.forEach((k, v) -> request.form(k, v.toString()));
|
||||||
request.header("cookie", cookiesStr);
|
request.header("cookie", cookiesStr);
|
||||||
@ -848,7 +866,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
JSONObject data = resJson.getJSONObject("data");
|
JSONObject data = resJson.getJSONObject("data");
|
||||||
if (data.containsKey("accessToken")) {
|
if (data.containsKey("accessToken")) {
|
||||||
String newToken = data.getString("accessToken");
|
String newToken = data.getString("accessToken");
|
||||||
log.info("【{}】获取到accessToken", cookieId);
|
log.info("【{}】获取到accessToken: {}", cookieId, newToken);
|
||||||
return newToken;
|
return newToken;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1069,7 +1087,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
} finally {
|
} finally {
|
||||||
activeMessageTasks.decrementAndGet();
|
activeMessageTasks.decrementAndGet();
|
||||||
messageSemaphore.release();
|
messageSemaphore.release();
|
||||||
|
|
||||||
// 定期记录活跃任务数(每100个任务记录一次)
|
// 定期记录活跃任务数(每100个任务记录一次)
|
||||||
// 对应Python: if self.active_message_tasks % 100 == 0 and self.active_message_tasks > 0
|
// 对应Python: if self.active_message_tasks % 100 == 0 and self.active_message_tasks > 0
|
||||||
if (currentTasks % 100 == 0 && currentTasks > 0) {
|
if (currentTasks % 100 == 0 && currentTasks > 0) {
|
||||||
@ -1149,7 +1167,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
/**
|
/**
|
||||||
* 判断是否为系统消息
|
* 判断是否为系统消息
|
||||||
* 对应Python的系统消息过滤逻辑 (Line 7626-7662)
|
* 对应Python的系统消息过滤逻辑 (Line 7626-7662)
|
||||||
*
|
*
|
||||||
* @param sendMessage 消息内容
|
* @param sendMessage 消息内容
|
||||||
* @return true=系统消息(需要过滤),false=正常消息
|
* @return true=系统消息(需要过滤),false=正常消息
|
||||||
*/
|
*/
|
||||||
@ -1157,7 +1175,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
if (sendMessage == null) {
|
if (sendMessage == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 15+种系统消息类型
|
// 15+种系统消息类型
|
||||||
return "[我已拍下,待付款]".equals(sendMessage)
|
return "[我已拍下,待付款]".equals(sendMessage)
|
||||||
|| "[你关闭了订单,钱款已原路退返]".equals(sendMessage)
|
|| "[你关闭了订单,钱款已原路退返]".equals(sendMessage)
|
||||||
@ -1177,7 +1195,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
/**
|
/**
|
||||||
* 判断是否为自动发货触发消息
|
* 判断是否为自动发货触发消息
|
||||||
* 对应Python的_is_auto_delivery_trigger()方法 (Line 981-997)
|
* 对应Python的_is_auto_delivery_trigger()方法 (Line 981-997)
|
||||||
*
|
*
|
||||||
* @param sendMessage 消息内容
|
* @param sendMessage 消息内容
|
||||||
* @return true=自动发货触发消息
|
* @return true=自动发货触发消息
|
||||||
*/
|
*/
|
||||||
@ -1197,7 +1215,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
/**
|
/**
|
||||||
* 提取卡片消息的标题
|
* 提取卡片消息的标题
|
||||||
* 对应Python的卡片消息解析逻辑 (Line 7673-7692)
|
* 对应Python的卡片消息解析逻辑 (Line 7673-7692)
|
||||||
*
|
*
|
||||||
* @param message 消息对象
|
* @param message 消息对象
|
||||||
* @return 卡片标题,解析失败返回null
|
* @return 卡片标题,解析失败返回null
|
||||||
*/
|
*/
|
||||||
@ -1208,26 +1226,26 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
if (!message.containsKey("1") || !(message.get("1") instanceof JSONObject)) {
|
if (!message.containsKey("1") || !(message.get("1") instanceof JSONObject)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
JSONObject message1 = message.getJSONObject("1");
|
JSONObject message1 = message.getJSONObject("1");
|
||||||
if (!message1.containsKey("6") || !(message1.get("6") instanceof JSONObject)) {
|
if (!message1.containsKey("6") || !(message1.get("6") instanceof JSONObject)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
JSONObject message6 = message1.getJSONObject("6");
|
JSONObject message6 = message1.getJSONObject("6");
|
||||||
if (!message6.containsKey("3") || !(message6.get("3") instanceof JSONObject)) {
|
if (!message6.containsKey("3") || !(message6.get("3") instanceof JSONObject)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
JSONObject message63 = message6.getJSONObject("3");
|
JSONObject message63 = message6.getJSONObject("3");
|
||||||
if (!message63.containsKey("5")) {
|
if (!message63.containsKey("5")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析JSON内容
|
// 解析JSON内容
|
||||||
String cardContentStr = message63.getString("5");
|
String cardContentStr = message63.getString("5");
|
||||||
JSONObject cardContent = JSON.parseObject(cardContentStr);
|
JSONObject cardContent = JSON.parseObject(cardContentStr);
|
||||||
|
|
||||||
if (cardContent.containsKey("dxCard")) {
|
if (cardContent.containsKey("dxCard")) {
|
||||||
JSONObject dxCard = cardContent.getJSONObject("dxCard");
|
JSONObject dxCard = cardContent.getJSONObject("dxCard");
|
||||||
if (dxCard.containsKey("item")) {
|
if (dxCard.containsKey("item")) {
|
||||||
@ -1241,9 +1259,9 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("【{}】解析卡片消息失败: {}", cookieId, e.getMessage());
|
log.debug("【{}】解析卡片消息失败: {}", cookieId, e.getMessage());
|
||||||
return null;
|
return null;
|
||||||
@ -2480,7 +2498,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
/**
|
/**
|
||||||
* 处理消息主逻辑 - 完整版(阶段1:基础消息处理)
|
* 处理消息主逻辑 - 完整版(阶段1:基础消息处理)
|
||||||
* 对应Python的handle_message()方法
|
* 对应Python的handle_message()方法
|
||||||
*
|
* <p>
|
||||||
* 阶段1包含:
|
* 阶段1包含:
|
||||||
* 1. 检查账号状态
|
* 1. 检查账号状态
|
||||||
* 2. 发送ACK确认
|
* 2. 发送ACK确认
|
||||||
@ -2538,7 +2556,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
String orderId = extractOrderId(message);
|
String orderId = extractOrderId(message);
|
||||||
if (orderId != null) {
|
if (orderId != null) {
|
||||||
String msgTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
String msgTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
.format(new java.util.Date());
|
.format(new java.util.Date());
|
||||||
log.info("[{}] 【{}】✅ 检测到订单ID: {},开始获取订单详情", msgTime, cookieId, orderId);
|
log.info("[{}] 【{}】✅ 检测到订单ID: {},开始获取订单详情", msgTime, cookieId, orderId);
|
||||||
|
|
||||||
// 提取用户ID和商品ID用于订单详情获取
|
// 提取用户ID和商品ID用于订单详情获取
|
||||||
@ -2563,7 +2581,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
try {
|
try {
|
||||||
log.info("【{}】🔍 完整消息结构: {}", cookieId, message.toJSONString());
|
log.info("【{}】🔍 完整消息结构: {}", cookieId, message.toJSONString());
|
||||||
String msgTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
String msgTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
.format(new java.util.Date());
|
.format(new java.util.Date());
|
||||||
|
|
||||||
// 安全地检查订单状态(红色提醒)
|
// 安全地检查订单状态(红色提醒)
|
||||||
String redReminder = null;
|
String redReminder = null;
|
||||||
@ -2607,7 +2625,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
String chatId;
|
String chatId;
|
||||||
long createTime;
|
long createTime;
|
||||||
String itemId;
|
String itemId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 安全地提取聊天消息信息
|
// 安全地提取聊天消息信息
|
||||||
if (!message.containsKey("1") || !(message.get("1") instanceof JSONObject)) {
|
if (!message.containsKey("1") || !(message.get("1") instanceof JSONObject)) {
|
||||||
@ -2623,7 +2641,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
|
|
||||||
// 提取消息时间
|
// 提取消息时间
|
||||||
createTime = message1.getLongValue("5");
|
createTime = message1.getLongValue("5");
|
||||||
|
|
||||||
// 提取消息详情
|
// 提取消息详情
|
||||||
JSONObject message10 = message1.getJSONObject("10");
|
JSONObject message10 = message1.getJSONObject("10");
|
||||||
sendUserName = message10.getString("senderNick");
|
sendUserName = message10.getString("senderNick");
|
||||||
@ -2659,23 +2677,23 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
|
|
||||||
// 格式化消息时间
|
// 格式化消息时间
|
||||||
String msgTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
String msgTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
.format(new java.util.Date(createTime));
|
.format(new java.util.Date(createTime));
|
||||||
|
|
||||||
// ========== 步骤9: 判断消息方向 ==========
|
// ========== 步骤9: 判断消息方向 ==========
|
||||||
// 对应Python: Line 7561-7568
|
// 对应Python: Line 7561-7568
|
||||||
if (sendUserId.equals(myId)) {
|
if (sendUserId.equals(myId)) {
|
||||||
log.info("[{}] 【手动发出】 商品({}): {}", msgTime, itemId, sendMessage);
|
log.info("[{}] 【手动发出】 商品({}): {}", msgTime, itemId, sendMessage);
|
||||||
|
|
||||||
// 暂停该chat_id的自动回复10分钟
|
// 暂停该chat_id的自动回复10分钟
|
||||||
pauseManager.pauseChat(chatId, cookieId);
|
pauseManager.pauseChat(chatId, cookieId);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 步骤10: 消息通知 ==========
|
// ========== 步骤10: 消息通知 ==========
|
||||||
// 对应Python: Line 7569-7582
|
// 对应Python: Line 7569-7582
|
||||||
log.info("[{}] 【收到】用户: {} (ID: {}), 商品({}): {}",
|
log.info("[{}] 【收到】用户: {} (ID: {}), 商品({}): {}",
|
||||||
msgTime, sendUserName, sendUserId, itemId, sendMessage);
|
msgTime, sendUserName, sendUserId, itemId, sendMessage);
|
||||||
|
|
||||||
// 🔔 立即发送消息通知(独立于自动回复功能)
|
// 🔔 立即发送消息通知(独立于自动回复功能)
|
||||||
// 检查是否为群组消息,如果是群组消息则跳过通知
|
// 检查是否为群组消息,如果是群组消息则跳过通知
|
||||||
@ -2686,7 +2704,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
if (sessionType == null) {
|
if (sessionType == null) {
|
||||||
sessionType = "1"; // 默认为个人消息类型
|
sessionType = "1"; // 默认为个人消息类型
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("30".equals(sessionType)) {
|
if ("30".equals(sessionType)) {
|
||||||
log.info("📱 检测到群组消息(sessionType=30),跳过消息通知");
|
log.info("📱 检测到群组消息(sessionType=30),跳过消息通知");
|
||||||
} else {
|
} else {
|
||||||
@ -2697,7 +2715,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
final String finalSendMessage = sendMessage;
|
final String finalSendMessage = sendMessage;
|
||||||
final String finalItemId = itemId;
|
final String finalItemId = itemId;
|
||||||
final String finalChatId = chatId;
|
final String finalChatId = chatId;
|
||||||
|
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
try {
|
try {
|
||||||
// 发送消息通知(简化版 - 实际应调用NotificationService)
|
// 发送消息通知(简化版 - 实际应调用NotificationService)
|
||||||
@ -2717,7 +2735,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
try {
|
try {
|
||||||
//处理系统消息的订单状态更新
|
//处理系统消息的订单状态更新
|
||||||
orderStatusHandler.handleSystemMessage(message, sendMessage, cookieId, msgTime);
|
orderStatusHandler.handleSystemMessage(message, sendMessage, cookieId, msgTime);
|
||||||
|
|
||||||
// 处理红色提醒消息
|
// 处理红色提醒消息
|
||||||
if (message.containsKey("3") && message.get("3") instanceof JSONObject) {
|
if (message.containsKey("3") && message.get("3") instanceof JSONObject) {
|
||||||
JSONObject message3 = message.getJSONObject("3");
|
JSONObject message3 = message.getJSONObject("3");
|
||||||
@ -2731,8 +2749,8 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
log.error("【{}】订单状态处理失败: {}", cookieId, e.getMessage());
|
log.error("【{}】订单状态处理失败: {}", cookieId, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ========== 步骤12: 系统消息过滤 ==========
|
// ========== 步骤12: 系统消息过滤 ==========
|
||||||
// 对应Python: Line 7626-7662
|
// 对应Python: Line 7626-7662
|
||||||
// 检查并过滤15+种系统消息
|
// 检查并过滤15+种系统消息
|
||||||
@ -2744,16 +2762,16 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
// ========== 步骤13: 自动发货触发检查 ==========
|
// ========== 步骤13: 自动发货触发检查 ==========
|
||||||
// 对应Python: Line 7664-7669
|
// 对应Python: Line 7664-7669
|
||||||
if (isAutoDeliveryTrigger(sendMessage)) {
|
if (isAutoDeliveryTrigger(sendMessage)) {
|
||||||
log.info("[{}] 【{}】检测到自动发货触发消息,即使在暂停期间也继续处理: {}",
|
log.info("[{}] 【{}】检测到自动发货触发消息,即使在暂停期间也继续处理: {}",
|
||||||
msgTime, cookieId, sendMessage);
|
msgTime, cookieId, sendMessage);
|
||||||
|
|
||||||
// 异步处理自动发货
|
// 异步处理自动发货
|
||||||
final String finalSendUserName = sendUserName;
|
final String finalSendUserName = sendUserName;
|
||||||
final String finalSendUserId = sendUserId;
|
final String finalSendUserId = sendUserId;
|
||||||
final String finalItemId = itemId;
|
final String finalItemId = itemId;
|
||||||
final String finalChatId = chatId;
|
final String finalChatId = chatId;
|
||||||
final String finalMsgTime = msgTime;
|
final String finalMsgTime = msgTime;
|
||||||
|
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
try {
|
try {
|
||||||
// 调用统一的自动发货处理方法(已在本类中实现)
|
// 调用统一的自动发货处理方法(已在本类中实现)
|
||||||
@ -2763,7 +2781,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
log.error("【{}】自动发货处理失败", cookieId, e);
|
log.error("【{}】自动发货处理失败", cookieId, e);
|
||||||
}
|
}
|
||||||
}, scheduledExecutor);
|
}, scheduledExecutor);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2771,27 +2789,27 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
// 对应Python: Line 7670-7749
|
// 对应Python: Line 7670-7749
|
||||||
if ("[卡片消息]".equals(sendMessage)) {
|
if ("[卡片消息]".equals(sendMessage)) {
|
||||||
String cardTitle = extractCardTitle(message);
|
String cardTitle = extractCardTitle(message);
|
||||||
|
|
||||||
if ("我已小刀,待刀成".equals(cardTitle)) {
|
if ("我已小刀,待刀成".equals(cardTitle)) {
|
||||||
log.info("[{}] 【{}】【系统】检测到\"我已小刀,待刀成\",即使在暂停期间也继续处理",
|
log.info("[{}] 【{}】【系统】检测到\"我已小刀,待刀成\",即使在暂停期间也继续处理",
|
||||||
msgTime, cookieId);
|
msgTime, cookieId);
|
||||||
|
|
||||||
// 检查商品是否属于当前cookies
|
// 检查商品是否属于当前cookies
|
||||||
if (itemId != null && !itemId.startsWith("auto_")) {
|
if (itemId != null && !itemId.startsWith("auto_")) {
|
||||||
// 商品归属验证(简化版 - 实际应查询数据库)
|
// 商品归属验证(简化版 - 实际应查询数据库)
|
||||||
log.warn("[{}] 【{}】✅ 商品 {} 归属验证通过", msgTime, cookieId, itemId);
|
log.warn("[{}] 【{}】✅ 商品 {} 归属验证通过", msgTime, cookieId, itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取订单ID(使用已在2538行定义的orderId变量)
|
// 提取订单ID(使用已在2538行定义的orderId变量)
|
||||||
orderId = extractOrderId(message);
|
orderId = extractOrderId(message);
|
||||||
if (orderId == null) {
|
if (orderId == null) {
|
||||||
log.warn("[{}] 【{}】❌ 未能提取到订单ID,无法执行免拼发货", msgTime, cookieId);
|
log.warn("[{}] 【{}】❌ 未能提取到订单ID,无法执行免拼发货", msgTime, cookieId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标记为小刀订单(简化版 - 实际应更新数据库)
|
// 标记为小刀订单(简化版 - 实际应更新数据库)
|
||||||
log.info("[{}] 【{}】✅ 订单 {} 已标记为小刀订单", msgTime, cookieId, orderId);
|
log.info("[{}] 【{}】✅ 订单 {} 已标记为小刀订单", msgTime, cookieId, orderId);
|
||||||
|
|
||||||
// 异步执行免拼发货
|
// 异步执行免拼发货
|
||||||
final String finalOrderId = orderId;
|
final String finalOrderId = orderId;
|
||||||
final String finalItemId = itemId;
|
final String finalItemId = itemId;
|
||||||
@ -2799,27 +2817,27 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
final String finalSendUserName = sendUserName;
|
final String finalSendUserName = sendUserName;
|
||||||
final String finalChatId = chatId;
|
final String finalChatId = chatId;
|
||||||
final String finalMsgTime = msgTime;
|
final String finalMsgTime = msgTime;
|
||||||
|
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
try {
|
try {
|
||||||
// 延迟2秒
|
// 延迟2秒
|
||||||
Thread.sleep(2000);
|
Thread.sleep(2000);
|
||||||
|
|
||||||
// 调用自动免拼发货方法(简化版 - 实际应调用API)
|
// 调用自动免拼发货方法(简化版 - 实际应调用API)
|
||||||
log.info("[{}] 【{}】延迟2秒后执行免拼发货(autoFreeShipping已调用)", finalMsgTime, cookieId);
|
log.info("[{}] 【{}】延迟2秒后执行免拼发货(autoFreeShipping已调用)", finalMsgTime, cookieId);
|
||||||
|
|
||||||
// 然后执行自动发货(handleAutoDelivery方法已存在)
|
// 然后执行自动发货(handleAutoDelivery方法已存在)
|
||||||
log.info("[{}] 【{}】免拼发货后继续自动发货流程", finalMsgTime, cookieId);
|
log.info("[{}] 【{}】免拼发货后继续自动发货流程", finalMsgTime, cookieId);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("【{}】处理免拼小刀异常", cookieId, e);
|
log.error("【{}】处理免拼小刀异常", cookieId, e);
|
||||||
}
|
}
|
||||||
}, scheduledExecutor);
|
}, scheduledExecutor);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
log.info("[{}] 【{}】收到卡片消息,标题: {}", msgTime, cookieId,
|
log.info("[{}] 【{}】收到卡片消息,标题: {}", msgTime, cookieId,
|
||||||
cardTitle != null ? cardTitle : "未知");
|
cardTitle != null ? cardTitle : "未知");
|
||||||
// 不是目标卡片消息,继续正常处理流程
|
// 不是目标卡片消息,继续正常处理流程
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2834,7 +2852,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
final String finalItemId = itemId;
|
final String finalItemId = itemId;
|
||||||
final String finalChatId = chatId;
|
final String finalChatId = chatId;
|
||||||
final String finalMsgTime = msgTime;
|
final String finalMsgTime = msgTime;
|
||||||
|
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
try {
|
try {
|
||||||
// 防抖回复逻辑(简化版 - 实际需实现消息去重和防抖计时器)
|
// 防抖回复逻辑(简化版 - 实际需实现消息去重和防抖计时器)
|
||||||
@ -2843,15 +2861,15 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
// 2. 管理防抖任务Map
|
// 2. 管理防抖任务Map
|
||||||
// 3. 取消旧任务并调度新任务
|
// 3. 取消旧任务并调度新任务
|
||||||
// 4. 延迟后调用processChatMessageReply
|
// 4. 延迟后调用processChatMessageReply
|
||||||
|
|
||||||
log.info("【{}】防抖回复调度已启动: chatId={}, 用户={}, 消息={}",
|
log.info("【{}】防抖回复调度已启动: chatId={}, 用户={}, 消息={}",
|
||||||
cookieId, finalChatId, finalSendUserName, finalSendMessage);
|
cookieId, finalChatId, finalSendUserName, finalSendMessage);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("【{}】防抖回复调度失败", cookieId, e);
|
log.error("【{}】防抖回复调度失败", cookieId, e);
|
||||||
}
|
}
|
||||||
}, scheduledExecutor);
|
}, scheduledExecutor);
|
||||||
|
|
||||||
log.debug("【{}】消息处理完成(阶段3 - 全部15个步骤)", cookieId);
|
log.debug("【{}】消息处理完成(阶段3 - 全部15个步骤)", cookieId);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -2862,7 +2880,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
/**
|
/**
|
||||||
* 解密消息内容
|
* 解密消息内容
|
||||||
* 对应Python的消息解密逻辑 (Line 7336-7391)
|
* 对应Python的消息解密逻辑 (Line 7336-7391)
|
||||||
*
|
*
|
||||||
* @param messageData 原始消息数据
|
* @param messageData 原始消息数据
|
||||||
* @return 解密后的消息对象,失败返回null
|
* @return 解密后的消息对象,失败返回null
|
||||||
*/
|
*/
|
||||||
@ -2870,9 +2888,9 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
try {
|
try {
|
||||||
// 获取同步数据
|
// 获取同步数据
|
||||||
JSONObject syncData = messageData.getJSONObject("body")
|
JSONObject syncData = messageData.getJSONObject("body")
|
||||||
.getJSONObject("syncPushPackage")
|
.getJSONObject("syncPushPackage")
|
||||||
.getJSONArray("data")
|
.getJSONArray("data")
|
||||||
.getJSONObject(0);
|
.getJSONObject(0);
|
||||||
|
|
||||||
// 检查是否有必要的字段
|
// 检查是否有必要的字段
|
||||||
if (!syncData.containsKey("data")) {
|
if (!syncData.containsKey("data")) {
|
||||||
@ -2881,29 +2899,29 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String data = syncData.getString("data");
|
String data = syncData.getString("data");
|
||||||
|
|
||||||
// 尝试Base64解码 + JSON解析(对应Python的第一次尝试)
|
// 尝试Base64解码 + JSON解析(对应Python的第一次尝试)
|
||||||
try {
|
try {
|
||||||
byte[] decodedBytes = java.util.Base64.getDecoder().decode(data);
|
byte[] decodedBytes = java.util.Base64.getDecoder().decode(data);
|
||||||
String decodedStr = new String(decodedBytes, "UTF-8");
|
String decodedStr = new String(decodedBytes, "UTF-8");
|
||||||
JSONObject parsedData = JSON.parseObject(decodedStr);
|
JSONObject parsedData = JSON.parseObject(decodedStr);
|
||||||
|
|
||||||
// 检查是否为系统消息(对应Python Line 7354-7366)
|
// 检查是否为系统消息(对应Python Line 7354-7366)
|
||||||
if (parsedData.containsKey("chatType")) {
|
if (parsedData.containsKey("chatType")) {
|
||||||
if (parsedData.containsKey("operation")) {
|
if (parsedData.containsKey("operation")) {
|
||||||
JSONObject operation = parsedData.getJSONObject("operation");
|
JSONObject operation = parsedData.getJSONObject("operation");
|
||||||
if (operation.containsKey("content")) {
|
if (operation.containsKey("content")) {
|
||||||
JSONObject content = operation.getJSONObject("content");
|
JSONObject content = operation.getJSONObject("content");
|
||||||
|
|
||||||
// 处理系统引导消息
|
// 处理系统引导消息
|
||||||
if (content.containsKey("sessionArouse")) {
|
if (content.containsKey("sessionArouse")) {
|
||||||
String msgTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
String msgTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
.format(new java.util.Date());
|
.format(new java.util.Date());
|
||||||
log.info("[{}] 【{}】【系统】小闲鱼智能提示(已跳过)", msgTime, cookieId);
|
log.info("[{}] 【{}】【系统】小闲鱼智能提示(已跳过)", msgTime, cookieId);
|
||||||
return null;
|
return null;
|
||||||
} else if (content.containsKey("contentType")) {
|
} else if (content.containsKey("contentType")) {
|
||||||
String msgTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
String msgTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
.format(new java.util.Date());
|
.format(new java.util.Date());
|
||||||
log.warn("[{}] 【{}】【系统】其他类型消息(已跳过)", msgTime, cookieId);
|
log.warn("[{}] 【{}】【系统】其他类型消息(已跳过)", msgTime, cookieId);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -2911,10 +2929,10 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果不是系统消息,返回解析的数据
|
// 如果不是系统消息,返回解析的数据
|
||||||
return parsedData;
|
return parsedData;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Base64解析失败,尝试解密(对应Python Line 7372-7373)
|
// Base64解析失败,尝试解密(对应Python Line 7372-7373)
|
||||||
try {
|
try {
|
||||||
@ -2925,7 +2943,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("【{}】解密消息过程异常: {}", cookieId, e.getMessage());
|
log.error("【{}】解密消息过程异常: {}", cookieId, e.getMessage());
|
||||||
return null;
|
return null;
|
||||||
@ -2948,8 +2966,8 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
JSONObject message1Obj = (JSONObject) message1;
|
JSONObject message1Obj = (JSONObject) message1;
|
||||||
if (message1Obj.containsKey("10") && message1Obj.get("10") instanceof JSONObject) {
|
if (message1Obj.containsKey("10") && message1Obj.get("10") instanceof JSONObject) {
|
||||||
JSONObject message10 = message1Obj.getJSONObject("10");
|
JSONObject message10 = message1Obj.getJSONObject("10");
|
||||||
return message10.getString("senderUserId") != null ?
|
return message10.getString("senderUserId") != null ?
|
||||||
message10.getString("senderUserId") : "unknown_user";
|
message10.getString("senderUserId") : "unknown_user";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -2959,7 +2977,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提取商品ID
|
* 提取商品ID
|
||||||
* 对应Python: Line 7436-7445
|
* 对应Python: Line 7436-7445
|
||||||
*/
|
*/
|
||||||
private String extractItemId(JSONObject message) {
|
private String extractItemId(JSONObject message) {
|
||||||
@ -2974,18 +2992,18 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有提取到,调用辅助方法 extractItemIdFromMessage
|
// 如果没有提取到,调用辅助方法 extractItemIdFromMessage
|
||||||
// 对应Python: self.extract_item_id_from_message(message) (Line 3010-3084)
|
// 对应Python: self.extract_item_id_from_message(message) (Line 3010-3084)
|
||||||
String extractedItemId = extractItemIdFromMessage(message);
|
String extractedItemId = extractItemIdFromMessage(message);
|
||||||
if (extractedItemId != null) {
|
if (extractedItemId != null) {
|
||||||
return extractedItemId;
|
return extractedItemId;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("【{}】提取商品ID失败: {}", cookieId, e.getMessage());
|
log.debug("【{}】提取商品ID失败: {}", cookieId, e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用默认值
|
// 使用默认值
|
||||||
String userId = extractUserId(message);
|
String userId = extractUserId(message);
|
||||||
return "auto_" + userId + "_" + System.currentTimeMillis();
|
return "auto_" + userId + "_" + System.currentTimeMillis();
|
||||||
@ -2994,7 +3012,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
/**
|
/**
|
||||||
* 从消息中提取商品ID的辅助方法
|
* 从消息中提取商品ID的辅助方法
|
||||||
* 对应Python: extract_item_id_from_message (Line 3010-3084)
|
* 对应Python: extract_item_id_from_message (Line 3010-3084)
|
||||||
*
|
*
|
||||||
* @param message 消息对象
|
* @param message 消息对象
|
||||||
* @return 商品ID,提取失败返回null
|
* @return 商品ID,提取失败返回null
|
||||||
*/
|
*/
|
||||||
@ -3015,7 +3033,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
// 方法2: 从message["3"]中提取
|
// 方法2: 从message["3"]中提取
|
||||||
if (message.containsKey("3") && message.get("3") instanceof JSONObject) {
|
if (message.containsKey("3") && message.get("3") instanceof JSONObject) {
|
||||||
JSONObject message3 = message.getJSONObject("3");
|
JSONObject message3 = message.getJSONObject("3");
|
||||||
|
|
||||||
// 从extension中提取
|
// 从extension中提取
|
||||||
if (message3.containsKey("extension") && message3.get("extension") instanceof JSONObject) {
|
if (message3.containsKey("extension") && message3.get("extension") instanceof JSONObject) {
|
||||||
JSONObject extension = message3.getJSONObject("extension");
|
JSONObject extension = message3.getJSONObject("extension");
|
||||||
@ -3028,7 +3046,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
return itemId;
|
return itemId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从bizData中提取
|
// 从bizData中提取
|
||||||
if (message3.containsKey("bizData") && message3.get("bizData") instanceof JSONObject) {
|
if (message3.containsKey("bizData") && message3.get("bizData") instanceof JSONObject) {
|
||||||
JSONObject bizData = message3.getJSONObject("bizData");
|
JSONObject bizData = message3.getJSONObject("bizData");
|
||||||
@ -3041,7 +3059,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
return itemId;
|
return itemId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从其他可能的字段中提取
|
// 从其他可能的字段中提取
|
||||||
for (Map.Entry<String, Object> entry : message3.entrySet()) {
|
for (Map.Entry<String, Object> entry : message3.entrySet()) {
|
||||||
if (entry.getValue() instanceof JSONObject) {
|
if (entry.getValue() instanceof JSONObject) {
|
||||||
@ -3056,7 +3074,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从消息内容中提取数字ID
|
// 从消息内容中提取数字ID
|
||||||
String content = message3.getString("content");
|
String content = message3.getString("content");
|
||||||
if (content != null && !content.isEmpty()) {
|
if (content != null && !content.isEmpty()) {
|
||||||
@ -3068,15 +3086,15 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 方法3: 遍历整个消息结构查找可能的商品ID
|
// 方法3: 遍历整个消息结构查找可能的商品ID
|
||||||
String foundItemId = findItemIdRecursive(message, "");
|
String foundItemId = findItemIdRecursive(message, "");
|
||||||
if (foundItemId != null) {
|
if (foundItemId != null) {
|
||||||
return foundItemId;
|
return foundItemId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("【{}】提取商品ID辅助方法失败: {}", cookieId, e.getMessage());
|
log.debug("【{}】提取商品ID辅助方法失败: {}", cookieId, e.getMessage());
|
||||||
return null;
|
return null;
|
||||||
@ -3090,7 +3108,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
private String findItemIdRecursive(Object obj, String path) {
|
private String findItemIdRecursive(Object obj, String path) {
|
||||||
if (obj instanceof JSONObject) {
|
if (obj instanceof JSONObject) {
|
||||||
JSONObject jsonObj = (JSONObject) obj;
|
JSONObject jsonObj = (JSONObject) obj;
|
||||||
|
|
||||||
// 直接查找itemId字段
|
// 直接查找itemId字段
|
||||||
for (String key : new String[]{"itemId", "item_id", "id"}) {
|
for (String key : new String[]{"itemId", "item_id", "id"}) {
|
||||||
if (jsonObj.containsKey(key)) {
|
if (jsonObj.containsKey(key)) {
|
||||||
@ -3104,7 +3122,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 递归查找子对象
|
// 递归查找子对象
|
||||||
for (Map.Entry<String, Object> entry : jsonObj.entrySet()) {
|
for (Map.Entry<String, Object> entry : jsonObj.entrySet()) {
|
||||||
String newPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
|
String newPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
|
||||||
@ -3114,7 +3132,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -67,7 +67,7 @@ public class CaptchaHandler {
|
|||||||
dragSlider(sliderElement, distance, cookieId);
|
dragSlider(sliderElement, distance, cookieId);
|
||||||
|
|
||||||
// 检查是否成功
|
// 检查是否成功
|
||||||
Thread.sleep(10000);
|
Thread.sleep(5000);
|
||||||
boolean success = checkSuccess(verificationUrl, cookieId);
|
boolean success = checkSuccess(verificationUrl, cookieId);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
@ -249,7 +249,7 @@ public class CaptchaHandler {
|
|||||||
double randomX1 = 200 + Math.random() * 400; // 200-600px范围
|
double randomX1 = 200 + Math.random() * 400; // 200-600px范围
|
||||||
double randomY1 = 100 + Math.random() * 200; // 100-300px范围
|
double randomY1 = 100 + Math.random() * 200; // 100-300px范围
|
||||||
page.mouse().move(randomX1, randomY1);
|
page.mouse().move(randomX1, randomY1);
|
||||||
page.mouse().click(randomX1, randomY1);
|
page.mouse().click(randomX1, randomY1, new Mouse.ClickOptions().setClickCount(1));
|
||||||
Thread.sleep(100 + (long)(Math.random() * 200));
|
Thread.sleep(100 + (long)(Math.random() * 200));
|
||||||
|
|
||||||
// 2. 再移动到接近滑块的位置(但不是精确位置)
|
// 2. 再移动到接近滑块的位置(但不是精确位置)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user