From e924e8c0e6d3511912fdd8e26db0d23511fc76cc Mon Sep 17 00:00:00 2001
From: Doghole 请求头顺序
- * X-Protocol-Id > X-Request-Id > EM-Sign > Authorization >
- * X-Android-Agent > Emapp-ViewMode > Content-Type > Content-Length >
- * Host > Connection: "Keep-Alive" > Accept-Encoding: "gzip" > User-Agent 从 X-Protocol-Id 到 Emapp-ViewMode,由本例添加,剩余为 okhttp 默认添加,
+ *
* 调用方法为 A 的,多次从 A 调用则加锁,从 B 调用时不受影响
diff --git a/src/main/java/quant/rich/emoney/annotation/ResponseDecodeExtension.java b/src/main/java/quant/rich/emoney/annotation/ResponseDecodeExtension.java
new file mode 100644
index 0000000..2a1640c
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/annotation/ResponseDecodeExtension.java
@@ -0,0 +1,19 @@
+package quant.rich.emoney.annotation;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * 注解在方法上以获取对 EmoneyProtocol 的额外操作
+ */
+@Documented
+@Retention(RUNTIME)
+@Target(METHOD)
+public @interface ResponseDecodeExtension {
+ String protocolId();
+ int order() default -1;
+}
diff --git a/src/main/java/quant/rich/emoney/client/EmoneyClient.java b/src/main/java/quant/rich/emoney/client/EmoneyClient.java
index 6e7cc56..1a38f7f 100644
--- a/src/main/java/quant/rich/emoney/client/EmoneyClient.java
+++ b/src/main/java/quant/rich/emoney/client/EmoneyClient.java
@@ -30,10 +30,22 @@ import okhttp3.OkHttpClient;
* 益盟操盘手基本请求客户端,提供基本功能
*
+ *
+ *
从 X-Protocol-Id 到 Emapp-ViewMode 由本例添加,剩余为 okhttp 默认添加, * User-Agent 由 ByteBuddy 重写 header 方法控制添加
* @see quant.rich.emoney.patch.okhttp.PatchOkHttp */ @@ -43,19 +55,44 @@ import okhttp3.OkHttpClient; public class EmoneyClient implements Cloneable { private static final String MBS_URL = "https://mbs.emoney.cn/"; + private static final String STRATEGY_URL = "https://mbs.emoney.cn/strategy/"; private static final String LOGIN_URL = "https://emapp.emoney.cn/user/auth/login"; private static final String RELOGIN_URL = "https://emapp.emoney.cn/user/auth/ReLogin"; private static final String LOGIN_X_PROTOCOL_ID = "user%2Fauth%2Flogin"; private static final String RELOGIN_X_PROTOCOL_ID = "user%2Fauth%2FReLogin"; private static volatile EmoneyRequestConfig emoneyRequestConfig; + + /** + * 根据 protocolId 返回 URL + * @param protocolId + * @return + */ + private static String getUrlByProtocolId(Serializable protocolId) { + if (protocolId instanceof Integer intProtocolId) { + switch (intProtocolId) { + case 9400: return STRATEGY_URL; + default: return MBS_URL; + } + } + else if (protocolId instanceof String strProtocolId) { + switch (strProtocolId) { + case LOGIN_X_PROTOCOL_ID: return LOGIN_URL; + case RELOGIN_X_PROTOCOL_ID: return RELOGIN_URL; + default: return null; + } + } + return null; + } + /** + * 从 Spring 上下文中获取载入的请求配置 + * @return + */ private static EmoneyRequestConfig getEmoneyRequestConfig() { if (emoneyRequestConfig == null) { synchronized (EmoneyClient.class) { - if (emoneyRequestConfig == null) { - emoneyRequestConfig = SpringContextHolder.getBean(EmoneyRequestConfig.class); - } + emoneyRequestConfig = SpringContextHolder.getBean(EmoneyRequestConfig.class); } } return emoneyRequestConfig; @@ -64,7 +101,7 @@ public class EmoneyClient implements Cloneable { private EmoneyClient() {} /** - * 根据系统配置自动选择登录方式 + * 根据系统配置自动选择登录方式,即匿名或不匿名 * @return * @see EmoneyRequestConfig */ @@ -149,7 +186,7 @@ public class EmoneyClient implements Cloneable { if (response.code() != 200) { // 不是 200,重新登录 - log.debug("ReLogin 重登录验证返回状态码 {}, 触发登录", response.code()); + log.debug("ReLogin 重登录验证返回状态码 {}, 需要重新登录", response.code()); return loginWithManaged(); } @@ -266,6 +303,11 @@ public class EmoneyClient implements Cloneable { new IllegalArgumentException()); } + String url = getUrlByProtocolId(xProtocolId); + if (StringUtils.isBlank(url)) { + throw new EmoneyRequestException("无法根据 xProtocolId " + xProtocolId + "获取请求 URL"); + } + try { OkHttpClient okHttpClient = OkHttpClientProvider.getInstance(); @@ -275,7 +317,7 @@ public class EmoneyClient implements Cloneable { MediaType.parse("application/x-protobuf-v3")); Request.Builder requestBuilder = new Request.Builder() - .url(MBS_URL) + .url(url) .post(body) // 这玩意可能也有顺序 // 按照 Fiddler HexView 顺序如下: @@ -293,6 +335,10 @@ public class EmoneyClient implements Cloneable { final Call call = okHttpClient.newCall(request); Response response = call.execute(); + // 错误的时候这里是 404,比如 strategy 是独立网页的时候,Content-Type 是 text/plain + if (response.code() != 200) { + throw new EmoneyRequestException("请求返回错误,状态码 " + response.code()); + } BaseResponse.Base_Response baseResponse = BaseResponse.Base_Response.parseFrom(response.body().bytes()); return baseResponse; } catch (InvalidProtocolBufferNanoException e) { diff --git a/src/main/java/quant/rich/emoney/client/WebviewClient.java b/src/main/java/quant/rich/emoney/client/WebviewClient.java deleted file mode 100644 index 5dfa83b..0000000 --- a/src/main/java/quant/rich/emoney/client/WebviewClient.java +++ /dev/null @@ -1,177 +0,0 @@ -package quant.rich.emoney.client; - -import java.net.Proxy; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.commons.lang3.StringUtils; - -import com.microsoft.playwright.Browser; -import com.microsoft.playwright.BrowserContext; -import com.microsoft.playwright.BrowserType; -import com.microsoft.playwright.Page; -import com.microsoft.playwright.Playwright; -import com.microsoft.playwright.Request; -import com.microsoft.playwright.BrowserType.LaunchOptions; -import com.microsoft.playwright.Route.ResumeOptions; -import com.microsoft.playwright.options.HttpHeader; -import com.microsoft.playwright.options.WaitUntilState; - -import lombok.Data; -import lombok.experimental.Accessors; -import quant.rich.emoney.component.LockByCaller; -import quant.rich.emoney.entity.config.EmoneyRequestConfig; -import quant.rich.emoney.entity.config.ProxyConfig; -import quant.rich.emoney.util.SpringContextHolder; - -public class WebviewClient { - - private static Playwright playwright; - private static boolean isReady = false; - private static volatile ProxyConfig proxyConfig; - private static volatile EmoneyRequestConfig emoneyRequestConfig; - - static { - try { - playwright = Playwright.create(); - isReady = true; - } - catch (Exception e) { - e.printStackTrace(); - } - } - - public static boolean isReady() { - return isReady; - } - - private static ProxyConfig getProxyConfig() { - if (proxyConfig == null) { - synchronized (WebviewClient.class) { - if (proxyConfig == null) { - proxyConfig = SpringContextHolder.getBean(ProxyConfig.class); - } - } - } - return proxyConfig; - } - - private static EmoneyRequestConfig getEmoneyRequestConfig() { - if (emoneyRequestConfig == null) { - synchronized (WebviewClient.class) { - if (emoneyRequestConfig == null) { - emoneyRequestConfig = SpringContextHolder.getBean(EmoneyRequestConfig.class); - } - } - } - return emoneyRequestConfig; - } - - @LockByCaller - public static WebviewResponseWrapper getIndexDetailWrapper(String indexCode) { - - String proxyUrl = getProxyConfig().getProxyUrl(); - LaunchOptions launchOptions = new BrowserType.LaunchOptions() - .setHeadless(true); - - // 设置代理 - if (StringUtils.isNotBlank(proxyUrl)) { - launchOptions.setProxy(proxyUrl); - } - - Browser browser = playwright.chromium().launch(launchOptions); - BrowserContext context = browser.newContext(new Browser.NewContextOptions() - // 设置 Webview User-Agent - .setUserAgent(getEmoneyRequestConfig().getWebviewUserAgent()) - // 设置是否忽略 HTTPS 证书 - .setIgnoreHTTPSErrors(getProxyConfig().getIgnoreHttpsVerification()) - ); - - // 设置全局请求头 - // 根据抓包获得,当前目标版本 5.8.1 - Map
+ * obj 不为空时,等效于 R.ok(obj)
+ * obj 为空时抛出 RException.badRequest()
+ * @param obj
+ * @return
+ */
public static R> judgeNonNull(Object obj) {
if (obj != null) return R.ok(obj);
throw RException.badRequest();
}
+ /**
+ * 根据 obj 是否为空返回对应内容
+ * obj 不为空时,等效于 R.ok(obj)
+ * obj 为空时抛出 RException.badRequest(failedMessage)
+ * @param obj
+ * @param failedMessage
+ * @return
+ */
+ public static R> judgeNonNull(Object obj, String failedMessage) {
+ if (obj != null) return R.ok(obj);
+ throw RException.badRequest(failedMessage);
+ }
+
public static R> judge(ThrowingSupplier> supplier) {
try {
return R.ok(supplier.get());
diff --git a/src/main/java/quant/rich/emoney/service/ConfigService.java b/src/main/java/quant/rich/emoney/service/ConfigService.java
index de85878..66bd3c9 100644
--- a/src/main/java/quant/rich/emoney/service/ConfigService.java
+++ b/src/main/java/quant/rich/emoney/service/ConfigService.java
@@ -17,13 +17,8 @@ import io.micrometer.core.instrument.util.IOUtils;
import jakarta.annotation.PostConstruct;
import java.io.IOException;
-import java.io.UncheckedIOException;
import java.lang.reflect.InvocationTargetException;
-import java.net.URISyntaxException;
import java.nio.charset.Charset;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -35,7 +30,6 @@ import org.reflections.scanners.Scanners;
import org.reflections.util.ConfigurationBuilder;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.core.io.ClassPathResource;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.stereotype.Service;
@@ -280,7 +274,7 @@ public class ConfigService implements InitializingBean {
try {
// 此处只是读取文件,并不关心该文件是否可写
configString = IOUtils.toString(SmartResourceResolver.loadResource(path), Charset.defaultCharset());
- } catch (UncheckedIOException e) {
+ } catch (IOException e) {
String field = fieldClassCache.inverse().get(configClass);
log.warn("Cannot read config {}.json: {}", field, e.getMessage());
return config;
diff --git a/src/main/java/quant/rich/emoney/service/IndexDetailService.java b/src/main/java/quant/rich/emoney/service/IndexDetailService.java
index 0b5cf8f..fbf23bd 100644
--- a/src/main/java/quant/rich/emoney/service/IndexDetailService.java
+++ b/src/main/java/quant/rich/emoney/service/IndexDetailService.java
@@ -6,7 +6,6 @@ import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.ArrayList;
@@ -52,6 +51,13 @@ import quant.rich.emoney.pojo.dto.ParamsIndexDetail;
/**
* 获取指标详情的服务
+ *
+ *