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 + * 例如: + * 当不指定时,不校验参数,单纯校验 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 路径要继承后自己写。

+ * 可获得的能力: + *

- * 来源: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 aa37305..90418f3 100644 Binary files a/src/main/resources/database.db and b/src/main/resources/database.db differ diff --git a/src/main/resources/static/admin/v1/static/js/helper.js b/src/main/resources/static/admin/v1/static/js/helper.js index b68453c..4a91b50 100644 --- a/src/main/resources/static/admin/v1/static/js/helper.js +++ b/src/main/resources/static/admin/v1/static/js/helper.js @@ -1,6 +1,6 @@ if (!window.Helper) { window.Helper = {} } window.Helper = { - emoneyPeriodToName: x => 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()