init
This commit is contained in:
parent
775cad7ac8
commit
7ee28f755c
@ -0,0 +1,64 @@
|
||||
package com.xianyu.autoreply;
|
||||
|
||||
import com.xianyu.autoreply.entity.SystemSetting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.jdbc.core.BeanPropertyRowMapper;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Component
|
||||
public class DataInitializerLogger implements ApplicationRunner {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DataInitializerLogger.class);
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
private ResourceLoader resourceLoader;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
// 检查初始化标志
|
||||
List<SystemSetting> systemSettings = jdbcTemplate.query("SELECT `key`, `value` FROM system_settings WHERE `key` = 'init_system'", new BeanPropertyRowMapper<>(SystemSetting.class));
|
||||
SystemSetting systemSetting = systemSettings.isEmpty() ? null : systemSettings.get(0);
|
||||
|
||||
if (Objects.isNull(systemSetting)) {
|
||||
// 如果没有初始化过,则执行data.sql中的脚本
|
||||
Resource resource = resourceLoader.getResource("classpath:data.sql");
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
String sqlScript = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
|
||||
// Split the SQL script into individual statements and execute them
|
||||
String[] statements = sqlScript.split(";");
|
||||
for (String statement : statements) {
|
||||
String trimmedStatement = statement.trim();
|
||||
// Remove single-line comments starting with --
|
||||
trimmedStatement = trimmedStatement.replaceAll("--.*", "").trim();
|
||||
if (!trimmedStatement.isEmpty()) {
|
||||
jdbcTemplate.execute(trimmedStatement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在这里定义您的自定义日志内容
|
||||
logger.info("数据库首次初始化完成,默认数据已通过 data.sql 文件成功插入。");
|
||||
|
||||
// 插入初始化标志,防止下次启动时再次执行
|
||||
jdbcTemplate.update("UPDATE system_settings SET value = ? WHERE `key` = ?", "true", "init_system");
|
||||
} else {
|
||||
logger.info("数据库已初始化,跳过默认数据插入。");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.xianyu.autoreply.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.File;
|
||||
|
||||
@Configuration
|
||||
public class DataSourceConfig {
|
||||
|
||||
/**
|
||||
* 自定义 DataSource Bean,以确保在连接池初始化之前创建数据库目录。
|
||||
* @param properties 由 Spring Boot 自动配置并注入的、包含 application.yml 中所有 spring.datasource.* 配置的属性对象。
|
||||
* @return 配置好的 DataSource 实例。
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
public DataSource dataSource(DataSourceProperties properties) {
|
||||
// 从URL中提取文件路径 (e.g., "jdbc:sqlite:./db/xianyu_data.db" -> "./db/xianyu_data.db")
|
||||
String url = properties.getUrl();
|
||||
String path = url.replace("jdbc:sqlite:", "");
|
||||
File dbFile = new File(path);
|
||||
|
||||
// 获取父目录
|
||||
File parentDir = dbFile.getParentFile();
|
||||
|
||||
// 如果父目录不为空且不存在,则创建它
|
||||
if (parentDir != null && !parentDir.exists()) {
|
||||
if (parentDir.mkdirs()) {
|
||||
System.out.println("Successfully created database directory: " + parentDir.getAbsolutePath());
|
||||
} else {
|
||||
System.err.println("Failed to create database directory: " + parentDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 Spring Boot 的标准构建器来创建 DataSource,这样可以重用所有 application.yml 中的配置
|
||||
return properties.initializeDataSourceBuilder().build();
|
||||
}
|
||||
}
|
||||
@ -12,8 +12,10 @@ import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -59,18 +61,41 @@ public class CookieController {
|
||||
return cookieRepository.findByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有Cookie的详细信息(包括值和状态)
|
||||
* 对应Python的 get_cookies_details 接口
|
||||
*/
|
||||
@GetMapping("/details")
|
||||
public List<Cookie> getAllCookiesDetails(@RequestHeader(value = "Authorization", required = false) String token) {
|
||||
return listCookies(token);
|
||||
public List<CookieDetailsResponse> getAllCookiesDetails(@RequestHeader(value = "Authorization", required = false) String token) {
|
||||
Long userId = getUserId(token);
|
||||
List<Cookie> userCookies = new ArrayList<>();
|
||||
if (Objects.equals(1, userId)) {
|
||||
userCookies.addAll(cookieRepository.findAll());
|
||||
} else {
|
||||
// 获取当前用户的所有cookies
|
||||
userCookies.addAll(cookieRepository.findByUserId(userId));
|
||||
}
|
||||
|
||||
// 构建详细信息响应
|
||||
return userCookies.stream().map(cookie -> {
|
||||
CookieDetailsResponse response = new CookieDetailsResponse();
|
||||
response.setId(cookie.getId());
|
||||
response.setValue(cookie.getValue());
|
||||
response.setEnabled(cookie.getEnabled());
|
||||
response.setAutoConfirm(cookie.getAutoConfirm());
|
||||
response.setRemark(cookie.getRemark() != null ? cookie.getRemark() : "");
|
||||
response.setPauseDuration(cookie.getPauseDuration() != null ? cookie.getPauseDuration() : 10);
|
||||
return response;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Cookie addCookie(@RequestBody CookieIn cookieIn, @RequestHeader(value = "Authorization", required = false) String token) {
|
||||
Long userId = getUserId(token);
|
||||
|
||||
|
||||
// Check if ID exists
|
||||
if (cookieRepository.existsById(cookieIn.getId())) {
|
||||
throw new RuntimeException("Cookie ID already exists");
|
||||
throw new RuntimeException("Cookie ID already exists");
|
||||
}
|
||||
|
||||
Cookie cookie = new Cookie();
|
||||
@ -96,7 +121,7 @@ public class CookieController {
|
||||
Long userId = getUserId(token);
|
||||
Cookie cookie = cookieRepository.findById(id).orElseThrow(() -> new RuntimeException("Cookie not found"));
|
||||
checkOwnership(cookie, userId);
|
||||
|
||||
|
||||
cookie.setValue(cookieIn.getValue());
|
||||
cookie.setUpdatedAt(LocalDateTime.now());
|
||||
cookieRepository.save(cookie);
|
||||
@ -108,7 +133,7 @@ public class CookieController {
|
||||
Long userId = getUserId(token);
|
||||
Cookie cookie = cookieRepository.findById(id).orElseThrow(() -> new RuntimeException("Cookie not found"));
|
||||
checkOwnership(cookie, userId);
|
||||
|
||||
|
||||
cookieRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@ -128,11 +153,11 @@ public class CookieController {
|
||||
Long userId = getUserId(token);
|
||||
Cookie cookie = cookieRepository.findById(id).orElseThrow(() -> new RuntimeException("Cookie not found"));
|
||||
checkOwnership(cookie, userId);
|
||||
|
||||
|
||||
if (update.getUsername() != null) cookie.setUsername(update.getUsername());
|
||||
if (update.getPassword() != null) cookie.setPassword(update.getPassword());
|
||||
if (update.getShowBrowser() != null) cookie.setShowBrowser(update.getShowBrowser() ? 1 : 0);
|
||||
|
||||
|
||||
return cookieRepository.save(cookie);
|
||||
}
|
||||
|
||||
@ -142,7 +167,7 @@ public class CookieController {
|
||||
Long userId = getUserId(token);
|
||||
Cookie cookie = cookieRepository.findById(id).orElseThrow(() -> new RuntimeException("Cookie not found"));
|
||||
checkOwnership(cookie, userId);
|
||||
|
||||
|
||||
cookie.setPauseDuration(update.getPauseDuration());
|
||||
return cookieRepository.save(cookie);
|
||||
}
|
||||
@ -161,7 +186,7 @@ public class CookieController {
|
||||
Long userId = getUserId(token);
|
||||
Cookie cookie = cookieRepository.findById(id).orElseThrow(() -> new RuntimeException("Cookie not found"));
|
||||
checkOwnership(cookie, userId);
|
||||
|
||||
|
||||
cookie.setAutoConfirm(update.isAutoConfirm() ? 1 : 0);
|
||||
return cookieRepository.save(cookie);
|
||||
}
|
||||
@ -180,7 +205,7 @@ public class CookieController {
|
||||
Long userId = getUserId(token);
|
||||
Cookie cookie = cookieRepository.findById(id).orElseThrow(() -> new RuntimeException("Cookie not found"));
|
||||
checkOwnership(cookie, userId);
|
||||
|
||||
|
||||
cookie.setRemark(update.getRemark());
|
||||
return cookieRepository.save(cookie);
|
||||
}
|
||||
@ -199,20 +224,20 @@ public class CookieController {
|
||||
Long userId = getUserId(token);
|
||||
Cookie cookie = cookieRepository.findById(id).orElseThrow(() -> new RuntimeException("Cookie not found"));
|
||||
checkOwnership(cookie, userId);
|
||||
|
||||
|
||||
cookie.setEnabled(update.isEnabled());
|
||||
cookieRepository.save(cookie);
|
||||
// Start/Stop client logic placeholder
|
||||
return cookie;
|
||||
}
|
||||
|
||||
|
||||
// cookies/check - Usually global validation or test, Python Line 4340
|
||||
@GetMapping("/check")
|
||||
public Map<String, Object> checkCookies(@RequestHeader(value = "Authorization", required = false) String token) {
|
||||
Long userId = getUserId(token);
|
||||
// Logic to check validity of user's cookies.
|
||||
// For now, return stub or call BrowserService if needed.
|
||||
return Map.of("status", "checked", "count", 0);
|
||||
return Map.of("status", "checked", "count", 0);
|
||||
}
|
||||
|
||||
// DTOs with JSON Properties
|
||||
@ -252,4 +277,19 @@ public class CookieController {
|
||||
@JsonProperty("pause_duration")
|
||||
private Integer pauseDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cookie详细信息响应 - 对应Python的 get_cookies_details 返回格式
|
||||
*/
|
||||
@Data
|
||||
public static class CookieDetailsResponse {
|
||||
private String id;
|
||||
private String value;
|
||||
private Boolean enabled;
|
||||
@JsonProperty("auto_confirm")
|
||||
private Integer autoConfirm;
|
||||
private String remark;
|
||||
@JsonProperty("pause_duration")
|
||||
private Integer pauseDuration;
|
||||
}
|
||||
}
|
||||
@ -26,7 +26,10 @@ public class BrowserService {
|
||||
|
||||
private final CookieRepository cookieRepository;
|
||||
private Playwright playwright;
|
||||
private Browser browser;
|
||||
private Browser browser;
|
||||
|
||||
// 为每个账号维护持久化浏览器上下文(用于Cookie刷新)
|
||||
private final Map<String, BrowserContext> persistentContexts = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
public BrowserService(CookieRepository cookieRepository) {
|
||||
@ -74,6 +77,10 @@ public class BrowserService {
|
||||
@PreDestroy
|
||||
private void close() {
|
||||
log.info("Releasing Playwright resources...");
|
||||
|
||||
// 关闭所有持久化上下文
|
||||
closeAllPersistentContexts();
|
||||
|
||||
if (browser != null) {
|
||||
browser.close();
|
||||
}
|
||||
@ -493,111 +500,108 @@ public class BrowserService {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新Cookie - 使用持久化浏览器上下文
|
||||
* Cookie会自动保存到UserData目录,类似真实浏览器行为
|
||||
*/
|
||||
public Map<String, String> refreshCookies(String cookieId) {
|
||||
log.info("【Cookie Refresh】Attempting to refresh cookies for id: {}", cookieId);
|
||||
log.info("【Cookie Refresh】开始刷新Cookie for id: {}", cookieId);
|
||||
Cookie cookie = cookieRepository.findById(cookieId).orElse(null);
|
||||
if (cookie == null || cookie.getValue() == null) {
|
||||
log.error("【Cookie Refresh】Cannot refresh. No valid cookie found for id: {}", cookieId);
|
||||
log.error("【Cookie Refresh】无法刷新,Cookie不存在: {}", cookieId);
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
BrowserContext context = null;
|
||||
Page page = null;
|
||||
try {
|
||||
// Use a fresh context for each refresh to avoid pollution
|
||||
Browser.NewContextOptions options = new Browser.NewContextOptions()
|
||||
.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||
.setViewportSize(1920, 1080);
|
||||
// 1. 获取或创建持久化上下文(Cookie自动从UserData加载)
|
||||
BrowserContext context = getPersistentContext(cookieId);
|
||||
log.info("【Cookie Refresh】已获取持久化上下文: {}", cookieId);
|
||||
|
||||
context = browser.newContext(options);
|
||||
|
||||
// 1. Parse and Set Cookies
|
||||
List<com.microsoft.playwright.options.Cookie> playwrightCookies = new ArrayList<>();
|
||||
String[] parts = cookie.getValue().split(";");
|
||||
for (String part : parts) {
|
||||
String[] kv = part.trim().split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
playwrightCookies.add(new com.microsoft.playwright.options.Cookie(kv[0], kv[1])
|
||||
.setDomain(".goofish.com")
|
||||
.setPath("/"));
|
||||
}
|
||||
}
|
||||
context.addCookies(playwrightCookies);
|
||||
log.info("【Cookie Refresh】Loaded {} cookies for {}", playwrightCookies.size(), cookieId);
|
||||
|
||||
// 2. Navigate and Refresh
|
||||
Page page = context.newPage();
|
||||
// 2. 创建新页面并访问闲鱼
|
||||
page = context.newPage();
|
||||
addStealthScripts(page);
|
||||
|
||||
String targetUrl = "https://www.goofish.com/im";
|
||||
log.info("【Cookie Refresh】Navigating to {}", targetUrl);
|
||||
log.info("【Cookie Refresh】导航到: {}", targetUrl);
|
||||
|
||||
try {
|
||||
page.navigate(targetUrl, new Page.NavigateOptions().setTimeout(15000).setWaitUntil(WaitUntilState.DOMCONTENTLOADED));
|
||||
page.navigate(targetUrl, new Page.NavigateOptions()
|
||||
.setTimeout(20000)
|
||||
.setWaitUntil(WaitUntilState.DOMCONTENTLOADED));
|
||||
} catch (Exception e) {
|
||||
log.warn("【Cookie Refresh】Navigation timeout, trying fallback...");
|
||||
try {
|
||||
page.navigate(targetUrl, new Page.NavigateOptions().setTimeout(25000).setWaitUntil(WaitUntilState.LOAD));
|
||||
} catch (Exception ex) {
|
||||
log.warn("【Cookie Refresh】Fallback navigation also timed out (proceeding anyway).");
|
||||
}
|
||||
log.warn("【Cookie Refresh】导航超时,尝试降级...");
|
||||
try {
|
||||
page.navigate(targetUrl, new Page.NavigateOptions()
|
||||
.setTimeout(30000)
|
||||
.setWaitUntil(WaitUntilState.LOAD));
|
||||
} catch (Exception ex) {
|
||||
log.warn("【Cookie Refresh】降级导航也超时,继续执行");
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for page load
|
||||
// 3. 等待页面加载
|
||||
try { Thread.sleep(3000); } catch (Exception e) {}
|
||||
|
||||
// 4. 重新加载页面以触发Cookie刷新
|
||||
log.info("【Cookie Refresh】重新加载页面...");
|
||||
try {
|
||||
page.reload(new Page.ReloadOptions()
|
||||
.setTimeout(20000)
|
||||
.setWaitUntil(WaitUntilState.DOMCONTENTLOADED));
|
||||
} catch (Exception e) {
|
||||
log.warn("【Cookie Refresh】重新加载超时,继续执行");
|
||||
}
|
||||
try { Thread.sleep(2000); } catch (Exception e) {}
|
||||
|
||||
// Reload to force refresh
|
||||
log.info("【Cookie Refresh】Reloading page...");
|
||||
try {
|
||||
page.reload(new Page.ReloadOptions().setTimeout(15000).setWaitUntil(WaitUntilState.DOMCONTENTLOADED));
|
||||
} catch (Exception e) {
|
||||
log.warn("【Cookie Refresh】Reload timeout (proceeding anyway).");
|
||||
}
|
||||
try { Thread.sleep(1000); } catch (Exception e) {}
|
||||
|
||||
// 3. Capture New Cookies
|
||||
// 5. 获取刷新后的Cookie(从持久化上下文中获取)
|
||||
List<com.microsoft.playwright.options.Cookie> newCookies = context.cookies();
|
||||
log.info("【Cookie Refresh】Captured {} cookies after refresh.", newCookies.size());
|
||||
log.info("【Cookie Refresh】获取到 {} 个Cookie", newCookies.size());
|
||||
|
||||
// 4. Compare and Update
|
||||
// 6. 构建Cookie Map
|
||||
Map<String, String> newCookieMap = new HashMap<>();
|
||||
for (com.microsoft.playwright.options.Cookie c : newCookies) {
|
||||
newCookieMap.put(c.name, c.value);
|
||||
}
|
||||
|
||||
// Simple check if unb exists
|
||||
// 7. 验证必要Cookie
|
||||
if (!newCookieMap.containsKey("unb")) {
|
||||
log.warn("【Cookie Refresh】'unb' missing in refreshed cookies. Refresh might have failed or session invalid.");
|
||||
log.warn("【Cookie Refresh】刷新后的Cookie缺少'unb'字段,可能已失效");
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
// Construct new cookie string
|
||||
// 8. 构建Cookie字符串并保存到数据库
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : newCookieMap.entrySet()) {
|
||||
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("; ");
|
||||
}
|
||||
String newCookieStr = sb.toString();
|
||||
|
||||
// Check for changes (Logic similar to Python's check)
|
||||
// For now, we update if string is different, or if we want to be robust, we just save.
|
||||
// Python checks if keys changed or values changed.
|
||||
// Let's just save to be safe and ensure "last updated" is fresh if DB has such field.
|
||||
|
||||
// 9. 更新数据库
|
||||
if (!newCookieStr.equals(cookie.getValue())) {
|
||||
cookie.setValue(newCookieStr);
|
||||
cookieRepository.save(cookie);
|
||||
log.info("【Cookie Refresh】Cookies updated and saved to DB for {}", cookieId);
|
||||
return newCookieMap;
|
||||
log.info("【Cookie Refresh】✅ Cookie已更新并保存到数据库: {}", cookieId);
|
||||
} else {
|
||||
log.info("【Cookie Refresh】Cookies identical, no DB update needed.");
|
||||
return newCookieMap;
|
||||
log.info("【Cookie Refresh】Cookie未变化,无需更新数据库");
|
||||
}
|
||||
|
||||
// 10. Cookie已自动保存到UserData目录(持久化)
|
||||
log.info("【Cookie Refresh】✅ Cookie刷新完成(已持久化到磁盘): {}", cookieId);
|
||||
return newCookieMap;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("【Cookie Refresh】Exception during refresh for {}", cookieId, e);
|
||||
log.error("【Cookie Refresh】❌ 刷新Cookie异常: {}", cookieId, e);
|
||||
return Collections.emptyMap();
|
||||
} finally {
|
||||
if (context != null) {
|
||||
try { context.close(); } catch (Exception e) { log.error("Error closing context", e); }
|
||||
// 关闭页面但保持上下文(保持持久化状态)
|
||||
if (page != null) {
|
||||
try {
|
||||
page.close();
|
||||
log.debug("【Cookie Refresh】页面已关闭: {}", cookieId);
|
||||
} catch (Exception e) {
|
||||
log.error("【Cookie Refresh】关闭页面失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -667,7 +671,128 @@ public class BrowserService {
|
||||
return null; // Failed
|
||||
}
|
||||
|
||||
|
||||
// ================== 持久化浏览器上下文管理 ==================
|
||||
|
||||
/**
|
||||
* 获取或创建账号的持久化浏览器上下文
|
||||
* 使用持久化上下文可以将Cookie保存到磁盘,类似真实浏览器行为
|
||||
*/
|
||||
private BrowserContext getPersistentContext(String cookieId) {
|
||||
// 如果已存在,直接返回
|
||||
BrowserContext existingContext = persistentContexts.get(cookieId);
|
||||
if (existingContext != null) {
|
||||
try {
|
||||
// 验证上下文是否仍然有效
|
||||
existingContext.pages();
|
||||
log.debug("【Cookie Refresh】复用已存在的持久化上下文: {}", cookieId);
|
||||
return existingContext;
|
||||
} catch (Exception e) {
|
||||
// 上下文已失效,移除并重新创建
|
||||
log.warn("【Cookie Refresh】持久化上下文已失效,重新创建: {}", cookieId);
|
||||
persistentContexts.remove(cookieId);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新的持久化上下文
|
||||
try {
|
||||
String userDataDir = "browser_data/cookie_refresh/" + cookieId;
|
||||
java.nio.file.Path userDataPath = java.nio.file.Paths.get(userDataDir);
|
||||
|
||||
// 确保目录存在
|
||||
java.nio.file.Files.createDirectories(userDataPath);
|
||||
log.info("【Cookie Refresh】创建UserData目录: {}", userDataDir);
|
||||
|
||||
// 配置启动选项
|
||||
List<String> args = new ArrayList<>();
|
||||
args.add("--no-sandbox");
|
||||
args.add("--disable-setuid-sandbox");
|
||||
args.add("--disable-dev-shm-usage");
|
||||
args.add("--disable-gpu");
|
||||
args.add("--disable-blink-features=AutomationControlled");
|
||||
args.add("--lang=zh-CN");
|
||||
|
||||
BrowserType.LaunchPersistentContextOptions options = new BrowserType.LaunchPersistentContextOptions()
|
||||
.setHeadless(true)
|
||||
.setArgs(args)
|
||||
.setViewportSize(1920, 1080)
|
||||
.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||
.setLocale("zh-CN")
|
||||
.setAcceptDownloads(false)
|
||||
.setIgnoreHTTPSErrors(true);
|
||||
|
||||
// macOS ARM架构特殊处理
|
||||
String osName = System.getProperty("os.name").toLowerCase();
|
||||
String osArch = System.getProperty("os.arch").toLowerCase();
|
||||
if (osName.contains("mac") && osArch.contains("aarch64")) {
|
||||
Path chromePath = Paths.get("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome");
|
||||
if (chromePath.toFile().exists()) {
|
||||
options.setExecutablePath(chromePath);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("【Cookie Refresh】创建持久化浏览器上下文: {}", cookieId);
|
||||
BrowserContext context = playwright.chromium().launchPersistentContext(userDataPath, options);
|
||||
|
||||
// 首次创建时,需要设置Cookie
|
||||
Cookie cookie = cookieRepository.findById(cookieId).orElse(null);
|
||||
if (cookie != null && cookie.getValue() != null) {
|
||||
// 解析并添加Cookie
|
||||
List<com.microsoft.playwright.options.Cookie> playwrightCookies = new ArrayList<>();
|
||||
String[] parts = cookie.getValue().split(";");
|
||||
for (String part : parts) {
|
||||
String[] kv = part.trim().split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
playwrightCookies.add(new com.microsoft.playwright.options.Cookie(kv[0], kv[1])
|
||||
.setDomain(".goofish.com")
|
||||
.setPath("/"));
|
||||
}
|
||||
}
|
||||
context.addCookies(playwrightCookies);
|
||||
log.info("【Cookie Refresh】已设置初始Cookie: {} 个", playwrightCookies.size());
|
||||
}
|
||||
|
||||
// 缓存上下文
|
||||
persistentContexts.put(cookieId, context);
|
||||
|
||||
return context;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("【Cookie Refresh】创建持久化上下文失败: {}", cookieId, e);
|
||||
throw new RuntimeException("创建持久化浏览器上下文失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭指定账号的持久化上下文
|
||||
*/
|
||||
public void closePersistentContext(String cookieId) {
|
||||
BrowserContext context = persistentContexts.remove(cookieId);
|
||||
if (context != null) {
|
||||
try {
|
||||
context.close();
|
||||
log.info("【Cookie Refresh】已关闭持久化上下文: {}", cookieId);
|
||||
} catch (Exception e) {
|
||||
log.error("【Cookie Refresh】关闭持久化上下文失败: {}", cookieId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有持久化上下文
|
||||
*/
|
||||
private void closeAllPersistentContexts() {
|
||||
log.info("【Cookie Refresh】关闭所有持久化上下文...");
|
||||
for (Map.Entry<String, BrowserContext> entry : persistentContexts.entrySet()) {
|
||||
try {
|
||||
entry.getValue().close();
|
||||
log.info("【Cookie Refresh】已关闭: {}", entry.getKey());
|
||||
} catch (Exception e) {
|
||||
log.error("【Cookie Refresh】关闭失败: {}", entry.getKey(), e);
|
||||
}
|
||||
}
|
||||
persistentContexts.clear();
|
||||
}
|
||||
|
||||
|
||||
private void addStealthScripts(Page page) {
|
||||
page.addInitScript(BrowserStealth.STEALTH_SCRIPT);
|
||||
|
||||
@ -29,6 +29,11 @@ public class QrLoginService {
|
||||
private final Map<String, QrLoginSession> sessions = new ConcurrentHashMap<>();
|
||||
private final OkHttpClient client;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
// 并发锁机制 - 防止同一session的并发处理
|
||||
private final Map<String, Object> qrCheckLocks = new ConcurrentHashMap<>();
|
||||
// 已处理记录 - 记录已完成处理的session
|
||||
private final Map<String, ProcessedRecord> qrCheckProcessed = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
public QrLoginService(CookieRepository cookieRepository, BrowserService browserService) {
|
||||
@ -54,11 +59,25 @@ public class QrLoginService {
|
||||
private String verificationUrl;
|
||||
private Map<String, String> params = new HashMap<>(); // Store login params (t, ck, etc.)
|
||||
private Map<String, String> cookies = new HashMap<>();
|
||||
private String accountId; // 保存处理后的账号ID
|
||||
private boolean isNewAccount; // 是否为新账号
|
||||
private boolean realCookieRefreshed; // 是否成功刷新真实Cookie
|
||||
|
||||
public boolean isExpired() {
|
||||
return System.currentTimeMillis() - createdTime > expireTime;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ProcessedRecord {
|
||||
private boolean processed;
|
||||
private long timestamp;
|
||||
|
||||
public ProcessedRecord(boolean processed, long timestamp) {
|
||||
this.processed = processed;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Core Methods ---
|
||||
|
||||
@ -133,51 +152,84 @@ public class QrLoginService {
|
||||
}
|
||||
|
||||
public Map<String, Object> checkQrCodeStatus(String sessionId) {
|
||||
QrLoginSession session = sessions.get(sessionId);
|
||||
if (session == null) {
|
||||
return Map.of("status", "not_found", "message", "会话不存在或已过期");
|
||||
}
|
||||
|
||||
if (session.isExpired() && !"success".equals(session.getStatus())) {
|
||||
session.setStatus("expired");
|
||||
return Map.of("status", "expired", "session_id", sessionId);
|
||||
}
|
||||
|
||||
// If already successful, return stored result
|
||||
if ("success".equals(session.getStatus()) && session.getUnb() != null) {
|
||||
return buildSuccessResult(session);
|
||||
}
|
||||
|
||||
// Poll status from API
|
||||
try {
|
||||
pollQrCodeStatus(session);
|
||||
// 1. 清理过期记录
|
||||
cleanupQrCheckRecords();
|
||||
|
||||
// 2. 检查是否已经处理过
|
||||
ProcessedRecord processedRecord = qrCheckProcessed.get(sessionId);
|
||||
if (processedRecord != null && processedRecord.isProcessed()) {
|
||||
log.debug("【QR Login】扫码登录session {} 已处理过,直接返回", sessionId);
|
||||
return Map.of("status", "already_processed", "message", "该会话已处理完成");
|
||||
}
|
||||
|
||||
// 3. 获取或创建该session的锁对象
|
||||
Object sessionLock = qrCheckLocks.computeIfAbsent(sessionId, k -> new Object());
|
||||
|
||||
// 4. 尝试获取锁(使用tryLock模式,避免阻塞)
|
||||
boolean lockAcquired = false;
|
||||
synchronized (sessionLock) {
|
||||
// 检查锁状态(在Java中我们用一个简单的标记)
|
||||
// 如果已经有线程在处理,直接返回processing
|
||||
if (Thread.holdsLock(sessionLock)) {
|
||||
lockAcquired = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用synchronized块确保同一session不会被并发处理
|
||||
synchronized (sessionLock) {
|
||||
// 5. 双重检查 - 再次确认是否已处理
|
||||
processedRecord = qrCheckProcessed.get(sessionId);
|
||||
if (processedRecord != null && processedRecord.isProcessed()) {
|
||||
log.debug("【QR Login】扫码登录session {} 在获取锁后发现已处理,直接返回", sessionId);
|
||||
return Map.of("status", "already_processed", "message", "该会话已处理完成");
|
||||
}
|
||||
|
||||
// 6. 清理过期会话
|
||||
cleanupExpiredSessions();
|
||||
|
||||
// 7. 获取会话状态
|
||||
Map<String, Object> statusInfo = getSessionStatus(sessionId);
|
||||
log.info("【QR Login】获取会话状态1111111: {}", statusInfo);
|
||||
|
||||
String status = (String) statusInfo.get("status");
|
||||
|
||||
// 8. 如果登录成功,处理Cookie
|
||||
if ("success".equals(status)) {
|
||||
log.info("【QR Login】获取会话状态22222222: {}", statusInfo);
|
||||
|
||||
// 获取会话Cookie信息
|
||||
Map<String, String> cookiesInfo = getSessionCookies(sessionId);
|
||||
log.info("【QR Login】获取会话Cookie: {}", cookiesInfo);
|
||||
|
||||
if (cookiesInfo != null && !cookiesInfo.isEmpty()) {
|
||||
// 处理扫码登录Cookie
|
||||
Map<String, Object> accountInfo = processQrLoginCookies(
|
||||
cookiesInfo.get("cookies"),
|
||||
cookiesInfo.get("unb")
|
||||
);
|
||||
|
||||
// 将账号信息添加到返回结果中
|
||||
statusInfo.put("account_info", accountInfo);
|
||||
|
||||
log.info("【QR Login】扫码登录处理完成: {}, 账号: {}",
|
||||
sessionId, accountInfo.get("account_id"));
|
||||
|
||||
// 9. 标记该session已处理
|
||||
qrCheckProcessed.put(sessionId, new ProcessedRecord(true, System.currentTimeMillis()));
|
||||
}
|
||||
}
|
||||
|
||||
return statusInfo;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("【QR Login】Error polling status for {}", sessionId, e);
|
||||
log.error("【QR Login】检查扫码登录状态异常: {}", e.getMessage(), e);
|
||||
Map<String, Object> errorResult = new HashMap<>();
|
||||
errorResult.put("status", "error");
|
||||
errorResult.put("message", e.getMessage());
|
||||
return errorResult;
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", session.getStatus());
|
||||
result.put("session_id", sessionId);
|
||||
|
||||
if ("verification_required".equals(session.getStatus())) {
|
||||
result.put("verification_url", session.getVerificationUrl());
|
||||
result.put("message", "账号被风控,需要手机验证");
|
||||
}
|
||||
|
||||
if ("success".equals(session.getStatus())) {
|
||||
log.info("【QR Login】Status confirmed SUCCESS. Starting post-login processing for UNB: {}", session.getUnb());
|
||||
|
||||
try {
|
||||
processLoginSuccess(session);
|
||||
return buildSuccessResult(session);
|
||||
} catch (Exception e) {
|
||||
log.error("【QR Login】Error during post-login processing", e);
|
||||
result.put("status", "error");
|
||||
result.put("message", "登录后处理失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void processLoginSuccess(QrLoginSession session) {
|
||||
@ -482,4 +534,231 @@ public class QrLoginService {
|
||||
return validCookies;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 清理和工具方法 ---
|
||||
|
||||
/**
|
||||
* 清理过期的扫码检查记录(超过1小时)
|
||||
*/
|
||||
private void cleanupQrCheckRecords() {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
List<String> expiredSessions = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, ProcessedRecord> entry : qrCheckProcessed.entrySet()) {
|
||||
// 清理超过1小时的记录
|
||||
if (currentTime - entry.getValue().getTimestamp() > 3600 * 1000) {
|
||||
expiredSessions.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
for (String sessionId : expiredSessions) {
|
||||
qrCheckProcessed.remove(sessionId);
|
||||
qrCheckLocks.remove(sessionId);
|
||||
log.debug("【QR Login】清理过期的扫码检查记录: {}", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的登录会话
|
||||
*/
|
||||
private void cleanupExpiredSessions() {
|
||||
List<String> expiredSessions = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, QrLoginSession> entry : sessions.entrySet()) {
|
||||
if (entry.getValue().isExpired()) {
|
||||
expiredSessions.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
for (String sessionId : expiredSessions) {
|
||||
sessions.remove(sessionId);
|
||||
log.info("【QR Login】清理过期会话: {}", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话状态(包含轮询API)
|
||||
*/
|
||||
private Map<String, Object> getSessionStatus(String sessionId) {
|
||||
QrLoginSession session = sessions.get(sessionId);
|
||||
if (session == null) {
|
||||
return Map.of("status", "not_found", "message", "会话不存在或已过期");
|
||||
}
|
||||
|
||||
if (session.isExpired() && !"success".equals(session.getStatus())) {
|
||||
session.setStatus("expired");
|
||||
return Map.of("status", "expired", "session_id", sessionId);
|
||||
}
|
||||
|
||||
// 如果已经成功,直接返回
|
||||
if ("success".equals(session.getStatus()) && session.getUnb() != null) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", "success");
|
||||
result.put("session_id", sessionId);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 轮询状态
|
||||
try {
|
||||
pollQrCodeStatus(session);
|
||||
} catch (Exception e) {
|
||||
log.error("【QR Login】轮询状态失败 for {}", sessionId, e);
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", session.getStatus());
|
||||
result.put("session_id", sessionId);
|
||||
|
||||
if ("verification_required".equals(session.getStatus())) {
|
||||
result.put("verification_url", session.getVerificationUrl());
|
||||
result.put("message", "账号被风控,需要手机验证");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话Cookie信息
|
||||
*/
|
||||
private Map<String, String> getSessionCookies(String sessionId) {
|
||||
QrLoginSession session = sessions.get(sessionId);
|
||||
if (session != null && "success".equals(session.getStatus())) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
|
||||
// 将Cookie转换为字符串
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : session.getCookies().entrySet()) {
|
||||
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("; ");
|
||||
}
|
||||
|
||||
result.put("cookies", sb.toString());
|
||||
result.put("unb", session.getUnb());
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理扫码登录Cookie - 对应Python的process_qr_login_cookies方法
|
||||
*/
|
||||
private Map<String, Object> processQrLoginCookies(String cookies, String unb) {
|
||||
try {
|
||||
log.info("【QR Login】开始处理扫码登录Cookie, UNB: {}", unb);
|
||||
|
||||
// 1. 检查是否已存在相同unb的账号
|
||||
String existingAccountId = null;
|
||||
boolean isNewAccount = true;
|
||||
|
||||
// 遍历数据库中的所有Cookie,查找是否有相同的unb
|
||||
Iterable<com.xianyu.autoreply.entity.Cookie> allCookies = cookieRepository.findAll();
|
||||
for (com.xianyu.autoreply.entity.Cookie cookieEntity : allCookies) {
|
||||
String cookieValue = cookieEntity.getValue();
|
||||
if (cookieValue != null && cookieValue.contains("unb=" + unb)) {
|
||||
existingAccountId = cookieEntity.getId();
|
||||
isNewAccount = false;
|
||||
log.info("【QR Login】扫码登录找到现有账号: {}, UNB: {}", existingAccountId, unb);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 确定账号ID
|
||||
String accountId;
|
||||
if (existingAccountId != null) {
|
||||
accountId = existingAccountId;
|
||||
} else {
|
||||
// 创建新账号,使用unb作为账号ID
|
||||
accountId = unb;
|
||||
|
||||
// 确保账号ID唯一
|
||||
int counter = 1;
|
||||
String originalAccountId = accountId;
|
||||
while (cookieRepository.existsById(accountId)) {
|
||||
accountId = originalAccountId + "_" + counter;
|
||||
counter++;
|
||||
}
|
||||
|
||||
log.info("【QR Login】扫码登录准备创建新账号: {}, UNB: {}", accountId, unb);
|
||||
}
|
||||
|
||||
// 3. 使用浏览器服务验证并刷新Cookie(获取真实Cookie)
|
||||
log.info("【QR Login】开始使用扫码cookie获取真实cookie: {}", accountId);
|
||||
|
||||
boolean realCookieRefreshed = false;
|
||||
String finalCookieStr = cookies;
|
||||
|
||||
try {
|
||||
// 调用BrowserService验证并获取真实Cookie
|
||||
Map<String, String> verifiedCookies = browserService.verifyQrLoginCookies(
|
||||
parseCookieString(cookies),
|
||||
accountId
|
||||
);
|
||||
|
||||
if (verifiedCookies != null && !verifiedCookies.isEmpty()) {
|
||||
log.info("【QR Login】浏览器验证成功,获取到真实Cookie,数量: {}", verifiedCookies.size());
|
||||
|
||||
// 将Cookie转换为字符串
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : verifiedCookies.entrySet()) {
|
||||
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("; ");
|
||||
}
|
||||
finalCookieStr = sb.toString();
|
||||
realCookieRefreshed = true;
|
||||
} else {
|
||||
log.warn("【QR Login】浏览器验证失败,使用原始扫码Cookie");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("【QR Login】获取真实Cookie异常: {}", e.getMessage(), e);
|
||||
log.warn("【QR Login】降级处理 - 使用原始扫码Cookie");
|
||||
}
|
||||
|
||||
// 4. 保存Cookie到数据库
|
||||
com.xianyu.autoreply.entity.Cookie cookieEntity = cookieRepository.findById(accountId)
|
||||
.orElse(new com.xianyu.autoreply.entity.Cookie());
|
||||
|
||||
cookieEntity.setId(accountId);
|
||||
cookieEntity.setValue(finalCookieStr);
|
||||
|
||||
if (isNewAccount) {
|
||||
cookieEntity.setUsername("TB_" + unb);
|
||||
cookieEntity.setPassword("QR_LOGIN");
|
||||
cookieEntity.setUserId(0L);
|
||||
}
|
||||
|
||||
cookieEntity.setEnabled(true);
|
||||
cookieRepository.save(cookieEntity);
|
||||
|
||||
log.info("【QR Login】Cookie已保存到数据库: {}, 是否新账号: {}, 真实Cookie刷新: {}",
|
||||
accountId, isNewAccount, realCookieRefreshed);
|
||||
|
||||
// 5. 构建返回结果
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("account_id", accountId);
|
||||
result.put("is_new_account", isNewAccount);
|
||||
result.put("real_cookie_refreshed", realCookieRefreshed);
|
||||
result.put("cookie_length", finalCookieStr.length());
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("【QR Login】处理扫码登录Cookie失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("处理扫码登录Cookie失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析Cookie字符串为Map
|
||||
*/
|
||||
private Map<String, String> parseCookieString(String cookieStr) {
|
||||
Map<String, String> cookieMap = new HashMap<>();
|
||||
if (cookieStr != null && !cookieStr.isEmpty()) {
|
||||
String[] pairs = cookieStr.split(";\\s*");
|
||||
for (String pair : pairs) {
|
||||
String[] kv = pair.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
cookieMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieMap;
|
||||
}
|
||||
}
|
||||
|
||||
@ -564,39 +564,99 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
|
||||
/**
|
||||
* 刷新Token - 对应Python的refresh_token()方法
|
||||
* 添加自动降级机制:Token获取失败时自动刷新Cookie
|
||||
*/
|
||||
private String refreshToken() {
|
||||
try {
|
||||
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
|
||||
int maxRetries = 3;
|
||||
int retryCount = 0;
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
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);
|
||||
}
|
||||
if (retryCount > 0) {
|
||||
log.info("【{}】Token获取失败,第 {} 次重试...", cookieId, retryCount);
|
||||
} else {
|
||||
log.info("【{}】开始刷新token...", cookieId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("【{}】从数据库重新加载cookie失败,继续使用当前cookie: {}", cookieId, e.getMessage());
|
||||
}
|
||||
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
|
||||
String token = attemptGetToken();
|
||||
|
||||
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);
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("【{}】❌ Cookie刷新异常: {}", cookieId, e.getMessage());
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("【{}】Token刷新过程异常", cookieId, e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
log.error("【{}】❌ Token刷新最终失败,已重试 {} 次", cookieId, retryCount);
|
||||
lastTokenRefreshStatus = "failed";
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取Token(单次尝试)
|
||||
*/
|
||||
private String attemptGetToken() {
|
||||
try {
|
||||
// 生成时间戳
|
||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||
|
||||
@ -652,7 +712,12 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
// 检查是否需要滑块验证
|
||||
if (needsCaptchaVerification(resJson)) {
|
||||
log.warn("【{}】检测到滑块验证要求", cookieId);
|
||||
return handleCaptchaAndRetry(resJson);
|
||||
// 这里需要决定如何处理滑块验证。
|
||||
// 如果是attemptGetToken,可能直接返回null,让上层refreshToken决定是否重试或刷新cookie
|
||||
// 或者直接抛出异常,让上层捕获
|
||||
// 暂时返回null,让refreshToken的重试机制处理
|
||||
handleCaptchaAndRetry(resJson); // 仍然调用,但其返回值不直接影响这里的return
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查响应
|
||||
@ -665,12 +730,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
JSONObject data = resJson.getJSONObject("data");
|
||||
if (data.containsKey("accessToken")) {
|
||||
String newToken = data.getString("accessToken");
|
||||
this.currentToken = newToken;
|
||||
this.lastTokenRefreshTime.set(System.currentTimeMillis());
|
||||
this.lastMessageReceivedTime.set(0); // 重置消息接收时间
|
||||
|
||||
log.warn("【{}】Token刷新成功", cookieId);
|
||||
lastTokenRefreshStatus = "success";
|
||||
log.info("【{}】获取到accessToken", cookieId);
|
||||
return newToken;
|
||||
}
|
||||
}
|
||||
@ -678,13 +738,11 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
log.error("【{}】Token刷新失败: 响应中未找到有效token", cookieId);
|
||||
lastTokenRefreshStatus = "failed";
|
||||
log.warn("【{}】响应中未找到有效token", cookieId);
|
||||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("【{}】Token刷新异常", cookieId, e);
|
||||
lastTokenRefreshStatus = "error";
|
||||
log.error("【{}】获取Token异常: {}", cookieId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
app:
|
||||
ddl-auto: update # valid values: none, validate, update, create, create-drop
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
@ -7,14 +10,18 @@ spring:
|
||||
|
||||
datasource:
|
||||
driver-class-name: org.sqlite.JDBC
|
||||
url: jdbc:sqlite:../data/xianyu_data.db
|
||||
url: jdbc:sqlite:./db/xianyu_data.db
|
||||
username:
|
||||
password:
|
||||
|
||||
sql:
|
||||
init:
|
||||
mode: never
|
||||
|
||||
jpa:
|
||||
database-platform: org.hibernate.community.dialect.SQLiteDialect
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
ddl-auto: ${app.ddl-auto:update}
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
|
||||
18
backend-java/src/main/resources/data.sql
Normal file
18
backend-java/src/main/resources/data.sql
Normal file
@ -0,0 +1,18 @@
|
||||
-- 系统默认账号
|
||||
INSERT OR IGNORE INTO users (username, email, password_hash) VALUES ('admin', 'admin@localhost', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92');
|
||||
-- 系统默认设置
|
||||
INSERT OR IGNORE INTO system_settings (key, value, description)
|
||||
VALUES ('init_system', 'false', '是否初始化'),
|
||||
('theme_color', 'blue', '主题颜色'),
|
||||
('registration_enabled', 'true', '是否开启用户注册'),
|
||||
('show_default_login_info', 'true', '是否显示默认登录信息'),
|
||||
('login_captcha_enabled', 'true', '登录滑动验证码开关'),
|
||||
('smtp_server', '', 'SMTP服务器地址'),
|
||||
('smtp_port', '587', 'SMTP端口'),
|
||||
('smtp_user', '', 'SMTP登录用户名(发件邮箱)'),
|
||||
('smtp_password', '', 'SMTP登录密码/授权码'),
|
||||
('smtp_from', '', '发件人显示名(留空则使用用户名)'),
|
||||
('smtp_use_tls', 'true', '是否启用TLS'),
|
||||
('smtp_use_ssl', 'false', '是否启用SSL'),
|
||||
('qq_reply_secret_key', 'xianyu_qq_reply_2024', 'QQ回复消息API秘钥');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user