From edcbfd4ffd44dff2814df5973274e97f1da9a94a Mon Sep 17 00:00:00 2001 From: Doghole Date: Sat, 15 Nov 2025 14:57:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=20EmoneyRequestConfig=20?= =?UTF-8?q?=E5=92=8C=20ProxyConfig=20=E8=AE=BE=E7=BD=AE=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=95=B0=E6=8D=AE=E5=BA=93=EF=BC=88SQLite=EF=BC=89?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E3=80=82=E9=BB=98=E8=AE=A4=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E7=9A=84=E8=AE=BE=E7=BD=AE=E5=92=8C=E5=88=A0=E9=99=A4=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E7=94=B1=20SQLite=20=E8=A7=A6=E5=8F=91=E5=99=A8?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 7 - .../rich/emoney/annotation/LockByCaller.java | 26 - .../rich/emoney/client/EmoneyClient.java | 57 +- .../emoney/client/OkHttpClientProvider.java | 24 +- .../emoney/component/CallerLockAspect.java | 29 +- .../EmoneyAutoPlatformExceptionHandler.java | 41 +- .../component/RequireAuthAndProxyAspect.java | 81 +++ .../emoney/config/ConfigAutoRegistrar.java | 26 +- .../config/ConfigServiceFactoryBean.java | 14 +- .../rich/emoney/config/ConstructionGuard.java | 21 +- .../emoney/config/SqliteMybatisConfig.java | 7 +- .../emoney/controller/IndexControllerV1.java | 5 +- .../emoney/controller/LoginControllerV1.java | 7 +- .../api/ProtoDecodeControllerV1.java | 41 +- .../controller/common/BaseController.java | 3 - .../controller/common/ServiceController.java | 122 ++++ .../common/UpdateBoolController.java | 91 --- .../common/UpdateBoolServiceController.java | 86 +++ .../config/ProxyConfigControllerV1.java | 28 - .../manage/IndexInfoControllerV1.java | 37 +- .../controller/manage/PlanControllerV1.java | 25 +- .../manage/ProxySettingControllerV1.java | 73 +-- .../manage/RequestInfoControllerV1.java | 61 +- .../entity/config/AndroidSdkLevelConfig.java | 1 + .../entity/config/DeviceInfoConfig.java | 18 +- .../entity/config/EmoneyRequestConfig.java | 539 ------------------ .../rich/emoney/entity/config/IndexInfo.java | 38 -- .../emoney/entity/config/IndexInfoConfig.java | 24 +- .../emoney/entity/config/PlatformConfig.java | 2 + .../emoney/entity/config/ProxyConfig.java | 101 ---- .../emoney/entity/postgre/StockStrategy.java | 42 ++ .../emoney/entity/sqlite/ProxySetting.java | 2 + .../emoney/entity/sqlite/RequestInfo.java | 55 +- .../emoney/entity/sqlite/StrategyAndPool.java | 16 +- .../rich/emoney/interfaces/ConfigInfo.java | 7 +- .../rich/emoney/patch/okhttp/PatchOkHttp.java | 9 +- .../emoney/patch/okhttp/PatchOkHttpRule.java | 20 +- .../java/quant/rich/emoney/pojo/dto/R.java | 16 + .../rich/emoney/service/ConfigService.java | 28 +- .../emoney/service/IndexDetailService.java | 52 +- .../rich/emoney/service/WarnService.java | 27 + .../service/sqlite/ProxySettingService.java | 32 ++ .../service/sqlite/RequestInfoService.java | 44 +- .../sqlite/StrategyAndPoolService.java | 14 +- .../quant/rich/emoney/util/EncryptUtils.java | 1 - .../quant/rich/emoney/util/GeoIPUtil.java | 13 +- .../emoney/util/SmartResourceResolver.java | 8 +- .../emoney/validator/ProxyConfigValid.java | 20 - .../validator/ProxyConfigValidator.java | 37 -- ...ConfigValid.java => RequestInfoValid.java} | 4 +- ...lidator.java => RequestInfoValidator.java} | 20 +- src/main/resources/application-remote.yml | 12 - src/main/resources/application.yml | 15 + .../extra/indexDetail/nonParams/10002700.json | 12 +- .../extra/indexDetail/nonParams/10012100.json | 12 +- .../extra/indexDetail/nonParams/10013500.json | 3 +- .../extra/indexDetail/params/10010600.json | 3 +- .../conf/system/androidSdkLevel.fallback.json | 3 +- .../conf/system/androidSdkLevel.json | 51 +- .../conf/system/emoneyLoginForm.json | 5 - .../resources/conf/system/emoneyRequest.json | 18 - src/main/resources/conf/system/platform.json | 1 + src/main/resources/database.db | Bin 40960 -> 49152 bytes .../static/admin/v1/static/js/helper.js | 223 +++++--- .../resources/webpage/admin/v1/include.html | 98 ++-- .../admin/v1/manage/indexInfo/index.html | 4 +- .../webpage/admin/v1/manage/plan/include.html | 4 +- .../webpage/admin/v1/manage/plan/index.html | 50 +- .../admin/v1/manage/proxySetting/include.html | 7 + .../admin/v1/manage/proxySetting/index.html | 88 +-- .../admin/v1/manage/requestInfo/include.html | 25 +- .../admin/v1/manage/requestInfo/index.html | 37 +- src/test/java/ByteBuddyTest.java | 2 +- src/test/java/EmoneyScraper.java | 68 --- src/test/java/FingerprintsSpliter.java | 2 - .../java/quant/rich/EmoneyIndexScraper.java | 13 +- src/test/java/quant/rich/PatchOkHttpTest.java | 2 +- 77 files changed, 1240 insertions(+), 1620 deletions(-) delete mode 100644 src/main/java/quant/rich/emoney/annotation/LockByCaller.java create mode 100644 src/main/java/quant/rich/emoney/component/RequireAuthAndProxyAspect.java create mode 100644 src/main/java/quant/rich/emoney/controller/common/ServiceController.java delete mode 100644 src/main/java/quant/rich/emoney/controller/common/UpdateBoolController.java create mode 100644 src/main/java/quant/rich/emoney/controller/common/UpdateBoolServiceController.java delete mode 100644 src/main/java/quant/rich/emoney/controller/config/ProxyConfigControllerV1.java delete mode 100644 src/main/java/quant/rich/emoney/entity/config/EmoneyRequestConfig.java delete mode 100644 src/main/java/quant/rich/emoney/entity/config/IndexInfo.java delete mode 100644 src/main/java/quant/rich/emoney/entity/config/ProxyConfig.java create mode 100644 src/main/java/quant/rich/emoney/entity/postgre/StockStrategy.java create mode 100644 src/main/java/quant/rich/emoney/service/WarnService.java delete mode 100644 src/main/java/quant/rich/emoney/validator/ProxyConfigValid.java delete mode 100644 src/main/java/quant/rich/emoney/validator/ProxyConfigValidator.java rename src/main/java/quant/rich/emoney/validator/{EmoneyRequestConfigValid.java => RequestInfoValid.java} (81%) rename src/main/java/quant/rich/emoney/validator/{EmoneyRequestConfigValidator.java => RequestInfoValidator.java} (56%) delete mode 100644 src/main/resources/conf/system/emoneyLoginForm.json delete mode 100644 src/main/resources/conf/system/emoneyRequest.json delete mode 100644 src/test/java/EmoneyScraper.java diff --git a/pom.xml b/pom.xml index 11e6db9..fcf22cc 100644 --- a/pom.xml +++ b/pom.xml @@ -171,13 +171,6 @@ test - - - com.microsoft.playwright - playwright - 1.51.0 - - org.apache.commons commons-text diff --git a/src/main/java/quant/rich/emoney/annotation/LockByCaller.java b/src/main/java/quant/rich/emoney/annotation/LockByCaller.java deleted file mode 100644 index dc17c5c..0000000 --- a/src/main/java/quant/rich/emoney/annotation/LockByCaller.java +++ /dev/null @@ -1,26 +0,0 @@ -package quant.rich.emoney.annotation; - -import java.lang.annotation.*; - -import quant.rich.emoney.component.CallerLockAspect; - -/** - * 在方法上添加此注解,可针对调用方加锁,即:
- * 调用方法为 A 的,多次从 A 调用则加锁,从 B 调用时不受影响
- * 需要开启 AOP:在任意配置类上增加注解:@EnableAspectJAutoProxy - * @see CallerLockAspect - */ -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface LockByCaller { - /** - * 可选参数,用于 SpEL 表达式获取 key - * 例如: - * 当不指定时,不校验参数,单纯校验 Caller - */ - String key() default ""; -} \ No newline at end of file diff --git a/src/main/java/quant/rich/emoney/client/EmoneyClient.java b/src/main/java/quant/rich/emoney/client/EmoneyClient.java index 1a38f7f..ea65575 100644 --- a/src/main/java/quant/rich/emoney/client/EmoneyClient.java +++ b/src/main/java/quant/rich/emoney/client/EmoneyClient.java @@ -17,11 +17,12 @@ import okhttp3.MediaType; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; -import quant.rich.emoney.entity.config.EmoneyRequestConfig; +import quant.rich.emoney.entity.sqlite.RequestInfo; import quant.rich.emoney.exception.EmoneyDecodeException; import quant.rich.emoney.exception.EmoneyIllegalRequestParamException; import quant.rich.emoney.exception.EmoneyRequestException; import quant.rich.emoney.exception.EmoneyResponseException; +import quant.rich.emoney.service.sqlite.RequestInfoService; import quant.rich.emoney.util.EncryptUtils; import quant.rich.emoney.util.SpringContextHolder; import okhttp3.OkHttpClient; @@ -61,7 +62,7 @@ public class EmoneyClient implements Cloneable { 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; + private static volatile RequestInfoService requestInfoService; /** * 根据 protocolId 返回 URL @@ -89,13 +90,17 @@ public class EmoneyClient implements Cloneable { * 从 Spring 上下文中获取载入的请求配置 * @return */ - private static EmoneyRequestConfig getEmoneyRequestConfig() { - if (emoneyRequestConfig == null) { + private static RequestInfo getDefaultRequestInfo() { + if (requestInfoService == null) { synchronized (EmoneyClient.class) { - emoneyRequestConfig = SpringContextHolder.getBean(EmoneyRequestConfig.class); + requestInfoService = SpringContextHolder.getBean(RequestInfoService.class); } } - return emoneyRequestConfig; + if (requestInfoService == null) { + log.warn("获取 RequestInfoService 实例失败"); + return null; + } + return requestInfoService.getDefaultRequestInfo(); } private EmoneyClient() {} @@ -103,10 +108,10 @@ public class EmoneyClient implements Cloneable { /** * 根据系统配置自动选择登录方式,即匿名或不匿名 * @return - * @see EmoneyRequestConfig + * @see RequestInfo */ public static Boolean loginWithManaged() { - if (getEmoneyRequestConfig().getIsAnonymous()) { + if (getDefaultRequestInfo().isAnonymous()) { return loginWithAnonymous(); } else { @@ -122,7 +127,7 @@ public class EmoneyClient implements Cloneable { */ @Deprecated public static Boolean loginWithUsernamePassword() { - ObjectNode formObject = getEmoneyRequestConfig().getUsernamePasswordLoginObject(); + ObjectNode formObject = getDefaultRequestInfo().getUsernamePasswordLoginObject(); return login(formObject); } @@ -135,7 +140,7 @@ public class EmoneyClient implements Cloneable { */ @Deprecated public static Boolean loginWithUsernamePassword(String username, String password) { - ObjectNode formObject = getEmoneyRequestConfig().getUsernamePasswordLoginObject(username, password); + ObjectNode formObject = getDefaultRequestInfo().getUsernamePasswordLoginObject(username, password); return login(formObject); } @@ -146,7 +151,7 @@ public class EmoneyClient implements Cloneable { */ @Deprecated public static Boolean loginWithAnonymous() { - ObjectNode formObject = getEmoneyRequestConfig().getAnonymousLoginObject(); + ObjectNode formObject = getDefaultRequestInfo().getAnonymousLoginObject(); return login(formObject); } @@ -155,8 +160,8 @@ public class EmoneyClient implements Cloneable { * @return */ public static Boolean relogin() { - EmoneyRequestConfig emoneyRequestConfig = getEmoneyRequestConfig(); - ObjectNode reloginObject = emoneyRequestConfig.getReloginObject(); + RequestInfo requestInfo = getDefaultRequestInfo(); + ObjectNode reloginObject = requestInfo.getReloginObject(); if (reloginObject == null) { // 无登录信息,直接触发登录 return loginWithManaged(); @@ -176,8 +181,8 @@ public class EmoneyClient implements Cloneable { .header("X-Request-Id", "1") .header("EM-Sign", EncryptUtils.getEMSign(content, "POST", RELOGIN_X_PROTOCOL_ID)) .header("Authorization", token) - .header("X-Android-Agent", emoneyRequestConfig.getXAndroidAgent()) - .header("Emapp-ViewMode", emoneyRequestConfig.getEmappViewMode()); + .header("X-Android-Agent", requestInfo.getXAndroidAgent()) + .header("Emapp-ViewMode", requestInfo.getEmappViewMode()); Request request = requestBuilder.build(); @@ -223,7 +228,7 @@ public class EmoneyClient implements Cloneable { private static Boolean login(ObjectNode formObject) { try { //OkHttpClient okHttpClient = new OkHttpClient(); - EmoneyRequestConfig emoneyRequestConfig = getEmoneyRequestConfig(); + RequestInfo requestInfo = getDefaultRequestInfo(); OkHttpClient okHttpClient = OkHttpClientProvider.getInstance(); MediaType type = MediaType.parse("application/json"); //type.charset(StandardCharsets.UTF_8); @@ -243,9 +248,9 @@ public class EmoneyClient implements Cloneable { .header("X-Request-Id", "null") .header("EM-Sign", EncryptUtils.getEMSign(content, "POST", LOGIN_X_PROTOCOL_ID)) .header("Authorization", "") - .header("X-Android-Agent", emoneyRequestConfig.getXAndroidAgent()) - .header("Emapp-ViewMode", emoneyRequestConfig.getEmappViewMode()); - //.header("User-Agent", emoneyRequestConfig.getOkHttpUserAgent()) + .header("X-Android-Agent", requestInfo.getXAndroidAgent()) + .header("Emapp-ViewMode", requestInfo.getEmappViewMode()); + //.header("User-Agent", requestInfo.getOkHttpUserAgent()) Request request = requestBuilder.build(); @@ -257,14 +262,14 @@ public class EmoneyClient implements Cloneable { Integer code = loginResult.get("result").get("code").asInt(); if (code == 0) { - emoneyRequestConfig + requestInfo .setAuthorization( loginResult .get("detail").get("token").asText()) .setUid( loginResult .get("detail").get("uid").asInt()) - .saveOrUpdate(); + .insertOrUpdate(); log.info("执行 emoney LOGIN 成功"); return true; } @@ -297,8 +302,8 @@ public class EmoneyClient implements Cloneable { throw new EmoneyIllegalRequestParamException("执行 emoney 请求错误,Protocol id 不能为 null!", new IllegalArgumentException()); } - EmoneyRequestConfig emoneyRequestConfig = getEmoneyRequestConfig(); - if (StringUtils.isBlank(emoneyRequestConfig.getAuthorization())) { + RequestInfo requestInfo = getDefaultRequestInfo(); + if (StringUtils.isBlank(requestInfo.getAuthorization())) { throw new EmoneyIllegalRequestParamException("执行 emoney 请求错误,Authorization 为空,是否未登录?", new IllegalArgumentException()); } @@ -325,9 +330,9 @@ public class EmoneyClient implements Cloneable { .header("X-Protocol-Id", xProtocolId.toString()) .header("X-Request-Id", xRequestId == null ? "null" : xRequestId.toString()) .header("EM-Sign", EncryptUtils.getEMSign(content, "POST", xProtocolId.toString())) - .header("Authorization", emoneyRequestConfig.getAuthorization()) - .header("X-Android-Agent", emoneyRequestConfig.getXAndroidAgent()) - .header("Emapp-ViewMode", emoneyRequestConfig.getEmappViewMode()) + .header("Authorization", requestInfo.getAuthorization()) + .header("X-Android-Agent", requestInfo.getXAndroidAgent()) + .header("Emapp-ViewMode", requestInfo.getEmappViewMode()) ; Request request = requestBuilder.build(); diff --git a/src/main/java/quant/rich/emoney/client/OkHttpClientProvider.java b/src/main/java/quant/rich/emoney/client/OkHttpClientProvider.java index 739d248..d617c44 100644 --- a/src/main/java/quant/rich/emoney/client/OkHttpClientProvider.java +++ b/src/main/java/quant/rich/emoney/client/OkHttpClientProvider.java @@ -23,7 +23,8 @@ import okhttp3.ResponseBody; import okio.BufferedSource; import okio.GzipSource; import okio.Okio; -import quant.rich.emoney.entity.config.ProxyConfig; +import quant.rich.emoney.entity.sqlite.ProxySetting; +import quant.rich.emoney.service.sqlite.ProxySettingService; import quant.rich.emoney.util.SpringContextHolder; /** @@ -33,17 +34,18 @@ import quant.rich.emoney.util.SpringContextHolder; */ public class OkHttpClientProvider { - private static volatile ProxyConfig proxyConfig; + private static volatile ProxySettingService proxySettingService; - private static ProxyConfig getProxyConfig() { - if (proxyConfig == null) { + private static ProxySetting getDefaultProxySetting() { + if (proxySettingService == null) { synchronized (OkHttpClientProvider.class) { - if (proxyConfig == null) { - proxyConfig = SpringContextHolder.getBean(ProxyConfig.class); - } + proxySettingService = SpringContextHolder.getBean(ProxySettingService.class); } } - return proxyConfig; + if (proxySettingService == null) { + return null; + } + return proxySettingService.getDefaultProxySetting(); } /** @@ -60,10 +62,10 @@ public class OkHttpClientProvider { * @return */ public static OkHttpClient getInstance(Consumer builderConsumer) { - ProxyConfig proxyConfig = getProxyConfig(); + ProxySetting proxySetting = getDefaultProxySetting(); return getInstance( - proxyConfig.getProxy(), - proxyConfig.getIgnoreHttpsVerification(), + proxySetting.getProxy(), + proxySetting.getIgnoreHttpsVerification(), builderConsumer); } diff --git a/src/main/java/quant/rich/emoney/component/CallerLockAspect.java b/src/main/java/quant/rich/emoney/component/CallerLockAspect.java index 1ca3752..c7bf8b0 100644 --- a/src/main/java/quant/rich/emoney/component/CallerLockAspect.java +++ b/src/main/java/quant/rich/emoney/component/CallerLockAspect.java @@ -8,9 +8,13 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; -import quant.rich.emoney.annotation.LockByCaller; import quant.rich.emoney.util.CallerLockUtil; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.reflect.Method; import java.util.concurrent.locks.ReentrantLock; @@ -20,7 +24,7 @@ public class CallerLockAspect { private final SpelExpressionParser parser = new SpelExpressionParser(); - @Around("@annotation(me.qwq.emoney.annotation.LockByCaller)") + @Around("@annotation(quant.rich.emoney.component.CallerLockAspect.LockByCaller)") public Object around(ProceedingJoinPoint pjp) throws Throwable { MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); @@ -52,4 +56,25 @@ public class CallerLockAspect { lock.unlock(); } } + + /** + * 在方法上添加此注解,可针对调用方加锁,即:
+ * 调用方法为 A 的,多次从 A 调用则加锁,从 B 调用时不受影响
+ * 需要开启 AOP:在任意配置类上增加注解:@EnableAspectJAutoProxy + * @see CallerLockAspect + */ + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @Documented + public static @interface LockByCaller { + /** + * 可选参数,用于 SpEL 表达式获取 key + * 例如:
    + *
  • @LockByCaller(key = "#userId")
  • + *
  • @LockByCaller(key = "#userId + ':' + #userName")
  • + *
+ * 当不指定时,不校验参数,单纯校验 Caller + */ + String key() default ""; + } } \ No newline at end of file diff --git a/src/main/java/quant/rich/emoney/component/EmoneyAutoPlatformExceptionHandler.java b/src/main/java/quant/rich/emoney/component/EmoneyAutoPlatformExceptionHandler.java index 7f1e8cf..cba5cc7 100644 --- a/src/main/java/quant/rich/emoney/component/EmoneyAutoPlatformExceptionHandler.java +++ b/src/main/java/quant/rich/emoney/component/EmoneyAutoPlatformExceptionHandler.java @@ -19,6 +19,7 @@ import jakarta.validation.ConstraintViolationException; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.jdbc.UncategorizedSQLException; import org.springframework.validation.BindException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -94,15 +95,7 @@ public class EmoneyAutoPlatformExceptionHandler { if (ex instanceof PageNotFoundException) { throw (PageNotFoundException) ex; } - String message = null; - if (ex.getMessage() != null) { - message = ex.getMessage(); - } - else if (ex.getCause() != null) { - message = ex.getCause().getMessage(); - } - ex.printStackTrace(); - log.warn("Resolved exception {}", message); + log.warn("Resolved exception {}", ex); log.warn(httpServletRequestToString(request)); return bodyOrPage(HttpStatus.INTERNAL_SERVER_ERROR, ex); } @@ -161,16 +154,9 @@ public class EmoneyAutoPlatformExceptionHandler { } private R bodyOrPage(HttpStatus httpStatus, Exception ex) { - boolean isPage = true; - String message = null; - if (ex instanceof RException || - ex instanceof LoginException) { - isPage = false; - message = ex.getMessage(); - } - else { - isPage = isPage(); - } + String message = getMessage(ex, ex instanceof UncategorizedSQLException); + boolean isPage = (ex instanceof RException || ex instanceof LoginException) ? + false : isPage(); if (isPage) { if (ex instanceof NoResourceFoundException nrfe) { if (StringUtils.isNotEmpty(nrfe.getMessage()) @@ -182,7 +168,7 @@ public class EmoneyAutoPlatformExceptionHandler { } throw ex == null ? new RuntimeException("Page exception raised") : new RuntimeException(ex); } - R r = message != null ? + R r = StringUtils.isNotEmpty(message) ? R.status(httpStatus).setMessage(message).setData(message) : R.status(httpStatus); return r; @@ -217,4 +203,19 @@ public class EmoneyAutoPlatformExceptionHandler { return sb.toString(); } + + public String getMessage(Throwable e, boolean causeMessageFirst) { + String causeMessage = null; + if (e.getCause() != null && e.getCause() != e) { + if (causeMessageFirst) { + return getMessage(e.getCause(), true); + } + else { + causeMessage = e.getCause().getMessage(); + } + } + String mainMessage = e.getMessage(); + if (mainMessage != null) return mainMessage; + return causeMessage; + } } diff --git a/src/main/java/quant/rich/emoney/component/RequireAuthAndProxyAspect.java b/src/main/java/quant/rich/emoney/component/RequireAuthAndProxyAspect.java new file mode 100644 index 0000000..5769f83 --- /dev/null +++ b/src/main/java/quant/rich/emoney/component/RequireAuthAndProxyAspect.java @@ -0,0 +1,81 @@ +package quant.rich.emoney.component; + +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.*; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import quant.rich.emoney.client.EmoneyClient; +import quant.rich.emoney.entity.sqlite.ProxySetting; +import quant.rich.emoney.entity.sqlite.RequestInfo; +import quant.rich.emoney.service.sqlite.ProxySettingService; +import quant.rich.emoney.service.sqlite.RequestInfoService; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; + + +@Aspect +@Component +public class RequireAuthAndProxyAspect { + + @Autowired + RequestInfoService requestInfoService; + + @Autowired + ProxySettingService proxySettingService; + + @Around("@annotation(quant.rich.emoney.component.RequireAuthAndProxyAspect.RequireAuthAndProxy)") + public Object around(ProceedingJoinPoint pjp) throws Throwable { + + ProxySetting defualtProxySetting = proxySettingService.getDefaultProxySetting(); + + if (defualtProxySetting == null) { + throw new RuntimeException("需要配置默认代理设置"); + } + + RequestInfo defaultRequestInfo = requestInfoService.getDefaultRequestInfo(); + + if (defaultRequestInfo == null) { + throw new RuntimeException("需要配置默认请求信息"); + } + + MethodSignature signature = (MethodSignature) pjp.getSignature(); + Method method = signature.getMethod(); + RequireAuthAndProxy annotation = method.getAnnotation(RequireAuthAndProxy.class); + + if (StringUtils.isBlank(defaultRequestInfo.getAuthorization())) { + if (!annotation.autoLogin()) { + throw new RuntimeException("需要手动为请求信息鉴权"); + } + if (!EmoneyClient.loginWithManaged()) { + throw new RuntimeException("鉴权登录失败"); + } + } + + return pjp.proceed(); + } + + /** + * 在方法上添加此注解,则进入该方法前先校验 defaultRequestInfo 已鉴权、代理已配置,否则不允许进入方法 + *

需要开启 AOP:在任意配置类上增加注解:@EnableAspectJAutoProxy + * @see RequireAuthAndProxyAspect + */ + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @Documented + public static @interface RequireAuthAndProxy { + /** + * 当存在默认请求配置但未鉴权时,是否自动鉴权 + * @return + */ + boolean autoLogin() default false; + } + +} \ No newline at end of file diff --git a/src/main/java/quant/rich/emoney/config/ConfigAutoRegistrar.java b/src/main/java/quant/rich/emoney/config/ConfigAutoRegistrar.java index c8302e8..eb21706 100644 --- a/src/main/java/quant/rich/emoney/config/ConfigAutoRegistrar.java +++ b/src/main/java/quant/rich/emoney/config/ConfigAutoRegistrar.java @@ -13,7 +13,9 @@ import quant.rich.emoney.interfaces.ConfigInfo; import quant.rich.emoney.interfaces.IConfig; /** - * 实现自动化注册 Config + * 实现自动化注册 Config

+ * Config 放在 quant.rich.emoney.entity.config 包下并且必须实现 IConfig 接口 + * @see quant.rich.emoney.interfaces.IConfig */ @Slf4j @DependsOn("configService") @@ -28,6 +30,7 @@ public class ConfigAutoRegistrar implements BeanDefinitionRegistryPostProcessor scanner.findCandidateComponents("quant.rich.emoney.entity.config").forEach(beanDefinition -> { String className = beanDefinition.getBeanClassName(); try { + // 确保其 field 规则与 configService 内 field 生成规则一致,即: // 如果 @ConfigInfo 指定了 field 的,使用该 field + "Config" // 作为 beanName,否则使用首字母小写的 simpleClassName 作为 @@ -37,18 +40,18 @@ public class ConfigAutoRegistrar implements BeanDefinitionRegistryPostProcessor + clazz.getSimpleName().substring(1); if (!IConfig.class.isAssignableFrom(clazz)) { - log.warn("Config {} does not implement IConfig, ignore", beanName); + log.error("Ignore config class {} which is not implemented IConfig interface", beanName); return; } if (!beanName.endsWith("Config")) { - log.warn("Config {}'s simple class name does not end with \"Config\", ignore", beanName); + log.error("Ignore config class {} which class name is not end with \"Config\"", beanName); return; } ConfigInfo info = clazz.getAnnotation(ConfigInfo.class); if (info == null) { - log.warn("Config {} does not have @ConfigInfo annotation, ignore", clazz.getName()); + log.error("Ignore config class {} which is not annotated with @ConfigInfo", clazz.getName()); return; } if (StringUtils.isNotBlank(info.field())) { @@ -58,15 +61,16 @@ public class ConfigAutoRegistrar implements BeanDefinitionRegistryPostProcessor BeanDefinitionBuilder factoryBean = BeanDefinitionBuilder .genericBeanDefinition(ConfigServiceFactoryBean.class) .addConstructorArgValue(clazz); - // 注意此处注册 factoryBean 不意味着 FactoryBean.getObject() 方法立即被执行, - // Spring 管理的 Bean 默认在其被使用时才创建,所以如果 getObject() 调用一些方 - // 法,这些方法会在初次使用 Bean 时才被创建。如果这些方法对于启动过程很重要, - // 需要在对应 Config(Bean) 上加上 @Bean 和 @Lazy(false) 注解,确保一旦准备好 - // 相应的 Bean 就会被创建。 + /** + * 注意此处通过 factoryBean 创建 bean 不意味着 FactoryBean.getObject() 方法 + * 会被立即执行。Spring 默认会在 bean 被使用时才会创建。如果该 bean 对程序 + * 启动很重要,需要立即创建的,需在其类上添加 @Bean 及 @Lazy(false) 注解, + * 确保一旦准备好,相应的 bean 就会被创建 + */ registry.registerBeanDefinition(beanName, factoryBean.getBeanDefinition()); - log.info("Add config {} to bean register", beanName); + log.info("Add config class {} to bean register", beanName); } catch (ClassNotFoundException e) { - throw new RuntimeException("Failed to load class: " + className, e); + throw new RuntimeException("Cannot found specific config class: " + className, e); } }); } diff --git a/src/main/java/quant/rich/emoney/config/ConfigServiceFactoryBean.java b/src/main/java/quant/rich/emoney/config/ConfigServiceFactoryBean.java index 9ecb298..88b7fa1 100644 --- a/src/main/java/quant/rich/emoney/config/ConfigServiceFactoryBean.java +++ b/src/main/java/quant/rich/emoney/config/ConfigServiceFactoryBean.java @@ -9,8 +9,9 @@ import quant.rich.emoney.interfaces.IConfig; import quant.rich.emoney.service.ConfigService; /** - * 实现配置项自动载入 - * @param + * 配置类工厂 + * @param 配置类 + * */ @Slf4j public class ConfigServiceFactoryBean> implements FactoryBean, BeanNameAware { @@ -37,24 +38,19 @@ public class ConfigServiceFactoryBean> implements FactoryBe @Override public T getObject() throws Exception { ConstructionGuard.enter(targetClass); - boolean success = true; try { T bean = configService.getConfig(targetClass); beanFactory.autowireBean(bean); beanFactory.initializeBean(bean, beanName); - configService.saveOrUpdate(bean); + //configService.saveOrUpdate(bean); return bean; } catch (Exception e) { - log.error("Fail to load config: " + targetClass.getName(), e); - success = false; + log.error("无法载入配置类: " + targetClass.getName(), e); throw e; } finally { ConstructionGuard.exit(targetClass); - if (success) { - log.debug("getObject() for {} success", targetClass.toString()); - } } } diff --git a/src/main/java/quant/rich/emoney/config/ConstructionGuard.java b/src/main/java/quant/rich/emoney/config/ConstructionGuard.java index ce7de19..876f851 100644 --- a/src/main/java/quant/rich/emoney/config/ConstructionGuard.java +++ b/src/main/java/quant/rich/emoney/config/ConstructionGuard.java @@ -7,6 +7,25 @@ import org.springframework.beans.factory.BeanCreationException; import lombok.extern.slf4j.Slf4j; +/** + * To prevent bean cyclic instantiation through BeanFactory:

+ * The reason is that some configuration classes are deserialized via json. + * During this process, the default behavior is to call their no-argument + * constructors. Inside these constructors, it’s likely that static methods + * from SpringContextHolder are invoked to obtain other configuration classes. + * However, those other configuration classes may in turn call the same static + * methods of SpringContextHolder to obtain yet other configuration classes.

+ * At this point, since the configuration class is still {@code null}, Spring attempts + * once again to produce the instance of this class via BeanFactory, leading to + * a cyclic instantiation process and eventually causing a stack overflow.

+ * Since SpringContextHolder actually operates outside of Spring’s management lifecycle, + * it is difficult to detect this issue at runtime. Therefore, this class should + * be used within the Factory’s getObject method, where an exception is thrown + * if cyclic instantiation occurs. The implementation of this class is based on ThreadLocal. + * + * @author Doghole + * @see ConfigServiceFactoryBean + */ @Slf4j public class ConstructionGuard { private static final ThreadLocal>> constructing = ThreadLocal.withInitial(HashSet::new); @@ -16,7 +35,6 @@ public class ConstructionGuard { } public static void enter(Class clazz) { - log.debug("Enter construction for {}", clazz.toString()); if (isConstructing(clazz)) { StringBuilder sb = new StringBuilder(); sb.append("Class ") @@ -31,6 +49,5 @@ public class ConstructionGuard { public static void exit(Class clazz) { constructing.get().remove(clazz); - log.debug("Exit construction for {}", clazz.toString()); } } \ No newline at end of file diff --git a/src/main/java/quant/rich/emoney/config/SqliteMybatisConfig.java b/src/main/java/quant/rich/emoney/config/SqliteMybatisConfig.java index 6192d25..ffb6489 100644 --- a/src/main/java/quant/rich/emoney/config/SqliteMybatisConfig.java +++ b/src/main/java/quant/rich/emoney/config/SqliteMybatisConfig.java @@ -4,8 +4,6 @@ import java.io.File; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; - import javax.sql.DataSource; import org.apache.ibatis.session.SqlSessionFactory; @@ -50,7 +48,10 @@ public class SqliteMybatisConfig { String filePath = hikariDataSource.getJdbcUrl(); if (filePath == null || !filePath.startsWith("jdbc:sqlite:")) { - log.warn("无法在 SQLite HikariDataSource 中找到合法 SQLite JDBC url, 数据库可能会加载失败。获取到的 jdbc-url: {}", filePath); + log.warn( + "无法在 SQLite HikariDataSource 中找到合法 SQLite JDBC url, " + + "数据库可能会加载失败。合法的 url 需在 application.yml(properties) " + + "中配置,以 jdbc:sqlite: 开头。当前获取到的 jdbc-url: {}", filePath); return; } filePath = filePath.substring("jdbc:sqlite:".length()).trim(); diff --git a/src/main/java/quant/rich/emoney/controller/IndexControllerV1.java b/src/main/java/quant/rich/emoney/controller/IndexControllerV1.java index c856ae7..1c532be 100644 --- a/src/main/java/quant/rich/emoney/controller/IndexControllerV1.java +++ b/src/main/java/quant/rich/emoney/controller/IndexControllerV1.java @@ -52,7 +52,8 @@ public class IndexControllerV1 extends BaseController { String username, String password, String newPassword, - String email) { + String email, + String apiToken) { if (EncryptUtils.passwordIsNotEmpty(newPassword)) { if (!platformConfig.getPassword().equals(password)) { @@ -66,7 +67,7 @@ public class IndexControllerV1 extends BaseController { else { throw RException.badRequest("用户名不能为空"); } - platformConfig.setEmail(email); + platformConfig.setEmail(email).setApiToken(apiToken); return R.judge(() -> { if (configService.saveOrUpdate(platformConfig)) { authService.setLogin(username, platformConfig.getPassword()); diff --git a/src/main/java/quant/rich/emoney/controller/LoginControllerV1.java b/src/main/java/quant/rich/emoney/controller/LoginControllerV1.java index 84e48eb..a56518d 100644 --- a/src/main/java/quant/rich/emoney/controller/LoginControllerV1.java +++ b/src/main/java/quant/rich/emoney/controller/LoginControllerV1.java @@ -18,6 +18,7 @@ import quant.rich.emoney.pojo.dto.R; import quant.rich.emoney.service.AuthService; import quant.rich.emoney.service.ConfigService; import quant.rich.emoney.util.EncryptUtils; +import quant.rich.emoney.util.TextUtils; @Controller @RequestMapping("/admin/v1") @@ -84,7 +85,11 @@ public class LoginControllerV1 extends BaseController { if (StringUtils.isAnyBlank(username) || !EncryptUtils.passwordIsNotEmpty(password)) { throw new LoginException("用户名和密码不能为空"); } - platformConfig.setUsername(username).setPassword(password).setIsInited(true); + platformConfig + .setUsername(username) + .setPassword(password) + .setIsInited(true) + .setApiToken(TextUtils.randomString(16)); boolean success = configService.saveOrUpdate(platformConfig); if (!success) { throw new LoginException("无法配置用户名和密码,请检查"); diff --git a/src/main/java/quant/rich/emoney/controller/api/ProtoDecodeControllerV1.java b/src/main/java/quant/rich/emoney/controller/api/ProtoDecodeControllerV1.java index fd1bfe0..bfec8f9 100644 --- a/src/main/java/quant/rich/emoney/controller/api/ProtoDecodeControllerV1.java +++ b/src/main/java/quant/rich/emoney/controller/api/ProtoDecodeControllerV1.java @@ -27,8 +27,8 @@ import jakarta.annotation.PostConstruct; import org.apache.commons.lang3.StringUtils; import org.reflections.Reflections; -import lombok.AllArgsConstructor; import lombok.Data; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import nano.BaseResponse.Base_Response; import quant.rich.emoney.annotation.ResponseDecodeExtension; @@ -37,7 +37,6 @@ import quant.rich.emoney.exception.RException; import quant.rich.emoney.pojo.dto.EmoneyConvertResult; import quant.rich.emoney.pojo.dto.EmoneyProtobufBody; import quant.rich.emoney.service.sqlite.ProtocolMatchService; -import quant.rich.emoney.service.sqlite.StrategyAndPoolService; import quant.rich.emoney.util.SpringBeanDetector; import quant.rich.emoney.util.SpringContextHolder; @@ -52,20 +51,18 @@ public class ProtoDecodeControllerV1 { @Autowired ProtocolMatchService protocolMatchService; - @Autowired - StrategyAndPoolService strategyAndPoolService; - @Autowired Reflections reflections; Map> responseDecodeExtensions = new HashMap>(); @Data - @AllArgsConstructor + @RequiredArgsConstructor private static class MethodInfo { - Method method; - Class declaringClass; - Integer order; + final Method method; + final Class declaringClass; + final Integer order; + Object instance; } @PostConstruct @@ -112,7 +109,7 @@ public class ProtoDecodeControllerV1 { for (List list : responseDecodeExtensions.values()) { list.sort(Comparator.comparingInt(info -> info.getOrder())); } - log.debug("共载入 {} 个 ProtocolID 的 {} 个方法", + log.debug("ResponseDecodeExtension: 共载入 {} 个 ProtocolID 的 {} 个方法", responseDecodeExtensions.keySet().size(), responseDecodeExtensions.values().size()); } @@ -274,22 +271,24 @@ public class ProtoDecodeControllerV1 { JsonNode jo = new ObjectMapper().valueToTree(nano); - // 协议 9400 则更新到 StrategyAndPool 里面去 - if (protocolId == 9400) { - strategyAndPoolService.updateByQueryResponse(jo); - } - // 查找 ResponseDecodeExtension List methodInfos = responseDecodeExtensions.get(protocolId.toString()); if (methodInfos != null) { for (MethodInfo methodInfo : methodInfos) { - Object instance = null; - if (methodInfo.getDeclaringClass() != null) { - // 获取 spring 管理的实例类 - instance = SpringContextHolder.getBean(methodInfo.getDeclaringClass()); + if (methodInfo.getInstance() != null) { + // instance 不为 null 则说明是已经取到的 spring bean, 直接调用 + methodInfo.getMethod().invoke(methodInfo.getInstance(), jo); + } + else if (methodInfo.getDeclaringClass() != null) { + // 获取 spring 管理的实例类 + Object instance = SpringContextHolder.getBean(methodInfo.getDeclaringClass()); + methodInfo.getMethod().invoke(instance, jo); + methodInfo.setInstance(instance); + } + else { + // 静态方法直接 invoke + methodInfo.getMethod().invoke(null, jo); } - // invoke - methodInfo.getMethod().invoke(instance, jo); } } diff --git a/src/main/java/quant/rich/emoney/controller/common/BaseController.java b/src/main/java/quant/rich/emoney/controller/common/BaseController.java index affe3b4..46d12bb 100644 --- a/src/main/java/quant/rich/emoney/controller/common/BaseController.java +++ b/src/main/java/quant/rich/emoney/controller/common/BaseController.java @@ -3,12 +3,9 @@ package quant.rich.emoney.controller.common; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; - import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; -import quant.rich.emoney.pojo.dto.R; import quant.rich.emoney.service.AuthService; @Controller diff --git a/src/main/java/quant/rich/emoney/controller/common/ServiceController.java b/src/main/java/quant/rich/emoney/controller/common/ServiceController.java new file mode 100644 index 0000000..eabec95 --- /dev/null +++ b/src/main/java/quant/rich/emoney/controller/common/ServiceController.java @@ -0,0 +1,122 @@ +package quant.rich.emoney.controller.common; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.core.ResolvableType; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import quant.rich.emoney.exception.RException; +import quant.rich.emoney.pojo.dto.LayPageReq; +import quant.rich.emoney.pojo.dto.LayPageResp; +import quant.rich.emoney.pojo.dto.R; + +/** + * 在控制器中提供实体类的 service

+ * + * 继承后,可直接通过 {@code thisType} 和 {@code thisService} 获取实体类型和对应的服务实例

+ * 也获得部分能力,但控制方法及 Mapping 路径要继承后自己写。

+ * 可获得的能力: + *

    + *
  • list + *
  • getOne + *
  • delete + *
      + * + * @param 实体类型 + */ +@Slf4j +public abstract class ServiceController extends BaseController { + + @Autowired + private ApplicationContext ctx; + + protected IService thisService; + + protected Class thisType; + + @PostConstruct + void init() { + @SuppressWarnings("rawtypes") + Map beans = ctx.getBeansOfType(IService.class); + ResolvableType thisType = ResolvableType.forClass(this.getClass()).as(ServiceController.class); + @SuppressWarnings("unchecked") + // 获取本类的实体类 + Class clazz = (Class) thisType.getGeneric(0).resolve(); + this.thisType = clazz; + for (IService service : beans.values()) { + ResolvableType type = ResolvableType.forClass(service.getClass()).as(IService.class); + Class entityType = type.getGeneric(0).resolve(); + if (entityType == clazz) { + this.thisService = service; + } + } + if (thisService == null) { + log.error("获取本例实体类服务失败,请检查"); + } + } + + @SuppressWarnings("unchecked") + protected IService getThisService() { + return (IService)this.thisService; + } + + /** + * 返回实例类列表 + * @param pageReq + * @return + */ + protected LayPageResp list(LayPageReq pageReq) { + Page planPage = getThisService().page(pageReq); + return new LayPageResp<>(planPage); + } + + /** + * 根据 id 获取实例化对象。如果 id 为空则返回通过默认无参构造器构造的新实例化对象 + * @param id + * @return + */ + protected R getOne(Serializable id) { + // id 为空,返回一个新实例化对象 + if (id == null) { + try { + return R.ok(thisType.getConstructor().newInstance()); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException | NoSuchMethodException | SecurityException e) { + final String s = "根据默认构造器创建新实例化对象失败"; + log.error(s, e); + throw RException.internalServerError(s); + } + } + + // 否则从数据库取 + T exist = getThisService().getById(id); + return R.judge(exist != null, exist, "无法找到对应 ID 的 ProxySetting"); + } + + /** + * 保存 + * @param object + * @return + */ + protected R save(T object) { + return + R.judge( + () -> getThisService().saveOrUpdate(object), + "新增或保存失败"); + } + + /** + * 删除 + * @param id + * @return + */ + protected R delete(Serializable id) { + return R.judge(getThisService().removeById(id), "删除失败,是否已删除?"); + } + +} diff --git a/src/main/java/quant/rich/emoney/controller/common/UpdateBoolController.java b/src/main/java/quant/rich/emoney/controller/common/UpdateBoolController.java deleted file mode 100644 index 03174ee..0000000 --- a/src/main/java/quant/rich/emoney/controller/common/UpdateBoolController.java +++ /dev/null @@ -1,91 +0,0 @@ -package quant.rich.emoney.controller.common; - -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.core.ResolvableType; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; -import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; -import com.baomidou.mybatisplus.core.metadata.TableInfo; -import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; -import com.baomidou.mybatisplus.extension.service.IService; -import com.fasterxml.jackson.databind.ObjectMapper; - -import jakarta.annotation.PostConstruct; -import lombok.extern.slf4j.Slf4j; -import quant.rich.emoney.exception.RException; -import quant.rich.emoney.pojo.dto.R; - -@Slf4j -public abstract class UpdateBoolController extends BaseController { - - ObjectMapper mapper = new ObjectMapper(); - - @Autowired - private ApplicationContext ctx; - - private Map, IService> serviceMap; - - @PostConstruct - void init() { - serviceMap = new HashMap<>(); - @SuppressWarnings("rawtypes") - Map beans = ctx.getBeansOfType(IService.class); - for (IService service : beans.values()) { - ResolvableType type = ResolvableType.forClass(service.getClass()).as(IService.class); - Class entityType = type.getGeneric(0).resolve(); - if (entityType != null) { - serviceMap.put(entityType, service); - } - } - } - - @SuppressWarnings("unchecked") - public IService getService(Class entityClass) { - return (IService) serviceMap.get(entityClass); - } - - @PostMapping("/updateBool") - @ResponseBody - protected - R updateBool(String id, String field, Boolean value) { - - - ResolvableType type = ResolvableType.forClass(this.getClass()).as(UpdateBoolController.class); - @SuppressWarnings("unchecked") - Class clazz = (Class) type.getGeneric(0).resolve(); - - TableInfo tableInfo = TableInfoHelper.getTableInfo(clazz); - Object converted = mapper.convertValue(id, tableInfo.getKeyType()); - - // 获取 Service - IService s = getService((Class) clazz); - - try { - String idField = tableInfo.getKeyColumn(); - Field declaredField = clazz.getDeclaredField(field); - Optional fieldInfo = tableInfo.getFieldList().stream() - .filter(f -> f.getProperty().equals(field)) - .findFirst(); - if (declaredField.getType().equals(Boolean.class)) { - return R.judge(s.update( - new UpdateWrapper() - .set(fieldInfo.get().getColumn(), value) - .eq(idField, converted) - ), "更新失败,请查看日志"); - } - } - catch (Exception e) { - log.error("update bool failed", e); - } - throw RException.badRequest().setLogRequest(true); - } - -} diff --git a/src/main/java/quant/rich/emoney/controller/common/UpdateBoolServiceController.java b/src/main/java/quant/rich/emoney/controller/common/UpdateBoolServiceController.java new file mode 100644 index 0000000..dd3a34a --- /dev/null +++ b/src/main/java/quant/rich/emoney/controller/common/UpdateBoolServiceController.java @@ -0,0 +1,86 @@ +package quant.rich.emoney.controller.common; + +import java.lang.reflect.Field; +import java.util.Optional; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.extern.slf4j.Slf4j; +import quant.rich.emoney.exception.RException; +import quant.rich.emoney.pojo.dto.R; + +/** + * 更新实体类中 Boolean 字段的抽象控制器,一般用于实体类中包含 Boolean 字段的前端更新 + *

      + * 前端实体类数据列表中,常有 CheckBox 或 Switch 等控件,希望通过点击数据表中的控件直接修改行对象 + * Boolean 值的,可用该方法。需要引入功能的需 extends 本类,如对 {@code Plan} 生效,则可在其控制器 + * {@code PlanController} 中:

      + * + * PlanController extends UpdateBoolController<Plan> + * @param 实体类型 + * @see #updateBool(String, String, Boolean) + */ +@Slf4j +public abstract class UpdateBoolServiceController extends ServiceController { + + protected ObjectMapper mapper = new ObjectMapper(); + + /** + * 更新布尔值主方法,以 form 形式 POST,uri: /updateBool,表单字段名需与该方法参数名一致 + * @param id 欲修改的实体类的实例化对象的主键值 + * @param field 欲修改的实体类的实例化对象的布尔字段名 + * @param value 需要修改为的布尔值 + * @return + */ + @PostMapping("/updateBool") + @ResponseBody + protected + R updateBool(String id, String field, Boolean value) { + + + // 获取表信息 + TableInfo tableInfo = TableInfoHelper.getTableInfo(thisType); + Object converted = mapper.convertValue(id, tableInfo.getKeyType()); + + // 获取 Service + + try { + // 获取主键名 + String idField = tableInfo.getKeyColumn(); + // 获取指定布尔字段的字段信息 + Field declaredField = thisType.getDeclaredField(field); + // 获取指定布尔字段在数据表中的映射字段信息 + Optional fieldInfo = tableInfo.getFieldList().stream() + .filter(f -> f.getProperty().equals(field)) + .findFirst(); + + if (fieldInfo.isEmpty()) { + throw RException.badRequest("无法根据 field: " + field + " 找到类内字段信息"); + } + + if (declaredField.getType().equals(Boolean.class) + || declaredField.getType().equals(boolean.class) + ) { + return R.judge(getThisService().update( + new UpdateWrapper() + .set(fieldInfo.get().getColumn(), value) + .eq(idField, converted) + ), "更新失败,请查看日志"); + } + else { + throw RException.badRequest("field: " + field + " 不为布尔值类型字段"); + } + } + catch (NoSuchFieldException | SecurityException e) { + throw RException.badRequest("获取字段 " + field + " 错误"); + } + } + +} diff --git a/src/main/java/quant/rich/emoney/controller/config/ProxyConfigControllerV1.java b/src/main/java/quant/rich/emoney/controller/config/ProxyConfigControllerV1.java deleted file mode 100644 index a573dc2..0000000 --- a/src/main/java/quant/rich/emoney/controller/config/ProxyConfigControllerV1.java +++ /dev/null @@ -1,28 +0,0 @@ -package quant.rich.emoney.controller.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -import lombok.extern.slf4j.Slf4j; -import quant.rich.emoney.controller.common.BaseController; -import quant.rich.emoney.entity.config.ProxyConfig; -import quant.rich.emoney.pojo.dto.R; - -@Slf4j -@Controller -@RequestMapping("/admin/v1/config/proxy") -public class ProxyConfigControllerV1 extends BaseController { - - @Autowired - ProxyConfig proxyConfig; - - - @GetMapping("/refreshIpThroughProxy") - @ResponseBody - public R refreshIpThroughProxy() { - return R.ok(proxyConfig.refreshIpThroughProxy()); - } -} diff --git a/src/main/java/quant/rich/emoney/controller/manage/IndexInfoControllerV1.java b/src/main/java/quant/rich/emoney/controller/manage/IndexInfoControllerV1.java index 301dc82..d702513 100644 --- a/src/main/java/quant/rich/emoney/controller/manage/IndexInfoControllerV1.java +++ b/src/main/java/quant/rich/emoney/controller/manage/IndexInfoControllerV1.java @@ -1,9 +1,9 @@ package quant.rich.emoney.controller.manage; -import java.io.IOException; import java.util.HashMap; import java.util.Map; +import org.springframework.aop.framework.AopProxyUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @@ -25,7 +25,7 @@ import quant.rich.emoney.service.IndexDetailService; public class IndexInfoControllerV1 extends BaseController { @Autowired - IndexInfoConfig indexInfo; + IndexInfoConfig indexInfoConfig; @Autowired IndexDetailService indexDetailService; @@ -35,6 +35,11 @@ public class IndexInfoControllerV1 extends BaseController { return "/admin/v1/manage/indexInfo/index"; } + /** + * 获取指标详情解释 + * @param indexCode + * @return + */ @GetMapping("/getIndexDetail") @ResponseBody public R getIndexDetail(String indexCode) { @@ -43,6 +48,11 @@ public class IndexInfoControllerV1 extends BaseController { indexDetailService.getIndexDetail(indexCode)); } + /** + * 强制刷新并获取指标详情解释 + * @param indexCode + * @return + */ @GetMapping("/forceRefreshAndGetIndexDetail") @ResponseBody public R forceRefreshAndGetIndexDetail(String indexCode) { @@ -51,21 +61,14 @@ public class IndexInfoControllerV1 extends BaseController { indexDetailService.forceRefreshAndGetIndexDetail(indexCode)); } - @GetMapping("/configIndOnline") - @ResponseBody - public R configIndOnline(String url) throws IOException { - - //return R.judge(() -> indexInfo.getOnlineConfigByUrl(url)); - return R.ok(indexInfo.getConfigIndOnline()); - } - @GetMapping("/getFields") @ResponseBody public R getFields(@RequestParam("fields") String[] fields) { if (fields == null || fields.length == 0) { - return R.ok(indexInfo); + return R.ok(indexInfoConfig); } - ObjectNode indexInfoJson = new ObjectMapper().valueToTree(indexInfo); + Object indexInfoConfigWithoutProxy = AopProxyUtils.getSingletonTarget(indexInfoConfig); + ObjectNode indexInfoJson = new ObjectMapper().valueToTree(indexInfoConfigWithoutProxy); Map map = new HashMap<>(); for (String field : fields) { map.put(field, indexInfoJson.get(field)); @@ -73,17 +76,21 @@ public class IndexInfoControllerV1 extends BaseController { return R.ok(map); } - + /** + * 根据给定 url 获取在线指标配置 + * @param url + * @return + */ @GetMapping("/getConfigIndOnlineByUrl") @ResponseBody public R getConfigOnlineByUrl(String url) { - return R.judge(() -> indexInfo.getOnlineConfigByUrl()); + return R.judge(() -> indexInfoConfig.getOnlineConfigByUrl(url)); } @GetMapping("/getIndexInfoConfig") @ResponseBody public R getIndexInfoConfig() { - return R.ok(indexInfo); + return R.ok(indexInfoConfig); } @GetMapping("/list") diff --git a/src/main/java/quant/rich/emoney/controller/manage/PlanControllerV1.java b/src/main/java/quant/rich/emoney/controller/manage/PlanControllerV1.java index fc0c0f4..179cd70 100644 --- a/src/main/java/quant/rich/emoney/controller/manage/PlanControllerV1.java +++ b/src/main/java/quant/rich/emoney/controller/manage/PlanControllerV1.java @@ -13,11 +13,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; -import com.baomidou.mybatisplus.core.toolkit.StringUtils; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; - import lombok.extern.slf4j.Slf4j; -import quant.rich.emoney.controller.common.UpdateBoolController; +import quant.rich.emoney.controller.common.UpdateBoolServiceController; import quant.rich.emoney.entity.sqlite.Plan; import quant.rich.emoney.exception.RException; import quant.rich.emoney.interfaces.IQueryableEnum; @@ -29,7 +26,7 @@ import quant.rich.emoney.service.sqlite.PlanService; @Slf4j @Controller @RequestMapping("/admin/v1/manage/plan") -public class PlanControllerV1 extends UpdateBoolController { +public class PlanControllerV1 extends UpdateBoolServiceController { @Autowired PlanService planService; @@ -42,35 +39,25 @@ public class PlanControllerV1 extends UpdateBoolController { @GetMapping("/list") @ResponseBody public LayPageResp list(LayPageReq pageReq) { - Page planPage = planService.page(pageReq); - return new LayPageResp<>(planPage); + return super.list(pageReq); } @GetMapping("/getOne") @ResponseBody public R getOne(String planId) { - // 如果 planId 是空,说明可能希望新建一个 Plan,需要返回默认实例化对象,否则从数据库取 - return - planId == null ? R.ok(new Plan()) : - R.judgeNonNull(planService.getById(planId), "无法找到对应 ID 的 Plan"); + return super.getOne(planId); } @PostMapping("/save") @ResponseBody public R save(@RequestBody Plan plan) { - if (StringUtils.isNotBlank(plan.getPlanId())) { - planService.updateById(plan); - } - else { - planService.save(plan.setPlanId(null)); - } - return R.ok(); + return super.save(plan); } @PostMapping("/delete") @ResponseBody public R delete(String planId) { - return R.judge(planService.removeById(planId), "删除失败,是否已删除?"); + return super.delete(planId); } @PostMapping("/batchOp") diff --git a/src/main/java/quant/rich/emoney/controller/manage/ProxySettingControllerV1.java b/src/main/java/quant/rich/emoney/controller/manage/ProxySettingControllerV1.java index 4ac7827..6e73a3b 100644 --- a/src/main/java/quant/rich/emoney/controller/manage/ProxySettingControllerV1.java +++ b/src/main/java/quant/rich/emoney/controller/manage/ProxySettingControllerV1.java @@ -1,11 +1,8 @@ package quant.rich.emoney.controller.manage; -import java.lang.reflect.Field; import java.util.Arrays; import java.util.List; import java.util.Objects; -import java.util.Optional; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @@ -16,14 +13,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; -import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; -import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; -import com.baomidou.mybatisplus.core.metadata.TableInfo; -import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; - import lombok.extern.slf4j.Slf4j; -import quant.rich.emoney.controller.common.BaseController; +import quant.rich.emoney.controller.common.UpdateBoolServiceController; import quant.rich.emoney.entity.sqlite.ProxySetting; import quant.rich.emoney.exception.RException; import quant.rich.emoney.pojo.dto.LayPageReq; @@ -34,7 +25,7 @@ import quant.rich.emoney.service.sqlite.ProxySettingService; @Slf4j @Controller @RequestMapping("/admin/v1/manage/proxySetting") -public class ProxySettingControllerV1 extends BaseController { +public class ProxySettingControllerV1 extends UpdateBoolServiceController { @Autowired ProxySettingService proxySettingService; @@ -47,63 +38,32 @@ public class ProxySettingControllerV1 extends BaseController { @GetMapping("/list") @ResponseBody public LayPageResp list(LayPageReq pageReq) { - Page planPage = proxySettingService.page(pageReq); - return new LayPageResp<>(planPage); + return super.list(pageReq); } @GetMapping("/getOne") @ResponseBody public R getOne(String id) { - - // 如果 planId 是空,说明可能希望新建一个 ProxySetting,需要返回默认实例化对象 - if (id == null) { - return R.ok(new ProxySetting()); - } - - // 否则从数据库取 - ProxySetting proxy = proxySettingService.getById(id); - return R.judge(proxy != null, proxy, "无法找到对应 ID 的 ProxySetting"); - } - - @PostMapping("/updateBool") - @ResponseBody - public R updateBool(String id, String field, Boolean value) { - TableInfo tableInfo = TableInfoHelper.getTableInfo(ProxySetting.class); - try { - Field declaredField = ProxySetting.class.getDeclaredField(field); - - Optional fieldInfo = tableInfo.getFieldList().stream() - .filter(f -> f.getProperty().equals(field)) - .findFirst(); - if (declaredField.getType().equals(Boolean.class)) { - proxySettingService.update( - new UpdateWrapper() - .eq("id", id) - .set(fieldInfo.get().getColumn(), value)); - return R.ok(); - } - } - catch (Exception e) {} - throw RException.badRequest(); + return super.getOne(id); } @PostMapping("/save") @ResponseBody public R save(@RequestBody ProxySetting proxySetting) { - if (!Objects.isNull(proxySetting.getId())) { - proxySettingService.updateById(proxySetting); - } - else { - proxySettingService.save(proxySetting.setId(null)); - } - return R.ok(); + return super.save(proxySetting); } @PostMapping("/delete") @ResponseBody public R delete(String id) { - return R.judge(proxySettingService.removeById(id), "删除失败,是否已删除?"); + return super.delete(id); } + + @Override + protected + R updateBool(String id, String field, Boolean value) { + return super.updateBool(id, field, value); + } @PostMapping("/batchOp") @ResponseBody @@ -149,5 +109,14 @@ public class ProxySettingControllerV1 extends BaseController { DISABLE_HTTPS_VERIFY, ENABLE_HTTP_VERIFY } + + + @GetMapping("/refreshIpThroughProxy") + @ResponseBody + public R refreshIpThroughProxy() { + return R.ok( + proxySettingService + .refreshIpThroughProxy()); + } } diff --git a/src/main/java/quant/rich/emoney/controller/manage/RequestInfoControllerV1.java b/src/main/java/quant/rich/emoney/controller/manage/RequestInfoControllerV1.java index 7d820fa..a5f5375 100644 --- a/src/main/java/quant/rich/emoney/controller/manage/RequestInfoControllerV1.java +++ b/src/main/java/quant/rich/emoney/controller/manage/RequestInfoControllerV1.java @@ -1,5 +1,8 @@ package quant.rich.emoney.controller.manage; +import java.util.Arrays; +import java.util.List; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.lang.NonNull; import org.springframework.stereotype.Controller; @@ -10,11 +13,14 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; - +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.extern.slf4j.Slf4j; -import quant.rich.emoney.controller.common.UpdateBoolController; +import quant.rich.emoney.controller.common.UpdateBoolServiceController; import quant.rich.emoney.entity.sqlite.RequestInfo; +import quant.rich.emoney.exception.RException; import quant.rich.emoney.pojo.dto.LayPageReq; import quant.rich.emoney.pojo.dto.LayPageResp; import quant.rich.emoney.pojo.dto.R; @@ -23,7 +29,7 @@ import quant.rich.emoney.service.sqlite.RequestInfoService; @Slf4j @Controller @RequestMapping("/admin/v1/manage/requestInfo") -public class RequestInfoControllerV1 extends UpdateBoolController { +public class RequestInfoControllerV1 extends UpdateBoolServiceController { @Autowired RequestInfoService requestInfoService; @@ -36,42 +42,57 @@ public class RequestInfoControllerV1 extends UpdateBoolController { @GetMapping("/list") @ResponseBody public LayPageResp list(LayPageReq pageReq) { - Page planPage = requestInfoService.page(pageReq); - return new LayPageResp<>(planPage); + return super.list(pageReq); } @GetMapping("/getOne") @ResponseBody public R getOne(Integer id) { - - // 如果 id 是空,说明可能希望新建并返回默认实例化对象 - if (id == null) { - return R.ok(new RequestInfo()); - } - - // 否则从数据库取 - RequestInfo requestInfo = requestInfoService.getById(id); - return R.judgeNonNull(requestInfo, "无法找到对应 ID 的请求信息"); + return super.getOne(id); } @PostMapping("/save") @ResponseBody - public R save(@RequestBody @NonNull RequestInfo plan) { - return R.judge(() -> requestInfoService.saveOrUpdate(plan)); + public R save(@RequestBody @NonNull RequestInfo requestInfo) { + return super.save(requestInfo); } @PostMapping("/delete") @ResponseBody public R delete(String id) { - return R.judge(requestInfoService.removeById(id), "删除失败,是否已删除?"); + return super.delete(id); + } + + @Override + protected + R updateBool(String id, String field, Boolean value) { + return super.updateBool(id, field, value); } @PostMapping("/batchOp") @ResponseBody public R batchOp( @RequestParam(value="ids[]", required=true) - String[] ids, String op) { - return null; + @Valid @NotEmpty String[] ids, @NotNull RequestInfoBatchOp op) { + + List idArray = Arrays.asList(ids); + + if (op == RequestInfoBatchOp.DELETE) { + return R.judge(getThisService().removeByIds(idArray)); + } + + LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); + uw.in(RequestInfo::getId, idArray); + switch (op) { + default: + throw RException.badRequest("未知操作"); + } + } + + private static enum RequestInfoBatchOp { + DELETE, + ENABLE_ANONYMOUS, + DISABLE_ANONYMOUS } } diff --git a/src/main/java/quant/rich/emoney/entity/config/AndroidSdkLevelConfig.java b/src/main/java/quant/rich/emoney/entity/config/AndroidSdkLevelConfig.java index 0608187..f0d315f 100644 --- a/src/main/java/quant/rich/emoney/entity/config/AndroidSdkLevelConfig.java +++ b/src/main/java/quant/rich/emoney/entity/config/AndroidSdkLevelConfig.java @@ -19,6 +19,7 @@ public class AndroidSdkLevelConfig implements IConfig { public AndroidSdkLevelConfig() { androidVerToSdk = new HashMap<>(); + androidVerToSdk.put("15", 35); androidVerToSdk.put("14", 34); androidVerToSdk.put("13", 33); androidVerToSdk.put("12L", 32); diff --git a/src/main/java/quant/rich/emoney/entity/config/DeviceInfoConfig.java b/src/main/java/quant/rich/emoney/entity/config/DeviceInfoConfig.java index b90757f..f8b32fe 100644 --- a/src/main/java/quant/rich/emoney/entity/config/DeviceInfoConfig.java +++ b/src/main/java/quant/rich/emoney/entity/config/DeviceInfoConfig.java @@ -49,6 +49,11 @@ public class DeviceInfoConfig implements IConfig { @Slf4j public static class DeviceInfo { + // 持久化在本地的 DeviceInfo 只有三个字段: + // model、deviceType 和 fingerprint + // 其中除 model 和 deviceType 外,其他字段全部从 fingerprint 派生 + // 也就是说只要提供 model、deviceType 和 fingerprint 就能创建一个 DeviceInfo 实例 + @JsonView(IConfig.Views.Persistence.class) private String model; private String brand; @@ -62,11 +67,12 @@ public class DeviceInfoConfig implements IConfig { private String buildType; private String buildTags; + /** + * 用以匹配 fingerprint 的正则表达式 + */ public static final Pattern PATTERN = Pattern.compile("^(?.*?)/(?.*?)/(?.*?):(?.*?)/(?.*?)/(?.*?):(?.*?)/(?.*?)$"); - - private DeviceInfo() { - } + private DeviceInfo() {} public DeviceInfo setFingerprint(String fingerprint) { Matcher m = PATTERN.matcher(fingerprint); @@ -126,8 +132,8 @@ public class DeviceInfoConfig implements IConfig { } public final String toString() { - return String.format("Model: %s, Fingerprint: %s", - getModel(), getFingerprint() + return String.format("Model: %s, DeviceType: %s, Fingerprint: %s", + getModel(), getDeviceType(), getFingerprint() ); } @@ -139,7 +145,7 @@ public class DeviceInfoConfig implements IConfig { } public int hashCode() { - return Objects.hash(getModel(), getFingerprint()); + return Objects.hash(getModel(), getDeviceType(), getFingerprint()); } } } diff --git a/src/main/java/quant/rich/emoney/entity/config/EmoneyRequestConfig.java b/src/main/java/quant/rich/emoney/entity/config/EmoneyRequestConfig.java deleted file mode 100644 index d7cbaa9..0000000 --- a/src/main/java/quant/rich/emoney/entity/config/EmoneyRequestConfig.java +++ /dev/null @@ -1,539 +0,0 @@ -package quant.rich.emoney.entity.config; - -import java.io.Serializable; -import java.util.Objects; - -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; -import org.springframework.beans.factory.annotation.Autowired; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import lombok.AccessLevel; -import lombok.Data; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; -import lombok.extern.slf4j.Slf4j; -import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo; -import quant.rich.emoney.interfaces.ConfigInfo; -import quant.rich.emoney.interfaces.IConfig; -import quant.rich.emoney.patch.okhttp.PatchOkHttp; -import quant.rich.emoney.patch.okhttp.PatchOkHttpRule; -import quant.rich.emoney.util.EncryptUtils; -import quant.rich.emoney.util.SpringContextHolder; -import quant.rich.emoney.util.TextUtils; -import quant.rich.emoney.validator.EmoneyRequestConfigValid; - -/** - * 用于配置请求时的请求行为,一般而言,请求头与安卓系统的信息有关(build.prop) - * 虽然部分请求对应服务器可能不进行审核,但合理的请求头能尽可能模仿真机行为,避免风险 - * @see DeviceInfoConfig - * @see AndroidSdkLevelConfig - * @see ChromeVersionsConfig - */ -@Data -@Accessors(chain = true) -@Slf4j -@EmoneyRequestConfigValid -@ConfigInfo(field = "emoneyRequest", name = "益盟请求设置", initDefault = true) -public class EmoneyRequestConfig implements IConfig { - - /** - * 是否匿名登录 - */ - private Boolean isAnonymous = true; - - /** - * 非匿名登录时的用户名 - */ - private String username = ""; - - /** - * 非匿名登录时的密码 - */ - private String password = ""; - - /** - * 鉴权信息 - */ - private String authorization; - - /** - * UID - */ - private Integer uid; - - /** - * 用于:

        - *
      • 益盟登录接口 guid = MD5(androidId)
      • - *
      • 益盟登录接口 exIdentify.AndroidID = androidId
      • - *
      - * 来源:
      本例随机生成并管理,需要符合 16 位 - * - */ - private String androidId = TextUtils.randomString("abcdef0123456789", 16); - - /** - * 用于:
        - *
      • Webview User-Agent
      • - *
      • Non-Webview Image User-Agent
      • - *
      - * 来源:DeviceInfoConfig - * @see DeviceInfoConfig - */ - @Setter(AccessLevel.PRIVATE) - private String androidVersion; - - /** - * 用于:
        - *
      • 益盟通讯接口请求头 X-Android-Agent = EMAPP/{emoneyVersion}(Android;{androidSdkLevel})
      • - *
      • 益盟登录接口 osVersion = androidSdkLevel
      • - *
      - * 来源:DeviceInfoConfig, 经由 AndroidSdkLevelConfig 转换,由本例代管 - * @see DeviceInfoConfig - * @see AndroidSdkLevelConfig - */ - @Setter(AccessLevel.PRIVATE) - private String androidSdkLevel; - - /** - * 用于:
        - *
      • 益盟登录接口 softwareType = softwareType
      • - *
      - * 来源:DeviceInfoConfig,由本例代管 - * @see DeviceInfoConfig - */ - private String softwareType; - - /** - * 用于:
        - *
      • 益盟通讯接口请求头 User-Agent = okHttpUserAgent
      • - *
      - * 一般由程序所使用的 OkHttp 版本决定
      - * 来源:本例管理 - */ - private String okHttpUserAgent = "okhttp/3.12.2"; - - /** - * 对应 build.prop 中 Build.MODEL, 用于:
        - *
      • WebView User-Agent
      • - *
      • 非 WebView 图片User-Agent
      • - *
      - * 来源:DeviceInfoConfig, 由本例代为管理 - * @see DeviceInfoConfig - */ - private String deviceName; - - /** - * 对应 build.prop 中 Build.FINGERPRINT, 用于:
        - *
      • 益盟登录接口 hardware = MD5(fingerprint)
      • - *
      • 益盟登录接口 exIdentify.OSFingerPrint = fingerprint
      • - *
      - * 注意最终生成的 exIdentify 是 jsonString, 且有对斜杠的转义
      - * 来源:DeviceInfoConfig, 由本例代为管理 - * @see DeviceInfoConfig - * - */ - private String fingerprint; - - - /** - * 对应 build.prop 中 Build.ID, 用于:
        - *
      • WebView User-Agent
      • - *
      • 非 WebView 图片User-Agent
      • - *
      - * 来源:DeviceInfoConfig, 由本例代为管理 - * @see DeviceInfoConfig - * - */ - private String buildId; - - /** - * 用于:
        - *
      • WebView User-Agent
      • - *
      - * 来源:ChromeVersionsConfig, 由本例代为管理 - * @see ChromeVersionsConfig - */ - private String chromeVersion; - - /** - * 用于:
        - *
      • 益盟通讯接口请求头 X-Android-Agent = - * EMAPP/{emoneyVersion}(Android;{androidSdkLevel})
      • - *
      - * 由程序版本决定
      - * 来源:本例管理 - * @see EmoneyRequestConfig.androidSdkLevel - */ - private String emoneyVersion = "5.8.1"; - - /** - * 用于:
        - *
      • 益盟通讯接口请求头 Emapp-ViewMode = emappViewMode
      • - *
      - * 由程序决定, 一般默认为 "1"
      - * 来源:本例管理 - */ - private String emappViewMode = "1"; - - /** - * OkHttp 用于注入 User-Agent 规则的 id - */ - @JsonIgnore - private Integer userAgentPatchRuleId; - - @Getter(AccessLevel.PRIVATE) - @Autowired - private AndroidSdkLevelConfig androidSdkLevelConfig; - - @Getter(AccessLevel.PRIVATE) - @Autowired - private DeviceInfoConfig deviceInfoConfig; - - @Getter(AccessLevel.PRIVATE) - @Autowired - private ChromeVersionsConfig chromeVersionsConfig; - - public void afterBeanInit() { - - try { - androidSdkLevelConfig = Objects.requireNonNullElseGet(androidSdkLevelConfig, () -> SpringContextHolder.getBean(AndroidSdkLevelConfig.class)); - deviceInfoConfig = Objects.requireNonNullElseGet(deviceInfoConfig, () -> SpringContextHolder.getBean(DeviceInfoConfig.class)); - chromeVersionsConfig = Objects.requireNonNullElseGet(chromeVersionsConfig, () -> SpringContextHolder.getBean(ChromeVersionsConfig.class)); - } - catch (IllegalStateException e) { - log.debug("试图从 SpringContextHolder 初始化 androidSdkLevelConfig, deviceInfoConfig 和 chromeVersionConfig, 但 SpringContextHolder 未准备好"); - } - - if (ObjectUtils.anyNull(fingerprint, buildId, deviceName, androidVersion, androidSdkLevel, softwareType)) { - // 任意是 null 的都要统一由 deviceInfo 进行设置 - initFromRandomDeviceInfo(); - } - else { - // 都不是 null,则由 fingerprint 来检查各项。 - // model 和 softwareType 本应交由 deviceInfoConfig 检查以 - // 应对可能的通过修改本地 json 来进行攻击的方式,可是本身 - // deviceInfoConfig 对 model 和 softwareType 的信息也来源 - // 于本地,万一本地的 deviceInfo(.fallback).json 也不值得信任? - // 所以只检查 fingerprint - - DeviceInfo deviceInfo; - boolean valid = true; - try { - deviceInfo = DeviceInfo.from(null, fingerprint); - Validate.validState(androidVersion.equals( - deviceInfo.getVersionRelease()), - "androidVersion(versionRelease) 与预设 fingerprint 不匹配"); - Validate.validState(androidSdkLevel.equals( - String.valueOf(androidSdkLevelConfig.getSdkLevel(deviceInfo.getVersionRelease()))), - "androidSdkLevel 与预设 fingerprint 不匹配"); - Validate.validState(buildId.equals(deviceInfo.getBuildId()), - "buildId 与预设 fingerprint 不匹配"); - } - catch (Exception e) { - valid = false; - } - if (!valid) { - initFromRandomDeviceInfo(); - } - } - - if (chromeVersion == null) { - chromeVersion = chromeVersionsConfig.getRandomChromeVersion(); - } - - // 注入 OkHttp - patchOkHttp(); - } - - /** - * 注入 User-Agent patch 规则 - */ - private EmoneyRequestConfig patchOkHttp() { - userAgentPatchRuleId = PatchOkHttp.apply( - PatchOkHttpRule.when() - .hostEndsWith("emoney.cn") - .not(r -> r.hostMatches("appstatic")) - .or(a -> a.hostContains("emapp")) - .or(b -> b.hasHeaderName("X-Protocol-Id")) - .overrideIf("User-Agent", getOkHttpUserAgent()).build() - .setId(userAgentPatchRuleId)); - return this; - } - - /** - * 从随机 deviceInfo 填充本例相关字段 - * @return - */ - private EmoneyRequestConfig initFromRandomDeviceInfo() { - DeviceInfo deviceInfo = deviceInfoConfig.getRandomDeviceInfo(); - // 更新 deviceInfo 后对应 androidId 也要修改,哪怕原来非空 - androidId = TextUtils.randomString("abcdef0123456789", 16); - return initFromDeviceInfo(deviceInfo); - } - - /** - * 从指定 deviceInfo 填充本例相关字段 - * @param deviceInfo - * @return - */ - private EmoneyRequestConfig initFromDeviceInfo(DeviceInfo deviceInfo) { - if (deviceInfo == null) { - log.error("deviceInfo is null"); - RuntimeException e = new RuntimeException("deviceInfo is null"); - e.printStackTrace(); - throw e; - } - deviceName = deviceInfo.getModel(); - androidVersion = deviceInfo.getVersionRelease(); - androidSdkLevel = String.valueOf(androidSdkLevelConfig.getSdkLevel(androidVersion)); - softwareType = deviceInfo.getDeviceType(); - fingerprint = deviceInfo.getFingerprint(); - buildId = deviceInfo.getBuildId(); - return this; - } - - public EmoneyRequestConfig() {} - - public EmoneyRequestConfig setFingerprint(String fingerprint) { - // 进入前即便 androidSdkLevelConfig 为 null 也要尝试获取一下 - // 因为为 null 时不一定是程序初始化时,也有可能是从前端 Post 而来的 - try { - androidSdkLevelConfig = Objects.requireNonNullElseGet(androidSdkLevelConfig, () -> SpringContextHolder.getBean(AndroidSdkLevelConfig.class)); - } - catch (IllegalStateException e) { - log.debug("SpringContext not ready"); - } - - if (ObjectUtils.allNotNull(deviceName, softwareType, fingerprint, androidSdkLevelConfig)) { - DeviceInfo deviceInfo = DeviceInfo.from(deviceName, fingerprint, softwareType); - initFromDeviceInfo(deviceInfo); - } - else { - this.fingerprint = fingerprint; - } - return this; - } - - /** - * 根据当前配置获取 guid,用于益盟登录接口 - * @return - */ - @JsonIgnore - public String getGuid() { - return EncryptUtils.toMD5String(androidId); - } - - /** - * 一般 Protobuf 请求 X-Android-Agent 头,由 emoneyVersion 和 androidSdkLevel 组成 - * @return - */ - @JsonIgnore - public String getXAndroidAgent() { - // EMAPP/{emoneyVersion}(Android;{androidSdkLevel}) - return - new StringBuilder() - .append("EMAPP/") - .append(getEmoneyVersion()) - .append("(Android;") - .append(getAndroidSdkLevel()) - .append(")").toString(); - } - - /** - * 用于 App 内用到 Webview 的地方 - * @return - */ - @JsonIgnore - public String getWebviewUserAgent() { - return new StringBuilder() - .append("Mozilla/5.0 (Linux; Android ") - .append(getAndroidVersion()) - .append("; ") - .append(getDeviceName()) - .append(" Build/") - .append(getBuildId()) - .append("; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/") - .append(getChromeVersion()) - .append(" Mobile Safari/537.36") - .toString(); - } - - /** - * 用于 App 内少量未用到 Webview 的地方,如首页获取图片等 - * @return - */ - @JsonIgnore - public String getNonWebviewResourceUserAgent() { - // Dalvik/2.1.0 (Linux; U; Android {安卓版本};{Build.DEVICE} Build/{Build.ID}) - return new StringBuilder() - .append("Dalvik/2.1.0 (Linux; U; Android ") - .append(getAndroidVersion()) - .append(";") - .append(getDeviceName()) - .append(" Build/") - .append(getBuildId()) - .append(")") - .toString(); - } - /** - * 根据当前配置获取 hardware,用于益盟登录接口 - * @return - */ - @JsonIgnore - public String getHardware() { - return EncryptUtils.toMD5String(getFingerprint()); - } - - /** - * 根据本例信息(包括保存的用户名和密码)生成一个用于登录的 ObjectNode - * @return - */ - @JsonIgnore - public ObjectNode getUsernamePasswordLoginObject() { - return getUsernamePasswordLoginObject(username, password); - } - - /** - * 根据指定用户名、密码和本例信息生成一个用于登录的 ObjectNode - * @param username 用户名 - * @param password 密码(可以是加密过的,也可以是明文) - * @return - */ - public ObjectNode getUsernamePasswordLoginObject(String username, String password) { - - if (StringUtils.isAnyBlank(username, password)) { - throw new RuntimeException("Try to generate a emoney login object but username and/or password is blank"); - } - - ObjectNode node = getAnonymousLoginObject(); - node.put("accId", username); - node.put("accType", 1); - - // 尝试解密 password 看是否成功,如果成功说明原本就已经是加密了的 - String tryDecryptPassword = EncryptUtils.decryptAesForEmoneyPassword(password); - - node.put("pwd", - tryDecryptPassword != null ? password : - EncryptUtils.encryptAesForEmoneyPassword(password) - ); - - return node; - } - - /** - * 根据本例信息生成一个用于匿名登录的 ObjectNode - * @return - */ - @JsonIgnore - public ObjectNode getAnonymousLoginObject() { - ObjectMapper mapper = new ObjectMapper(); - ObjectNode node = mapper.createObjectNode(); - ObjectNode exIdentify = mapper.createObjectNode(); - exIdentify.put("IMEI", ""); - exIdentify.put("AndroidID", getAndroidId()); - exIdentify.put("MAC", ""); - exIdentify.put("OSFingerPrint", getFingerprint()); - String exIdentifyString = exIdentify.toString().replace("/", "\\/"); - - // 这玩意最好按照顺序来,当前这个顺序是 5.8.1 的顺序 - String guid = getGuid(); - node.put("appVersion", getEmoneyVersion()); - node.put("productId", 4); - node.put("softwareType", getSoftwareType()); - node.put("deviceName", getDeviceName()); - node.put("ssid", "0"); - node.put("platform", "android"); - node.put("exIdentify", exIdentifyString); - node.put("osVersion", getAndroidSdkLevel()); - node.put("accId", guid); - node.put("guid", guid); - node.put("accType", 4); - node.put("pwd", ""); - node.put("channelId", "1711"); - node.put("hardware", getHardware()); - - return node; - } - - /** - * 根据本例信息获取 Relogin ObjectNode - * @return 如果 authorization 和 uid 任意 null 则本例返回 null - */ - @JsonIgnore - public ObjectNode getReloginObject() { - - if (getUid() == null || StringUtils.isBlank(getAuthorization())) { - return null; - } - - ObjectMapper mapper = new ObjectMapper(); - ObjectNode node = mapper.createObjectNode(); - ObjectNode exIdentify = mapper.createObjectNode(); - exIdentify.put("IMEI", ""); - exIdentify.put("AndroidID", getAndroidId()); - exIdentify.put("MAC", ""); - exIdentify.put("OSFingerPrint", getFingerprint()); - String exIdentifyString = exIdentify.toString().replace("/", "\\/"); - - // 这玩意最好按照顺序来,当前这个顺序是 5.8.1 的顺序 - String guid = getGuid(); - node.put("appVersion", getEmoneyVersion()); - node.put("productId", 4); - node.put("softwareType", getSoftwareType()); - node.put("deviceName", getDeviceName()); - node.put("ssid", "0"); - node.put("platform", "android"); - node.put("token", getAuthorization()); // 和登录不同的地方: token - node.put("exIdentify", exIdentifyString); - node.put("uid", getUid()); // 和登录不同的地方: uid - node.put("osVersion", getAndroidSdkLevel()); - node.put("guid", guid); - node.put("channelId", "1711"); - node.put("hardware", getHardware()); - - return node; - } - - /** - * 设置密码:
        - *
      • null or empty,保存空字符串
      • - *
      • 尝试解密成功,说明是密文,直接保存
      • - *
      • 尝试解密失败,说明是明文,加密保存
      • - *
      - * @param password - * @return - */ - public EmoneyRequestConfig setPassword(String password) { - if (StringUtils.isEmpty(password)) { - this.password = ""; - return this; - } - String tryDecryptPassword = EncryptUtils.decryptAesForEmoneyPassword(password); - if (tryDecryptPassword != null) { - this.password = password; - } - else { - this.password = EncryptUtils.encryptAesForEmoneyPassword(password); - } - return this; - } - - /** - * 确保 androidVersion/androidSdkLevel 不为 null - */ - public EmoneyRequestConfig beforeSaving() { - setFingerprint(this.fingerprint); - return this; - } - - public EmoneyRequestConfig afterSaving() { - patchOkHttp(); - return this; - } -} diff --git a/src/main/java/quant/rich/emoney/entity/config/IndexInfo.java b/src/main/java/quant/rich/emoney/entity/config/IndexInfo.java deleted file mode 100644 index 4b9437b..0000000 --- a/src/main/java/quant/rich/emoney/entity/config/IndexInfo.java +++ /dev/null @@ -1,38 +0,0 @@ -package quant.rich.emoney.entity.config; - -import java.util.ArrayList; -import java.util.List; - -import lombok.Data; -import lombok.experimental.Accessors; -import quant.rich.emoney.enums.StockSpan; - -@Data -@Accessors(chain=true) -public class IndexInfo { - - private List paramInfoList = new ArrayList<>(); - - private String code; - - private String name; - - private Boolean isCalc; - - private List supportPeriod = new ArrayList<>(); - - @Data - @Accessors(chain=true) - public static class ParamInfo { - - private String name; - - private Integer max; - - private Integer min; - - private Integer defaultValue; - - } - -} diff --git a/src/main/java/quant/rich/emoney/entity/config/IndexInfoConfig.java b/src/main/java/quant/rich/emoney/entity/config/IndexInfoConfig.java index d4aeccb..e7f17ad 100644 --- a/src/main/java/quant/rich/emoney/entity/config/IndexInfoConfig.java +++ b/src/main/java/quant/rich/emoney/entity/config/IndexInfoConfig.java @@ -1,25 +1,21 @@ package quant.rich.emoney.entity.config; import java.io.IOException; -import java.util.concurrent.TimeUnit; - -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.databind.JsonNode; -import lombok.AccessLevel; import lombok.Data; -import lombok.Getter; import lombok.experimental.Accessors; -import okhttp3.ConnectionPool; import okhttp3.Request; import okhttp3.Response; -import quant.rich.emoney.annotation.LockByCaller; import quant.rich.emoney.client.OkHttpClientProvider; +import quant.rich.emoney.component.CallerLockAspect.LockByCaller; import quant.rich.emoney.interfaces.ConfigInfo; import quant.rich.emoney.interfaces.IConfig; +import quant.rich.emoney.service.sqlite.RequestInfoService; +import quant.rich.emoney.util.SpringContextHolder; /** * 指标信息配置,只做运行时管理,不做保存 @@ -36,11 +32,6 @@ public class IndexInfoConfig implements IConfig { @JsonView(IConfig.Views.Persistence.class) private JsonNode configIndOnline; - @Autowired - @JsonIgnore - @Getter(AccessLevel.PRIVATE) - private EmoneyRequestConfig emoneyRequestConfig; - public IndexInfoConfig() {} public String getConfigIndOnlineStr() { @@ -49,10 +40,13 @@ public class IndexInfoConfig implements IConfig { @LockByCaller @JsonIgnore - public String getOnlineConfigByUrl() throws IOException { + public String getOnlineConfigByUrl(String url) throws IOException { synchronized (this) { + if (SpringContextHolder.getBean(RequestInfoService.class).getDefaultRequestInfo() == null) { + throw new RuntimeException("请先新增请求配置并作为默认配置"); + } Request request = new Request.Builder() - .url(configIndOnlineUrl) + .url(url) .header("Cache-Control", "no-cache") .get() .build(); @@ -68,7 +62,5 @@ public class IndexInfoConfig implements IConfig { } } } - - public static ConnectionPool connectionPool = new ConnectionPool(10, 5, TimeUnit.MINUTES); } diff --git a/src/main/java/quant/rich/emoney/entity/config/PlatformConfig.java b/src/main/java/quant/rich/emoney/entity/config/PlatformConfig.java index 3dc8238..236f360 100644 --- a/src/main/java/quant/rich/emoney/entity/config/PlatformConfig.java +++ b/src/main/java/quant/rich/emoney/entity/config/PlatformConfig.java @@ -15,6 +15,8 @@ public class PlatformConfig implements IConfig { private String password; private String email; + + private String apiToken; private Boolean isInited; diff --git a/src/main/java/quant/rich/emoney/entity/config/ProxyConfig.java b/src/main/java/quant/rich/emoney/entity/config/ProxyConfig.java deleted file mode 100644 index 34119ed..0000000 --- a/src/main/java/quant/rich/emoney/entity/config/ProxyConfig.java +++ /dev/null @@ -1,101 +0,0 @@ -package quant.rich.emoney.entity.config; - -import java.net.InetSocketAddress; -import java.net.Proxy; - -import org.apache.commons.lang3.ObjectUtils; - -import com.fasterxml.jackson.annotation.JsonView; -import lombok.Data; -import lombok.experimental.Accessors; -import lombok.extern.slf4j.Slf4j; -import quant.rich.emoney.interceptor.EnumOptionsInterceptor.EnumOptions; -import quant.rich.emoney.interfaces.ConfigInfo; -import quant.rich.emoney.interfaces.IConfig; -import quant.rich.emoney.pojo.dto.IpInfo; -import quant.rich.emoney.util.GeoIPUtil; -import quant.rich.emoney.validator.ProxyConfigValid; - -/** - * 独立出来一个代理设置的原因是后续可能需要做一个代理池,这样的话独立配置比较适合后续扩展 - */ -@Data -@Accessors(chain = true) -@Slf4j -@ProxyConfigValid -@ConfigInfo(field = "proxy", name = "代理设置", initDefault = true) -public class ProxyConfig implements IConfig { - - /** - * 代理类型 - */ - @EnumOptions("ProxyTypeEnum") - @JsonView(IConfig.Views.Persistence.class) - private Proxy.Type proxyType = Proxy.Type.DIRECT; - - /** - * 代理主机 - */ - @JsonView(IConfig.Views.Persistence.class) - private String proxyHost = ""; - - /** - * 代理端口 - */ - @JsonView(IConfig.Views.Persistence.class) - private Integer proxyPort = 1; - - /** - * 是否忽略 HTTPS 证书校验 - */ - @JsonView(IConfig.Views.Persistence.class) - private Boolean ignoreHttpsVerification = false; - - /** - * 通过代理后的 IP,不做存储,只做呈现 - */ - private IpInfo ipInfo; - - public void afterBeanInit() { - //refreshIpThroughProxy(); - } - - public synchronized IpInfo refreshIpThroughProxy() { - ipInfo = GeoIPUtil.getIpInfoThroughProxy(this); - return ipInfo; - } - - - public ProxyConfig() {} - - /** - * 根据配置获取代理 - * @return - */ - public Proxy getProxy() { - if (getProxyType() != null && getProxyType() != Proxy.Type.DIRECT) { - return new Proxy(getProxyType(), - new InetSocketAddress(getProxyHost(), getProxyPort())); - } - return Proxy.NO_PROXY; - } - - public String getProxyUrl() { - if (ObjectUtils.anyNull(getProxyType(), getProxyHost(), getProxyPort())) { - return null; - } - StringBuilder sb = new StringBuilder(); - if (getProxyType() == Proxy.Type.SOCKS) { - sb.append("socks5://"); - } - else if (getProxyType() == Proxy.Type.HTTP) { - sb.append("http://"); - } - else { - return null; - } - sb.append(getProxyHost()).append(':').append(getProxyPort()); - return sb.toString(); - } - -} diff --git a/src/main/java/quant/rich/emoney/entity/postgre/StockStrategy.java b/src/main/java/quant/rich/emoney/entity/postgre/StockStrategy.java new file mode 100644 index 0000000..00078dc --- /dev/null +++ b/src/main/java/quant/rich/emoney/entity/postgre/StockStrategy.java @@ -0,0 +1,42 @@ +package quant.rich.emoney.entity.postgre; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 益盟个股策略信息,包含策略类型和日期 + */ +@Data +@Accessors(chain=true) +public class StockStrategy { + + private String tsCode; + + public StockStrategy setTsCodeFromGoodsId(Integer goodsId) { + // 自动将益盟 goodsId 转换成 tsCode + // 1301325 -> 301325.SZ + // 600325 -> 600325.SH + // 1920009 -> 920009.BJ + String goodsIdStr = goodsId.toString(); + RuntimeException e = new RuntimeException("无法将 goodsId " + goodsIdStr + " 转换为 tsCode"); + if (goodsIdStr.length() == 6) { + // SH + return setTsCode(goodsIdStr + ".SH"); + } + else if (goodsIdStr.length() == 7) { + if (goodsIdStr.charAt(0) != '1') { + throw e; + } + if (goodsIdStr.charAt(1) == '9') { + // BJ + return setTsCode(goodsIdStr.substring(1) + ".BJ"); + } + // SZ + return setTsCode(goodsIdStr.substring(1) + ".SZ"); + } + throw e; + } + + + +} diff --git a/src/main/java/quant/rich/emoney/entity/sqlite/ProxySetting.java b/src/main/java/quant/rich/emoney/entity/sqlite/ProxySetting.java index 292be50..992e30f 100644 --- a/src/main/java/quant/rich/emoney/entity/sqlite/ProxySetting.java +++ b/src/main/java/quant/rich/emoney/entity/sqlite/ProxySetting.java @@ -26,6 +26,8 @@ public class ProxySetting { @TableId(value="id", type=IdType.AUTO) private Integer id; + private Boolean isDefault; + @Nonnull private String proxyName; diff --git a/src/main/java/quant/rich/emoney/entity/sqlite/RequestInfo.java b/src/main/java/quant/rich/emoney/entity/sqlite/RequestInfo.java index 0b0c7e4..9e024d6 100644 --- a/src/main/java/quant/rich/emoney/entity/sqlite/RequestInfo.java +++ b/src/main/java/quant/rich/emoney/entity/sqlite/RequestInfo.java @@ -7,12 +7,14 @@ import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.activerecord.Model; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.AccessLevel; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.Setter; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; @@ -20,21 +22,36 @@ import quant.rich.emoney.entity.config.AndroidSdkLevelConfig; import quant.rich.emoney.entity.config.ChromeVersionsConfig; import quant.rich.emoney.entity.config.DeviceInfoConfig; import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo; -import quant.rich.emoney.entity.config.EmoneyRequestConfig; import quant.rich.emoney.util.EncryptUtils; import quant.rich.emoney.util.SpringContextHolder; import quant.rich.emoney.util.TextUtils; +import quant.rich.emoney.validator.RequestInfoValid; +/** + * 用于配置请求时的请求行为,一般而言,请求头与安卓系统的信息有关(build.prop) + * 虽然部分请求对应服务器可能不进行审核,但合理的请求头能尽可能模仿真机行为,避免风险 + * @see DeviceInfoConfig + * @see AndroidSdkLevelConfig + * @see ChromeVersionsConfig + */ @Data +@EqualsAndHashCode(callSuper=false) @Accessors(chain = true) @Slf4j +@RequestInfoValid @TableName(value = "request_info") -public class RequestInfo { +public class RequestInfo extends Model { - private static AndroidSdkLevelConfig androidSdkLevelConfig = SpringContextHolder.getBean(AndroidSdkLevelConfig.class); - private static DeviceInfoConfig deviceInfoConfig = SpringContextHolder.getBean(DeviceInfoConfig.class); - private static ChromeVersionsConfig chromeVersionsConfig = SpringContextHolder.getBean(ChromeVersionsConfig.class); + private static final long serialVersionUID = -3113053377999289627L; + + private volatile static AndroidSdkLevelConfig androidSdkLevelConfig = SpringContextHolder.getBean(AndroidSdkLevelConfig.class); + private volatile static DeviceInfoConfig deviceInfoConfig = SpringContextHolder.getBean(DeviceInfoConfig.class); + private volatile static ChromeVersionsConfig chromeVersionsConfig = SpringContextHolder.getBean(ChromeVersionsConfig.class); + /** + * 使用随机设备信息(DeviceInfo)初始化对象

      + * @see DeviceInfo + */ public RequestInfo() { DeviceInfo deviceInfo = deviceInfoConfig.getRandomDeviceInfo(); setRelativeFieldsFromDeviceInfo(deviceInfo); @@ -43,15 +60,15 @@ public class RequestInfo { @TableId(value = "id", type = IdType.AUTO) private Integer id; + /** + * 该请求是否设为默认请求 + */ + private Boolean isDefault = false; + /** * 该请求信息配置的名称,助记用 */ private String name = ""; - - /** - * 是否匿名登录 - */ - private Boolean isAnonymous = true; /** * 非匿名登录时的用户名 @@ -88,8 +105,9 @@ public class RequestInfo { *

    • Webview User-Agent
    • *
    • Non-Webview Image User-Agent
    • *
    - * 来源:DeviceInfoConfig + * 来源:DeviceInfo * @see DeviceInfoConfig + * @see DeviceInfo */ @Setter(AccessLevel.PRIVATE) @TableField(exist=false) @@ -100,8 +118,9 @@ public class RequestInfo { *
  • 益盟通讯接口请求头 X-Android-Agent = EMAPP/{emoneyVersion}(Android;{androidSdkLevel})
  • *
  • 益盟登录接口 osVersion = androidSdkLevel
  • *
- * 来源:DeviceInfoConfig, 经由 AndroidSdkLevelConfig 转换,由本例代管 + * 来源:DeviceInfo, 经由 AndroidSdkLevelConfig 转换,由本例代管 * @see DeviceInfoConfig + * @see DeviceInfo * @see AndroidSdkLevelConfig */ @Setter(AccessLevel.PRIVATE) @@ -112,8 +131,9 @@ public class RequestInfo { * 用于:
    *
  • 益盟登录接口 softwareType = softwareType
  • *
- * 来源:DeviceInfoConfig,由本例代管 + * 来源:DeviceInfo * @see DeviceInfoConfig + * @see DeviceInfo */ private String softwareType; @@ -131,8 +151,9 @@ public class RequestInfo { *
  • WebView User-Agent
  • *
  • 非 WebView 图片User-Agent
  • * - * 来源:DeviceInfoConfig, 由本例代为管理 + * 来源:DeviceInfo * @see DeviceInfoConfig + * @see DeviceInfo */ private String deviceName; @@ -177,7 +198,7 @@ public class RequestInfo { * * 由程序版本决定
    * 来源:本例管理 - * @see EmoneyRequestConfig.androidSdkLevel + * @see #androidSdkLevel */ private String emoneyVersion = "5.8.1"; @@ -429,5 +450,9 @@ public class RequestInfo { return node; } + + public boolean isAnonymous() { + return !StringUtils.isAnyBlank(getUsername(), getPassword()); + } } diff --git a/src/main/java/quant/rich/emoney/entity/sqlite/StrategyAndPool.java b/src/main/java/quant/rich/emoney/entity/sqlite/StrategyAndPool.java index 9f4ebad..5f5e78d 100644 --- a/src/main/java/quant/rich/emoney/entity/sqlite/StrategyAndPool.java +++ b/src/main/java/quant/rich/emoney/entity/sqlite/StrategyAndPool.java @@ -21,6 +21,7 @@ public class StrategyAndPool implements Comparable { private String strategyName; private Integer strategyId; private String poolName; + private String type; @TableId private Integer poolId; @@ -28,7 +29,8 @@ public class StrategyAndPool implements Comparable { } - public StrategyAndPool(String strategyName, Integer strategyId, String poolName, Integer poolId) { + public StrategyAndPool(String type, String strategyName, Integer strategyId, String poolName, Integer poolId) { + this.type = type; this.strategyName = strategyName; this.strategyId = strategyId; this.poolName = poolName; @@ -38,18 +40,20 @@ public class StrategyAndPool implements Comparable { @Override public boolean equals(Object o) { if (this == o) return true; + if (o == null) return false; if (!(o instanceof StrategyAndPool)) return false; StrategyAndPool strategyAndPool = (StrategyAndPool) o; return - strategyName == strategyAndPool.strategyName && - strategyId == strategyAndPool.strategyId && - poolName == strategyAndPool.poolName && - poolId == strategyAndPool.poolId; + Objects.equals(strategyName, strategyAndPool.strategyName) && + Objects.equals(strategyId, strategyAndPool.strategyId) && + Objects.equals(poolName, strategyAndPool.poolName) && + Objects.equals(poolId, strategyAndPool.poolId) && + Objects.equals(type, strategyAndPool.type); } @Override public int hashCode() { - return Objects.hash(strategyName, strategyId, poolName, poolId); + return Objects.hash(strategyName, strategyId, poolName, poolId, type); } /** diff --git a/src/main/java/quant/rich/emoney/interfaces/ConfigInfo.java b/src/main/java/quant/rich/emoney/interfaces/ConfigInfo.java index 0f9694d..d09feda 100644 --- a/src/main/java/quant/rich/emoney/interfaces/ConfigInfo.java +++ b/src/main/java/quant/rich/emoney/interfaces/ConfigInfo.java @@ -21,7 +21,12 @@ import org.springframework.stereotype.Component; @Retention(RetentionPolicy.RUNTIME) public @interface ConfigInfo { /** - * @return 配置 field 标识,用以自动注入、持久化配置文件名 + * 配置 field 标识,用以自动注入、持久化配置文件名。

    + * 例: + *

      + *
    • 指定 field = "website", 则 bean 名为 websiteConfig 持久化文件名为 websiteConfig.json + *
    • 未指定 field, 类名为 ProxyConfig, 则 bean 名为 proxyConfig, 持久化文件名为 proxyConfig.json + * */ String field() default ""; diff --git a/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttp.java b/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttp.java index 511b3c2..f41c655 100644 --- a/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttp.java +++ b/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttp.java @@ -41,10 +41,11 @@ public class PatchOkHttp { randomIds[0] = random.nextInt(); } rule.setId(randomIds[0]); + log.debug("PatchOkHttp.apply(rule.id={})", randomIds[0]); } rules.add(rule); - log.debug("PatchOkHttp.apply() 在 ClassLoader {} 中运行", PatchOkHttp.class.getClassLoader()); + //log.debug("PatchOkHttp.apply() 在 ClassLoader {} 中运行", PatchOkHttp.class.getClassLoader()); if (!isHooked) hook(); return rule.getId(); } @@ -60,12 +61,10 @@ public class PatchOkHttp { } public static void match(RequestContext ctx, String currentHeader, Consumer consumer) { - if (!logOnce) { - log.debug("PatchOkHttp.match() 在 ClassLoader {} 中运行", PatchOkHttp.class.getClassLoader()); - logOnce = true; - } + // log.debug("PatchOkHttp.match() 在 ClassLoader {} 中运行", PatchOkHttp.class.getClassLoader()); for (PatchOkHttpRule rule : PatchOkHttp.rules) { if (rule.matches(ctx)) { + log.debug("PatchOkHttp.match() 匹配到规则 rule.id={}", rule.getId()); rule.apply(ctx, currentHeader, consumer); } } diff --git a/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttpRule.java b/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttpRule.java index f2c4d1d..924e52a 100644 --- a/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttpRule.java +++ b/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttpRule.java @@ -154,7 +154,7 @@ public class PatchOkHttpRule { return this; } - public Builder overrideIf(String headerName, String value) { + public Builder overrideHeader(String headerName, String value) { actions.add((ctx, curr, setter) -> { if (curr.equalsIgnoreCase(headerName)) { log.debug("matches and applying - host: {}, currHeader {}, targetHeader {}, value: {}, classLoader: {}", ctx.host, curr, headerName, @@ -164,6 +164,24 @@ public class PatchOkHttpRule { }); return this; } + + /** + * 如果满足条件则覆写指定 Header。当覆写值可能动态变化时,使用本方法提供 supplier + * @param headerName + * @param valueSupplier + * @return + */ + public Builder overrideHeader(String headerName, Supplier valueSupplier) { + actions.add((ctx, curr, setter) -> { + if (curr.equalsIgnoreCase(headerName)) { + String value = valueSupplier.get(); + log.debug("matches and applying - host: {}, currHeader {}, targetHeader {}, value: {}, classLoader: {}", ctx.host, curr, headerName, + value, this.getClass().getClassLoader()); + setter.accept(value); + } + }); + return this; + } public PatchOkHttpRule build() { return new PatchOkHttpRule(condition, actions); diff --git a/src/main/java/quant/rich/emoney/pojo/dto/R.java b/src/main/java/quant/rich/emoney/pojo/dto/R.java index 21e644a..28e3a2e 100644 --- a/src/main/java/quant/rich/emoney/pojo/dto/R.java +++ b/src/main/java/quant/rich/emoney/pojo/dto/R.java @@ -206,6 +206,22 @@ public class R implements Serializable { } } + /** + * 提供一返回值为 boolean 的 supplier,如果成功则返回 R.ok(), 失败则抛出 RException.badRequest(defaltMessage), + * 抛出错误则抛出 RException.badRequest(e.getMessage()) + * @param supplier + * @param defaultMessage + * @return + */ + public static R judge(ThrowingSupplier supplier, String defaultMessage) { + try { + return R.judge(supplier.get(), defaultMessage); + } + catch (Exception e) { + throw RException.badRequest(e.getMessage()); + } + } + public static R judgeThrow(ThrowingSupplier supplier) throws Exception { 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 66bd3c9..77f263c 100644 --- a/src/main/java/quant/rich/emoney/service/ConfigService.java +++ b/src/main/java/quant/rich/emoney/service/ConfigService.java @@ -197,21 +197,26 @@ public class ConfigService implements InitializingBean { } /** - * 获取 Config + * 获取给定类型的 Config 实例 *

      - * 当缓存中有时从缓存取, 缓存没有时从数据库取并更新到缓存, 数据库也没有时, 如果指定的 Config 的 @ConfigInfo - * 注解开启了 initDefault = true, 则尝试返回一个初始 Config,否则返回 null + * 当缓存中有时从缓存取,缓存没有时从数据库取并更新到缓存,数据库也没有时, + * 如果指定的 Config 的 @ConfigInfo + * 注解开启了 initDefault = true, 则尝试返回一个初始 Config,否则返回 null. + * 原本不存在的 Config 如果 @ConfigInfo 开启了 save(),会持久化到本地。 * * @param - * @param clazz + * @param clazz 配置类 * @return + * @see ConfigInfo + * @see #getOrCreateConfig(String) + * */ public > Config getConfig(Class clazz) { if (classObjectCache.containsKey(clazz)) { try { return getCache(clazz); } catch (Exception e) { - log.warn("Cannot get config info of " + clazz.toString() + " from cache, try to read from database", e); + log.warn("Cannot get config info of {} from cache, try to read from database", clazz.toString(), e); } } String field = fieldClassCache.inverse().get(clazz); @@ -244,11 +249,6 @@ public class ConfigService implements InitializingBean { try { String filePath = getConfigFilePath(field, false); SmartResourceResolver.saveText(filePath, configJoString); - //Path dirPath = Paths.get(filePath).getParent(); - //if (Files.notExists(dirPath)) { - // Files.createDirectories(dirPath); - //} - //Files.writeString(Path.of(filePath), configJoString); } catch (IOException e) { log.error("Cannot write config to local file {}.json, error: {}", field, e.getMessage()); return false; @@ -300,7 +300,7 @@ public class ConfigService implements InitializingBean { Class configClass = (Class) fieldClassCache.get(field); if (configClass == null) { - log.warn("Cannot get class info from fieldClassCache, field name: {}", field); + log.warn("Cannot get config class from fieldClassCache, field name {} not exist", field); return null; } ConfigInfo info = getConfigInfo(configClass); @@ -313,10 +313,12 @@ public class ConfigService implements InitializingBean { // 也就是无论如何,fallback 都不应由程序来写入 String filePath = getConfigFilePath(field, false); config = getFromFile(filePath, configClass); + boolean needSave = false; if (config == null) { log.info("Cannot init config from local file of {}Config, try fallback", field); // 走 fallback 流程 config = getFromFile(getConfigFilePath(field, true), configClass); + needSave = true; } if (config == null) { @@ -328,6 +330,7 @@ public class ConfigService implements InitializingBean { config = configClass.getDeclaredConstructor() .newInstance(); + needSave = true; } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { // 一般是初始化方法内出现未被捕获的错误 @@ -340,7 +343,8 @@ public class ConfigService implements InitializingBean { if (config == null) { log.warn("Cannot read or init from default/fallback for config {}Config, it will be null", field); } - else { + else if (needSave) { + // 走了 fallback 或者初始化了则保存一份 saveOrUpdate(config); } diff --git a/src/main/java/quant/rich/emoney/service/IndexDetailService.java b/src/main/java/quant/rich/emoney/service/IndexDetailService.java index fbf23bd..207298e 100644 --- a/src/main/java/quant/rich/emoney/service/IndexDetailService.java +++ b/src/main/java/quant/rich/emoney/service/IndexDetailService.java @@ -39,15 +39,17 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import quant.rich.emoney.client.OkHttpClientProvider; -import quant.rich.emoney.entity.config.EmoneyRequestConfig; +import quant.rich.emoney.component.RequireAuthAndProxyAspect.RequireAuthAndProxy; import quant.rich.emoney.entity.config.IndexInfoConfig; import quant.rich.emoney.entity.config.SmartViewWriter; +import quant.rich.emoney.entity.sqlite.RequestInfo; import quant.rich.emoney.pojo.dto.IndexDetail; import quant.rich.emoney.pojo.dto.NonParamsIndexDetail; import quant.rich.emoney.pojo.dto.NonParamsIndexDetail.NonParamsIndexDetailData; import quant.rich.emoney.util.EncryptUtils; import quant.rich.emoney.util.SmartResourceResolver; import quant.rich.emoney.pojo.dto.ParamsIndexDetail; +import quant.rich.emoney.service.sqlite.RequestInfoService; /** * 获取指标详情的服务 @@ -67,7 +69,7 @@ public class IndexDetailService { IndexInfoConfig indexInfoConfig; @Autowired - EmoneyRequestConfig emoneyRequestConfig; + RequestInfoService requestInfoService; static final String filePath = "./conf/extra/indexDetail/"; static final ObjectMapper mapper = new ObjectMapper(); @@ -84,6 +86,7 @@ public class IndexDetailService { * @return */ @CacheEvict(cacheNames="@indexDetailService.getIndexDetail(Serializable)", key="#indexCode.toString()") + @RequireAuthAndProxy(autoLogin = true) public IndexDetail forceRefreshAndGetIndexDetail(Serializable indexCode) { // 刷新的本质就是从网络获取,因为此处已经清理了缓存,所以直接从网络获取后再 @@ -101,6 +104,7 @@ public class IndexDetailService { @Cacheable(cacheNames="@indexDetailService.getIndexDetail(Serializable)", key="#indexCode.toString()") + @RequireAuthAndProxy(autoLogin = true) public IndexDetail getIndexDetail(Serializable indexCode) { if (indexCode == null) { @@ -137,11 +141,16 @@ public class IndexDetailService { /** * 从网络获取有参指标详情 + *

      本例用到的 requestInfo 涉及鉴权 * @param indexCode + * @see RequestInfoService#getDefaultRequestInfo() * @return */ private ParamsIndexDetail getParamsIndexDetailOnline(Serializable indexCode) { - + RequestInfo requestInfo = requestInfoService.getDefaultRequestInfo(); + if (requestInfo == null || StringUtils.isBlank(requestInfo.getAuthorization())) { + throw new RuntimeException("无法获取已鉴权的 RequestInfo"); + } try { OkHttpClient client = OkHttpClientProvider.getInstance(); String url = "https://emapp.emoney.cn/Config/AppIndicator/Get"; @@ -154,9 +163,9 @@ public class IndexDetailService { .header("X-Protocol-Id", "Config%2FAppIndicator%2FGet") .header("X-Request-Id", "null") .header("EM-Sign", EncryptUtils.getEMSign(content, "POST", "Config%2FAppIndicator%2FGet")) - .header("Authorization", emoneyRequestConfig.getAuthorization()) - .header("X-Android-Agent", emoneyRequestConfig.getXAndroidAgent()) - .header("Emapp-ViewMode", emoneyRequestConfig.getEmappViewMode()) + .header("Authorization", requestInfoService.getDefaultRequestInfo().getAuthorization()) + .header("X-Android-Agent", requestInfoService.getDefaultRequestInfo().getXAndroidAgent()) + .header("Emapp-ViewMode", requestInfoService.getDefaultRequestInfo().getEmappViewMode()) .build(); final Response response = client.newCall(request).execute(); String responseText = response.body().string(); @@ -223,8 +232,11 @@ public class IndexDetailService { /** * 从网络获取指定 indexCode 的无参指标详情 + *

      本例用到的 requestInfo 不需要 PatchOkHttp 覆写,但要求鉴权参数拼接到 url 中,故要求鉴权 *

      会一并尝试获取其他在本地未有的无参指标

      * @param indexCode + * @see RequestInfo#getWebviewUserAgent() + * @see RequestInfoService#getDefaultRequestInfo() * @return */ private NonParamsIndexDetail getNonParamsIndexDetailOnline(Serializable indexCode) { @@ -234,7 +246,7 @@ public class IndexDetailService { .header("Host", "appstatic.emoney.cn") .header("Connection", "keep-alive") .header("Upgrade-Insecure-Requests", "1") - .header("User-Agent", emoneyRequestConfig.getWebviewUserAgent()) + .header("User-Agent", requestInfoService.getDefaultRequestInfo().getWebviewUserAgent()) .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9") .header("X-Request-With", "cn.emoney.emstock") .header("Sec-Fetch-Site", "none") @@ -259,7 +271,7 @@ public class IndexDetailService { Document doc = Jsoup.parseBodyFragment(responseBody); doc.select("script[src]").forEach(el -> { String absoluteURI = resolveUrl(url, el.attr("src")); - log.info("script uri: {}", absoluteURI); + log.debug("script uri: {}", absoluteURI); if (absoluteURI != null) { scripts.add(absoluteURI); } @@ -281,7 +293,7 @@ public class IndexDetailService { Request.Builder scriptBuilder = new Request.Builder() .header("Host", "appstatic.emoney.cn") .header("Connection", "keep-alive") - .header("User-Agent", emoneyRequestConfig.getWebviewUserAgent()) + .header("User-Agent", requestInfoService.getDefaultRequestInfo().getWebviewUserAgent()) .header("Accept", "*/*") .header("X-Request-With", "cn.emoney.emstock") .header("Sec-Fetch-Site", "same-origin") @@ -381,7 +393,9 @@ public class IndexDetailService { NonParamsIndexDetail existed; try { existed = mapper.readValue(inputStream, NonParamsIndexDetail.class); - if (!existed.getOriginal().equals(detail.getOriginal())) { + if (existed.getOriginal() == null || + !existed.getOriginal().equals(detail.getOriginal())) { + log.debug("本地 NonParamsIndexDetail {} 原始数据与最新数据不一致,更新", indexCode); saveIndexDetail(detail); } } @@ -415,10 +429,10 @@ public class IndexDetailService { /** * 为指定 NonParamsIndexDetail 加载在线图片并转换成 base64 - *

      该方法涉及 authorization(token)/webviewUserAgent,需确保 EmoneyRequestConfig 已正确注入并登录

      + *

      该方法涉及 authorization(token)/webviewUserAgent,需确保 RequestInfo 已正确配置并登录

      * @param detail * @return - * @see EmoneyRequestConfig + * @see RequestInfo */ private NonParamsIndexDetail loadImages(NonParamsIndexDetail detail) { OkHttpClient client = OkHttpClientProvider.getInstance(); @@ -444,7 +458,7 @@ public class IndexDetailService { .url(imageUrl) .header("Host", host) .header("Connection", "keep-alive") - .header("User-Agent", emoneyRequestConfig.getWebviewUserAgent()) + .header("User-Agent", requestInfoService.getDefaultRequestInfo().getWebviewUserAgent()) .header("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8") .header("X-Request-With", "cn.emoney.emstock") .header("Sec-Fetch-Site", "same-origin") @@ -548,17 +562,23 @@ public class IndexDetailService { /** * 获取 NonParamsIndexDetail URL - *

      该 Url 涉及 authorization(token),需确保 EmoneyRequestConfig 已正确注入并登录

      + *

      该 Url 涉及 authorization(token),需确保 RequestInfo 已正确配置并登录

      * @param indexCode * @return - * @see EmoneyRequestConfig + * @see RequestInfo */ private String buildNonParamsIndexUrl(Serializable indexCode) { + + RequestInfo requestInfo = requestInfoService.getDefaultRequestInfo(); + if (requestInfo == null || StringUtils.isBlank(requestInfo.getAuthorization())) { + throw new RuntimeException("无法获取已鉴权的 RequestInfo"); + } + StringBuilder urlBuilder = new StringBuilder(); urlBuilder.append("https://appstatic.emoney.cn/html/emapp/stock/note/?name="); urlBuilder.append(indexCode.toString()); urlBuilder.append("&emoneyScaleType=0&emoneyLandMode=0&token="); - urlBuilder.append(emoneyRequestConfig.getAuthorization()); + urlBuilder.append(requestInfoService.getDefaultRequestInfo().getAuthorization()); return urlBuilder.toString(); } diff --git a/src/main/java/quant/rich/emoney/service/WarnService.java b/src/main/java/quant/rich/emoney/service/WarnService.java new file mode 100644 index 0000000..6e9ff9c --- /dev/null +++ b/src/main/java/quant/rich/emoney/service/WarnService.java @@ -0,0 +1,27 @@ +package quant.rich.emoney.service; + +import org.springframework.stereotype.Service; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class WarnService { + + + + public static class Warn { + } + + public static enum WarnType { + NO_SPECIFIC_REQUEST_INFO("未设置指定的请求信息"); + + @Getter + private String info; + WarnType(String info) { + this.info = info; + } + } + +} diff --git a/src/main/java/quant/rich/emoney/service/sqlite/ProxySettingService.java b/src/main/java/quant/rich/emoney/service/sqlite/ProxySettingService.java index 8672b2a..4955dca 100644 --- a/src/main/java/quant/rich/emoney/service/sqlite/ProxySettingService.java +++ b/src/main/java/quant/rich/emoney/service/sqlite/ProxySettingService.java @@ -2,11 +2,43 @@ package quant.rich.emoney.service.sqlite; import org.springframework.stereotype.Service; import com.baomidou.dynamic.datasource.annotation.DS; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import quant.rich.emoney.entity.sqlite.ProxySetting; import quant.rich.emoney.mapper.sqlite.ProxySettingMapper; +import quant.rich.emoney.pojo.dto.IpInfo; +import quant.rich.emoney.util.GeoIPUtil; @DS("sqlite") @Service public class ProxySettingService extends SqliteServiceImpl { + + private volatile IpInfo ipInfo; + + /** + * 获取默认代理配置 + * @return + */ + public ProxySetting getDefaultProxySetting() { + return getOne( + new LambdaQueryWrapper() + .eq(ProxySetting::getIsDefault, true)); + } + + /** + * 获取默认代理配置的 IP 信息 + * @return + */ + public IpInfo getDefaultProxySettingIpInfo() { + ProxySetting proxySetting = getDefaultProxySetting(); + if (proxySetting == null) return null; + if (ipInfo != null) return ipInfo; + return refreshIpThroughProxy(); + } + + + public synchronized IpInfo refreshIpThroughProxy() { + ipInfo = GeoIPUtil.getIpInfoThroughProxy(getDefaultProxySetting()); + return ipInfo; + } } diff --git a/src/main/java/quant/rich/emoney/service/sqlite/RequestInfoService.java b/src/main/java/quant/rich/emoney/service/sqlite/RequestInfoService.java index 8fcb10d..3cd6efb 100644 --- a/src/main/java/quant/rich/emoney/service/sqlite/RequestInfoService.java +++ b/src/main/java/quant/rich/emoney/service/sqlite/RequestInfoService.java @@ -1,14 +1,52 @@ package quant.rich.emoney.service.sqlite; +import java.util.function.Supplier; + +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import com.baomidou.dynamic.datasource.annotation.DS; - -import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; import quant.rich.emoney.entity.sqlite.RequestInfo; import quant.rich.emoney.mapper.sqlite.RequestInfoMapper; +import quant.rich.emoney.patch.okhttp.PatchOkHttp; +import quant.rich.emoney.patch.okhttp.PatchOkHttpRule; @DS("sqlite") @Service +@Slf4j +@Lazy(false) public class RequestInfoService extends SqliteServiceImpl { - + + private volatile Integer userAgentPatchRuleId; + + @PostConstruct + void postConstruct() { + userAgentPatchRuleId = PatchOkHttp.apply( + PatchOkHttpRule.when() + .hostEndsWith("emoney.cn") + .not(r -> r.hostMatches("appstatic")) + .or(a -> a.hostContains("emapp")) + .or(b -> b.hasHeaderName("X-Protocol-Id")) + .overrideHeader("User-Agent", new Supplier() { + @Override + public String get() { + log.debug("触发获取请求配置的 OkHttpUserAgent"); + return getDefaultRequestInfo().getOkHttpUserAgent(); + } + }).build().setId(userAgentPatchRuleId)); + } + + public RequestInfo getDefaultRequestInfo() { + RequestInfo requestInfo = getOne( + new LambdaQueryWrapper().eq(RequestInfo::getIsDefault, true) + ); + if (requestInfo == null) { + requestInfo = new RequestInfo(); + save(requestInfo); + } + return requestInfo; + } + } diff --git a/src/main/java/quant/rich/emoney/service/sqlite/StrategyAndPoolService.java b/src/main/java/quant/rich/emoney/service/sqlite/StrategyAndPoolService.java index 13f5b04..a3aa210 100644 --- a/src/main/java/quant/rich/emoney/service/sqlite/StrategyAndPoolService.java +++ b/src/main/java/quant/rich/emoney/service/sqlite/StrategyAndPoolService.java @@ -3,10 +3,14 @@ package quant.rich.emoney.service.sqlite; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import com.baomidou.dynamic.datasource.annotation.DS; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -76,6 +80,7 @@ public class StrategyAndPoolService extends SqliteServiceImpl set = new HashSet<>(); + String[] types = new String[] {"band", "tech", "val"}; // 波段/技术/基本面 for (JsonNode node : output) { for (String type : types) { @@ -98,17 +104,23 @@ public class StrategyAndPoolService extends SqliteServiceImpl() + .eq(StrategyAndPool::getPoolId, strategyAndPool.getPoolId()))) { + set.add(strategyAndPool); + } } } } } this.saveOrUpdateBatch(set); + log.info("新增 {} 条 StrategyAndPool", set.size()); } } diff --git a/src/main/java/quant/rich/emoney/util/EncryptUtils.java b/src/main/java/quant/rich/emoney/util/EncryptUtils.java index 7c595ad..6a06637 100644 --- a/src/main/java/quant/rich/emoney/util/EncryptUtils.java +++ b/src/main/java/quant/rich/emoney/util/EncryptUtils.java @@ -24,7 +24,6 @@ import org.bouncycastle.util.encoders.Hex; import lombok.extern.slf4j.Slf4j; import okhttp3.MediaType; -import okhttp3.Request; import okhttp3.RequestBody; @Slf4j diff --git a/src/main/java/quant/rich/emoney/util/GeoIPUtil.java b/src/main/java/quant/rich/emoney/util/GeoIPUtil.java index 725af2f..76cf0a2 100644 --- a/src/main/java/quant/rich/emoney/util/GeoIPUtil.java +++ b/src/main/java/quant/rich/emoney/util/GeoIPUtil.java @@ -10,7 +10,7 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import quant.rich.emoney.client.OkHttpClientProvider; -import quant.rich.emoney.entity.config.ProxyConfig; +import quant.rich.emoney.entity.sqlite.ProxySetting; import quant.rich.emoney.pojo.dto.IpInfo; import java.io.IOException; @@ -36,10 +36,13 @@ public class GeoIPUtil { } @Async - public static IpInfo getIpInfoThroughProxy(ProxyConfig proxyConfig) { + public static IpInfo getIpInfoThroughProxy(ProxySetting proxySetting) { + if (proxySetting == null) { + throw new RuntimeException("代理为空"); + } return CallerLockUtil.tryCallWithCallerLock(() -> { - Proxy proxy = proxyConfig.getProxy(); - boolean ignoreHttpsVerification = proxyConfig.getIgnoreHttpsVerification(); + Proxy proxy = proxySetting.getProxy(); + boolean ignoreHttpsVerification = proxySetting.getIgnoreHttpsVerification(); // OkHttp 客户端配置 OkHttpClient client = OkHttpClientProvider.getInstance( proxy, ignoreHttpsVerification, @@ -82,7 +85,7 @@ public class GeoIPUtil { log.warn("Proxy ipv6 error {}", e.getMessage()); } return queryIpInfoGeoLite(ipInfo); - }, 100, proxyConfig).orElse(IpInfo.EMPTY); + }, 100, proxySetting).orElse(IpInfo.EMPTY); } /** diff --git a/src/main/java/quant/rich/emoney/util/SmartResourceResolver.java b/src/main/java/quant/rich/emoney/util/SmartResourceResolver.java index e667443..3458301 100644 --- a/src/main/java/quant/rich/emoney/util/SmartResourceResolver.java +++ b/src/main/java/quant/rich/emoney/util/SmartResourceResolver.java @@ -48,25 +48,25 @@ public class SmartResourceResolver { Path externalPath = resolveExternalPath(relativePath); if (externalPath != null && Files.exists(externalPath)) { - log.debug("从外部文件系统加载资源: {}", externalPath); + log.debug("Load resource externally: {}", externalPath); return Files.newInputStream(externalPath); } // 否则回退到 classpath(JAR、WAR、IDE) InputStream in = SmartResourceResolver.class.getClassLoader().getResourceAsStream(relativePath); if (in != null) { - log.debug("从 classpath 内部加载资源: {}", relativePath); + log.debug("Load resource within internal classpath: {}", relativePath); return in; } - throw new FileNotFoundException("无法找到资源: " + relativePath); + throw new FileNotFoundException("Cannot find resources: " + relativePath); } public static void saveText(String relativePath, String content) throws IOException { Path outputPath = resolveExternalPath(relativePath); Files.createDirectories(outputPath.getParent()); // 确保目录存在 Files.writeString(outputPath, content, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - log.debug("写入外部资源文件成功: {}", outputPath); + log.debug("Write resources externally success: {}", outputPath); } private static Path resolveExternalPath(String relativePath) { diff --git a/src/main/java/quant/rich/emoney/validator/ProxyConfigValid.java b/src/main/java/quant/rich/emoney/validator/ProxyConfigValid.java deleted file mode 100644 index 0c982fa..0000000 --- a/src/main/java/quant/rich/emoney/validator/ProxyConfigValid.java +++ /dev/null @@ -1,20 +0,0 @@ -package quant.rich.emoney.validator; - -import java.lang.annotation.Documented; -import java.lang.annotation.Target; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -import jakarta.validation.Constraint; -import jakarta.validation.Payload; - -@Documented -@Constraint(validatedBy = ProxyConfigValidator.class) -@Target({ ElementType.TYPE }) -@Retention(RetentionPolicy.RUNTIME) -public @interface ProxyConfigValid { - String message() default "非法的 ProxyConfig"; - Class[] groups() default {}; - Class[] payload() default {}; -} diff --git a/src/main/java/quant/rich/emoney/validator/ProxyConfigValidator.java b/src/main/java/quant/rich/emoney/validator/ProxyConfigValidator.java deleted file mode 100644 index 81038d6..0000000 --- a/src/main/java/quant/rich/emoney/validator/ProxyConfigValidator.java +++ /dev/null @@ -1,37 +0,0 @@ -package quant.rich.emoney.validator; - -import java.net.Proxy; -import java.util.Objects; -import org.apache.commons.lang3.StringUtils; - -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; -import quant.rich.emoney.entity.config.ProxyConfig; -import quant.rich.emoney.interfaces.IConfig; -import quant.rich.emoney.pojo.dto.IpInfo; - -public class ProxyConfigValidator implements IValidator, ConstraintValidator> { - - @Override - public boolean isValid(IConfig value, ConstraintValidatorContext context) { - - if (value == null) return true; - if (!(value instanceof ProxyConfig config)) return true; - - if (config.getProxyType() != null && config.getProxyType() != Proxy.Type.DIRECT) { - if (StringUtils.isBlank(config.getProxyHost())) { - return invalid(context, "设置代理为 HTTP 或 SOCKS 时,代理地址不允许为空"); - } - if (Objects.isNull(config.getProxyPort()) || config.getProxyPort() <= 0 || config.getProxyPort() > 65535) { - return invalid(context, "端口不合法"); - } - // 非匿名须判断用户名密码是否为空 - IpInfo ipInfo = config.refreshIpThroughProxy(); - return !ipInfo.isEmpty(); - } - - return true; - - } - -} diff --git a/src/main/java/quant/rich/emoney/validator/EmoneyRequestConfigValid.java b/src/main/java/quant/rich/emoney/validator/RequestInfoValid.java similarity index 81% rename from src/main/java/quant/rich/emoney/validator/EmoneyRequestConfigValid.java rename to src/main/java/quant/rich/emoney/validator/RequestInfoValid.java index e995e2b..8f29f0a 100644 --- a/src/main/java/quant/rich/emoney/validator/EmoneyRequestConfigValid.java +++ b/src/main/java/quant/rich/emoney/validator/RequestInfoValid.java @@ -10,10 +10,10 @@ import jakarta.validation.Constraint; import jakarta.validation.Payload; @Documented -@Constraint(validatedBy = EmoneyRequestConfigValidator.class) +@Constraint(validatedBy = RequestInfoValidator.class) @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) -public @interface EmoneyRequestConfigValid { +public @interface RequestInfoValid { String message() default "非法的 EmoneyRequestConfig"; Class[] groups() default {}; Class[] payload() default {}; diff --git a/src/main/java/quant/rich/emoney/validator/EmoneyRequestConfigValidator.java b/src/main/java/quant/rich/emoney/validator/RequestInfoValidator.java similarity index 56% rename from src/main/java/quant/rich/emoney/validator/EmoneyRequestConfigValidator.java rename to src/main/java/quant/rich/emoney/validator/RequestInfoValidator.java index 2861ff8..8d579a9 100644 --- a/src/main/java/quant/rich/emoney/validator/EmoneyRequestConfigValidator.java +++ b/src/main/java/quant/rich/emoney/validator/RequestInfoValidator.java @@ -7,27 +7,21 @@ import org.apache.commons.lang3.StringUtils; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; import quant.rich.emoney.entity.config.DeviceInfoConfig; -import quant.rich.emoney.entity.config.EmoneyRequestConfig; -import quant.rich.emoney.interfaces.IConfig; +import quant.rich.emoney.entity.sqlite.RequestInfo; -public class EmoneyRequestConfigValidator implements IValidator, ConstraintValidator> { +public class RequestInfoValidator implements IValidator, ConstraintValidator { static final Pattern androidIdPattern = Pattern.compile("^[0-9a-f]{16}$"); @Override - public boolean isValid(IConfig value, ConstraintValidatorContext context) { + public boolean isValid(RequestInfo value, ConstraintValidatorContext context) { if (value == null) return true; - if (!(value instanceof EmoneyRequestConfig config)) return true; + if (!(value instanceof RequestInfo config)) return true; - if (!config.getIsAnonymous()) { - // 非匿名须判断用户名密码是否为空 - if (StringUtils.isAnyBlank(config.getUsername(), config.getPassword())) { - context.disableDefaultConstraintViolation(); - context.buildConstraintViolationWithTemplate("配置非匿名时用户名和密码不能为空") - .addConstraintViolation(); - return false; - } + // 如果有用户名则必须设置密码 + if (!StringUtils.isBlank(config.getUsername()) && StringUtils.isBlank(config.getPassword())) { + return invalid(context, "当设置了用户名时,必须提供密码"); } if (!androidIdPattern.matcher(config.getAndroidId()).matches()) { diff --git a/src/main/resources/application-remote.yml b/src/main/resources/application-remote.yml index 07d0b81..aa16fff 100644 --- a/src/main/resources/application-remote.yml +++ b/src/main/resources/application-remote.yml @@ -27,15 +27,3 @@ spring: init: mode: always continue-on-error: true -mybatis-plus: - banner: false - mapper-locations: - - classpath*:mapper/postgre/*.xml - - classpath*:mapper/sqlite/*.xml - type-aliases-package: - - quant.rich.emoney.entity.postgre - - quant.rich.emoney.entity.sqlite - type-handlers-package: quant.rich.emoney.mybatis.typehandler - configuration: - map-underscore-to-camel-case: true - default-enum-type-handler: org.apache.ibatis.type.EnumOrdinalTypeHandler diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7262c31..c2d7885 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -36,6 +36,21 @@ spring: encoding: UTF-8 cache: false +mybatis-plus-join.banner: false +mybatis-plus: + global-config: + banner: false + mapper-locations: + - classpath*:mapper/postgre/*.xml + - classpath*:mapper/sqlite/*.xml + type-aliases-package: + - quant.rich.emoney.entity.postgre + - quant.rich.emoney.entity.sqlite + type-handlers-package: quant.rich.emoney.mybatis.typehandler + configuration: + map-underscore-to-camel-case: true + default-enum-type-handler: org.apache.ibatis.type.EnumOrdinalTypeHandler + kaptcha: border: "no" image: diff --git a/src/main/resources/conf/extra/indexDetail/nonParams/10002700.json b/src/main/resources/conf/extra/indexDetail/nonParams/10002700.json index bc54d37..e1f06dd 100644 --- a/src/main/resources/conf/extra/indexDetail/nonParams/10002700.json +++ b/src/main/resources/conf/extra/indexDetail/nonParams/10002700.json @@ -1 +1,11 @@ -{"id":"45","name":"FUNDCPX","nameCode":"10002700","data":[{"title":"基金操盘线:","items":["基金操盘线是一款可以识别趋势的指标。","操作原则:B点买入,S点卖出;红色持有标的,蓝色抛出标的。","趋势分为上涨趋势和下跌趋势,由于是趋势指标所以只在趋势明朗后才会出现B点买入信号或者S点卖出信号,一般不会在拐点位置出现信号,因此B点不是最早的买入点,S点是最后的卖出点。"],"image":null}],"original":"{\"id\":45,\"data\":[{\"title\":\"基金操盘线:\",\"items\":[\"基金操盘线是一款可以识别趋势的指标。\",\"操作原则:B点买入,S点卖出;红色持有标的,蓝色抛出标的。\",\"趋势分为上涨趋势和下跌趋势,由于是趋势指标所以只在趋势明朗后才会出现B点买入信号或者S点卖出信号,一般不会在拐点位置出现信号,因此B点不是最早的买入点,S点是最后的卖出点。\"]}],\"name\":\"FUNDCPX\",\"nameCode\":\"10002700\"}","indexCode":"10002700","indexName":"FUNDCPX","details":[{"content":"基金操盘线:","type":"TITLE"},{"content":"基金操盘线是一款可以识别趋势的指标。","type":"TEXT"},{"content":"操作原则:B点买入,S点卖出;红色持有标的,蓝色抛出标的。","type":"TEXT"},{"content":"趋势分为上涨趋势和下跌趋势,由于是趋势指标所以只在趋势明朗后才会出现B点买入信号或者S点卖出信号,一般不会在拐点位置出现信号,因此B点不是最早的买入点,S点是最后的卖出点。","type":"TEXT"}]} \ No newline at end of file +{ + "id" : "45", + "name" : "FUNDCPX", + "nameCode" : "10002700", + "data" : [ { + "title" : "基金操盘线:", + "items" : [ "基金操盘线是一款可以识别趋势的指标。", "操作原则:B点买入,S点卖出;红色持有标的,蓝色抛出标的。", "趋势分为上涨趋势和下跌趋势,由于是趋势指标所以只在趋势明朗后才会出现B点买入信号或者S点卖出信号,一般不会在拐点位置出现信号,因此B点不是最早的买入点,S点是最后的卖出点。" ], + "image" : null + } ], + "original" : "{\"id\":45,\"data\":[{\"title\":\"基金操盘线:\",\"items\":[\"基金操盘线是一款可以识别趋势的指标。\",\"操作原则:B点买入,S点卖出;红色持有标的,蓝色抛出标的。\",\"趋势分为上涨趋势和下跌趋势,由于是趋势指标所以只在趋势明朗后才会出现B点买入信号或者S点卖出信号,一般不会在拐点位置出现信号,因此B点不是最早的买入点,S点是最后的卖出点。\"]}],\"name\":\"FUNDCPX\",\"nameCode\":\"10002700\"}" +} \ No newline at end of file diff --git a/src/main/resources/conf/extra/indexDetail/nonParams/10012100.json b/src/main/resources/conf/extra/indexDetail/nonParams/10012100.json index 4abf7e0..8e85bd5 100644 --- a/src/main/resources/conf/extra/indexDetail/nonParams/10012100.json +++ b/src/main/resources/conf/extra/indexDetail/nonParams/10012100.json @@ -1 +1,11 @@ -{"id":null,"name":"中期线","nameCode":"10012100","data":[{"title":"中期线:","items":["代表标的的中期趋势:","曲线向上走时,代表中期趋势上涨;","曲线向下走时,代表中期趋势下跌。"],"image":null}],"original":"{\"name\":\"中期线\",\"nameCode\":\"10012100\",\"data\":[{\"title\":\"中期线:\",\"items\":[\"代表标的的中期趋势:\",\"曲线向上走时,代表中期趋势上涨;\",\"曲线向下走时,代表中期趋势下跌。\"]}]}","indexCode":"10012100","indexName":"中期线","details":[{"content":"中期线:","type":"TITLE"},{"content":"代表标的的中期趋势:","type":"TEXT"},{"content":"曲线向上走时,代表中期趋势上涨;","type":"TEXT"},{"content":"曲线向下走时,代表中期趋势下跌。","type":"TEXT"}]} \ No newline at end of file +{ + "id" : null, + "name" : "中期线", + "nameCode" : "10012100", + "data" : [ { + "title" : "中期线:", + "items" : [ "代表标的的中期趋势:", "曲线向上走时,代表中期趋势上涨;", "曲线向下走时,代表中期趋势下跌。" ], + "image" : null + } ], + "original" : "{\"name\":\"中期线\",\"nameCode\":\"10012100\",\"data\":[{\"title\":\"中期线:\",\"items\":[\"代表标的的中期趋势:\",\"曲线向上走时,代表中期趋势上涨;\",\"曲线向下走时,代表中期趋势下跌。\"]}]}" +} \ No newline at end of file diff --git a/src/main/resources/conf/extra/indexDetail/nonParams/10013500.json b/src/main/resources/conf/extra/indexDetail/nonParams/10013500.json index 4016a69..d412e3e 100644 --- a/src/main/resources/conf/extra/indexDetail/nonParams/10013500.json +++ b/src/main/resources/conf/extra/indexDetail/nonParams/10013500.json @@ -6,5 +6,6 @@ "title" : "量王精选(投教):", "items" : [ "红色实心柱子是量王,黄色实心柱子是天量;量王出现说明近阶段的量能达到了一定的程度,呈现交易相对活跃状态。具体用法参考量王精选相关教程。" ], "image" : null - } ] + } ], + "original" : "{\"data\":[{\"title\":\"量王精选(投教):\",\"items\":[\"红色实心柱子是量王,黄色实心柱子是天量;量王出现说明近阶段的量能达到了一定的程度,呈现交易相对活跃状态。具体用法参考量王精选相关教程。\"]}],\"name\":\"量王精选(投教)\",\"nameCode\":\"10013500\"}" } \ No newline at end of file diff --git a/src/main/resources/conf/extra/indexDetail/params/10010600.json b/src/main/resources/conf/extra/indexDetail/params/10010600.json index 3e18939..206430e 100644 --- a/src/main/resources/conf/extra/indexDetail/params/10010600.json +++ b/src/main/resources/conf/extra/indexDetail/params/10010600.json @@ -2,5 +2,6 @@ "id" : "52", "name" : "ATR", "code" : "10010600", - "descriptions" : [ "算法:今日振幅、今日最高与昨收差价、今日最低与昨收差价中的最大值,为真实波幅,求真实波幅的N日移动平均", "参数:N为天数,一般取14" ] + "descriptions" : [ "算法:今日振幅、今日最高与昨收差价、今日最低与昨收差价中的最大值,为真实波幅,求真实波幅的N日移动平均", "参数:N为天数,一般取14" ], + "original" : "{\"id\":52,\"code\":\"10010600\",\"name\":\"ATR\",\"descriptions\":[\"算法:今日振幅、今日最高与昨收差价、今日最低与昨收差价中的最大值,为真实波幅,求真实波幅的N日移动平均\",\"参数:N为天数,一般取14\"]}" } \ No newline at end of file diff --git a/src/main/resources/conf/system/androidSdkLevel.fallback.json b/src/main/resources/conf/system/androidSdkLevel.fallback.json index ff2dfb3..f6f606f 100644 --- a/src/main/resources/conf/system/androidSdkLevel.fallback.json +++ b/src/main/resources/conf/system/androidSdkLevel.fallback.json @@ -33,6 +33,7 @@ "12": 31, "12L": 32, "13": 33, - "14": 34 + "14": 34, + "15": 35 } } \ No newline at end of file diff --git a/src/main/resources/conf/system/androidSdkLevel.json b/src/main/resources/conf/system/androidSdkLevel.json index 35d6b44..f46fdb2 100644 --- a/src/main/resources/conf/system/androidSdkLevel.json +++ b/src/main/resources/conf/system/androidSdkLevel.json @@ -1,38 +1,39 @@ { "androidVerToSdk" : { - "4.4W" : 20, - "4.0.1" : 15, - "12L" : 32, - "10" : 29, - "11" : 30, - "2.0.1" : 6, - "12" : 31, - "13" : 33, - "14" : 34, - "2.3.3" : 10, "1.0" : 1, "1.1" : 2, - "2.0" : 5, - "2.1" : 7, - "3.0" : 11, - "2.2" : 8, - "3.1" : 12, - "4.0" : 14, - "2.3" : 9, - "3.2" : 13, - "4.1" : 16, - "5.0" : 21, "1.5" : 3, + "1.6" : 4, + "2.0" : 5, + "2.0.1" : 6, + "2.1" : 7, + "2.2" : 8, + "2.3" : 9, + "2.3.3" : 10, + "3.0" : 11, + "3.1" : 12, + "3.2" : 13, + "4.0" : 14, + "4.0.1" : 15, + "4.1" : 16, "4.2" : 17, + "4.3" : 18, + "4.4" : 19, + "4.4W" : 20, + "5.0" : 21, "5.1" : 22, "6.0" : 23, - "1.6" : 4, - "4.3" : 18, "7.0" : 24, - "9" : 28, - "4.4" : 19, "7.1" : 25, "8.0" : 26, - "8.1" : 27 + "8.1" : 27, + "9" : 28, + "10" : 29, + "11" : 30, + "12" : 31, + "12L" : 32, + "13" : 33, + "14" : 34, + "15" : 35 } } \ No newline at end of file diff --git a/src/main/resources/conf/system/emoneyLoginForm.json b/src/main/resources/conf/system/emoneyLoginForm.json deleted file mode 100644 index 5082c14..0000000 --- a/src/main/resources/conf/system/emoneyLoginForm.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "emoneyLoginFormDataList" : null, - "selectedId" : "57de6ca2423e0f64d8626477e1f8a46b", - "isRandom" : false -} \ No newline at end of file diff --git a/src/main/resources/conf/system/emoneyRequest.json b/src/main/resources/conf/system/emoneyRequest.json deleted file mode 100644 index 148444b..0000000 --- a/src/main/resources/conf/system/emoneyRequest.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "isAnonymous" : true, - "username" : "", - "password" : "", - "authorization" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkpXVCJ9.eyJ1dWQiOjEwMTUyNzk3MzEsInVpZCI6MjkyNTE0NDEsImRpZCI6IjM5N2RmZjEwOWEwOWFmOGY2NGJhNWMzYmYxNmE0ODA2IiwidHlwIjo0LCJhY2MiOiIzOTdkZmYxMDlhMDlhZjhmNjRiYTVjM2JmMTZhNDgwNiIsInN3dCI6MSwibGd0IjoxNzU4MjY0NjYwNzU0LCJuYmYiOjE3NTgyNjQ2NjAsImV4cCI6MTc1OTk5MjY2MCwiaWF0IjoxNzU4MjY0NjYwfQ.Y1aU7PlyuhGauY9aJCgkdqYC5gqcS4SioiHlPX2sSNc", - "uid" : 29251441, - "androidId" : "2aa9eb6eea32a4c3", - "androidVersion" : "13", - "androidSdkLevel" : "33", - "softwareType" : "Mobile", - "okHttpUserAgent" : "okhttp/3.12.2", - "deviceName" : "2112123AG", - "fingerprint" : "Xiaomi/psyche_global/psyche:13/RKQ1.211001.001/V14.0.6.0.TLDTWXM:user/release-keys", - "buildId" : "RKQ1.211001.001", - "chromeVersion" : "87.0.4280.141", - "emoneyVersion" : "5.8.1", - "emappViewMode" : "1" -} \ No newline at end of file diff --git a/src/main/resources/conf/system/platform.json b/src/main/resources/conf/system/platform.json index 4df3bb7..af394c5 100644 --- a/src/main/resources/conf/system/platform.json +++ b/src/main/resources/conf/system/platform.json @@ -2,5 +2,6 @@ "username" : "admin", "password" : "81667f60a8c11d4c8e9d2e0670ff24667e6c72d49b0b15562525bcbd", "email" : "huocaizhu@gmail.com", + "apiToken" : "vgb2IHmax9Mjji4R", "isInited" : true } \ No newline at end of file diff --git a/src/main/resources/database.db b/src/main/resources/database.db index aa37305307fbc0dd082fc1c84098cb67b8adbf0f..90418f3277596ca1cae7aa4aba26f1902afe69bd 100644 GIT binary patch literal 49152 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 SI3KCi__bd(*?WIf{Qm*lI9bmC literal 40960 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 x < 10000 && `${x} 分钟线` || [null, '日线', '周线', '月线', '季线', '半年线', '年线'][x/10000], + emoneyPeriodToName: x => x < 10000 && `${x} 分钟线` || [null, '日线', '周线', '月线', '季线', '半年线', '年线'][x / 10000], allEmoneyPeriods: [1, 5, 15, 30, 60, 120, 10000, 20000, 30000, 40000, 50000, 60000], showIndexDetailLayer: async function(obj, forceRefresh) { // obj: {indexCode: _, indexName: _} @@ -56,12 +56,12 @@ window.Helper = { } layer.close(load); }, - trimChars: function (str, chars) { - const escaped = chars.split('').map(c => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join(''); - const pattern = new RegExp(`^[${escaped}]+|[${escaped}]+$`, 'g'); - return str.replace(pattern, ''); + trimChars: function(str, chars) { + const escaped = chars.split('').map(c => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join(''); + const pattern = new RegExp(`^[${escaped}]+|[${escaped}]+$`, 'g'); + return str.replace(pattern, ''); }, - setLayerMainBtn: function (layero, index) { + setLayerMainBtn: function(layero, index) { var btns = layero.find('.layui-layer-btn>*'), j = 1; if (index < 0) index = btns.length + index; for (let i = 0; i < btns.length; i++) { @@ -73,7 +73,7 @@ window.Helper = { btn.setAttribute('class', filtered.join(' ')); } }, - openR: function (option) { + openR: function(option) { const defaultOption = { type: 1, area: '500px', skin: 'layui-anim layui-anim-rl layui-layer-adminRight', @@ -84,11 +84,11 @@ window.Helper = { }, /** * 按照通用配置来渲染表格 - * option: 和 table.render 选项基本一致, 但需要额外提供: - * idName: 该表格行对象 id 的名称 - * baseUrl: 当前基本 URL, 将在此基础上拼接 list/updateBool/batchOp 等内容 - * batchOpEnum: 批量操作枚举名,若不提供则不渲染批量操作控件和回调逻辑,若为 true 则仅渲染批量删除逻辑 - * 除此以外,cols 内列如果需要开关选项的, 在相应 col 内添加 switchTemplet: true + * @param option 和 table.render 选项基本一致, 但需要额外提供: + * @param option.idName 该表格行对象 id 的名称 + * @param option.baseUrl 当前基本 URL, 将在此基础上拼接 list/updateBool/batchOp 等内容 + * @param option.batchOpEnum 批量操作枚举名,若不提供则不渲染批量操作控件和回调逻辑,若为 true 则仅渲染批量删除逻辑 + *
      除此以外,cols 内列如果需要开关选项的, 在相应 col 内添加 switchTemplet: true */ renderTable: (option) => { const defaultOption = { @@ -99,29 +99,48 @@ window.Helper = { if (!option.baseUrl) throw new Error('baseUrl 不允许为空'); if (!option.baseUrl.endsWith('/')) option.baseUrl += '/'; option.url = option.baseUrl + 'list'; - let tableSwitchTemplet = function () { - // 以 elem 选择器 + '.' + switchFilter 作为 filter - const filter = `${option.elem}.switchFilter`; - layui.form.on(`switch(${filter})`, function (obj) { - console.log(obj, obj.elem.checked); - const data = { - field: obj.elem.dataset.field, - value: obj.elem.checked, - id: obj.elem.dataset.id - }; - $.ajax({ - url: option.baseUrl + 'updateBool', method: 'POST', - data:data, - success: () => Dog.success({time: 1000}), - error: function (res) { - Dog.error({msg: res}) - // 恢复 enabled 状态 - obj.elem.checked = !obj.elem.checked; - layui.form.render('checkbox') - return - } - }) - }); + let tableSwitchTemplet = function() { + // 以 elem 选择器 + '.' + switchFilter 作为 filter + const filter = `${option.elem}.switchFilter`; + layui.form.on(`switch(${filter})`, function(obj) { + console.log(obj, obj.elem.checked); + const data = { + field: obj.elem.dataset.field, + value: obj.elem.checked, + id: obj.elem.dataset.id + }; + $.ajax({ + url: option.baseUrl + 'updateBool', method: 'POST', + data: data, + success: () => { + Dog.success({ time: 1000 }); + let colOption = option.cols[0].find(x => x.field === obj.elem.dataset.field); + let refresh = colOption.refresh, mutex = colOption.mutex; + let tableId = obj.elem.closest('[lay-table-id]').getAttribute('lay-table-id'); + let tableFilter = document.getElementById(tableId).getAttribute('lay-filter'); + if (refresh) { + Dog.reloadTable(tableFilter); + } + else if (mutex) { + // 互斥, 把当前表格内当前列的所有开关都设为与当前状态相反的状态 + // 先找到所有数据 + layui.table.getData(tableFilter).forEach((v, i) => { + if (v[option.idName] != obj.elem.dataset.id) { + // 非我行类,全部置反 + $(`[lay-table-id="${tableFilter}"] tr[data-index="${i}"] [data-field="${obj.elem.dataset.field}"][lay-filter="${filter}"]`).removeAttr('checked') + } + }) + } + }, + error: function(res) { + Dog.error({ msg: res }) + // 恢复 enabled 状态 + obj.elem.checked = !obj.elem.checked; + layui.form.render('checkbox') + return + } + }) + }); return d => { var fieldName = d.LAY_COL.field; return ` { const buttonEl = $(`[lay-filter="${submitButtonFilter}"]`); if (!buttonEl.length) { - Dog.error({msg: `找不到对应 filter 为 ${submitButtonFilter} 的 button`}); + Dog.error({ msg: `找不到对应 filter 为 ${submitButtonFilter} 的 button` }); return } const form = $(buttonEl.parents('.layui-form')[0] || buttonEl.parents('form')[0]); if (!form.length) { - Dog.error({msg: `找不到对应 filter 为 ${submitButtonFilter} 的 button 对应的表单`}); + Dog.error({ msg: `找不到对应 filter 为 ${submitButtonFilter} 的 button 对应的表单` }); return } // 获取 form 内所有表单 const els = form.find('input[name], select[name], textarea[name], button[name]'); - let obj = {form: form[0], field: {}}; + let obj = { form: form[0], field: {} }; $.each(els, (i, el) => { const name = el.name; if (!name) return true @@ -193,12 +212,15 @@ window.Helper = { method: 'POST', contentType: 'application/json', data: JSON.stringify(obj.field), - success: function (r) { - Dog.success({onClose: () => { - if (window.editLayer) layui.layer.close(window.editLayer); - Dog.reloadTable()}}) + success: function(r) { + Dog.success({ + onClose: () => { + if (window.editLayer) layui.layer.close(window.editLayer); + Dog.reloadTable() + } + }) }, - error: res => Dog.error({msg: res}), + error: res => Dog.error({ msg: res }), }); } }) @@ -213,20 +235,23 @@ window.Helper = { const type = fieldEl.type; switch (type) { case 'checkbox': - const checked = fieldEl.value = fieldEl.chceked = val == 'true' || val == true; + const checked = fieldEl.value = val == 'true' || val == true; const laySkin = fieldEl.getAttribute('lay-skin'); + if (checked) fieldEl.setAttribute('checked', ''); + else fieldEl.removeAttribute('checked'); if (laySkin) { - switchFuncs[key] = function (obj) { - obj.elem.value = obj.elem.checked; - layui.form.render(); - } - layui.form.on(`switch(${key})`, function (obj) { - switchFuncs[obj.elem.name](obj); - }) - layui.event.call(this, 'form', `switch(${key})`, { - elem: fieldEl, - value: checked - }); + switchFuncs[key] = function(obj) { + obj.elem.value = obj.elem.checked; + layui.form.render(); + } + layui.form.on(`switch(${key})`, function(obj) { + switchFuncs[obj.elem.name](obj); + }); + layui.form.render(); + layui.event.call(this, 'form', `switch(${key})`, { + elem: fieldEl, + value: checked + }); } break; case 'radio': @@ -252,19 +277,81 @@ window.Helper = { switchFuncs = $.extend(switchFuncs, extraSwitchFuncs) } }, - tableSwitchTemplet: idName => { - layui.form.on('switch(switchFilter)', function (obj) { - console.log(obj, obj.elem.checked); - $.ajax({ - + /** + * @param option.elem dropdown 选择器 + * @param option.tableFilter 数据表格 filter,用以确定操作对哪个表格的数据生效 + * @param option.idName 数据表格 idName + */ + renderDropdown: option => { + const defaultOption = { + }; + option = $.extend(defaultOption, option); + if (!option.elem) throw new Error('elem 选择器不允许为空'); + if (!option.idName) throw new Error('idName 不允许为空'); + if (!option.tableFilter) throw new Error('数据表格 filter 不允许为空'); + if (!option.url) throw new Error('url 不允许为空'); + let click = (data, othis) => { + var checked = layui.table.checkStatus(option.tableFilter), ids = []; + if (!checked.data.length) { + Dog.error('未选中任何项', { time: 1000 }); + return; + } + $.each(checked.data, function(i, row) { + ids.push(row[option.idName]); + }); + data = $.extend(data, { ids: ids }); + var op = async function() { + $.ajax({ + url: option.url, + method: 'POST', + data: data, + success: () => + Dog.success({ + msg: '批量操作成功', + time: 1000, + onClose: () => Dog.reloadTable(option.tableFilter) + }), + error: res => Dog.error({msg: res, time: 2000}) + }) + } + data.op != 'DELETE' ? op() : layer.confirm('确认批量删除吗?该操作不可恢复', function() { + op(); }) - }) - return d => { - var fieldName = d.LAY_COL.field; - return ``; - } + }; + option.click = click; + layui.dropdown.render(option); }, - randomHexString: n => [...Array(n)].map(() => 'abcdef0123456789'[~~(Math.random() * 16)]).join('') + /** + * 复制文本 + * @param text 欲复制的文本 + */ + copyText: text => { + navigator.clipboard.writeText(text).then(() => { + Dog.success({ msg: '复制成功' }) + }).catch(err => { + console.error(err); + Dog.error({ msg: '复制失败' }) + }); + }, + randomHexString: n => [...Array(n)].map(() => 'abcdef0123456789'[~~(Math.random() * 16)]).join(''), + randomWordString: n => [...Array(n)].map(() => 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'[~~(Math.random() * 62)]).join(''), + /** + * 监听所有的 input change 事件,包括未来生成的,通过脚本设置值的。只需要运行一次 + */ + listenAllInputChange: function() { + if (window.Helper && window.Helper.__listenAllInputChangeFlag) return; + window.Helper.__listenAllInputChangeFlag = !0; + const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); + Object.defineProperty(HTMLInputElement.prototype, 'value', { + get: descriptor.get, + set: function(val) { + const old = descriptor.get.call(this); + descriptor.set.call(this, val); + + if (val !== old) { + this.dispatchEvent(new Event('input', { bubbles: true })); + } + } + }); + } } \ No newline at end of file diff --git a/src/main/resources/webpage/admin/v1/include.html b/src/main/resources/webpage/admin/v1/include.html index ece2c68..3f8e813 100644 --- a/src/main/resources/webpage/admin/v1/include.html +++ b/src/main/resources/webpage/admin/v1/include.html @@ -53,29 +53,29 @@ class="fa-fw fa-solid fa-screwdriver-wrench"> 管理
      -
      - 计划任务 - -
      请求配置
      -
      - 指标配置 - -
      代理配置
      +
      + 计划任务 + +
      +
      + 指标信息 + +
      Protocol 配置 + class="fa-fw fa-regular fa-handshake"> 协议信息
      @@ -88,16 +88,6 @@ class="fa-fw fa-solid fa-gears"> 设置
      -
      - 请求头设置 - -
      -
      - 代理设置 - -
    • IP 属地: 加载中... [[${@proxyConfig.ipInfo.geoString}]] - + th:if="${ipInfo != null}" + class="layui-badge layui-bg-cyan">[[${ipInfo.geoString}]] + +
      - [[${@proxyConfig.ipInfo.ip}]] + [[${ipInfo.ip}]]
      -
      - [[${@proxyConfig.ipInfo.ipv6}]] +
      + [[${ipInfo.ipv6}]]
      -
    • + + + + + +
    diff --git a/src/main/resources/webpage/admin/v1/manage/proxySetting/include.html b/src/main/resources/webpage/admin/v1/manage/proxySetting/include.html index 7dd5093..2fed639 100644 --- a/src/main/resources/webpage/admin/v1/manage/proxySetting/include.html +++ b/src/main/resources/webpage/admin/v1/manage/proxySetting/include.html @@ -11,6 +11,13 @@ +
    + +
    + +
    +
    diff --git a/src/main/resources/webpage/admin/v1/manage/proxySetting/index.html b/src/main/resources/webpage/admin/v1/manage/proxySetting/index.html index ac3bc13..d817886 100644 --- a/src/main/resources/webpage/admin/v1/manage/proxySetting/index.html +++ b/src/main/resources/webpage/admin/v1/manage/proxySetting/index.html @@ -10,7 +10,7 @@

    - + 代理设置新增

    @@ -40,95 +40,33 @@ }) .use(['table', 'form', 'dropdown', 'layer', 'xmSelect', 'cron'], function(){ var dropdown = layui.dropdown, table = layui.table, form = layui.form; - table.render({ + Helper.renderTable({ elem: '#proxySettings', - url:'/admin/v1/manage/proxySetting/list', + baseUrl:'/admin/v1/manage/proxySetting', page:true, skin:'line', + idName: 'id', cols: [ [ {type:'checkbox'}, {field:'id', hide: true, width: 60, title: 'ID'}, + {field:'isDefault', title: '默认', width: 95, switchTemplet: true, mutex: true}, {field:'proxyName', title: '名称'}, {field:'proxyType', title: '类型'}, {field:'proxyHost', title: '主机'}, {field:'proxyPort', title: '端口'}, - {field:'ignoreHttpsVerification', title: '忽略 HTTPS 校验', width: 95, templet: Helper.tableSwitchTemplet('id')}, + {field:'ignoreHttpsVerification', title: '忽略 HTTPS 校验', width: 95, switchTemplet: true}, {field:'operation', title: '操作', toolbar: '#operationTpl'} ]] }); - form.on('switch(switchFilter)', function(obj) { - console.log(obj); - console.log(obj.elem.checked); - $.ajax({ - url: '/admin/v1/manage/proxySetting/updateBool', - method: 'POST', - data: { - id: obj.elem.dataset.id, - field: obj.elem.dataset.field, - value: obj.elem.checked - }, - success: () => Dog.success({time: 1000}), - error: function (res) { - Dog.error({msg: res}) - // 恢复 enabled 状态 - obj.elem.checked = !obj.elem.checked; - layui.form.render('checkbox') - return - } - }) - }); - dropdown.render({ + Helper.renderDropdown({ elem: '.operdown', data: [ {title: '删除', op: 'DELETE'}, - {title: '启用', op: 'ENABLE'}, - {title: '停用', op: 'DISABLE'}, - {title: '开启交易日校验', op: 'ENABLE_OPEN_DAY_CHECK'}, - {title: '关闭交易日校验', op: 'DISABLE_OPEN_DAY_CHECK'}], - click: function (data, othis){ - var checked = layui.table.checkStatus('proxySettings'), planIds = []; - if (!checked.data.length) { - layui.layer.msg('未选中任何项', {time: 1000}); - return; - } - $.each(checked.data, function (i, plan){ - planIds.push(plan.planId); - }); - data = $.extend(data, {ids: planIds}); - var op = async function() { - $.ajax({ - url: '/admin/v1/manage/plan/batchOp', - method: 'POST', - data: data, - success: function () { - layer.msg('批量操作成功', { - offset: '15px', - icon: 1, - time: 1000 - }, - function() { - layui.table.reload('proxySettings', { - page: { - curr: $(".layui-laypage-em").next().html() //当前页码值 - } - }); - } - ) - }, - error: function (res) { - var r = res.responseJSON; - layer.msg(r&&r.data||'服务器错误', { - offset: '15px', - icon: 2, - time: 1000 - }); - return - } - }) - } - data.op != 'DELETE' ? op() : layer.confirm('确认批量删除吗?该操作不可恢复', function(){ - op(); - }) - } + {title: '连通性检查', op: 'CHECK'}, + {title: '停用 HTTPS 证书校验', op: 'DISABLE_HTTPS_VERIFY'}, + {title: '启用 HTTPS 证书校验', op: 'ENABLE_HTTP_VERIFY'}], + tableFilter: 'proxySettings', + url: '/admin/v1/manage/proxySetting/batchOp', + idName: 'id' }); }) diff --git a/src/main/resources/webpage/admin/v1/manage/requestInfo/include.html b/src/main/resources/webpage/admin/v1/manage/requestInfo/include.html index a9f23da..112cba1 100644 --- a/src/main/resources/webpage/admin/v1/manage/requestInfo/include.html +++ b/src/main/resources/webpage/admin/v1/manage/requestInfo/include.html @@ -6,15 +6,15 @@
    - +
    - +
    - +
    @@ -98,6 +98,7 @@ diff --git a/src/test/java/ByteBuddyTest.java b/src/test/java/ByteBuddyTest.java index 3bab031..fc6ea33 100644 --- a/src/test/java/ByteBuddyTest.java +++ b/src/test/java/ByteBuddyTest.java @@ -16,7 +16,7 @@ public class ByteBuddyTest { PatchOkHttp.apply( r -> r.not(a -> a.isHttps()) - .overrideIf("User-Agent", "okhttp/3.12.2") + .overrideHeader("User-Agent", "okhttp/3.12.2") ); OkHttpClient client = new OkHttpClient(); diff --git a/src/test/java/EmoneyScraper.java b/src/test/java/EmoneyScraper.java deleted file mode 100644 index fba929a..0000000 --- a/src/test/java/EmoneyScraper.java +++ /dev/null @@ -1,68 +0,0 @@ -import com.microsoft.playwright.*; -import com.microsoft.playwright.BrowserType.LaunchOptions; -import com.microsoft.playwright.Route.ResumeOptions; -import com.microsoft.playwright.options.HttpHeader; -import com.microsoft.playwright.options.Proxy; -import com.microsoft.playwright.options.WaitUntilState; - -import java.util.*; - -public class EmoneyScraper { - public static void main(String[] args) { - try (Playwright playwright = Playwright.create()) { - // 设置浏览器上下文选项,包括 User-Agent - Proxy proxy = new Proxy("http://127.0.0.1:8888"); - LaunchOptions launchOptions = new BrowserType.LaunchOptions() - .setHeadless(true) - .setProxy(proxy); - Browser browser = playwright.chromium().launch(launchOptions); - BrowserContext context = browser.newContext(new Browser.NewContextOptions() - .setUserAgent("Mozilla/5.0 (Linux; Android 9; PCRT00 Build/PQ3B.190801.11191547; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Mobile Safari/537.36")); - - // 设置全局请求头 - Map headers = new HashMap<>(); - headers.put("Upgrade-Insecure-Requests", "1"); - headers.put("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"); - headers.put("X-Requested-With", "cn.emoney.emstock"); - headers.put("Sec-Fetch-Site", "none"); - headers.put("Sec-Fetch-Mode", "navigate"); - headers.put("Sec-Fetch-User", "?1"); - headers.put("Sec-Fetch-Dest", "document"); - headers.put("Accept-Encoding", "gzip, deflate"); - headers.put("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7"); - - context.setExtraHTTPHeaders(headers); - context.route("**/*", route -> { - Request request = route.request(); - List requestHeaderList = request.headersArray(); - Map requestHeaders = new HashMap<>(); - for (HttpHeader header : requestHeaderList) { - requestHeaders.put(header.name, header.value); - } - requestHeaders.remove("sec-ch-ua"); - requestHeaders.remove("sec-ch-ua-mobile"); - requestHeaders.remove("sec-ch-ua-platform"); - route.resume(new ResumeOptions().setHeaders(requestHeaders)); - }); - - Page page = context.newPage(); - - page.onResponse(handler -> { - String url = handler.request().url(); - if (url.endsWith(".js")) { - String str = handler.request().response().text(); - System.out.println(str); - } - }); - - String url = "https://appstatic.emoney.cn/html/emapp/stock/note/?name=10003001&emoneyScaleType=0&emoneyLandMode=0&token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkpXVCJ9.eyJ1dWQiOjEwMTA2NjAwNDcsInVpZCI6MjQ3NjY5MDIsImRpZCI6IjdmOTA3N2VjZTlmMWIxMjQ4NmZmYjRmNDhjMzdkODJhIiwidHlwIjoxLCJhY2MiOiJlbXkxNzMwOTc4Iiwic3d0IjoxLCJsZ3QiOjE3NDQ1OTY2NTY3MTgsIm5iZiI6MTc0NDU5NjY1NiwiZXhwIjoxNzQ2MzI0NjU2LCJpYXQiOjE3NDQ1OTY2NTZ9.SHLqiavrzkwtLfxaWbA6GfnF7iBqbjnv86PTOhumiGc"; - - page.navigate(url, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE)); - - String html = page.content(); - System.out.println(html); - - browser.close(); - } - } -} diff --git a/src/test/java/FingerprintsSpliter.java b/src/test/java/FingerprintsSpliter.java index bf4eecc..f6b4e05 100644 --- a/src/test/java/FingerprintsSpliter.java +++ b/src/test/java/FingerprintsSpliter.java @@ -2,10 +2,8 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import java.util.regex.Matcher; import java.util.regex.Pattern; -import quant.rich.emoney.entity.config.EmoneyRequestConfig; import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo; public class FingerprintsSpliter { diff --git a/src/test/java/quant/rich/EmoneyIndexScraper.java b/src/test/java/quant/rich/EmoneyIndexScraper.java index 5b6cdee..cf28bf0 100644 --- a/src/test/java/quant/rich/EmoneyIndexScraper.java +++ b/src/test/java/quant/rich/EmoneyIndexScraper.java @@ -34,9 +34,9 @@ import okhttp3.Response; import quant.rich.emoney.EmoneyAutoApplication; import quant.rich.emoney.client.EmoneyClient; import quant.rich.emoney.client.OkHttpClientProvider; -import quant.rich.emoney.entity.config.EmoneyRequestConfig; import quant.rich.emoney.pojo.dto.NonParamsIndexDetail; import quant.rich.emoney.pojo.dto.NonParamsIndexDetail.NonParamsIndexDetailData; +import quant.rich.emoney.service.sqlite.RequestInfoService; @SpringBootTest @ContextConfiguration(classes = EmoneyAutoApplication.class) @@ -47,7 +47,7 @@ public class EmoneyIndexScraper { private static final ObjectMapper MAPPER = new ObjectMapper(); @Autowired - EmoneyRequestConfig emoneyRequestConfig; + RequestInfoService requestInfoService; static { MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); @@ -58,7 +58,7 @@ public class EmoneyIndexScraper { urlBuilder.append("https://appstatic.emoney.cn/html/emapp/stock/note/?name="); urlBuilder.append(indexCode.toString()); urlBuilder.append("&emoneyScaleType=0&emoneyLandMode=0&token="); - urlBuilder.append(emoneyRequestConfig.getAuthorization()); + urlBuilder.append(requestInfoService.getDefaultRequestInfo().getAuthorization()); return urlBuilder.toString(); } @@ -75,7 +75,7 @@ public class EmoneyIndexScraper { .header("Host", "appstatic.emoney.cn") .header("Connection", "keep-alive") .header("Upgrade-Insecure-Requests", "1") - .header("User-Agent", emoneyRequestConfig.getWebviewUserAgent()) + .header("User-Agent", requestInfoService.getDefaultRequestInfo().getWebviewUserAgent()) .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9") .header("X-Request-With", "cn.emoney.emstock") .header("Sec-Fetch-Site", "none") @@ -124,7 +124,7 @@ public class EmoneyIndexScraper { Request.Builder scriptBuilder = new Request.Builder() .header("Host", "appstatic.emoney.cn") .header("Connection", "keep-alive") - .header("User-Agent", emoneyRequestConfig.getWebviewUserAgent()) + .header("User-Agent", requestInfoService.getDefaultRequestInfo().getWebviewUserAgent()) .header("Accept", "*/*") .header("X-Request-With", "cn.emoney.emstock") .header("Sec-Fetch-Site", "same-origin") @@ -160,7 +160,6 @@ public class EmoneyIndexScraper { // 将每个 jsonString 转换为 jsonArray,进一步转换成 IndexDetail List valid = new ArrayList<>(); - List arrayNodes = new ArrayList<>(); for (String jsonString : matchGroups) { try { JsonNode root = MAPPER.readTree(jsonString); @@ -189,7 +188,7 @@ public class EmoneyIndexScraper { Request.Builder imageBuilder = new Request.Builder() .header("Host", "appstatic.emoney.cn") .header("Connection", "keep-alive") - .header("User-Agent", emoneyRequestConfig.getWebviewUserAgent()) + .header("User-Agent", requestInfoService.getDefaultRequestInfo().getWebviewUserAgent()) .header("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8") .header("X-Request-With", "cn.emoney.emstock") .header("Sec-Fetch-Site", "same-origin") diff --git a/src/test/java/quant/rich/PatchOkHttpTest.java b/src/test/java/quant/rich/PatchOkHttpTest.java index e21fdd9..18f808f 100644 --- a/src/test/java/quant/rich/PatchOkHttpTest.java +++ b/src/test/java/quant/rich/PatchOkHttpTest.java @@ -31,7 +31,7 @@ public class PatchOkHttpTest { .or(c -> c.hostContains("localhost")) .or(a -> a.hostContains("emapp")) .or(b -> b.hasHeaderName("X-Protocol-Id")) - .overrideIf("User-Agent", "okhttp/3.12.2") + .overrideHeader("User-Agent", "okhttp/3.12.2") .build(); context = new RequestContext(new Request.Builder()