删除 EmoneyRequestConfig 和 ProxyConfig 设置,改为数据库(SQLite)配置。默认配置的设置和删除逻辑由
SQLite 触发器配置。
This commit is contained in:
7
pom.xml
7
pom.xml
@@ -171,13 +171,6 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/com.microsoft.playwright/playwright -->
|
||||
<dependency>
|
||||
<groupId>com.microsoft.playwright</groupId>
|
||||
<artifactId>playwright</artifactId>
|
||||
<version>1.51.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-text</artifactId>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package quant.rich.emoney.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
import quant.rich.emoney.component.CallerLockAspect;
|
||||
|
||||
/**
|
||||
* 在方法上添加此注解,可针对调用方加锁,即:<br>
|
||||
* 调用方法为 A 的,多次从 A 调用则加锁,从 B 调用时不受影响<br>
|
||||
* 需要开启 AOP:在任意配置类上增加注解:<i><code>@EnableAspectJAutoProxy</code></i>
|
||||
* @see CallerLockAspect
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface LockByCaller {
|
||||
/**
|
||||
* 可选参数,用于 SpEL 表达式获取 key
|
||||
* 例如:<ul>
|
||||
* <li>@LockByCaller(key = "#userId")</li>
|
||||
* <li>@LockByCaller(key = "#userId + ':' + #userName")</li>
|
||||
* </ul>
|
||||
* 当不指定时,不校验参数,单纯校验 Caller
|
||||
*/
|
||||
String key() default "";
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
if (proxySettingService == null) {
|
||||
return null;
|
||||
}
|
||||
return proxyConfig;
|
||||
return proxySettingService.getDefaultProxySetting();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,10 +62,10 @@ public class OkHttpClientProvider {
|
||||
* @return
|
||||
*/
|
||||
public static OkHttpClient getInstance(Consumer<OkHttpClient.Builder> builderConsumer) {
|
||||
ProxyConfig proxyConfig = getProxyConfig();
|
||||
ProxySetting proxySetting = getDefaultProxySetting();
|
||||
return getInstance(
|
||||
proxyConfig.getProxy(),
|
||||
proxyConfig.getIgnoreHttpsVerification(),
|
||||
proxySetting.getProxy(),
|
||||
proxySetting.getIgnoreHttpsVerification(),
|
||||
builderConsumer);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在方法上添加此注解,可针对调用方加锁,即:<br>
|
||||
* 调用方法为 A 的,多次从 A 调用则加锁,从 B 调用时不受影响<br>
|
||||
* 需要开启 AOP:在任意配置类上增加注解:<i><code>@EnableAspectJAutoProxy</code></i>
|
||||
* @see CallerLockAspect
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public static @interface LockByCaller {
|
||||
/**
|
||||
* 可选参数,用于 SpEL 表达式获取 key
|
||||
* 例如:<ul>
|
||||
* <li>@LockByCaller(key = "#userId")</li>
|
||||
* <li>@LockByCaller(key = "#userId + ':' + #userName")</li>
|
||||
* </ul>
|
||||
* 当不指定时,不校验参数,单纯校验 Caller
|
||||
*/
|
||||
String key() default "";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 已鉴权、代理已配置,否则不允许进入方法
|
||||
* <p>需要开启 AOP:在任意配置类上增加注解:<i><code>@EnableAspectJAutoProxy</code></i>
|
||||
* @see RequireAuthAndProxyAspect
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public static @interface RequireAuthAndProxy {
|
||||
/**
|
||||
* 当存在默认请求配置但未鉴权时,是否自动鉴权
|
||||
* @return
|
||||
*/
|
||||
boolean autoLogin() default false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,7 +13,9 @@ import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
|
||||
/**
|
||||
* 实现自动化注册 Config
|
||||
* 实现自动化注册 Config<p>
|
||||
* 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ import quant.rich.emoney.interfaces.IConfig;
|
||||
import quant.rich.emoney.service.ConfigService;
|
||||
|
||||
/**
|
||||
* 实现配置项自动载入
|
||||
* @param <T>
|
||||
* 配置类工厂
|
||||
* @param <T> 配置类
|
||||
*
|
||||
*/
|
||||
@Slf4j
|
||||
public class ConfigServiceFactoryBean<T extends IConfig<T>> implements FactoryBean<T>, BeanNameAware {
|
||||
@@ -37,24 +38,19 @@ public class ConfigServiceFactoryBean<T extends IConfig<T>> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,25 @@ import org.springframework.beans.factory.BeanCreationException;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* To prevent bean cyclic instantiation through BeanFactory:<p>
|
||||
* 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.<p>
|
||||
* 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.<p>
|
||||
* 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<Set<Class<?>>> 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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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("无法配置用户名和密码,请检查");
|
||||
|
||||
@@ -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<String, List<MethodInfo>> responseDecodeExtensions = new HashMap<String, List<MethodInfo>>();
|
||||
|
||||
@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<MethodInfo> 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<MethodInfo> 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);
|
||||
}
|
||||
// invoke
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<p>
|
||||
*
|
||||
* 继承后,可直接通过 {@code thisType} 和 {@code thisService} 获取实体类型和对应的服务实例<p>
|
||||
* 也获得部分能力,但控制方法及 Mapping 路径要继承后自己写。<p>
|
||||
* 可获得的能力:
|
||||
* <ul>
|
||||
* <li>list
|
||||
* <li>getOne
|
||||
* <li>delete
|
||||
* <ul>
|
||||
*
|
||||
* @param <T> 实体类型
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class ServiceController<T> extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext ctx;
|
||||
|
||||
protected IService<?> thisService;
|
||||
|
||||
protected Class<?> thisType;
|
||||
|
||||
@PostConstruct
|
||||
void init() {
|
||||
@SuppressWarnings("rawtypes")
|
||||
Map<String, IService> beans = ctx.getBeansOfType(IService.class);
|
||||
ResolvableType thisType = ResolvableType.forClass(this.getClass()).as(ServiceController.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
// 获取本类的实体类
|
||||
Class<T> clazz = (Class<T>) thisType.getGeneric(0).resolve();
|
||||
this.thisType = clazz;
|
||||
for (IService<?> service : beans.values()) {
|
||||
ResolvableType type = ResolvableType.forClass(service.getClass()).as(IService.class);
|
||||
Class<?> entityType = type.getGeneric(0).resolve();
|
||||
if (entityType == clazz) {
|
||||
this.thisService = service;
|
||||
}
|
||||
}
|
||||
if (thisService == null) {
|
||||
log.error("获取本例实体类服务失败,请检查");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected IService<T> getThisService() {
|
||||
return (IService<T>)this.thisService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回实例类列表
|
||||
* @param pageReq
|
||||
* @return
|
||||
*/
|
||||
protected LayPageResp<?> list(LayPageReq<T> pageReq) {
|
||||
Page<T> planPage = getThisService().page(pageReq);
|
||||
return new LayPageResp<>(planPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 id 获取实例化对象。如果 id 为空则返回通过默认无参构造器构造的新实例化对象
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
protected R<?> getOne(Serializable id) {
|
||||
// id 为空,返回一个新实例化对象
|
||||
if (id == null) {
|
||||
try {
|
||||
return R.ok(thisType.getConstructor().newInstance());
|
||||
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
|
||||
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
|
||||
final String s = "根据默认构造器创建新实例化对象失败";
|
||||
log.error(s, e);
|
||||
throw RException.internalServerError(s);
|
||||
}
|
||||
}
|
||||
|
||||
// 否则从数据库取
|
||||
T exist = getThisService().getById(id);
|
||||
return R.judge(exist != null, exist, "无法找到对应 ID 的 ProxySetting");
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
* @param object
|
||||
* @return
|
||||
*/
|
||||
protected R<?> save(T object) {
|
||||
return
|
||||
R.judge(
|
||||
() -> getThisService().saveOrUpdate(object),
|
||||
"新增或保存失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
protected R<?> delete(Serializable id) {
|
||||
return R.judge(getThisService().removeById(id), "删除失败,是否已删除?");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package quant.rich.emoney.controller.common;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.exception.RException;
|
||||
import quant.rich.emoney.pojo.dto.R;
|
||||
|
||||
@Slf4j
|
||||
public abstract class UpdateBoolController<T> extends BaseController {
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext ctx;
|
||||
|
||||
private Map<Class<?>, IService<?>> serviceMap;
|
||||
|
||||
@PostConstruct
|
||||
void init() {
|
||||
serviceMap = new HashMap<>();
|
||||
@SuppressWarnings("rawtypes")
|
||||
Map<String, IService> beans = ctx.getBeansOfType(IService.class);
|
||||
for (IService<?> service : beans.values()) {
|
||||
ResolvableType type = ResolvableType.forClass(service.getClass()).as(IService.class);
|
||||
Class<?> entityType = type.getGeneric(0).resolve();
|
||||
if (entityType != null) {
|
||||
serviceMap.put(entityType, service);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public IService<T> getService(Class<T> entityClass) {
|
||||
return (IService<T>) serviceMap.get(entityClass);
|
||||
}
|
||||
|
||||
@PostMapping("/updateBool")
|
||||
@ResponseBody
|
||||
protected
|
||||
R<?> updateBool(String id, String field, Boolean value) {
|
||||
|
||||
|
||||
ResolvableType type = ResolvableType.forClass(this.getClass()).as(UpdateBoolController.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<T> clazz = (Class<T>) type.getGeneric(0).resolve();
|
||||
|
||||
TableInfo tableInfo = TableInfoHelper.getTableInfo(clazz);
|
||||
Object converted = mapper.convertValue(id, tableInfo.getKeyType());
|
||||
|
||||
// 获取 Service
|
||||
IService<T> s = getService((Class<T>) clazz);
|
||||
|
||||
try {
|
||||
String idField = tableInfo.getKeyColumn();
|
||||
Field declaredField = clazz.getDeclaredField(field);
|
||||
Optional<TableFieldInfo> fieldInfo = tableInfo.getFieldList().stream()
|
||||
.filter(f -> f.getProperty().equals(field))
|
||||
.findFirst();
|
||||
if (declaredField.getType().equals(Boolean.class)) {
|
||||
return R.judge(s.update(
|
||||
new UpdateWrapper<T>()
|
||||
.set(fieldInfo.get().getColumn(), value)
|
||||
.eq(idField, converted)
|
||||
), "更新失败,请查看日志");
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error("update bool failed", e);
|
||||
}
|
||||
throw RException.badRequest().setLogRequest(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package quant.rich.emoney.controller.common;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.exception.RException;
|
||||
import quant.rich.emoney.pojo.dto.R;
|
||||
|
||||
/**
|
||||
* 更新实体类中 Boolean 字段的抽象控制器,一般用于实体类中包含 Boolean 字段的前端更新
|
||||
* <p>
|
||||
* 前端实体类数据列表中,常有 CheckBox 或 Switch 等控件,希望通过点击数据表中的控件直接修改行对象
|
||||
* Boolean 值的,可用该方法。需要引入功能的需 extends 本类,如对 {@code Plan} 生效,则可在其控制器
|
||||
* {@code PlanController} 中:<p>
|
||||
*
|
||||
* <code> PlanController <b>extends</b> UpdateBoolController<Plan></code>
|
||||
* @param <T> 实体类型
|
||||
* @see #updateBool(String, String, Boolean)
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class UpdateBoolServiceController<T> extends ServiceController<T> {
|
||||
|
||||
protected ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 更新布尔值主方法,以 form 形式 POST,uri: /updateBool,表单字段名需与该方法参数名一致
|
||||
* @param id 欲修改的实体类的实例化对象的主键值
|
||||
* @param field 欲修改的实体类的实例化对象的布尔字段名
|
||||
* @param value 需要修改为的布尔值
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/updateBool")
|
||||
@ResponseBody
|
||||
protected
|
||||
R<?> updateBool(String id, String field, Boolean value) {
|
||||
|
||||
|
||||
// 获取表信息
|
||||
TableInfo tableInfo = TableInfoHelper.getTableInfo(thisType);
|
||||
Object converted = mapper.convertValue(id, tableInfo.getKeyType());
|
||||
|
||||
// 获取 Service
|
||||
|
||||
try {
|
||||
// 获取主键名
|
||||
String idField = tableInfo.getKeyColumn();
|
||||
// 获取指定布尔字段的字段信息
|
||||
Field declaredField = thisType.getDeclaredField(field);
|
||||
// 获取指定布尔字段在数据表中的映射字段信息
|
||||
Optional<TableFieldInfo> fieldInfo = tableInfo.getFieldList().stream()
|
||||
.filter(f -> f.getProperty().equals(field))
|
||||
.findFirst();
|
||||
|
||||
if (fieldInfo.isEmpty()) {
|
||||
throw RException.badRequest("无法根据 field: " + field + " 找到类内字段信息");
|
||||
}
|
||||
|
||||
if (declaredField.getType().equals(Boolean.class)
|
||||
|| declaredField.getType().equals(boolean.class)
|
||||
) {
|
||||
return R.judge(getThisService().update(
|
||||
new UpdateWrapper<T>()
|
||||
.set(fieldInfo.get().getColumn(), value)
|
||||
.eq(idField, converted)
|
||||
), "更新失败,请查看日志");
|
||||
}
|
||||
else {
|
||||
throw RException.badRequest("field: " + field + " 不为布尔值类型字段");
|
||||
}
|
||||
}
|
||||
catch (NoSuchFieldException | SecurityException e) {
|
||||
throw RException.badRequest("获取字段 " + field + " 错误");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package quant.rich.emoney.controller.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.controller.common.BaseController;
|
||||
import quant.rich.emoney.entity.config.ProxyConfig;
|
||||
import quant.rich.emoney.pojo.dto.R;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/admin/v1/config/proxy")
|
||||
public class ProxyConfigControllerV1 extends BaseController {
|
||||
|
||||
@Autowired
|
||||
ProxyConfig proxyConfig;
|
||||
|
||||
|
||||
@GetMapping("/refreshIpThroughProxy")
|
||||
@ResponseBody
|
||||
public R<?> refreshIpThroughProxy() {
|
||||
return R.ok(proxyConfig.refreshIpThroughProxy());
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package quant.rich.emoney.controller.manage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.aop.framework.AopProxyUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -25,7 +25,7 @@ import quant.rich.emoney.service.IndexDetailService;
|
||||
public class IndexInfoControllerV1 extends BaseController {
|
||||
|
||||
@Autowired
|
||||
IndexInfoConfig indexInfo;
|
||||
IndexInfoConfig indexInfoConfig;
|
||||
|
||||
@Autowired
|
||||
IndexDetailService indexDetailService;
|
||||
@@ -35,6 +35,11 @@ public class IndexInfoControllerV1 extends BaseController {
|
||||
return "/admin/v1/manage/indexInfo/index";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指标详情解释
|
||||
* @param indexCode
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/getIndexDetail")
|
||||
@ResponseBody
|
||||
public R<?> getIndexDetail(String indexCode) {
|
||||
@@ -43,6 +48,11 @@ public class IndexInfoControllerV1 extends BaseController {
|
||||
indexDetailService.getIndexDetail(indexCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制刷新并获取指标详情解释
|
||||
* @param indexCode
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/forceRefreshAndGetIndexDetail")
|
||||
@ResponseBody
|
||||
public R<?> forceRefreshAndGetIndexDetail(String indexCode) {
|
||||
@@ -51,21 +61,14 @@ public class IndexInfoControllerV1 extends BaseController {
|
||||
indexDetailService.forceRefreshAndGetIndexDetail(indexCode));
|
||||
}
|
||||
|
||||
@GetMapping("/configIndOnline")
|
||||
@ResponseBody
|
||||
public R<?> configIndOnline(String url) throws IOException {
|
||||
|
||||
//return R.judge(() -> indexInfo.getOnlineConfigByUrl(url));
|
||||
return R.ok(indexInfo.getConfigIndOnline());
|
||||
}
|
||||
|
||||
@GetMapping("/getFields")
|
||||
@ResponseBody
|
||||
public R<?> getFields(@RequestParam("fields") String[] fields) {
|
||||
if (fields == null || fields.length == 0) {
|
||||
return R.ok(indexInfo);
|
||||
return R.ok(indexInfoConfig);
|
||||
}
|
||||
ObjectNode indexInfoJson = new ObjectMapper().valueToTree(indexInfo);
|
||||
Object indexInfoConfigWithoutProxy = AopProxyUtils.getSingletonTarget(indexInfoConfig);
|
||||
ObjectNode indexInfoJson = new ObjectMapper().valueToTree(indexInfoConfigWithoutProxy);
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
for (String field : fields) {
|
||||
map.put(field, indexInfoJson.get(field));
|
||||
@@ -73,17 +76,21 @@ public class IndexInfoControllerV1 extends BaseController {
|
||||
return R.ok(map);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据给定 url 获取在线指标配置
|
||||
* @param url
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/getConfigIndOnlineByUrl")
|
||||
@ResponseBody
|
||||
public R<?> getConfigOnlineByUrl(String url) {
|
||||
return R.judge(() -> indexInfo.getOnlineConfigByUrl());
|
||||
return R.judge(() -> indexInfoConfig.getOnlineConfigByUrl(url));
|
||||
}
|
||||
|
||||
@GetMapping("/getIndexInfoConfig")
|
||||
@ResponseBody
|
||||
public R<?> getIndexInfoConfig() {
|
||||
return R.ok(indexInfo);
|
||||
return R.ok(indexInfoConfig);
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
|
||||
@@ -13,11 +13,8 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.controller.common.UpdateBoolController;
|
||||
import quant.rich.emoney.controller.common.UpdateBoolServiceController;
|
||||
import quant.rich.emoney.entity.sqlite.Plan;
|
||||
import quant.rich.emoney.exception.RException;
|
||||
import quant.rich.emoney.interfaces.IQueryableEnum;
|
||||
@@ -29,7 +26,7 @@ import quant.rich.emoney.service.sqlite.PlanService;
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/admin/v1/manage/plan")
|
||||
public class PlanControllerV1 extends UpdateBoolController<Plan> {
|
||||
public class PlanControllerV1 extends UpdateBoolServiceController<Plan> {
|
||||
|
||||
@Autowired
|
||||
PlanService planService;
|
||||
@@ -42,35 +39,25 @@ public class PlanControllerV1 extends UpdateBoolController<Plan> {
|
||||
@GetMapping("/list")
|
||||
@ResponseBody
|
||||
public LayPageResp<?> list(LayPageReq<Plan> pageReq) {
|
||||
Page<Plan> planPage = planService.page(pageReq);
|
||||
return new LayPageResp<>(planPage);
|
||||
return super.list(pageReq);
|
||||
}
|
||||
|
||||
@GetMapping("/getOne")
|
||||
@ResponseBody
|
||||
public R<?> getOne(String planId) {
|
||||
// 如果 planId 是空,说明可能希望新建一个 Plan,需要返回默认实例化对象,否则从数据库取
|
||||
return
|
||||
planId == null ? R.ok(new Plan()) :
|
||||
R.judgeNonNull(planService.getById(planId), "无法找到对应 ID 的 Plan");
|
||||
return super.getOne(planId);
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
@ResponseBody
|
||||
public R<?> save(@RequestBody Plan plan) {
|
||||
if (StringUtils.isNotBlank(plan.getPlanId())) {
|
||||
planService.updateById(plan);
|
||||
}
|
||||
else {
|
||||
planService.save(plan.setPlanId(null));
|
||||
}
|
||||
return R.ok();
|
||||
return super.save(plan);
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@ResponseBody
|
||||
public R<?> delete(String planId) {
|
||||
return R.judge(planService.removeById(planId), "删除失败,是否已删除?");
|
||||
return super.delete(planId);
|
||||
}
|
||||
|
||||
@PostMapping("/batchOp")
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package quant.rich.emoney.controller.manage;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -16,14 +13,8 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.controller.common.BaseController;
|
||||
import quant.rich.emoney.controller.common.UpdateBoolServiceController;
|
||||
import quant.rich.emoney.entity.sqlite.ProxySetting;
|
||||
import quant.rich.emoney.exception.RException;
|
||||
import quant.rich.emoney.pojo.dto.LayPageReq;
|
||||
@@ -34,7 +25,7 @@ import quant.rich.emoney.service.sqlite.ProxySettingService;
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/admin/v1/manage/proxySetting")
|
||||
public class ProxySettingControllerV1 extends BaseController {
|
||||
public class ProxySettingControllerV1 extends UpdateBoolServiceController<ProxySetting> {
|
||||
|
||||
@Autowired
|
||||
ProxySettingService proxySettingService;
|
||||
@@ -47,62 +38,31 @@ public class ProxySettingControllerV1 extends BaseController {
|
||||
@GetMapping("/list")
|
||||
@ResponseBody
|
||||
public LayPageResp<?> list(LayPageReq<ProxySetting> pageReq) {
|
||||
Page<ProxySetting> planPage = proxySettingService.page(pageReq);
|
||||
return new LayPageResp<>(planPage);
|
||||
return super.list(pageReq);
|
||||
}
|
||||
|
||||
@GetMapping("/getOne")
|
||||
@ResponseBody
|
||||
public R<?> getOne(String id) {
|
||||
|
||||
// 如果 planId 是空,说明可能希望新建一个 ProxySetting,需要返回默认实例化对象
|
||||
if (id == null) {
|
||||
return R.ok(new ProxySetting());
|
||||
}
|
||||
|
||||
// 否则从数据库取
|
||||
ProxySetting proxy = proxySettingService.getById(id);
|
||||
return R.judge(proxy != null, proxy, "无法找到对应 ID 的 ProxySetting");
|
||||
}
|
||||
|
||||
@PostMapping("/updateBool")
|
||||
@ResponseBody
|
||||
public R<?> updateBool(String id, String field, Boolean value) {
|
||||
TableInfo tableInfo = TableInfoHelper.getTableInfo(ProxySetting.class);
|
||||
try {
|
||||
Field declaredField = ProxySetting.class.getDeclaredField(field);
|
||||
|
||||
Optional<TableFieldInfo> fieldInfo = tableInfo.getFieldList().stream()
|
||||
.filter(f -> f.getProperty().equals(field))
|
||||
.findFirst();
|
||||
if (declaredField.getType().equals(Boolean.class)) {
|
||||
proxySettingService.update(
|
||||
new UpdateWrapper<ProxySetting>()
|
||||
.eq("id", id)
|
||||
.set(fieldInfo.get().getColumn(), value));
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
catch (Exception e) {}
|
||||
throw RException.badRequest();
|
||||
return super.getOne(id);
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
@ResponseBody
|
||||
public R<?> save(@RequestBody ProxySetting proxySetting) {
|
||||
if (!Objects.isNull(proxySetting.getId())) {
|
||||
proxySettingService.updateById(proxySetting);
|
||||
}
|
||||
else {
|
||||
proxySettingService.save(proxySetting.setId(null));
|
||||
}
|
||||
return R.ok();
|
||||
return super.save(proxySetting);
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@ResponseBody
|
||||
public R<?> delete(String id) {
|
||||
return R.judge(proxySettingService.removeById(id), "删除失败,是否已删除?");
|
||||
return super.delete(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
R<?> updateBool(String id, String field, Boolean value) {
|
||||
return super.updateBool(id, field, value);
|
||||
}
|
||||
|
||||
@PostMapping("/batchOp")
|
||||
@@ -150,4 +110,13 @@ public class ProxySettingControllerV1 extends BaseController {
|
||||
ENABLE_HTTP_VERIFY
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/refreshIpThroughProxy")
|
||||
@ResponseBody
|
||||
public R<?> refreshIpThroughProxy() {
|
||||
return R.ok(
|
||||
proxySettingService
|
||||
.refreshIpThroughProxy());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package quant.rich.emoney.controller.manage;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -10,11 +13,14 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.controller.common.UpdateBoolController;
|
||||
import quant.rich.emoney.controller.common.UpdateBoolServiceController;
|
||||
import quant.rich.emoney.entity.sqlite.RequestInfo;
|
||||
import quant.rich.emoney.exception.RException;
|
||||
import quant.rich.emoney.pojo.dto.LayPageReq;
|
||||
import quant.rich.emoney.pojo.dto.LayPageResp;
|
||||
import quant.rich.emoney.pojo.dto.R;
|
||||
@@ -23,7 +29,7 @@ import quant.rich.emoney.service.sqlite.RequestInfoService;
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/admin/v1/manage/requestInfo")
|
||||
public class RequestInfoControllerV1 extends UpdateBoolController<RequestInfo> {
|
||||
public class RequestInfoControllerV1 extends UpdateBoolServiceController<RequestInfo> {
|
||||
|
||||
@Autowired
|
||||
RequestInfoService requestInfoService;
|
||||
@@ -36,42 +42,57 @@ public class RequestInfoControllerV1 extends UpdateBoolController<RequestInfo> {
|
||||
@GetMapping("/list")
|
||||
@ResponseBody
|
||||
public LayPageResp<?> list(LayPageReq<RequestInfo> pageReq) {
|
||||
Page<RequestInfo> planPage = requestInfoService.page(pageReq);
|
||||
return new LayPageResp<>(planPage);
|
||||
return super.list(pageReq);
|
||||
}
|
||||
|
||||
@GetMapping("/getOne")
|
||||
@ResponseBody
|
||||
public R<?> getOne(Integer id) {
|
||||
|
||||
// 如果 id 是空,说明可能希望新建并返回默认实例化对象
|
||||
if (id == null) {
|
||||
return R.ok(new RequestInfo());
|
||||
}
|
||||
|
||||
// 否则从数据库取
|
||||
RequestInfo requestInfo = requestInfoService.getById(id);
|
||||
return R.judgeNonNull(requestInfo, "无法找到对应 ID 的请求信息");
|
||||
return super.getOne(id);
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
@ResponseBody
|
||||
public R<?> save(@RequestBody @NonNull RequestInfo plan) {
|
||||
return R.judge(() -> requestInfoService.saveOrUpdate(plan));
|
||||
public R<?> save(@RequestBody @NonNull RequestInfo requestInfo) {
|
||||
return super.save(requestInfo);
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@ResponseBody
|
||||
public R<?> delete(String id) {
|
||||
return R.judge(requestInfoService.removeById(id), "删除失败,是否已删除?");
|
||||
return super.delete(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
R<?> updateBool(String id, String field, Boolean value) {
|
||||
return super.updateBool(id, field, value);
|
||||
}
|
||||
|
||||
@PostMapping("/batchOp")
|
||||
@ResponseBody
|
||||
public R<?> batchOp(
|
||||
@RequestParam(value="ids[]", required=true)
|
||||
String[] ids, String op) {
|
||||
return null;
|
||||
@Valid @NotEmpty String[] ids, @NotNull RequestInfoBatchOp op) {
|
||||
|
||||
List<String> idArray = Arrays.asList(ids);
|
||||
|
||||
if (op == RequestInfoBatchOp.DELETE) {
|
||||
return R.judge(getThisService().removeByIds(idArray));
|
||||
}
|
||||
|
||||
LambdaUpdateWrapper<RequestInfo> uw = new LambdaUpdateWrapper<>();
|
||||
uw.in(RequestInfo::getId, idArray);
|
||||
switch (op) {
|
||||
default:
|
||||
throw RException.badRequest("未知操作");
|
||||
}
|
||||
}
|
||||
|
||||
private static enum RequestInfoBatchOp {
|
||||
DELETE,
|
||||
ENABLE_ANONYMOUS,
|
||||
DISABLE_ANONYMOUS
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public class AndroidSdkLevelConfig implements IConfig<AndroidSdkLevelConfig> {
|
||||
|
||||
public AndroidSdkLevelConfig() {
|
||||
androidVerToSdk = new HashMap<>();
|
||||
androidVerToSdk.put("15", 35);
|
||||
androidVerToSdk.put("14", 34);
|
||||
androidVerToSdk.put("13", 33);
|
||||
androidVerToSdk.put("12L", 32);
|
||||
|
||||
@@ -49,6 +49,11 @@ public class DeviceInfoConfig implements IConfig<DeviceInfoConfig> {
|
||||
@Slf4j
|
||||
public static class DeviceInfo {
|
||||
|
||||
// 持久化在本地的 DeviceInfo 只有三个字段:
|
||||
// model、deviceType 和 fingerprint
|
||||
// 其中除 model 和 deviceType 外,其他字段全部从 fingerprint 派生
|
||||
// 也就是说只要提供 model、deviceType 和 fingerprint 就能创建一个 DeviceInfo 实例
|
||||
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
private String model;
|
||||
private String brand;
|
||||
@@ -62,11 +67,12 @@ public class DeviceInfoConfig implements IConfig<DeviceInfoConfig> {
|
||||
private String buildType;
|
||||
private String buildTags;
|
||||
|
||||
/**
|
||||
* 用以匹配 fingerprint 的正则表达式
|
||||
*/
|
||||
public static final Pattern PATTERN = Pattern.compile("^(?<brand>.*?)/(?<product>.*?)/(?<device>.*?):(?<versionRelease>.*?)/(?<buildId>.*?)/(?<buildNumber>.*?):(?<buildType>.*?)/(?<buildTags>.*?)$");
|
||||
|
||||
|
||||
private DeviceInfo() {
|
||||
}
|
||||
private DeviceInfo() {}
|
||||
|
||||
public DeviceInfo setFingerprint(String fingerprint) {
|
||||
Matcher m = PATTERN.matcher(fingerprint);
|
||||
@@ -126,8 +132,8 @@ public class DeviceInfoConfig implements IConfig<DeviceInfoConfig> {
|
||||
}
|
||||
|
||||
public final String toString() {
|
||||
return String.format("Model: %s, Fingerprint: %s",
|
||||
getModel(), getFingerprint()
|
||||
return String.format("Model: %s, DeviceType: %s, Fingerprint: %s",
|
||||
getModel(), getDeviceType(), getFingerprint()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -139,7 +145,7 @@ public class DeviceInfoConfig implements IConfig<DeviceInfoConfig> {
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return Objects.hash(getModel(), getFingerprint());
|
||||
return Objects.hash(getModel(), getDeviceType(), getFingerprint());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,539 +0,0 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
import quant.rich.emoney.patch.okhttp.PatchOkHttp;
|
||||
import quant.rich.emoney.patch.okhttp.PatchOkHttpRule;
|
||||
import quant.rich.emoney.util.EncryptUtils;
|
||||
import quant.rich.emoney.util.SpringContextHolder;
|
||||
import quant.rich.emoney.util.TextUtils;
|
||||
import quant.rich.emoney.validator.EmoneyRequestConfigValid;
|
||||
|
||||
/**
|
||||
* 用于配置请求时的请求行为,一般而言,请求头与安卓系统的信息有关(build.prop)
|
||||
* 虽然部分请求对应服务器可能不进行审核,但合理的请求头能尽可能模仿真机行为,避免风险
|
||||
* @see DeviceInfoConfig
|
||||
* @see AndroidSdkLevelConfig
|
||||
* @see ChromeVersionsConfig
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Slf4j
|
||||
@EmoneyRequestConfigValid
|
||||
@ConfigInfo(field = "emoneyRequest", name = "益盟请求设置", initDefault = true)
|
||||
public class EmoneyRequestConfig implements IConfig<EmoneyRequestConfig> {
|
||||
|
||||
/**
|
||||
* 是否匿名登录
|
||||
*/
|
||||
private Boolean isAnonymous = true;
|
||||
|
||||
/**
|
||||
* 非匿名登录时的用户名
|
||||
*/
|
||||
private String username = "";
|
||||
|
||||
/**
|
||||
* 非匿名登录时的密码
|
||||
*/
|
||||
private String password = "";
|
||||
|
||||
/**
|
||||
* 鉴权信息
|
||||
*/
|
||||
private String authorization;
|
||||
|
||||
/**
|
||||
* UID
|
||||
*/
|
||||
private Integer uid;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟登录接口 <code><i>guid</i> = MD5(<b>androidId</b>)</code></li>
|
||||
* <li>益盟登录接口 <code><i>exIdentify.AndroidID</i> = <b>androidId</b></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><br>本例随机生成并管理,需要符合 16 位
|
||||
*
|
||||
*/
|
||||
private String androidId = TextUtils.randomString("abcdef0123456789", 16);
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>Webview <code><i>User-Agent</i></li>
|
||||
* <li>Non-Webview Image <code><i>User-Agent</i></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b></code>
|
||||
* @see DeviceInfoConfig
|
||||
*/
|
||||
@Setter(AccessLevel.PRIVATE)
|
||||
private String androidVersion;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟通讯接口请求头 <code><i>X-Android-Agent</i> = EMAPP/{<b>emoneyVersion</b>}(Android;{<b>androidSdkLevel</b>})</code></li>
|
||||
* <li>益盟登录接口 <code><i>osVersion</i> = <b>androidSdkLevel</b></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 经由 <code><b>AndroidSdkLevelConfig</b></code> 转换,由本例代管</code>
|
||||
* @see DeviceInfoConfig
|
||||
* @see AndroidSdkLevelConfig
|
||||
*/
|
||||
@Setter(AccessLevel.PRIVATE)
|
||||
private String androidSdkLevel;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟登录接口 <code><i>softwareType</i> = <b>softwareType</b></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>,由本例代管</code>
|
||||
* @see DeviceInfoConfig
|
||||
*/
|
||||
private String softwareType;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟通讯接口请求头 <code><i>User-Agent</i> = <b>okHttpUserAgent</b></code></li>
|
||||
* </ul>
|
||||
* 一般由程序所使用的 OkHttp 版本决定<br>
|
||||
* <b>来源:</b>本例管理
|
||||
*/
|
||||
private String okHttpUserAgent = "okhttp/3.12.2";
|
||||
|
||||
/**
|
||||
* 对应 build.prop 中 Build.MODEL, <b>用于:</b><ul>
|
||||
* <li>WebView <code><i>User-Agent</i></code></li>
|
||||
* <li>非 WebView 图片<code><i>User-Agent</i></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
|
||||
* @see DeviceInfoConfig
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* 对应 build.prop 中 Build.FINGERPRINT, <b>用于:</b><ul>
|
||||
* <li>益盟登录接口 <code><i>hardware</i> = MD5(<b>fingerprint</b>)</code></li>
|
||||
* <li>益盟登录接口 <code><i>exIdentify.OSFingerPrint</i> = <b>fingerprint</b></code></li>
|
||||
* </ul>
|
||||
* <font color="red">注意最终生成的 exIdentify 是 jsonString, 且有对斜杠的转义</font><br>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
|
||||
* @see DeviceInfoConfig
|
||||
*
|
||||
*/
|
||||
private String fingerprint;
|
||||
|
||||
|
||||
/**
|
||||
* 对应 build.prop 中 Build.ID, <b>用于:</b><ul>
|
||||
* <li>WebView <code><i>User-Agent</i></code></li>
|
||||
* <li>非 WebView 图片<code><i>User-Agent</i></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
|
||||
* @see DeviceInfoConfig
|
||||
*
|
||||
*/
|
||||
private String buildId;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>WebView <code><i>User-Agent</i></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>ChromeVersionsConfig</b>, 由本例代为管理
|
||||
* @see ChromeVersionsConfig
|
||||
*/
|
||||
private String chromeVersion;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟通讯接口请求头 <code><i>X-Android-Agent</i> =
|
||||
* EMAPP/{<b>emoneyVersion</b>}(Android;{androidSdkLevel})</code></li>
|
||||
* </ul>
|
||||
* 由程序版本决定<br>
|
||||
* <b>来源:</b>本例管理
|
||||
* @see EmoneyRequestConfig.androidSdkLevel
|
||||
*/
|
||||
private String emoneyVersion = "5.8.1";
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟通讯接口请求头 <code><i>Emapp-ViewMode</i> = <b>emappViewMode</b></code></li>
|
||||
* </ul>
|
||||
* 由程序决定, 一般默认为 "1"<br>
|
||||
* <b>来源:</b>本例管理
|
||||
*/
|
||||
private String emappViewMode = "1";
|
||||
|
||||
/**
|
||||
* OkHttp 用于注入 User-Agent 规则的 id
|
||||
*/
|
||||
@JsonIgnore
|
||||
private Integer userAgentPatchRuleId;
|
||||
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
@Autowired
|
||||
private AndroidSdkLevelConfig androidSdkLevelConfig;
|
||||
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
@Autowired
|
||||
private DeviceInfoConfig deviceInfoConfig;
|
||||
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
@Autowired
|
||||
private ChromeVersionsConfig chromeVersionsConfig;
|
||||
|
||||
public void afterBeanInit() {
|
||||
|
||||
try {
|
||||
androidSdkLevelConfig = Objects.requireNonNullElseGet(androidSdkLevelConfig, () -> SpringContextHolder.getBean(AndroidSdkLevelConfig.class));
|
||||
deviceInfoConfig = Objects.requireNonNullElseGet(deviceInfoConfig, () -> SpringContextHolder.getBean(DeviceInfoConfig.class));
|
||||
chromeVersionsConfig = Objects.requireNonNullElseGet(chromeVersionsConfig, () -> SpringContextHolder.getBean(ChromeVersionsConfig.class));
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
log.debug("试图从 SpringContextHolder 初始化 androidSdkLevelConfig, deviceInfoConfig 和 chromeVersionConfig, 但 SpringContextHolder 未准备好");
|
||||
}
|
||||
|
||||
if (ObjectUtils.anyNull(fingerprint, buildId, deviceName, androidVersion, androidSdkLevel, softwareType)) {
|
||||
// 任意是 null 的都要统一由 deviceInfo 进行设置
|
||||
initFromRandomDeviceInfo();
|
||||
}
|
||||
else {
|
||||
// 都不是 null,则由 fingerprint 来检查各项。
|
||||
// model 和 softwareType 本应交由 deviceInfoConfig 检查以
|
||||
// 应对可能的通过修改本地 json 来进行攻击的方式,可是本身
|
||||
// deviceInfoConfig 对 model 和 softwareType 的信息也来源
|
||||
// 于本地,万一本地的 deviceInfo(.fallback).json 也不值得信任?
|
||||
// 所以只检查 fingerprint
|
||||
|
||||
DeviceInfo deviceInfo;
|
||||
boolean valid = true;
|
||||
try {
|
||||
deviceInfo = DeviceInfo.from(null, fingerprint);
|
||||
Validate.validState(androidVersion.equals(
|
||||
deviceInfo.getVersionRelease()),
|
||||
"androidVersion(versionRelease) 与预设 fingerprint 不匹配");
|
||||
Validate.validState(androidSdkLevel.equals(
|
||||
String.valueOf(androidSdkLevelConfig.getSdkLevel(deviceInfo.getVersionRelease()))),
|
||||
"androidSdkLevel 与预设 fingerprint 不匹配");
|
||||
Validate.validState(buildId.equals(deviceInfo.getBuildId()),
|
||||
"buildId 与预设 fingerprint 不匹配");
|
||||
}
|
||||
catch (Exception e) {
|
||||
valid = false;
|
||||
}
|
||||
if (!valid) {
|
||||
initFromRandomDeviceInfo();
|
||||
}
|
||||
}
|
||||
|
||||
if (chromeVersion == null) {
|
||||
chromeVersion = chromeVersionsConfig.getRandomChromeVersion();
|
||||
}
|
||||
|
||||
// 注入 OkHttp
|
||||
patchOkHttp();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入 User-Agent patch 规则
|
||||
*/
|
||||
private EmoneyRequestConfig patchOkHttp() {
|
||||
userAgentPatchRuleId = PatchOkHttp.apply(
|
||||
PatchOkHttpRule.when()
|
||||
.hostEndsWith("emoney.cn")
|
||||
.not(r -> r.hostMatches("appstatic"))
|
||||
.or(a -> a.hostContains("emapp"))
|
||||
.or(b -> b.hasHeaderName("X-Protocol-Id"))
|
||||
.overrideIf("User-Agent", getOkHttpUserAgent()).build()
|
||||
.setId(userAgentPatchRuleId));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从随机 deviceInfo 填充本例相关字段
|
||||
* @return
|
||||
*/
|
||||
private EmoneyRequestConfig initFromRandomDeviceInfo() {
|
||||
DeviceInfo deviceInfo = deviceInfoConfig.getRandomDeviceInfo();
|
||||
// 更新 deviceInfo 后对应 androidId 也要修改,哪怕原来非空
|
||||
androidId = TextUtils.randomString("abcdef0123456789", 16);
|
||||
return initFromDeviceInfo(deviceInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定 deviceInfo 填充本例相关字段
|
||||
* @param deviceInfo
|
||||
* @return
|
||||
*/
|
||||
private EmoneyRequestConfig initFromDeviceInfo(DeviceInfo deviceInfo) {
|
||||
if (deviceInfo == null) {
|
||||
log.error("deviceInfo is null");
|
||||
RuntimeException e = new RuntimeException("deviceInfo is null");
|
||||
e.printStackTrace();
|
||||
throw e;
|
||||
}
|
||||
deviceName = deviceInfo.getModel();
|
||||
androidVersion = deviceInfo.getVersionRelease();
|
||||
androidSdkLevel = String.valueOf(androidSdkLevelConfig.getSdkLevel(androidVersion));
|
||||
softwareType = deviceInfo.getDeviceType();
|
||||
fingerprint = deviceInfo.getFingerprint();
|
||||
buildId = deviceInfo.getBuildId();
|
||||
return this;
|
||||
}
|
||||
|
||||
public EmoneyRequestConfig() {}
|
||||
|
||||
public EmoneyRequestConfig setFingerprint(String fingerprint) {
|
||||
// 进入前即便 androidSdkLevelConfig 为 null 也要尝试获取一下
|
||||
// 因为为 null 时不一定是程序初始化时,也有可能是从前端 Post 而来的
|
||||
try {
|
||||
androidSdkLevelConfig = Objects.requireNonNullElseGet(androidSdkLevelConfig, () -> SpringContextHolder.getBean(AndroidSdkLevelConfig.class));
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
log.debug("SpringContext not ready");
|
||||
}
|
||||
|
||||
if (ObjectUtils.allNotNull(deviceName, softwareType, fingerprint, androidSdkLevelConfig)) {
|
||||
DeviceInfo deviceInfo = DeviceInfo.from(deviceName, fingerprint, softwareType);
|
||||
initFromDeviceInfo(deviceInfo);
|
||||
}
|
||||
else {
|
||||
this.fingerprint = fingerprint;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前配置获取 guid,用于益盟登录接口
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getGuid() {
|
||||
return EncryptUtils.toMD5String(androidId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 一般 Protobuf 请求 X-Android-Agent 头,由 emoneyVersion 和 androidSdkLevel 组成
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getXAndroidAgent() {
|
||||
// EMAPP/{emoneyVersion}(Android;{androidSdkLevel})
|
||||
return
|
||||
new StringBuilder()
|
||||
.append("EMAPP/")
|
||||
.append(getEmoneyVersion())
|
||||
.append("(Android;")
|
||||
.append(getAndroidSdkLevel())
|
||||
.append(")").toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于 App 内用到 Webview 的地方
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getWebviewUserAgent() {
|
||||
return new StringBuilder()
|
||||
.append("Mozilla/5.0 (Linux; Android ")
|
||||
.append(getAndroidVersion())
|
||||
.append("; ")
|
||||
.append(getDeviceName())
|
||||
.append(" Build/")
|
||||
.append(getBuildId())
|
||||
.append("; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/")
|
||||
.append(getChromeVersion())
|
||||
.append(" Mobile Safari/537.36")
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于 App 内少量未用到 Webview 的地方,如首页获取图片等
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getNonWebviewResourceUserAgent() {
|
||||
// Dalvik/2.1.0 (Linux; U; Android {安卓版本};{Build.DEVICE} Build/{Build.ID})
|
||||
return new StringBuilder()
|
||||
.append("Dalvik/2.1.0 (Linux; U; Android ")
|
||||
.append(getAndroidVersion())
|
||||
.append(";")
|
||||
.append(getDeviceName())
|
||||
.append(" Build/")
|
||||
.append(getBuildId())
|
||||
.append(")")
|
||||
.toString();
|
||||
}
|
||||
/**
|
||||
* 根据当前配置获取 hardware,用于益盟登录接口
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getHardware() {
|
||||
return EncryptUtils.toMD5String(getFingerprint());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据本例信息(包括保存的用户名和密码)生成一个用于登录的 ObjectNode
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public ObjectNode getUsernamePasswordLoginObject() {
|
||||
return getUsernamePasswordLoginObject(username, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定用户名、密码和本例信息生成一个用于登录的 ObjectNode
|
||||
* @param username 用户名
|
||||
* @param password 密码(可以是加密过的,也可以是明文)
|
||||
* @return
|
||||
*/
|
||||
public ObjectNode getUsernamePasswordLoginObject(String username, String password) {
|
||||
|
||||
if (StringUtils.isAnyBlank(username, password)) {
|
||||
throw new RuntimeException("Try to generate a emoney login object but username and/or password is blank");
|
||||
}
|
||||
|
||||
ObjectNode node = getAnonymousLoginObject();
|
||||
node.put("accId", username);
|
||||
node.put("accType", 1);
|
||||
|
||||
// 尝试解密 password 看是否成功,如果成功说明原本就已经是加密了的
|
||||
String tryDecryptPassword = EncryptUtils.decryptAesForEmoneyPassword(password);
|
||||
|
||||
node.put("pwd",
|
||||
tryDecryptPassword != null ? password :
|
||||
EncryptUtils.encryptAesForEmoneyPassword(password)
|
||||
);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据本例信息生成一个用于匿名登录的 ObjectNode
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public ObjectNode getAnonymousLoginObject() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ObjectNode node = mapper.createObjectNode();
|
||||
ObjectNode exIdentify = mapper.createObjectNode();
|
||||
exIdentify.put("IMEI", "");
|
||||
exIdentify.put("AndroidID", getAndroidId());
|
||||
exIdentify.put("MAC", "");
|
||||
exIdentify.put("OSFingerPrint", getFingerprint());
|
||||
String exIdentifyString = exIdentify.toString().replace("/", "\\/");
|
||||
|
||||
// 这玩意最好按照顺序来,当前这个顺序是 5.8.1 的顺序
|
||||
String guid = getGuid();
|
||||
node.put("appVersion", getEmoneyVersion());
|
||||
node.put("productId", 4);
|
||||
node.put("softwareType", getSoftwareType());
|
||||
node.put("deviceName", getDeviceName());
|
||||
node.put("ssid", "0");
|
||||
node.put("platform", "android");
|
||||
node.put("exIdentify", exIdentifyString);
|
||||
node.put("osVersion", getAndroidSdkLevel());
|
||||
node.put("accId", guid);
|
||||
node.put("guid", guid);
|
||||
node.put("accType", 4);
|
||||
node.put("pwd", "");
|
||||
node.put("channelId", "1711");
|
||||
node.put("hardware", getHardware());
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据本例信息获取 Relogin ObjectNode
|
||||
* @return 如果 authorization 和 uid 任意 null 则本例返回 null
|
||||
*/
|
||||
@JsonIgnore
|
||||
public ObjectNode getReloginObject() {
|
||||
|
||||
if (getUid() == null || StringUtils.isBlank(getAuthorization())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ObjectNode node = mapper.createObjectNode();
|
||||
ObjectNode exIdentify = mapper.createObjectNode();
|
||||
exIdentify.put("IMEI", "");
|
||||
exIdentify.put("AndroidID", getAndroidId());
|
||||
exIdentify.put("MAC", "");
|
||||
exIdentify.put("OSFingerPrint", getFingerprint());
|
||||
String exIdentifyString = exIdentify.toString().replace("/", "\\/");
|
||||
|
||||
// 这玩意最好按照顺序来,当前这个顺序是 5.8.1 的顺序
|
||||
String guid = getGuid();
|
||||
node.put("appVersion", getEmoneyVersion());
|
||||
node.put("productId", 4);
|
||||
node.put("softwareType", getSoftwareType());
|
||||
node.put("deviceName", getDeviceName());
|
||||
node.put("ssid", "0");
|
||||
node.put("platform", "android");
|
||||
node.put("token", getAuthorization()); // 和登录不同的地方: token
|
||||
node.put("exIdentify", exIdentifyString);
|
||||
node.put("uid", getUid()); // 和登录不同的地方: uid
|
||||
node.put("osVersion", getAndroidSdkLevel());
|
||||
node.put("guid", guid);
|
||||
node.put("channelId", "1711");
|
||||
node.put("hardware", getHardware());
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置密码:<ul>
|
||||
* <li>null or empty,保存空字符串</li>
|
||||
* <li>尝试解密成功,说明是密文,直接保存</li>
|
||||
* <li>尝试解密失败,说明是明文,加密保存</li>
|
||||
* </ul>
|
||||
* @param password
|
||||
* @return
|
||||
*/
|
||||
public EmoneyRequestConfig setPassword(String password) {
|
||||
if (StringUtils.isEmpty(password)) {
|
||||
this.password = "";
|
||||
return this;
|
||||
}
|
||||
String tryDecryptPassword = EncryptUtils.decryptAesForEmoneyPassword(password);
|
||||
if (tryDecryptPassword != null) {
|
||||
this.password = password;
|
||||
}
|
||||
else {
|
||||
this.password = EncryptUtils.encryptAesForEmoneyPassword(password);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保 androidVersion/androidSdkLevel 不为 null
|
||||
*/
|
||||
public EmoneyRequestConfig beforeSaving() {
|
||||
setFingerprint(this.fingerprint);
|
||||
return this;
|
||||
}
|
||||
|
||||
public EmoneyRequestConfig afterSaving() {
|
||||
patchOkHttp();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import quant.rich.emoney.enums.StockSpan;
|
||||
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class IndexInfo {
|
||||
|
||||
private List<ParamInfo> paramInfoList = new ArrayList<>();
|
||||
|
||||
private String code;
|
||||
|
||||
private String name;
|
||||
|
||||
private Boolean isCalc;
|
||||
|
||||
private List<StockSpan> supportPeriod = new ArrayList<>();
|
||||
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public static class ParamInfo {
|
||||
|
||||
private String name;
|
||||
|
||||
private Integer max;
|
||||
|
||||
private Integer min;
|
||||
|
||||
private Integer defaultValue;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +1,21 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.Accessors;
|
||||
import okhttp3.ConnectionPool;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import quant.rich.emoney.annotation.LockByCaller;
|
||||
import quant.rich.emoney.client.OkHttpClientProvider;
|
||||
import quant.rich.emoney.component.CallerLockAspect.LockByCaller;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
import quant.rich.emoney.service.sqlite.RequestInfoService;
|
||||
import quant.rich.emoney.util.SpringContextHolder;
|
||||
|
||||
/**
|
||||
* 指标信息配置,只做运行时管理,不做保存
|
||||
@@ -36,11 +32,6 @@ public class IndexInfoConfig implements IConfig<IndexInfoConfig> {
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
private JsonNode configIndOnline;
|
||||
|
||||
@Autowired
|
||||
@JsonIgnore
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
private EmoneyRequestConfig emoneyRequestConfig;
|
||||
|
||||
public IndexInfoConfig() {}
|
||||
|
||||
public String getConfigIndOnlineStr() {
|
||||
@@ -49,10 +40,13 @@ public class IndexInfoConfig implements IConfig<IndexInfoConfig> {
|
||||
|
||||
@LockByCaller
|
||||
@JsonIgnore
|
||||
public String getOnlineConfigByUrl() throws IOException {
|
||||
public String getOnlineConfigByUrl(String url) throws IOException {
|
||||
synchronized (this) {
|
||||
if (SpringContextHolder.getBean(RequestInfoService.class).getDefaultRequestInfo() == null) {
|
||||
throw new RuntimeException("请先新增请求配置并作为默认配置");
|
||||
}
|
||||
Request request = new Request.Builder()
|
||||
.url(configIndOnlineUrl)
|
||||
.url(url)
|
||||
.header("Cache-Control", "no-cache")
|
||||
.get()
|
||||
.build();
|
||||
@@ -69,6 +63,4 @@ public class IndexInfoConfig implements IConfig<IndexInfoConfig> {
|
||||
}
|
||||
}
|
||||
|
||||
public static ConnectionPool connectionPool = new ConnectionPool(10, 5, TimeUnit.MINUTES);
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ public class PlatformConfig implements IConfig<PlatformConfig> {
|
||||
|
||||
private String email;
|
||||
|
||||
private String apiToken;
|
||||
|
||||
private Boolean isInited;
|
||||
|
||||
public PlatformConfig() {
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.interceptor.EnumOptionsInterceptor.EnumOptions;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
import quant.rich.emoney.pojo.dto.IpInfo;
|
||||
import quant.rich.emoney.util.GeoIPUtil;
|
||||
import quant.rich.emoney.validator.ProxyConfigValid;
|
||||
|
||||
/**
|
||||
* 独立出来一个代理设置的原因是后续可能需要做一个代理池,这样的话独立配置比较适合后续扩展
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Slf4j
|
||||
@ProxyConfigValid
|
||||
@ConfigInfo(field = "proxy", name = "代理设置", initDefault = true)
|
||||
public class ProxyConfig implements IConfig<ProxyConfig> {
|
||||
|
||||
/**
|
||||
* 代理类型
|
||||
*/
|
||||
@EnumOptions("ProxyTypeEnum")
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
private Proxy.Type proxyType = Proxy.Type.DIRECT;
|
||||
|
||||
/**
|
||||
* 代理主机
|
||||
*/
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
private String proxyHost = "";
|
||||
|
||||
/**
|
||||
* 代理端口
|
||||
*/
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
private Integer proxyPort = 1;
|
||||
|
||||
/**
|
||||
* 是否忽略 HTTPS 证书校验
|
||||
*/
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
private Boolean ignoreHttpsVerification = false;
|
||||
|
||||
/**
|
||||
* 通过代理后的 IP,不做存储,只做呈现
|
||||
*/
|
||||
private IpInfo ipInfo;
|
||||
|
||||
public void afterBeanInit() {
|
||||
//refreshIpThroughProxy();
|
||||
}
|
||||
|
||||
public synchronized IpInfo refreshIpThroughProxy() {
|
||||
ipInfo = GeoIPUtil.getIpInfoThroughProxy(this);
|
||||
return ipInfo;
|
||||
}
|
||||
|
||||
|
||||
public ProxyConfig() {}
|
||||
|
||||
/**
|
||||
* 根据配置获取代理
|
||||
* @return
|
||||
*/
|
||||
public Proxy getProxy() {
|
||||
if (getProxyType() != null && getProxyType() != Proxy.Type.DIRECT) {
|
||||
return new Proxy(getProxyType(),
|
||||
new InetSocketAddress(getProxyHost(), getProxyPort()));
|
||||
}
|
||||
return Proxy.NO_PROXY;
|
||||
}
|
||||
|
||||
public String getProxyUrl() {
|
||||
if (ObjectUtils.anyNull(getProxyType(), getProxyHost(), getProxyPort())) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (getProxyType() == Proxy.Type.SOCKS) {
|
||||
sb.append("socks5://");
|
||||
}
|
||||
else if (getProxyType() == Proxy.Type.HTTP) {
|
||||
sb.append("http://");
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
sb.append(getProxyHost()).append(':').append(getProxyPort());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package quant.rich.emoney.entity.postgre;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 益盟个股策略信息,包含策略类型和日期
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class StockStrategy {
|
||||
|
||||
private String tsCode;
|
||||
|
||||
public StockStrategy setTsCodeFromGoodsId(Integer goodsId) {
|
||||
// 自动将益盟 goodsId 转换成 tsCode
|
||||
// 1301325 -> 301325.SZ
|
||||
// 600325 -> 600325.SH
|
||||
// 1920009 -> 920009.BJ
|
||||
String goodsIdStr = goodsId.toString();
|
||||
RuntimeException e = new RuntimeException("无法将 goodsId " + goodsIdStr + " 转换为 tsCode");
|
||||
if (goodsIdStr.length() == 6) {
|
||||
// SH
|
||||
return setTsCode(goodsIdStr + ".SH");
|
||||
}
|
||||
else if (goodsIdStr.length() == 7) {
|
||||
if (goodsIdStr.charAt(0) != '1') {
|
||||
throw e;
|
||||
}
|
||||
if (goodsIdStr.charAt(1) == '9') {
|
||||
// BJ
|
||||
return setTsCode(goodsIdStr.substring(1) + ".BJ");
|
||||
}
|
||||
// SZ
|
||||
return setTsCode(goodsIdStr.substring(1) + ".SZ");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -26,6 +26,8 @@ public class ProxySetting {
|
||||
@TableId(value="id", type=IdType.AUTO)
|
||||
private Integer id;
|
||||
|
||||
private Boolean isDefault;
|
||||
|
||||
@Nonnull
|
||||
private String proxyName;
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -20,21 +22,36 @@ import quant.rich.emoney.entity.config.AndroidSdkLevelConfig;
|
||||
import quant.rich.emoney.entity.config.ChromeVersionsConfig;
|
||||
import quant.rich.emoney.entity.config.DeviceInfoConfig;
|
||||
import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo;
|
||||
import quant.rich.emoney.entity.config.EmoneyRequestConfig;
|
||||
import quant.rich.emoney.util.EncryptUtils;
|
||||
import quant.rich.emoney.util.SpringContextHolder;
|
||||
import quant.rich.emoney.util.TextUtils;
|
||||
import quant.rich.emoney.validator.RequestInfoValid;
|
||||
|
||||
/**
|
||||
* 用于配置请求时的请求行为,一般而言,请求头与安卓系统的信息有关(build.prop)
|
||||
* 虽然部分请求对应服务器可能不进行审核,但合理的请求头能尽可能模仿真机行为,避免风险
|
||||
* @see DeviceInfoConfig
|
||||
* @see AndroidSdkLevelConfig
|
||||
* @see ChromeVersionsConfig
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper=false)
|
||||
@Accessors(chain = true)
|
||||
@Slf4j
|
||||
@RequestInfoValid
|
||||
@TableName(value = "request_info")
|
||||
public class RequestInfo {
|
||||
public class RequestInfo extends Model<RequestInfo> {
|
||||
|
||||
private static AndroidSdkLevelConfig androidSdkLevelConfig = SpringContextHolder.getBean(AndroidSdkLevelConfig.class);
|
||||
private static DeviceInfoConfig deviceInfoConfig = SpringContextHolder.getBean(DeviceInfoConfig.class);
|
||||
private static ChromeVersionsConfig chromeVersionsConfig = SpringContextHolder.getBean(ChromeVersionsConfig.class);
|
||||
private static final long serialVersionUID = -3113053377999289627L;
|
||||
|
||||
private volatile static AndroidSdkLevelConfig androidSdkLevelConfig = SpringContextHolder.getBean(AndroidSdkLevelConfig.class);
|
||||
private volatile static DeviceInfoConfig deviceInfoConfig = SpringContextHolder.getBean(DeviceInfoConfig.class);
|
||||
private volatile static ChromeVersionsConfig chromeVersionsConfig = SpringContextHolder.getBean(ChromeVersionsConfig.class);
|
||||
|
||||
/**
|
||||
* 使用随机设备信息(DeviceInfo)初始化对象<p>
|
||||
* @see DeviceInfo
|
||||
*/
|
||||
public RequestInfo() {
|
||||
DeviceInfo deviceInfo = deviceInfoConfig.getRandomDeviceInfo();
|
||||
setRelativeFieldsFromDeviceInfo(deviceInfo);
|
||||
@@ -43,16 +60,16 @@ public class RequestInfo {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Integer id;
|
||||
|
||||
/**
|
||||
* 该请求是否设为默认请求
|
||||
*/
|
||||
private Boolean isDefault = false;
|
||||
|
||||
/**
|
||||
* 该请求信息配置的名称,助记用
|
||||
*/
|
||||
private String name = "";
|
||||
|
||||
/**
|
||||
* 是否匿名登录
|
||||
*/
|
||||
private Boolean isAnonymous = true;
|
||||
|
||||
/**
|
||||
* 非匿名登录时的用户名
|
||||
*/
|
||||
@@ -88,8 +105,9 @@ public class RequestInfo {
|
||||
* <li>Webview <code><i>User-Agent</i></li>
|
||||
* <li>Non-Webview Image <code><i>User-Agent</i></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b></code>
|
||||
* <b>来源:</b><code><b>DeviceInfo</b></code>
|
||||
* @see DeviceInfoConfig
|
||||
* @see DeviceInfo
|
||||
*/
|
||||
@Setter(AccessLevel.PRIVATE)
|
||||
@TableField(exist=false)
|
||||
@@ -100,8 +118,9 @@ public class RequestInfo {
|
||||
* <li>益盟通讯接口请求头 <code><i>X-Android-Agent</i> = EMAPP/{<b>emoneyVersion</b>}(Android;{<b>androidSdkLevel</b>})</code></li>
|
||||
* <li>益盟登录接口 <code><i>osVersion</i> = <b>androidSdkLevel</b></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 经由 <code><b>AndroidSdkLevelConfig</b></code> 转换,由本例代管</code>
|
||||
* <b>来源:</b><code><b>DeviceInfo</b>, 经由 <code><b>AndroidSdkLevelConfig</b></code> 转换,由本例代管</code>
|
||||
* @see DeviceInfoConfig
|
||||
* @see DeviceInfo
|
||||
* @see AndroidSdkLevelConfig
|
||||
*/
|
||||
@Setter(AccessLevel.PRIVATE)
|
||||
@@ -112,8 +131,9 @@ public class RequestInfo {
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟登录接口 <code><i>softwareType</i> = <b>softwareType</b></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>,由本例代管</code>
|
||||
* <b>来源:</b><code><b>DeviceInfo</b>
|
||||
* @see DeviceInfoConfig
|
||||
* @see DeviceInfo
|
||||
*/
|
||||
private String softwareType;
|
||||
|
||||
@@ -131,8 +151,9 @@ public class RequestInfo {
|
||||
* <li>WebView <code><i>User-Agent</i></code></li>
|
||||
* <li>非 WebView 图片<code><i>User-Agent</i></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
|
||||
* <b>来源:</b><code><b>DeviceInfo</b>
|
||||
* @see DeviceInfoConfig
|
||||
* @see DeviceInfo
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
@@ -177,7 +198,7 @@ public class RequestInfo {
|
||||
* </ul>
|
||||
* 由程序版本决定<br>
|
||||
* <b>来源:</b>本例管理
|
||||
* @see EmoneyRequestConfig.androidSdkLevel
|
||||
* @see #androidSdkLevel
|
||||
*/
|
||||
private String emoneyVersion = "5.8.1";
|
||||
|
||||
@@ -430,4 +451,8 @@ public class RequestInfo {
|
||||
return node;
|
||||
}
|
||||
|
||||
public boolean isAnonymous() {
|
||||
return !StringUtils.isAnyBlank(getUsername(), getPassword());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public class StrategyAndPool implements Comparable<StrategyAndPool> {
|
||||
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<StrategyAndPool> {
|
||||
|
||||
}
|
||||
|
||||
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<StrategyAndPool> {
|
||||
@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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,12 @@ import org.springframework.stereotype.Component;
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ConfigInfo {
|
||||
/**
|
||||
* @return 配置 field 标识,用以自动注入、持久化配置文件名
|
||||
* 配置 field 标识,用以自动注入、持久化配置文件名。<p>
|
||||
* 例:
|
||||
* <ul>
|
||||
* <li>指定 field = "website", 则 bean 名为 websiteConfig 持久化文件名为 websiteConfig.json
|
||||
* <li>未指定 field, 类名为 ProxyConfig, 则 bean 名为 proxyConfig, 持久化文件名为 proxyConfig.json
|
||||
*
|
||||
*/
|
||||
String field() default "";
|
||||
|
||||
|
||||
@@ -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<String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -165,6 +165,24 @@ public class PatchOkHttpRule {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果满足条件则覆写指定 Header。当覆写值可能动态变化时,使用本方法提供 supplier
|
||||
* @param headerName
|
||||
* @param valueSupplier
|
||||
* @return
|
||||
*/
|
||||
public Builder overrideHeader(String headerName, Supplier<String> 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);
|
||||
}
|
||||
|
||||
@@ -206,6 +206,22 @@ public class R<T> implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供一返回值为 boolean 的 supplier,如果成功则返回 R.ok(), 失败则抛出 RException.badRequest(defaltMessage),
|
||||
* 抛出错误则抛出 RException.badRequest(e.getMessage())
|
||||
* @param supplier
|
||||
* @param defaultMessage
|
||||
* @return
|
||||
*/
|
||||
public static R<?> judge(ThrowingSupplier<Boolean> 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());
|
||||
|
||||
@@ -197,21 +197,26 @@ public class ConfigService implements InitializingBean {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Config
|
||||
* 获取给定类型的 Config 实例
|
||||
* <p>
|
||||
* 当缓存中有时从缓存取, 缓存没有时从数据库取并更新到缓存, 数据库也没有时, 如果指定的 Config 的 @ConfigInfo
|
||||
* 注解开启了 initDefault = true, 则尝试返回一个初始 Config,否则返回 null
|
||||
* 当缓存中有时从缓存取,缓存没有时从数据库取并更新到缓存,数据库也没有时,
|
||||
* 如果指定的 Config 的 @ConfigInfo
|
||||
* 注解开启了 initDefault = true, 则尝试返回一个初始 Config,否则返回 null.
|
||||
* 原本不存在的 Config 如果 @ConfigInfo 开启了 save(),会持久化到本地。
|
||||
*
|
||||
* @param <Config>
|
||||
* @param clazz
|
||||
* @param clazz 配置类
|
||||
* @return
|
||||
* @see ConfigInfo
|
||||
* @see #getOrCreateConfig(String)
|
||||
*
|
||||
*/
|
||||
public <Config extends IConfig<Config>> Config getConfig(Class<Config> 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<Config> configClass = (Class<Config>) 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
* 从网络获取有参指标详情
|
||||
* <p>本例用到的 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 的无参指标详情
|
||||
* <p>本例用到的 requestInfo 不需要 PatchOkHttp 覆写,但要求鉴权参数拼接到 url 中,故要求鉴权
|
||||
* <p>会一并尝试获取其他在本地未有的无参指标</p>
|
||||
* @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
|
||||
* <p>该方法涉及 authorization(token)/webviewUserAgent,需确保 EmoneyRequestConfig 已正确注入并登录</p>
|
||||
* <p>该方法涉及 authorization(token)/webviewUserAgent,需确保 RequestInfo 已正确配置并登录</p>
|
||||
* @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
|
||||
* <p>该 Url 涉及 authorization(token),需确保 EmoneyRequestConfig 已正确注入并登录</p>
|
||||
* <p>该 Url 涉及 authorization(token),需确保 RequestInfo 已正确配置并登录</p>
|
||||
* @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();
|
||||
}
|
||||
|
||||
|
||||
27
src/main/java/quant/rich/emoney/service/WarnService.java
Normal file
27
src/main/java/quant/rich/emoney/service/WarnService.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ProxySettingMapper, ProxySetting> {
|
||||
|
||||
private volatile IpInfo ipInfo;
|
||||
|
||||
/**
|
||||
* 获取默认代理配置
|
||||
* @return
|
||||
*/
|
||||
public ProxySetting getDefaultProxySetting() {
|
||||
return getOne(
|
||||
new LambdaQueryWrapper<ProxySetting>()
|
||||
.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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<RequestInfoMapper, RequestInfo> {
|
||||
|
||||
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<String>() {
|
||||
@Override
|
||||
public String get() {
|
||||
log.debug("触发获取请求配置的 OkHttpUserAgent");
|
||||
return getDefaultRequestInfo().getOkHttpUserAgent();
|
||||
}
|
||||
}).build().setId(userAgentPatchRuleId));
|
||||
}
|
||||
|
||||
public RequestInfo getDefaultRequestInfo() {
|
||||
RequestInfo requestInfo = getOne(
|
||||
new LambdaQueryWrapper<RequestInfo>().eq(RequestInfo::getIsDefault, true)
|
||||
);
|
||||
if (requestInfo == null) {
|
||||
requestInfo = new RequestInfo();
|
||||
save(requestInfo);
|
||||
}
|
||||
return requestInfo;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<StrategyAndPoolMap
|
||||
* @param jsonNode
|
||||
*/
|
||||
@ResponseDecodeExtension(protocolId="9400")
|
||||
@Async
|
||||
public void updateByQueryResponse(JsonNode jsonNode) {
|
||||
// jsonNode.output[].band[]/.tech[]
|
||||
|
||||
@@ -91,6 +96,7 @@ public class StrategyAndPoolService extends SqliteServiceImpl<StrategyAndPoolMap
|
||||
}
|
||||
|
||||
Set<StrategyAndPool> 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<StrategyAndPoolMap
|
||||
if (strategies != null && strategies.getNodeType() == JsonNodeType.ARRAY) {
|
||||
for (JsonNode simpleStrategy : strategies) {
|
||||
StrategyAndPool strategyAndPool = new StrategyAndPool(
|
||||
type,
|
||||
simpleStrategy.get("strategyName").asText(),
|
||||
simpleStrategy.get("strategyId").asInt(),
|
||||
simpleStrategy.get("poolName").asText(),
|
||||
simpleStrategy.get("poolId").asInt()
|
||||
);
|
||||
if (!set.contains(strategyAndPool) && !exists(
|
||||
new LambdaQueryWrapper<StrategyAndPool>()
|
||||
.eq(StrategyAndPool::getPoolId, strategyAndPool.getPoolId()))) {
|
||||
set.add(strategyAndPool);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.saveOrUpdateBatch(set);
|
||||
log.info("新增 {} 条 StrategyAndPool", set.size());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<? extends Payload>[] payload() default {};
|
||||
}
|
||||
@@ -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<ProxyConfigValid, IConfig<ProxyConfig>> {
|
||||
|
||||
@Override
|
||||
public boolean isValid(IConfig<ProxyConfig> 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<? extends Payload>[] payload() default {};
|
||||
@@ -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<EmoneyRequestConfigValid, IConfig<EmoneyRequestConfig>> {
|
||||
public class RequestInfoValidator implements IValidator, ConstraintValidator<RequestInfoValid, RequestInfo> {
|
||||
|
||||
static final Pattern androidIdPattern = Pattern.compile("^[0-9a-f]{16}$");
|
||||
|
||||
@Override
|
||||
public boolean isValid(IConfig<EmoneyRequestConfig> 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()) {
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"}]}
|
||||
{
|
||||
"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\"}"
|
||||
}
|
||||
@@ -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"}]}
|
||||
{
|
||||
"id" : null,
|
||||
"name" : "中期线",
|
||||
"nameCode" : "10012100",
|
||||
"data" : [ {
|
||||
"title" : "中期线:",
|
||||
"items" : [ "代表标的的中期趋势:", "曲线向上走时,代表中期趋势上涨;", "曲线向下走时,代表中期趋势下跌。" ],
|
||||
"image" : null
|
||||
} ],
|
||||
"original" : "{\"name\":\"中期线\",\"nameCode\":\"10012100\",\"data\":[{\"title\":\"中期线:\",\"items\":[\"代表标的的中期趋势:\",\"曲线向上走时,代表中期趋势上涨;\",\"曲线向下走时,代表中期趋势下跌。\"]}]}"
|
||||
}
|
||||
@@ -6,5 +6,6 @@
|
||||
"title" : "量王精选(投教):",
|
||||
"items" : [ "红色实心柱子是量王,黄色实心柱子是天量;量王出现说明近阶段的量能达到了一定的程度,呈现交易相对活跃状态。具体用法参考量王精选相关教程。" ],
|
||||
"image" : null
|
||||
} ]
|
||||
} ],
|
||||
"original" : "{\"data\":[{\"title\":\"量王精选(投教):\",\"items\":[\"红色实心柱子是量王,黄色实心柱子是天量;量王出现说明近阶段的量能达到了一定的程度,呈现交易相对活跃状态。具体用法参考量王精选相关教程。\"]}],\"name\":\"量王精选(投教)\",\"nameCode\":\"10013500\"}"
|
||||
}
|
||||
@@ -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\"]}"
|
||||
}
|
||||
@@ -33,6 +33,7 @@
|
||||
"12": 31,
|
||||
"12L": 32,
|
||||
"13": 33,
|
||||
"14": 34
|
||||
"14": 34,
|
||||
"15": 35
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"emoneyLoginFormDataList" : null,
|
||||
"selectedId" : "57de6ca2423e0f64d8626477e1f8a46b",
|
||||
"isRandom" : false
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -2,5 +2,6 @@
|
||||
"username" : "admin",
|
||||
"password" : "81667f60a8c11d4c8e9d2e0670ff24667e6c72d49b0b15562525bcbd",
|
||||
"email" : "huocaizhu@gmail.com",
|
||||
"apiToken" : "vgb2IHmax9Mjji4R",
|
||||
"isInited" : true
|
||||
}
|
||||
Binary file not shown.
@@ -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) {
|
||||
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 则仅渲染批量删除逻辑
|
||||
* <br>除此以外,cols 内列如果需要开关选项的, 在相应 col 内添加 switchTemplet: true
|
||||
*/
|
||||
renderTable: (option) => {
|
||||
const defaultOption = {
|
||||
@@ -99,10 +99,10 @@ window.Helper = {
|
||||
if (!option.baseUrl) throw new Error('baseUrl 不允许为空');
|
||||
if (!option.baseUrl.endsWith('/')) option.baseUrl += '/';
|
||||
option.url = option.baseUrl + 'list';
|
||||
let tableSwitchTemplet = function () {
|
||||
let tableSwitchTemplet = function() {
|
||||
// 以 elem 选择器 + '.' + switchFilter 作为 filter
|
||||
const filter = `${option.elem}.switchFilter`;
|
||||
layui.form.on(`switch(${filter})`, function (obj) {
|
||||
layui.form.on(`switch(${filter})`, function(obj) {
|
||||
console.log(obj, obj.elem.checked);
|
||||
const data = {
|
||||
field: obj.elem.dataset.field,
|
||||
@@ -111,10 +111,29 @@ window.Helper = {
|
||||
};
|
||||
$.ajax({
|
||||
url: option.baseUrl + 'updateBool', method: 'POST',
|
||||
data:data,
|
||||
success: () => Dog.success({time: 1000}),
|
||||
error: function (res) {
|
||||
Dog.error({msg: res})
|
||||
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')
|
||||
@@ -141,17 +160,17 @@ window.Helper = {
|
||||
layui.form.on(`submit(${submitButtonFilter})`, _ => {
|
||||
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: () => {
|
||||
success: function(r) {
|
||||
Dog.success({
|
||||
onClose: () => {
|
||||
if (window.editLayer) layui.layer.close(window.editLayer);
|
||||
Dog.reloadTable()}})
|
||||
Dog.reloadTable()
|
||||
}
|
||||
})
|
||||
},
|
||||
error: res => Dog.error({msg: res}),
|
||||
error: res => Dog.error({ msg: res }),
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -213,16 +235,19 @@ 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) {
|
||||
switchFuncs[key] = function(obj) {
|
||||
obj.elem.value = obj.elem.checked;
|
||||
layui.form.render();
|
||||
}
|
||||
layui.form.on(`switch(${key})`, function (obj) {
|
||||
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
|
||||
@@ -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({
|
||||
|
||||
})
|
||||
})
|
||||
return d => {
|
||||
var fieldName = d.LAY_COL.field;
|
||||
return `<input type="checkbox" lay-skin="switch" lay-text="|"
|
||||
data-field="${fieldName}" data-id="${d[idName]}"
|
||||
${d[fieldName] ? 'checked' : ''} lay-filter="switchFilter">`;
|
||||
/**
|
||||
* @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();
|
||||
})
|
||||
};
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -53,29 +53,29 @@
|
||||
class="fa-fw fa-solid fa-screwdriver-wrench"></i> 管理
|
||||
</a>
|
||||
<dl class="layui-nav-child">
|
||||
<dd>
|
||||
<a th:href="@{/admin/v1/manage/plan}"> <i
|
||||
class="fa-fw fa-regular fa-calendar"></i> 计划任务
|
||||
</a>
|
||||
</dd>
|
||||
<dd>
|
||||
<a th:href="@{/admin/v1/manage/requestInfo}"> <i
|
||||
class="fas fa-fw fa-envelope-open-text"></i> 请求配置
|
||||
</a>
|
||||
</dd>
|
||||
<dd>
|
||||
<a th:href="@{/admin/v1/manage/indexInfo}"> <i
|
||||
class="fa-fw fa-solid fa-chart-line "></i> 指标配置
|
||||
</a>
|
||||
</dd>
|
||||
<dd>
|
||||
<a th:href="@{/admin/v1/manage/proxySetting}"> <i
|
||||
class="fa-fw fa-solid fa-network-wired"></i> 代理配置
|
||||
</a>
|
||||
</dd>
|
||||
<dd>
|
||||
<a th:href="@{/admin/v1/manage/plan}"> <i
|
||||
class="fa-fw fa-regular fa-calendar"></i> 计划任务
|
||||
</a>
|
||||
</dd>
|
||||
<dd>
|
||||
<a th:href="@{/admin/v1/manage/indexInfo}"> <i
|
||||
class="fa-fw fa-solid fa-chart-line "></i> 指标信息
|
||||
</a>
|
||||
</dd>
|
||||
<dd>
|
||||
<a th:href="@{/admin/v1/manage/protocolMatch}"> <i
|
||||
class="fa-fw fa-regular fa-handshake"></i> Protocol 配置
|
||||
class="fa-fw fa-regular fa-handshake"></i> 协议信息
|
||||
</a>
|
||||
</dd>
|
||||
<dd>
|
||||
@@ -88,16 +88,6 @@
|
||||
class="fa-fw fa-solid fa-gears"></i> 设置
|
||||
</a>
|
||||
<dl class="layui-nav-child">
|
||||
<dd>
|
||||
<a th:href="@{/admin/v1/config/emoneyRequest}"> <i
|
||||
class="fa-fw fa-solid fa-heading"></i> 请求头设置
|
||||
</a>
|
||||
</dd>
|
||||
<dd>
|
||||
<a th:href="@{/admin/v1/config/proxy}"> <i
|
||||
class="fa-fw fa-solid fa-network-wired"></i> 代理设置
|
||||
</a>
|
||||
</dd>
|
||||
</dl></li>
|
||||
<li class="layui-nav-item"
|
||||
style="float: right; margin-right: 1px;" lay-unselect=""><a
|
||||
@@ -115,23 +105,31 @@
|
||||
</dd>
|
||||
</dl></li>
|
||||
<li class="layui-nav-item ipInfo"
|
||||
th:with="ipInfo=${@proxySettingService.getDefaultProxySettingIpInfo()}"
|
||||
style="float: right; margin-right: 1px" lay-unselect=""><a
|
||||
id="ipThroughProxy" href="javascript:manualRefreshIp()"
|
||||
title="立即刷新"> IP 属地: <span
|
||||
th:if="${@proxyConfig.ipInfo == null}"
|
||||
th:if="${ipInfo == null}"
|
||||
class="layui-badge layui-bg-cyan">加载中...</span> <span
|
||||
th:if="${@proxyConfig.ipInfo != null}"
|
||||
class="layui-badge layui-bg-cyan">[[${@proxyConfig.ipInfo.geoString}]]</span>
|
||||
</a> <th:block th:if="${@proxyConfig.ipInfo != null}">
|
||||
th:if="${ipInfo != null}"
|
||||
class="layui-badge layui-bg-cyan">[[${ipInfo.geoString}]]</span>
|
||||
</a>
|
||||
<th:block th:if="${ipInfo != null}">
|
||||
<dl class="layui-nav-child">
|
||||
<dd class="ip">
|
||||
<a title="点击复制">[[${@proxyConfig.ipInfo.ip}]]</a>
|
||||
<a title="点击复制">[[${ipInfo.ip}]]</a>
|
||||
</dd>
|
||||
<dd class="ipv6" th:if="${@proxyConfig.ipInfo.ipv6 != null}">
|
||||
<a title="点击复制">[[${@proxyConfig.ipInfo.ipv6}]]</a>
|
||||
<dd class="ipv6" th:if="${ipInfo.ipv6 != null}">
|
||||
<a title="点击复制">[[${ipInfo.ipv6}]]</a>
|
||||
</dd>
|
||||
</dl>
|
||||
</th:block></li>
|
||||
</th:block>
|
||||
<th:block th:if="${ipInfo == null}">
|
||||
<dl class="layui-nav-child" style="display: none">
|
||||
</dl>
|
||||
</th:block>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<script type="text/html" id="editUser">
|
||||
@@ -161,6 +159,17 @@
|
||||
<input type="password" name="newPassword" placeholder="" autocomplete="off" class="layui-input"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-row layui-col-space5">
|
||||
<div class="layui-col-xs10">
|
||||
<label class="layui-form-label">API Token</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" readonly name="apiToken" placeholder="" autocomplete="off" class="layui-input"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-xs2">
|
||||
<button type="button" class="layui-btn layui-col-xs12" id="refreshToken">刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:none" class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit="*" lay-filter="editUser">提交</button>
|
||||
@@ -186,11 +195,7 @@
|
||||
document.querySelectorAll('dd.ip,dd.ipv6').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
let text = el.querySelector('a').textContent;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
Dog.success({msg: '复制成功'})
|
||||
}).catch(err => {
|
||||
Dog.error({msg: '复制失败'})
|
||||
});
|
||||
Helper.copyText(text);
|
||||
})
|
||||
});
|
||||
async function refreshIpThroughProxy() {
|
||||
@@ -198,13 +203,16 @@
|
||||
let geoEl =
|
||||
document.querySelector('#ipThroughProxy>span');
|
||||
//geoEl.textContent = '加载中...';
|
||||
let res = await (await fetch('/admin/v1/config/proxy/refreshIpThroughProxy')).json();
|
||||
let res = await (await fetch('/admin/v1/manage/proxySetting/refreshIpThroughProxy')).json();
|
||||
if (res.ok) {
|
||||
geoEl.classList.remove('layui-bg-red');
|
||||
geoEl.classList.add('layui-bg-cyan');
|
||||
geoEl.textContent = res.data.geoString || '获取失败';
|
||||
let ipMenu = document.querySelector('.ipInfo>dl');
|
||||
let genIpEL = (clazz, title) => {
|
||||
let el = ipMenu.querySelector('.' + clazz);
|
||||
if (!el) {
|
||||
ipMenu.style.display = '';
|
||||
el = document.createElement('dd');
|
||||
let a = document.createElement('a');
|
||||
a.setAttribute('title', title || '点击复制');
|
||||
@@ -225,6 +233,11 @@
|
||||
ipv6El.remove();
|
||||
}
|
||||
}
|
||||
else {
|
||||
geoEl.textContent = res.message;
|
||||
geoEl.classList.remove('layui-bg-cyan');
|
||||
geoEl.classList.add('layui-bg-red');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('刷新失败:', e);
|
||||
}
|
||||
@@ -251,6 +264,7 @@
|
||||
element.on('nav(demo)', function (elem) {
|
||||
layer.msg(elem.text());
|
||||
});
|
||||
|
||||
$('.change-user-info').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -270,6 +284,7 @@
|
||||
var el = $(layero), o = el.find('#old-pass'), n = el.find('#new-pass');
|
||||
el.find('[name="username"]').val(r.data.username);
|
||||
el.find('[name="email"]').val(r.data.email);
|
||||
el.find('[name="apiToken"]').val(r.data.apiToken);
|
||||
el.on('input propertychange', '#new-pass input', function () {
|
||||
var that = $(this);
|
||||
if (that.val()) {
|
||||
@@ -287,6 +302,19 @@
|
||||
n.find('input').removeAttr('lay-verify');
|
||||
}
|
||||
});
|
||||
|
||||
const apiTokenEl = document.querySelector('[name="apiToken"]');
|
||||
apiTokenEl.addEventListener('click', e => {
|
||||
let currToken = apiTokenEl.value;
|
||||
if (currToken) {
|
||||
Helper.copyText(currToken);
|
||||
}
|
||||
});
|
||||
const refreshTokenBtn = document.getElementById('refreshToken');
|
||||
refreshTokenBtn.addEventListener('click', e => {
|
||||
apiTokenEl.value = Helper.randomWordString(16);
|
||||
});
|
||||
|
||||
layui.form.on('submit(editUser)', function (data) {
|
||||
if (data.field.password && data.field.newPassword) {
|
||||
data.field['password'] = sha3_224(data.field.password);
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
table.render({
|
||||
elem: '#indexInfos',
|
||||
url: '/admin/v1/manage/indexInfo/configIndOnline',
|
||||
url: '/admin/v1/manage/indexInfo/getFields',
|
||||
skin: 'line',
|
||||
cols: [ [
|
||||
{field: 'indexCode', title: '指标代码'},
|
||||
@@ -82,7 +82,7 @@
|
||||
},
|
||||
parseData: (json) => {
|
||||
console.log(json)
|
||||
const jo = json.data, indMap = jo.indMap;
|
||||
const jo = json.data.configIndOnline, indMap = jo.indMap;
|
||||
const data = [];
|
||||
Object.keys(indMap).forEach(key => {
|
||||
const indInfo = indMap[key];
|
||||
|
||||
@@ -215,11 +215,11 @@
|
||||
})
|
||||
|
||||
const loadConfigLayer = layui.layer.load(2);
|
||||
const json = await (await fetch('/admin/v1/manage/indexInfo/configIndOnline')).json();
|
||||
const json = await (await fetch('/admin/v1/manage/indexInfo/getFields?fields=configIndOnline')).json();
|
||||
|
||||
if (json.ok) {
|
||||
const indexNameSelectEl = document.querySelector('[name="indexCode"]');
|
||||
const jo = json.data, indMap = jo.indMap;
|
||||
const jo = json.data.configIndOnline, indMap = jo.indMap;
|
||||
let selected = undefined;
|
||||
const optionsFragment = document.createDocumentFragment();
|
||||
Object.keys(indMap).forEach(key => {
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
{field:'operation', title: '操作', toolbar: '#operationTpl'}
|
||||
]]
|
||||
})
|
||||
dropdown.render({
|
||||
Helper.renderDropdown({
|
||||
elem: '.operdown',
|
||||
data: [
|
||||
{title: '删除', op: 'DELETE'},
|
||||
@@ -75,51 +75,9 @@
|
||||
{title: '停用', op: 'DISABLE'},
|
||||
{title: '开启交易日校验', op: 'ENABLE_OPEN_DAY_CHECK'},
|
||||
{title: '关闭交易日校验', op: 'DISABLE_OPEN_DAY_CHECK'}],
|
||||
click: function (data, othis){
|
||||
var checked = layui.table.checkStatus('plans'), 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('plans', {
|
||||
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();
|
||||
})
|
||||
}
|
||||
tableFilter: 'plans',
|
||||
idName: 'planId'
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
<input type="text" lay-verify="required" name="proxyName" placeholder="" autocomplete="off" class="layui-input"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">默认<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox"
|
||||
name="isDefault" lay-skin="switch" lay-filter="isDefault" lay-text="ON|OFF">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">代理类型<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="manage-body">
|
||||
<div>
|
||||
<h1 class="manage-title">
|
||||
<i class="fa-fw fa-regular fa-calendar"></i>
|
||||
<i class="fa-fw fa-solid fa-network-wired"></i>
|
||||
<b>代理设置</b><a href="javascript:openNewForm()" class="operate">新增</a>
|
||||
</h1>
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
<div class="layui-form" style="margin:10px 15px" id="editRequestInfoForm" lay-filter="editRequestInfoForm">
|
||||
<div class="layui-form-item">
|
||||
<input type="hidden" name="id"/>
|
||||
<label class="layui-form-label">计划名称<span>*</span></label>
|
||||
<label class="layui-form-label">名称<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" lay-verify="required" name="name" placeholder="" autocomplete="off" class="layui-input"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">匿名<span>*</span></label>
|
||||
<label class="layui-form-label">默认<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="isAnonymous" lay-skin="switch" lay-filter="isAnonymous" checked lay-text="ON|OFF">
|
||||
<input type="checkbox" name="isDefault" lay-skin="switch" lay-filter="isDefault" lay-text="ON|OFF">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item non-anonymous">
|
||||
@@ -98,6 +98,7 @@
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
Helper.onSubmitForm('submitRequestInfo', '/admin/v1/manage/requestInfo/save');
|
||||
Helper.listenAllInputChange();
|
||||
function refreshAndroidId() {
|
||||
let androidIdEl = document.querySelector('[name="androidId"]');
|
||||
androidIdEl.value = Helper.randomHexString(16);
|
||||
@@ -140,12 +141,14 @@
|
||||
success: async function (layero, layerIndex) {
|
||||
Helper.setLayerMainBtn(layero, -1);
|
||||
var el = $(layero), extraSwitchFuncs = [];
|
||||
// 覆写 isAnonymous
|
||||
extraSwitchFuncs.isAnonymous = function (obj) {
|
||||
const checked = obj.elem.value = obj.elem.checked;
|
||||
|
||||
$('#editRequestInfoForm [name="username"],#editRequestInfoForm [name="password"]').on('input', function(){
|
||||
const usernameEl = document.querySelector('#editRequestInfoForm [name="username"]');
|
||||
const passwordEl = document.querySelector('#editRequestInfoForm [name="password"]');
|
||||
const nonAnonymouses = document.querySelectorAll('.non-anonymous');
|
||||
const checked = usernameEl.value !== '' || passwordEl.value !== '';
|
||||
nonAnonymouses.forEach(non => {
|
||||
if (!checked) {
|
||||
if (checked) {
|
||||
// 非匿名,那 username/password 等就需要选必选
|
||||
if (!non.querySelector('label>span')) {
|
||||
$(non).children('label').append('<span>*</span>');
|
||||
@@ -158,7 +161,15 @@
|
||||
$(non).find('lay-verify').removeAttr('lay-verify');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 覆写 isAnonymous
|
||||
extraSwitchFuncs.isAnonymous = function (obj) {
|
||||
const checked = obj.elem.value = obj.elem.checked;
|
||||
// 这里没功能的, 因为已经没有了 isAnonymous 字段,留在这里只是做个示例
|
||||
};
|
||||
|
||||
Helper.fillEditForm(r, layero, layerIndex, extraSwitchFuncs);
|
||||
layui.event.call(this, 'form', 'switch(isAnonymous)', {
|
||||
elem: el[0].querySelector('[name="isAnonymous"]'),
|
||||
|
||||
@@ -47,9 +47,8 @@
|
||||
idName: 'id',
|
||||
cols: [ [
|
||||
{type:'checkbox'},
|
||||
{field:'id', width: 60, title: 'ID'},
|
||||
{field:'isDefault', title: '默认', width: 95, switchTemplet: true, mutex: true},
|
||||
{field:'name', title: '名称'},
|
||||
{field:'isAnonymous', title: '匿名', width: 95, switchTemplet: true},
|
||||
{field:'username', title: '用户名', width: 95},
|
||||
{field:'uid', title: 'UID'},
|
||||
{field:'androidVersion', title: '安卓版本'},
|
||||
@@ -57,37 +56,15 @@
|
||||
{field:'operation', title: '操作', toolbar: '#operationTpl'}
|
||||
]]
|
||||
})
|
||||
dropdown.render({
|
||||
Helper.renderDropdown({
|
||||
elem: '.operdown',
|
||||
data: [
|
||||
{title: '删除', op: 'DELETE'},
|
||||
{title: '启用', op: 'enable'},
|
||||
{title: '停用', op: 'disable'},
|
||||
{title: '开启交易日校验', op: 'enableOpenDayCheck'},
|
||||
{title: '关闭交易日校验', op: 'disableOpenDayCheck'}],
|
||||
click: function (data, othis){
|
||||
var checked = layui.table.checkStatus('requestInfos'), requestInfoIds = [];
|
||||
if (!checked.data.length) {
|
||||
Dog.error({msg: '未选中任何项', time: 1000});
|
||||
return;
|
||||
}
|
||||
$.each(checked.data, function (i, requestInfo){
|
||||
requestInfoIds.push(requestInfo.id);
|
||||
});
|
||||
data = $.extend(data, {ids: requestInfoIds});
|
||||
var op = function() {
|
||||
$.ajax({
|
||||
{title: '开启匿名', op: 'ENABLE_ANONYMOUS'},
|
||||
{title: '关闭匿名', op: 'DISABLE_ANONYMOUS'}],
|
||||
tableFilter: 'requestInfos',
|
||||
url: '/admin/v1/manage/requestInfo/batchOp',
|
||||
method: 'POST',
|
||||
data: data,
|
||||
success: () => Dog.success({msg: '批量操作成功', onClose: () => Dog.reloadTable('plans')}),
|
||||
error: res => Dog.error({msg: res, time: 1000})
|
||||
})
|
||||
}
|
||||
data.op !== 'DELETE' ? op() : layer.confirm('确认批量删除吗?该操作不可恢复', function(){
|
||||
op();
|
||||
})
|
||||
}
|
||||
idName: 'id'
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<String, String> 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<HttpHeader> requestHeaderList = request.headersArray();
|
||||
Map<String, String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<NonParamsIndexDetail> valid = new ArrayList<>();
|
||||
List<ArrayNode> 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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user