This commit is contained in:
wangli 2026-01-25 23:41:14 +08:00
parent 936a511b79
commit b6c2f7407b
9 changed files with 255 additions and 6 deletions

View File

@ -52,7 +52,7 @@ public class GoofishAccountEntity {
private String username;
@ColumnDefault("''")
private String nickName;
private String nickname;
@ColumnDefault("''")
private String password;

View File

@ -37,9 +37,17 @@
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -22,6 +22,7 @@ import top.biwin.xianyu.core.entity.GoofishAccountEntity;
import top.biwin.xianyu.core.repository.GoofishAccountRepository;
import top.biwin.xianyu.goofish.dto.QrLoginSessionDto;
import top.biwin.xianyu.goofish.service.BrowserService;
import top.biwin.xianyu.goofish.service.GoofishApiService;
import top.biwin.xinayu.common.dto.response.GoofishAccountSimpleInfo;
import top.biwin.xinayu.common.dto.response.GoofishQrStatusResponse;
import top.biwin.xinayu.common.dto.response.QrLoginResponse;
@ -49,6 +50,8 @@ public class QrLoginService {
@Autowired
private BrowserService browserService;
@Autowired
private GoofishApiService goofishApiService;
@Autowired
private OkHttpClient okHttpClient;
@Autowired
private ObjectMapper objectMapper;
@ -91,7 +94,6 @@ public class QrLoginService {
log.info("【QR Login】Got login params for session: {}", sessionId);
// 3. Generate QR Code Data
// Construct URL: https://passport.goofish.com/newlogin/qrcode/generate.do
HttpUrl.Builder urlBuilder = HttpUrl.parse("https://passport.goofish.com/newlogin/qrcode/generate.do").newBuilder();
for (Map.Entry<String, String> entry : loginParams.entrySet()) {
urlBuilder.addQueryParameter(entry.getKey(), entry.getValue());
@ -702,7 +704,7 @@ public class QrLoginService {
accountId
);
if (verifiedCookies != null && !verifiedCookies.isEmpty()) {
if (!verifiedCookies.isEmpty()) {
log.info("【QR Login】浏览器验证成功获取到真实Cookie数量: {}", verifiedCookies.size());
// 将Cookie转换为字符串
@ -724,8 +726,8 @@ public class QrLoginService {
goofishAccount.setCookie(finalCookieStr);
if (isNewAccount) {
goofishAccount.setNickName(verifiedCookies.getOrDefault("nickName", ""));
goofishAccount.setUsername("TB_" + unb);
goofishAccount.setNickname(verifiedCookies.getOrDefault("nickName", ""));
goofishAccount.setUsername(goofishApiService.getAccount(accountId, finalCookieStr));
goofishAccount.setPassword("QR_LOGIN");
goofishAccount.setUserId(userId);
}

View File

@ -170,7 +170,7 @@ public class BrowserService {
log.error("【QR Login】Error creating browser context for verification", e);
}
return null; // Failed
return new HashMap<>(); // Failed
}
}

View File

@ -0,0 +1,50 @@
package top.biwin.xianyu.goofish.service;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import top.biwin.xianyu.core.repository.GoofishAccountRepository;
import top.biwin.xianyu.goofish.service.api.GoofishApi;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Objects;
/**
* TODO
*
* @author wangli
* @since 2026-01-25 15:59
*/
@Component
@Data
public class GoofishApiService {
@Autowired
private List<GoofishApi<?>> apis;
@Autowired
private GoofishAccountRepository goofishAccountRepository;
/**
* 获取账号
*
* @return
*/
public String getAccount(String goofishId, @Nullable String cookieStr) {
GoofishApi<?> goofishApi = getApi("getAccount");
if (!StringUtils.hasText(cookieStr)) {
cookieStr = goofishAccountRepository.getReferenceById(goofishId).getCookie();
}
return (String) goofishApi.call(goofishId, cookieStr, "{}");
}
private GoofishApi getApi(String apiName) {
if (CollectionUtils.isEmpty(apis)) {
throw new IllegalStateException("未初始化闲鱼 API");
}
return apis.stream().filter(i -> Objects.equals(i.getName(), apiName)).findFirst().orElseThrow(() -> new IllegalStateException("未找到指定 API"));
}
}

View File

@ -0,0 +1,100 @@
package top.biwin.xianyu.goofish.service.api;
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 java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* 获取闲鱼的账号
*
* @author wangli
* @since 2026-01-25 17:20
*/
@Component
@Slf4j
public class GetAccountApi extends GoofishAbstractApi<String> {
@Override
public String getName() {
return "getAccount";
}
@Override
public String getApi() {
return "mtop.idle.web.user.page.account";
}
@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));
try (HttpResponse response = HttpRequest.post(apiUrl)
.header("Cookie", cookieStr)
.body("data=" + URLEncoder.encode(dataStr, StandardCharsets.UTF_8))
.execute()) {
String body = response.body();
log.info("【{}】Auto confirm response: {}", 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")) {
return json.getJSONObject("data").getJSONObject("module").getJSONObject("base").getStr("displayNick");
}
}
} catch (Exception e) {
log.error("获取闲鱼账号异常:{}", e.getMessage(), e);
}
return null;
}
public static void main(String[] args) {
String jsonStr = "{\n" +
" \"api\": \"mtop.idle.web.user.page.account\",\n" +
" \"data\": {\n" +
" \"module\": {\n" +
" \"certify\": {\n" +
" \"realPersonCertified\": true,\n" +
" \"alipayCertified\": true,\n" +
" \"xianyuCertified\": true\n" +
" },\n" +
" \"base\": {\n" +
" \"displayNick\": \"半夏落地\"\n" +
" }\n" +
" },\n" +
" \"needDecryptKeys\": [\n" +
" \"baseInfo.encryptedUserId\"\n" +
" ]\n" +
" },\n" +
" \"ret\": [\n" +
" \"SUCCESS::调用成功\"\n" +
" ],\n" +
" \"traceId\": \"2150444f17693348617105274e29cb\",\n" +
" \"v\": \"1.0\"\n" +
"}";
JSONObject json = JSONUtil.parseObj(jsonStr);
if (json.containsKey("ret")) {
JSONArray ret = json.getJSONArray("ret");
if (ret != null && !ret.isEmpty() && ret.getStr(0).startsWith("SUCCESS")) {
json.getJSONObject("data").getJSONObject("module").getJSONObject("base").getStr("displayNick");
}
}
}
}

View File

@ -0,0 +1,66 @@
package top.biwin.xianyu.goofish.service.api;
import cn.hutool.crypto.SecureUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* TODO
*
* @author wangli
* @since 2026-01-25 17:16
*/
public abstract class GoofishAbstractApi<T> implements GoofishApi<T> {
@Value("${goofish.api.appKey:34839810}")
private String appKey;
@Value("${goofish.api.hostUrl:https://h5api.m.goofish.com/h5/}")
private String apiHostUrl;
protected final String buildApiUrl() {
return apiHostUrl + getApi() + "/" + getVersion() + "/";
}
protected Map<String, String> buildQueryParams(String cookieStr, String dataStr) {
Map<String, String> api_query_params = new HashMap<>();
api_query_params.put("jsv", "2.7.2");
api_query_params.put("appKey", appKey);
String t = String.valueOf(System.currentTimeMillis());
api_query_params.put("t", t);
api_query_params.put("sign", generateSign(t, getApiToken(cookieStr), dataStr));
api_query_params.put("v", "1.0");
api_query_params.put("type", "originaljson");
api_query_params.put("accountSite", "xianyu");
api_query_params.put("dataType", "json");
api_query_params.put("timeout", "20000");
api_query_params.put("api", "");
api_query_params.put("sessionOption", "AutoLoginOnly");
api_query_params.put("spm_cnt", "a21ybx.account.0.0");
return api_query_params;
}
private String getApiToken(String cookieStr) {
if (!StringUtils.hasText(cookieStr)) {
throw new IllegalArgumentException("缺少 Cookie 数据");
}
String token = "";
String[] parts = cookieStr.split(";");
for (String part : parts) {
String[] kv = part.trim().split("=");
if (kv.length > 0 && "_m_h5_tk".equals(kv[0])) {
if (kv.length > 1) {
token = kv[1].split("_")[0];
}
break;
}
}
return token;
}
private String generateSign(String millisTimeStampStr, String token, String data) {
String src = token + "&" + millisTimeStampStr + "&" + appKey + "&" + data;
return SecureUtil.md5(src);
}
}

View File

@ -0,0 +1,18 @@
package top.biwin.xianyu.goofish.service.api;
/**
* TODO
*
* @author wangli
* @since 2026-01-25 17:16
*/
public interface GoofishApi<T> {
String getName();
String getApi();
String getVersion();
T call(String goofishId, String cookie, String dataStr);
}

View File

@ -62,5 +62,10 @@ jwt:
# 7 天 = 604,800,000 毫秒
refresh-token-expiration: 604800000
goofish:
api:
hostUrl: https://h5api.m.goofish.com/h5/
appKey: 34839810
browser:
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'