This commit is contained in:
wangli 2026-01-29 23:53:11 +08:00
parent 7fc7cc26a7
commit 628d9c9567
9 changed files with 307 additions and 57 deletions

View File

@ -13,6 +13,6 @@ public interface GoofishApi<T> {
String getVersion();
T call(String goofishId, String cookie, String dataStr);
T call(String goofishId, String cookieStr, String dataStr);
}

View File

@ -60,7 +60,6 @@ public class GetAccountApi extends GoofishAbstractApi<String> {
log.debug("【{}】获取到的账号名为: {}", goofishId, account);
return account;
}
}
} catch (Exception e) {
log.error("获取闲鱼账号异常:{}", e.getMessage(), e);

View File

@ -0,0 +1,70 @@
package top.biwin.xianyu.goofish.api.impl;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import top.biwin.xianyu.goofish.api.GoofishAbstractApi;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* TODO
*
* @author wangli
* @since 2026-01-29 21:41
*/
@Component
@Slf4j
public class GetDisplayNameApi extends GoofishAbstractApi<String> {
@Override
public String getName() {
return "getDisplayName";
}
@Override
public String getApi() {
return "mtop.idle.web.user.page.nav";
}
@Override
public String getVersion() {
return "1.0";
}
@Override
public String call(String goofishId, String cookieStr, String dataStr) {
String apiUrl = buildApiUrl() + HttpUtil.toParams(buildQueryParams(cookieStr, dataStr));
log.debug("【{}】获取闲鱼昵称 ApiUrl: {}", goofishId, apiUrl);
log.debug("【{}】获取闲鱼昵称时使用的 Cookie 为: {}", goofishId, cookieStr);
try (HttpResponse response = HttpRequest.post(apiUrl)
.header("Cookie", cookieStr)
.header("content-type", "application/x-www-form-urlencoded")
.header("priority", "u=1, i")
.body("data=" + URLEncoder.encode(dataStr, StandardCharsets.UTF_8))
.execute()) {
String body = response.body();
log.info("【{}】获取闲鱼昵称时,服务端返回的完整响应为: {}", goofishId, body);
JSONObject json = JSONUtil.parseObj(body);
if (json.containsKey("ret")) {
JSONArray ret = json.getJSONArray("ret");
if (ret != null && !ret.isEmpty() && ret.getStr(0).startsWith("SUCCESS")) {
String displayName = json.getJSONObject("data").getJSONObject("module").getJSONObject("base").getStr("displayName");
log.info("【{}】获取到的闲鱼昵称为: {}", goofishId, displayName);
return displayName;
}
}
} catch (Exception e) {
log.error("获取闲鱼用户 ID 异常: {}", e.getMessage(), e);
}
return null;
}
}

View File

@ -0,0 +1,70 @@
package top.biwin.xianyu.goofish.api.impl;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import top.biwin.xianyu.goofish.api.GoofishAbstractApi;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* TODO
*
* @author wangli
* @since 2026-01-29 21:41
*/
@Component
@Slf4j
public class GetUserIdApi extends GoofishAbstractApi<Long> {
@Override
public String getName() {
return "getUserId";
}
@Override
public String getApi() {
return "mtop.taobao.idlemessage.pc.loginuser.get";
}
@Override
public String getVersion() {
return "1.0";
}
@Override
public Long call(String goofishId, String cookieStr, String dataStr) {
String apiUrl = buildApiUrl() + HttpUtil.toParams(buildQueryParams(cookieStr, dataStr));
log.debug("【{}】获取闲鱼用户 ID ApiUrl: {}", goofishId, apiUrl);
log.debug("【{}】获取闲鱼用户 ID 时使用的 Cookie 为: {}", goofishId, cookieStr);
try (HttpResponse response = HttpRequest.post(apiUrl)
.header("Cookie", cookieStr)
.header("content-type", "application/x-www-form-urlencoded")
.header("priority", "u=1, i")
.body("data=" + URLEncoder.encode(dataStr, StandardCharsets.UTF_8))
.execute()) {
String body = response.body();
log.info("【{}】获取闲鱼用户 ID 时,服务端返回的完整响应为: {}", goofishId, body);
JSONObject json = JSONUtil.parseObj(body);
if (json.containsKey("ret")) {
JSONArray ret = json.getJSONArray("ret");
if (ret != null && !ret.isEmpty() && ret.getStr(0).startsWith("SUCCESS")) {
Long userId = json.getJSONObject("data").getLong("userId");
log.info("【{}】获取到的闲鱼用户 ID 为: {}", goofishId, userId);
return userId;
}
}
} catch (Exception e) {
log.error("获取闲鱼用户 ID 异常: {}", e.getMessage(), e);
}
return 0L;
}
}

View File

@ -15,9 +15,11 @@ import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -184,6 +186,36 @@ public class BrowserService {
return new HashMap<>(); // Failed
}
private BrowserContext createPersistentContext(String goofishId, Boolean showBrowser, Double slowMo) throws IOException {
String userDataDir = persistenceData + "user_" + goofishId;
Path userDataPath = Paths.get(userDataDir);
Files.createDirectories(userDataPath);
log.debug("【{}-loadPersistentContext】创建浏览器持久化目录: {}, headless model: {}", goofishId, userDataDir, showBrowser);
List<String> args = new ArrayList<>();
args.add("--no-sandbox");
args.add("--disable-setuid-sandbox");
args.add("--disable-dev-shm-usage");
args.add("--disable-blink-features=AutomationControlled");
args.add("--disable-web-security");
args.add("--disable-features=VizDisplayCompositor");
args.add("--lang=zh-CN");
args.add("--start-maximized");
BrowserType.LaunchPersistentContextOptions options = new BrowserType.LaunchPersistentContextOptions()
.setHeadless(!showBrowser)
.setSlowMo(Objects.nonNull(slowMo) ? slowMo : 1000D)
.setArgs(args)
.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")
.setLocale("zh-CN")
.setAcceptDownloads(false)
.setExecutablePath(Paths.get(browserLocation))
.setIgnoreHTTPSErrors(true);
log.debug("【{}-loadPersistentContext】创建持久化浏览器上下文成功", goofishId);
return playwright.chromium().launchPersistentContext(userDataPath, options);
}
/**
*
* @param goofishId
@ -201,33 +233,7 @@ public class BrowserService {
return browserContext;
}
try {
String userDataDir = persistenceData + "user_" + goofishId;
Path userDataPath = Paths.get(userDataDir);
Files.createDirectories(userDataPath);
log.debug("【{}-loadPersistentContext】创建浏览器持久化目录: {}, headless model: {}", goofishId, userDataDir, showBrowser);
List<String> args = new ArrayList<>();
args.add("--no-sandbox");
args.add("--disable-setuid-sandbox");
args.add("--disable-dev-shm-usage");
args.add("--disable-blink-features=AutomationControlled");
args.add("--disable-web-security");
args.add("--disable-features=VizDisplayCompositor");
args.add("--lang=zh-CN");
args.add("--start-maximized");
BrowserType.LaunchPersistentContextOptions options = new BrowserType.LaunchPersistentContextOptions()
.setHeadless(!showBrowser)
.setSlowMo(Objects.nonNull(slowMo) ? slowMo : 0)
.setArgs(args)
.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")
.setLocale("zh-CN")
.setAcceptDownloads(false)
.setExecutablePath(Paths.get(browserLocation))
.setIgnoreHTTPSErrors(true);
log.debug("【{}-loadPersistentContext】创建持久化浏览器上下文", goofishId);
BrowserContext context = playwright.chromium().launchPersistentContext(userDataPath, options);
browserContext = createPersistentContext(goofishId, showBrowser, slowMo);
if (StringUtils.hasText(goofishCookieStr)) {
log.debug("【{}-loadPersistentContext】添加指定 Cookies: {}", goofishId, goofishCookieStr);
@ -238,18 +244,52 @@ public class BrowserService {
playwrightCookies.add(new Cookie(kv[0], kv[1]).setDomain(".goofish.com").setPath("/"));
}
}
context.addCookies(playwrightCookies);
browserContext.addCookies(playwrightCookies);
log.debug("【{}-loadPersistentContext】已设置初始 Cookie: {} 个", goofishId, playwrightCookies);
}
persistentContexts.put(goofishId, context);
return context;
persistentContexts.put(goofishId, browserContext);
return browserContext;
} catch (Exception e) {
log.error("【{}-loadPersistentContext】创建持久化上下文失败", goofishId, e);
log.error("【{}-loadPersistentContext】创建持久化上下文失败: {}", goofishId, e.getMessage(), e);
throw new RuntimeException("创建持久化浏览器上下文失败", e);
}
}
public boolean renamePersistentContext(String sourceId, String targetId, Boolean showBrowser) {
BrowserContext browserContext = persistentContexts.get(sourceId);
if (Objects.isNull(browserContext)) {
log.debug("【{}-renamePersistentContext】重命名持久化浏览器数据失败未找到原数据目录: {}", sourceId, sourceId);
return false;
}
String userDataDir = persistenceData + "user_" + sourceId;
Path sourceUserDataPath = Paths.get(userDataDir);
Path targetUserDataPath = sourceUserDataPath.resolveSibling("user_" + targetId);
try {
Files.move(sourceUserDataPath, targetUserDataPath, StandardCopyOption.ATOMIC_MOVE);
log.debug("【{}-renamePersistentContext】重命名持久化浏览器数据成功原目录: {},目标目录: {}", sourceId, sourceUserDataPath, targetUserDataPath);
persistentContexts.remove(sourceId);
browserContext.close();
// BrowserContext context = createPersistentContext(targetId, showBrowser, 1000D);
// context.close();
// persistentContexts.put(targetId, context);
log.debug("【{}-renamePersistentContext】持久化浏览器缓存替换成功", sourceId);
return true;
} catch (Exception e) {
log.error("【{}-renamePersistentContext】重命名持久化浏览器数据异常原目录: {},目标目录: {},异常信息: {}", sourceId, sourceUserDataPath, targetUserDataPath, e.getMessage(), e);
}
return false;
}
public void closePersistentContext(String goofishId) {
BrowserContext browserContext = persistentContexts.get(goofishId);
if(Objects.isNull(browserContext)) {
return;
}
browserContext.close();
}
/**
* 该方法一般是在 loadPersistentContext 函数后面调用设置 cookie
*/

View File

@ -34,7 +34,36 @@ public class GoofishApiService {
public String getAccount(String goofishId, @Nullable String cookieStr) {
GoofishApi<?> goofishApi = getApi("getAccount");
if (!StringUtils.hasText(cookieStr)) {
cookieStr = goofishAccountRepository.getReferenceById(goofishId).getCookie();
cookieStr = goofishAccountRepository.findByUsername(goofishId)
.orElseThrow(() -> new IllegalArgumentException("无法获取闲鱼用户名,缺少 Cookie 信息"))
.getCookie();
}
return (String) goofishApi.call(goofishId, cookieStr, "{}");
}
/**
* 获取
*
* @param goofishId
* @param cookieStr
* @return
*/
public Long getUserId(String goofishId, @Nullable String cookieStr) {
GoofishApi<?> goofishApi = getApi("getUserId");
if (!StringUtils.hasText(cookieStr)) {
cookieStr = goofishAccountRepository.findByUsername(goofishId)
.orElseThrow(() -> new IllegalArgumentException("无法获取闲鱼用户 ID缺少 Cookie 信息"))
.getCookie();
}
return (Long) goofishApi.call(goofishId, cookieStr, "{}");
}
public String getNickName(String goofishId, @Nullable String cookieStr) {
GoofishApi<?> goofishApi = getApi("getDisplayName");
if (!StringUtils.hasText(cookieStr)) {
cookieStr = goofishAccountRepository.findByUsername(goofishId)
.orElseThrow(() -> new IllegalArgumentException("无法获取闲鱼用户 ID缺少 Cookie 信息"))
.getCookie();
}
return (String) goofishApi.call(goofishId, cookieStr, "{}");
}

View File

@ -9,6 +9,8 @@ import com.microsoft.playwright.options.Cookie;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import top.biwin.xianyu.core.entity.GoofishAccountEntity;
import top.biwin.xianyu.core.repository.GoofishAccountRepository;
import top.biwin.xianyu.goofish.util.CookieUtils;
import java.util.List;
@ -31,6 +33,8 @@ public class GoofishPwdLoginService {
private BrowserService browserService;
@Autowired
private GoofishApiService goofishApiService;
@Autowired
private GoofishAccountRepository goofishAccountRepository;
/**
*
@ -39,7 +43,7 @@ public class GoofishPwdLoginService {
* @param showBrowser 是否有头headless
* @param systemUsername 当前系统登录用户
*/
public void processPasswordLogin(String account, String password, Boolean showBrowser, String systemUsername) {
public void processPasswordLogin(String account, String password, Boolean showBrowser, Long systemUsername) {
BrowserContext context = browserService.loadPersistentContext(account, showBrowser, 1000D, null);
Page page = context.pages().isEmpty() ? context.newPage() : context.pages().get(0);
page.addInitScript(STEALTH_SCRIPT);
@ -67,15 +71,15 @@ public class GoofishPwdLoginService {
// 点击协议并登录
ElementHandle agreement = loginFrame.querySelector("#fm-agreement-checkbox");
if (agreement != null && !agreement.isChecked()) {
log.debug("【{}】正在查协议复选框,并勾选......", account);
log.debug("【{}】正在协议复选框,并勾选......", account);
agreement.click();
}
log.debug("【{}】单击登录按钮...", account);
loginFrame.click("button.fm-button.fm-submit.password-login");
// TODO 这里点击后可能并不能成功会出现滑块
// 这里点击后可能并不能成功会出现滑块
if (Objects.nonNull(findLoginFrame(page, account))) {
// TODO 内部应该要有重试处理滑块的逻辑
// 内部应该要有重试处理滑块的逻辑
boolean resolveResult = attemptSolveSlider(loginFrame, account);
log.debug("【{}】滑块结果: {}", account, resolveResult ? "成功" : "失败");
}
@ -88,23 +92,35 @@ public class GoofishPwdLoginService {
page.waitForTimeout(10000);
}
// TODO 处理登录成功后的账号 cookie并存库更新persistentData 目录名称
if (checkLoggedIn(context, account)) {
// 处理登录成功后的账号 cookie并存库更新persistentData 目录名称
String cookieStr = buildCookieStr(context);
Long goofishUserId = goofishApiService.getUserId(account, cookieStr);
if (goofishUserId > 0L) {
// 能正确登录说明已经登录成功
GoofishAccountEntity entity = new GoofishAccountEntity();
entity.setId(String.valueOf(goofishUserId));
entity.setCookie(cookieStr);
entity.setUserId(systemUsername);
entity.setAutoConfirm(1);
entity.setUsername(account);
entity.setNickname(goofishApiService.getNickName(account, cookieStr));
entity.setPassword(password);
entity.setShowBrowser(Objects.equals(showBrowser, Boolean.TRUE) ? 1 : 0);
goofishAccountRepository.save(entity);
browserService.renamePersistentContext(account, String.valueOf(goofishUserId), showBrowser);
log.info("【{}】账号密码登录成功", account);
} else {
log.error("【{}】账号密码登录失败", account);
}
log.debug("....");
}
private boolean checkLoggedIn(BrowserContext context, String account) {
private String buildCookieStr(BrowserContext context) {
List<Cookie> cookies = context.cookies().stream().filter(i -> Objects.equals(i.domain, ".goofish.com")).toList();
if (CollUtil.isEmpty(cookies)) {
return false;
return null;
}
String remoteAccount = goofishApiService.getAccount(account, CookieUtils.buildCookieStr(cookies));
return Objects.equals(remoteAccount, account);
return CookieUtils.buildCookieStr(cookies);
}
private Frame findLoginFrame(Page page, String account) {

View File

@ -1,7 +1,9 @@
package top.biwin.xinayu.server.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@ -10,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.biwin.xianyu.core.entity.GoofishAccountEntity;
import top.biwin.xianyu.core.repository.GoofishAccountRepository;
import top.biwin.xianyu.goofish.service.GoofishApiService;
import top.biwin.xianyu.goofish.service.GoofishPwdLoginService;
import top.biwin.xianyu.goofish.service.QrLoginService;
import top.biwin.xinayu.common.dto.request.GoofishAddCookieRequest;
@ -22,6 +25,7 @@ import top.biwin.xinayu.server.util.CurrentUserUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
@ -30,6 +34,7 @@ import java.util.stream.Collectors;
* @author wangli
* @since 2026-01-22 22:34
*/
@Slf4j
@RestController
@RequestMapping("/goofish")
public class GoofishAccountController {
@ -39,6 +44,8 @@ public class GoofishAccountController {
private GoofishAccountRepository goofishAccountRepository;
@Autowired
private GoofishPwdLoginService goofishPwdLoginService;
@Autowired
private GoofishApiService goofishApiService;
/**
* 获取所有Cookie的详细信息包括值和状态
@ -89,12 +96,11 @@ public class GoofishAccountController {
.build());
}
// TODO 自动化操作浏览器完成登陆并写 goofishAccount
// 自动化操作浏览器完成登陆并写 goofishAccount
goofishPwdLoginService.processPasswordLogin(request.getAccount(),
request.getPassword(),
request.getShow_browser() != null && request.getShow_browser(),
CurrentUserUtil.getCurrentUsername());
CurrentUserUtil.getCurrentUserId());
return ResponseEntity.ok(BaseResponse.builder()
.success(true)
@ -105,17 +111,19 @@ public class GoofishAccountController {
@PostMapping("/cookies")
public ResponseEntity<BaseResponse> loginByCookie(@RequestBody GoofishAddCookieRequest request) {
Long goofishUserId = goofishApiService.getUserId(request.getId(), request.getValue());
log.info("尝试利用 cookie 获取闲鱼用户 ID{}", goofishUserId > 0L ? goofishUserId : "Cookie 失效");
GoofishAccountEntity entity = goofishAccountRepository.findByUsername(request.getId())
.map(account -> {
account.setCookie(account.getCookie());
account.setCookie(request.getValue());
return account;
})
.orElseGet(() -> {
GoofishAccountEntity newAccount = new GoofishAccountEntity();
// TODO 该属性应该利用 cookie 真实去获取 这里暂时写死
newAccount.setId("10000");
newAccount.setId(String.valueOf(goofishUserId));
newAccount.setCookie(request.getValue());
newAccount.setUserId(1L);
newAccount.setUserId(CurrentUserUtil.getCurrentUserId());
newAccount.setAutoConfirm(1);
newAccount.setUsername(request.getId());
return newAccount;
@ -127,4 +135,22 @@ public class GoofishAccountController {
.message("手动添加 cookie 成功")
.build());
}
@DeleteMapping("/cookies/{id}")
public void deleteAccount(@PathVariable("id") String goofishId) {
GoofishAccountEntity entity;
try {
entity = goofishAccountRepository.getReferenceById(goofishId);
} catch (Exception e) {
log.info("未找到【{}】账号的数据", goofishId);
return;
}
if (!CurrentUserUtil.isSuperAdmin()) {
if (!Objects.equals(entity.getUserId(), CurrentUserUtil.getCurrentUserId())) {
log.warn("该闲鱼账号不归属当前登录用户!");
}
}
goofishAccountRepository.delete(entity);
}
}

View File

@ -1,5 +1,5 @@
-- 系统默认账号
INSERT OR IGNORE INTO admin_user (username, email, password_hash) VALUES ('admin', 'admin@localhost', '$2a$12$Ozdr6p4aCMIrt8KvRalWseNfMhl7exyeolzZXvheRgY3lD5fZTyNm');
INSERT OR IGNORE INTO admin_user (username, email, password_hash, `role`) VALUES ('admin', 'admin@localhost', '$2a$12$Ozdr6p4aCMIrt8KvRalWseNfMhl7exyeolzZXvheRgY3lD5fZTyNm', 'SUPER_ADMIN');
-- 系统默认设置
INSERT OR IGNORE INTO system_settings (key, value, description)
VALUES ('init_system', 'false', '是否初始化'),