删除 EmoneyRequestConfig 和 ProxyConfig 设置,改为数据库(SQLite)配置。默认配置的设置和删除逻辑由

SQLite 触发器配置。
This commit is contained in:
2025-11-15 14:57:02 +08:00
parent 6ccfe67aff
commit edcbfd4ffd
77 changed files with 1240 additions and 1620 deletions

View File

@@ -171,13 +171,6 @@
<scope>test</scope> <scope>test</scope>
</dependency> </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> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId> <artifactId>commons-text</artifactId>

View File

@@ -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 "";
}

View File

@@ -17,11 +17,12 @@ import okhttp3.MediaType;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; 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.EmoneyDecodeException;
import quant.rich.emoney.exception.EmoneyIllegalRequestParamException; import quant.rich.emoney.exception.EmoneyIllegalRequestParamException;
import quant.rich.emoney.exception.EmoneyRequestException; import quant.rich.emoney.exception.EmoneyRequestException;
import quant.rich.emoney.exception.EmoneyResponseException; import quant.rich.emoney.exception.EmoneyResponseException;
import quant.rich.emoney.service.sqlite.RequestInfoService;
import quant.rich.emoney.util.EncryptUtils; import quant.rich.emoney.util.EncryptUtils;
import quant.rich.emoney.util.SpringContextHolder; import quant.rich.emoney.util.SpringContextHolder;
import okhttp3.OkHttpClient; 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 LOGIN_X_PROTOCOL_ID = "user%2Fauth%2Flogin";
private static final String RELOGIN_X_PROTOCOL_ID = "user%2Fauth%2FReLogin"; private static final String RELOGIN_X_PROTOCOL_ID = "user%2Fauth%2FReLogin";
private static volatile EmoneyRequestConfig emoneyRequestConfig; private static volatile RequestInfoService requestInfoService;
/** /**
* 根据 protocolId 返回 URL * 根据 protocolId 返回 URL
@@ -89,13 +90,17 @@ public class EmoneyClient implements Cloneable {
* 从 Spring 上下文中获取载入的请求配置 * 从 Spring 上下文中获取载入的请求配置
* @return * @return
*/ */
private static EmoneyRequestConfig getEmoneyRequestConfig() { private static RequestInfo getDefaultRequestInfo() {
if (emoneyRequestConfig == null) { if (requestInfoService == null) {
synchronized (EmoneyClient.class) { 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() {} private EmoneyClient() {}
@@ -103,10 +108,10 @@ public class EmoneyClient implements Cloneable {
/** /**
* 根据系统配置自动选择登录方式,即匿名或不匿名 * 根据系统配置自动选择登录方式,即匿名或不匿名
* @return * @return
* @see EmoneyRequestConfig * @see RequestInfo
*/ */
public static Boolean loginWithManaged() { public static Boolean loginWithManaged() {
if (getEmoneyRequestConfig().getIsAnonymous()) { if (getDefaultRequestInfo().isAnonymous()) {
return loginWithAnonymous(); return loginWithAnonymous();
} }
else { else {
@@ -122,7 +127,7 @@ public class EmoneyClient implements Cloneable {
*/ */
@Deprecated @Deprecated
public static Boolean loginWithUsernamePassword() { public static Boolean loginWithUsernamePassword() {
ObjectNode formObject = getEmoneyRequestConfig().getUsernamePasswordLoginObject(); ObjectNode formObject = getDefaultRequestInfo().getUsernamePasswordLoginObject();
return login(formObject); return login(formObject);
} }
@@ -135,7 +140,7 @@ public class EmoneyClient implements Cloneable {
*/ */
@Deprecated @Deprecated
public static Boolean loginWithUsernamePassword(String username, String password) { public static Boolean loginWithUsernamePassword(String username, String password) {
ObjectNode formObject = getEmoneyRequestConfig().getUsernamePasswordLoginObject(username, password); ObjectNode formObject = getDefaultRequestInfo().getUsernamePasswordLoginObject(username, password);
return login(formObject); return login(formObject);
} }
@@ -146,7 +151,7 @@ public class EmoneyClient implements Cloneable {
*/ */
@Deprecated @Deprecated
public static Boolean loginWithAnonymous() { public static Boolean loginWithAnonymous() {
ObjectNode formObject = getEmoneyRequestConfig().getAnonymousLoginObject(); ObjectNode formObject = getDefaultRequestInfo().getAnonymousLoginObject();
return login(formObject); return login(formObject);
} }
@@ -155,8 +160,8 @@ public class EmoneyClient implements Cloneable {
* @return * @return
*/ */
public static Boolean relogin() { public static Boolean relogin() {
EmoneyRequestConfig emoneyRequestConfig = getEmoneyRequestConfig(); RequestInfo requestInfo = getDefaultRequestInfo();
ObjectNode reloginObject = emoneyRequestConfig.getReloginObject(); ObjectNode reloginObject = requestInfo.getReloginObject();
if (reloginObject == null) { if (reloginObject == null) {
// 无登录信息,直接触发登录 // 无登录信息,直接触发登录
return loginWithManaged(); return loginWithManaged();
@@ -176,8 +181,8 @@ public class EmoneyClient implements Cloneable {
.header("X-Request-Id", "1") .header("X-Request-Id", "1")
.header("EM-Sign", EncryptUtils.getEMSign(content, "POST", RELOGIN_X_PROTOCOL_ID)) .header("EM-Sign", EncryptUtils.getEMSign(content, "POST", RELOGIN_X_PROTOCOL_ID))
.header("Authorization", token) .header("Authorization", token)
.header("X-Android-Agent", emoneyRequestConfig.getXAndroidAgent()) .header("X-Android-Agent", requestInfo.getXAndroidAgent())
.header("Emapp-ViewMode", emoneyRequestConfig.getEmappViewMode()); .header("Emapp-ViewMode", requestInfo.getEmappViewMode());
Request request = requestBuilder.build(); Request request = requestBuilder.build();
@@ -223,7 +228,7 @@ public class EmoneyClient implements Cloneable {
private static Boolean login(ObjectNode formObject) { private static Boolean login(ObjectNode formObject) {
try { try {
//OkHttpClient okHttpClient = new OkHttpClient(); //OkHttpClient okHttpClient = new OkHttpClient();
EmoneyRequestConfig emoneyRequestConfig = getEmoneyRequestConfig(); RequestInfo requestInfo = getDefaultRequestInfo();
OkHttpClient okHttpClient = OkHttpClientProvider.getInstance(); OkHttpClient okHttpClient = OkHttpClientProvider.getInstance();
MediaType type = MediaType.parse("application/json"); MediaType type = MediaType.parse("application/json");
//type.charset(StandardCharsets.UTF_8); //type.charset(StandardCharsets.UTF_8);
@@ -243,9 +248,9 @@ public class EmoneyClient implements Cloneable {
.header("X-Request-Id", "null") .header("X-Request-Id", "null")
.header("EM-Sign", EncryptUtils.getEMSign(content, "POST", LOGIN_X_PROTOCOL_ID)) .header("EM-Sign", EncryptUtils.getEMSign(content, "POST", LOGIN_X_PROTOCOL_ID))
.header("Authorization", "") .header("Authorization", "")
.header("X-Android-Agent", emoneyRequestConfig.getXAndroidAgent()) .header("X-Android-Agent", requestInfo.getXAndroidAgent())
.header("Emapp-ViewMode", emoneyRequestConfig.getEmappViewMode()); .header("Emapp-ViewMode", requestInfo.getEmappViewMode());
//.header("User-Agent", emoneyRequestConfig.getOkHttpUserAgent()) //.header("User-Agent", requestInfo.getOkHttpUserAgent())
Request request = requestBuilder.build(); Request request = requestBuilder.build();
@@ -257,14 +262,14 @@ public class EmoneyClient implements Cloneable {
Integer code = loginResult.get("result").get("code").asInt(); Integer code = loginResult.get("result").get("code").asInt();
if (code == 0) { if (code == 0) {
emoneyRequestConfig requestInfo
.setAuthorization( .setAuthorization(
loginResult loginResult
.get("detail").get("token").asText()) .get("detail").get("token").asText())
.setUid( .setUid(
loginResult loginResult
.get("detail").get("uid").asInt()) .get("detail").get("uid").asInt())
.saveOrUpdate(); .insertOrUpdate();
log.info("执行 emoney LOGIN 成功"); log.info("执行 emoney LOGIN 成功");
return true; return true;
} }
@@ -297,8 +302,8 @@ public class EmoneyClient implements Cloneable {
throw new EmoneyIllegalRequestParamException("执行 emoney 请求错误Protocol id 不能为 null!", throw new EmoneyIllegalRequestParamException("执行 emoney 请求错误Protocol id 不能为 null!",
new IllegalArgumentException()); new IllegalArgumentException());
} }
EmoneyRequestConfig emoneyRequestConfig = getEmoneyRequestConfig(); RequestInfo requestInfo = getDefaultRequestInfo();
if (StringUtils.isBlank(emoneyRequestConfig.getAuthorization())) { if (StringUtils.isBlank(requestInfo.getAuthorization())) {
throw new EmoneyIllegalRequestParamException("执行 emoney 请求错误Authorization 为空,是否未登录?", throw new EmoneyIllegalRequestParamException("执行 emoney 请求错误Authorization 为空,是否未登录?",
new IllegalArgumentException()); new IllegalArgumentException());
} }
@@ -325,9 +330,9 @@ public class EmoneyClient implements Cloneable {
.header("X-Protocol-Id", xProtocolId.toString()) .header("X-Protocol-Id", xProtocolId.toString())
.header("X-Request-Id", xRequestId == null ? "null" : xRequestId.toString()) .header("X-Request-Id", xRequestId == null ? "null" : xRequestId.toString())
.header("EM-Sign", EncryptUtils.getEMSign(content, "POST", xProtocolId.toString())) .header("EM-Sign", EncryptUtils.getEMSign(content, "POST", xProtocolId.toString()))
.header("Authorization", emoneyRequestConfig.getAuthorization()) .header("Authorization", requestInfo.getAuthorization())
.header("X-Android-Agent", emoneyRequestConfig.getXAndroidAgent()) .header("X-Android-Agent", requestInfo.getXAndroidAgent())
.header("Emapp-ViewMode", emoneyRequestConfig.getEmappViewMode()) .header("Emapp-ViewMode", requestInfo.getEmappViewMode())
; ;
Request request = requestBuilder.build(); Request request = requestBuilder.build();

View File

@@ -23,7 +23,8 @@ import okhttp3.ResponseBody;
import okio.BufferedSource; import okio.BufferedSource;
import okio.GzipSource; import okio.GzipSource;
import okio.Okio; 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; import quant.rich.emoney.util.SpringContextHolder;
/** /**
@@ -33,17 +34,18 @@ import quant.rich.emoney.util.SpringContextHolder;
*/ */
public class OkHttpClientProvider { public class OkHttpClientProvider {
private static volatile ProxyConfig proxyConfig; private static volatile ProxySettingService proxySettingService;
private static ProxyConfig getProxyConfig() { private static ProxySetting getDefaultProxySetting() {
if (proxyConfig == null) { if (proxySettingService == null) {
synchronized (OkHttpClientProvider.class) { synchronized (OkHttpClientProvider.class) {
if (proxyConfig == null) { proxySettingService = SpringContextHolder.getBean(ProxySettingService.class);
proxyConfig = SpringContextHolder.getBean(ProxyConfig.class);
}
} }
} }
return proxyConfig; if (proxySettingService == null) {
return null;
}
return proxySettingService.getDefaultProxySetting();
} }
/** /**
@@ -60,10 +62,10 @@ public class OkHttpClientProvider {
* @return * @return
*/ */
public static OkHttpClient getInstance(Consumer<OkHttpClient.Builder> builderConsumer) { public static OkHttpClient getInstance(Consumer<OkHttpClient.Builder> builderConsumer) {
ProxyConfig proxyConfig = getProxyConfig(); ProxySetting proxySetting = getDefaultProxySetting();
return getInstance( return getInstance(
proxyConfig.getProxy(), proxySetting.getProxy(),
proxyConfig.getIgnoreHttpsVerification(), proxySetting.getIgnoreHttpsVerification(),
builderConsumer); builderConsumer);
} }

View File

@@ -8,9 +8,13 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import quant.rich.emoney.annotation.LockByCaller;
import quant.rich.emoney.util.CallerLockUtil; 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.lang.reflect.Method;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@@ -20,7 +24,7 @@ public class CallerLockAspect {
private final SpelExpressionParser parser = new SpelExpressionParser(); 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 { public Object around(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature(); MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod(); Method method = signature.getMethod();
@@ -52,4 +56,25 @@ public class CallerLockAspect {
lock.unlock(); 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 "";
}
} }

View File

@@ -19,6 +19,7 @@ import jakarta.validation.ConstraintViolationException;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.jdbc.UncategorizedSQLException;
import org.springframework.validation.BindException; import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MethodArgumentNotValidException;
@@ -94,15 +95,7 @@ public class EmoneyAutoPlatformExceptionHandler {
if (ex instanceof PageNotFoundException) { if (ex instanceof PageNotFoundException) {
throw (PageNotFoundException) ex; throw (PageNotFoundException) ex;
} }
String message = null; log.warn("Resolved exception {}", ex);
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(httpServletRequestToString(request)); log.warn(httpServletRequestToString(request));
return bodyOrPage(HttpStatus.INTERNAL_SERVER_ERROR, ex); return bodyOrPage(HttpStatus.INTERNAL_SERVER_ERROR, ex);
} }
@@ -161,16 +154,9 @@ public class EmoneyAutoPlatformExceptionHandler {
} }
private R<?> bodyOrPage(HttpStatus httpStatus, Exception ex) { private R<?> bodyOrPage(HttpStatus httpStatus, Exception ex) {
boolean isPage = true; String message = getMessage(ex, ex instanceof UncategorizedSQLException);
String message = null; boolean isPage = (ex instanceof RException || ex instanceof LoginException) ?
if (ex instanceof RException || false : isPage();
ex instanceof LoginException) {
isPage = false;
message = ex.getMessage();
}
else {
isPage = isPage();
}
if (isPage) { if (isPage) {
if (ex instanceof NoResourceFoundException nrfe) { if (ex instanceof NoResourceFoundException nrfe) {
if (StringUtils.isNotEmpty(nrfe.getMessage()) if (StringUtils.isNotEmpty(nrfe.getMessage())
@@ -182,7 +168,7 @@ public class EmoneyAutoPlatformExceptionHandler {
} }
throw ex == null ? new RuntimeException("Page exception raised") : new RuntimeException(ex); 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).setMessage(message).setData(message)
: R.status(httpStatus); : R.status(httpStatus);
return r; return r;
@@ -217,4 +203,19 @@ public class EmoneyAutoPlatformExceptionHandler {
return sb.toString(); 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;
}
} }

View File

@@ -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;
}
}

View File

@@ -13,7 +13,9 @@ import quant.rich.emoney.interfaces.ConfigInfo;
import quant.rich.emoney.interfaces.IConfig; import quant.rich.emoney.interfaces.IConfig;
/** /**
* 实现自动化注册 Config * 实现自动化注册 Config<p>
* Config 放在 quant.rich.emoney.entity.config 包下并且必须实现 IConfig 接口
* @see quant.rich.emoney.interfaces.IConfig
*/ */
@Slf4j @Slf4j
@DependsOn("configService") @DependsOn("configService")
@@ -28,6 +30,7 @@ public class ConfigAutoRegistrar implements BeanDefinitionRegistryPostProcessor
scanner.findCandidateComponents("quant.rich.emoney.entity.config").forEach(beanDefinition -> { scanner.findCandidateComponents("quant.rich.emoney.entity.config").forEach(beanDefinition -> {
String className = beanDefinition.getBeanClassName(); String className = beanDefinition.getBeanClassName();
try { try {
// 确保其 field 规则与 configService 内 field 生成规则一致,即: // 确保其 field 规则与 configService 内 field 生成规则一致,即:
// 如果 @ConfigInfo 指定了 field 的,使用该 field + "Config" // 如果 @ConfigInfo 指定了 field 的,使用该 field + "Config"
// 作为 beanName否则使用首字母小写的 simpleClassName 作为 // 作为 beanName否则使用首字母小写的 simpleClassName 作为
@@ -37,18 +40,18 @@ public class ConfigAutoRegistrar implements BeanDefinitionRegistryPostProcessor
+ clazz.getSimpleName().substring(1); + clazz.getSimpleName().substring(1);
if (!IConfig.class.isAssignableFrom(clazz)) { 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; return;
} }
if (!beanName.endsWith("Config")) { 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; return;
} }
ConfigInfo info = clazz.getAnnotation(ConfigInfo.class); ConfigInfo info = clazz.getAnnotation(ConfigInfo.class);
if (info == null) { 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; return;
} }
if (StringUtils.isNotBlank(info.field())) { if (StringUtils.isNotBlank(info.field())) {
@@ -58,15 +61,16 @@ public class ConfigAutoRegistrar implements BeanDefinitionRegistryPostProcessor
BeanDefinitionBuilder factoryBean = BeanDefinitionBuilder BeanDefinitionBuilder factoryBean = BeanDefinitionBuilder
.genericBeanDefinition(ConfigServiceFactoryBean.class) .genericBeanDefinition(ConfigServiceFactoryBean.class)
.addConstructorArgValue(clazz); .addConstructorArgValue(clazz);
// 注意此处注册 factoryBean 不意味着 FactoryBean.getObject() 方法立即被执行, /**
// Spring 管理的 Bean 默认在其被使用时才创建,所以如果 getObject() 调用一些方 * 注意此处通过 factoryBean 创建 bean 不意味着 FactoryBean.getObject() 方法
// 法,这些方法会在初次使用 Bean 时才创建。如果这些方法对于启动过程很重要, * 会被立即执行。Spring 默认会在 bean 被使用时才创建。如果该 bean 对程序
// 需要在对应 Config(Bean) 上加上 @Bean @Lazy(false) 注解,确保一旦准备好 * 启动很重要,需要立即创建的,需在其类上添加 @Bean @Lazy(false) 注解,
// 相应的 Bean 就会被创建 * 确保一旦准备好,相应的 bean 就会被创建
*/
registry.registerBeanDefinition(beanName, factoryBean.getBeanDefinition()); 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) { } catch (ClassNotFoundException e) {
throw new RuntimeException("Failed to load class: " + className, e); throw new RuntimeException("Cannot found specific config class: " + className, e);
} }
}); });
} }

View File

@@ -9,8 +9,9 @@ import quant.rich.emoney.interfaces.IConfig;
import quant.rich.emoney.service.ConfigService; import quant.rich.emoney.service.ConfigService;
/** /**
* 实现配置项自动载入 * 配置类工厂
* @param <T> * @param <T> 配置类
*
*/ */
@Slf4j @Slf4j
public class ConfigServiceFactoryBean<T extends IConfig<T>> implements FactoryBean<T>, BeanNameAware { 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 @Override
public T getObject() throws Exception { public T getObject() throws Exception {
ConstructionGuard.enter(targetClass); ConstructionGuard.enter(targetClass);
boolean success = true;
try { try {
T bean = configService.getConfig(targetClass); T bean = configService.getConfig(targetClass);
beanFactory.autowireBean(bean); beanFactory.autowireBean(bean);
beanFactory.initializeBean(bean, beanName); beanFactory.initializeBean(bean, beanName);
configService.saveOrUpdate(bean); //configService.saveOrUpdate(bean);
return bean; return bean;
} }
catch (Exception e) { catch (Exception e) {
log.error("Fail to load config: " + targetClass.getName(), e); log.error("无法载入配置类: " + targetClass.getName(), e);
success = false;
throw e; throw e;
} }
finally { finally {
ConstructionGuard.exit(targetClass); ConstructionGuard.exit(targetClass);
if (success) {
log.debug("getObject() for {} success", targetClass.toString());
}
} }
} }

View File

@@ -7,6 +7,25 @@ import org.springframework.beans.factory.BeanCreationException;
import lombok.extern.slf4j.Slf4j; 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, its 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 Springs management lifecycle,
* it is difficult to detect this issue at runtime. Therefore, this class should
* be used within the Factorys 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 @Slf4j
public class ConstructionGuard { public class ConstructionGuard {
private static final ThreadLocal<Set<Class<?>>> constructing = ThreadLocal.withInitial(HashSet::new); private static final ThreadLocal<Set<Class<?>>> constructing = ThreadLocal.withInitial(HashSet::new);
@@ -16,7 +35,6 @@ public class ConstructionGuard {
} }
public static void enter(Class<?> clazz) { public static void enter(Class<?> clazz) {
log.debug("Enter construction for {}", clazz.toString());
if (isConstructing(clazz)) { if (isConstructing(clazz)) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("Class ") sb.append("Class ")
@@ -31,6 +49,5 @@ public class ConstructionGuard {
public static void exit(Class<?> clazz) { public static void exit(Class<?> clazz) {
constructing.get().remove(clazz); constructing.get().remove(clazz);
log.debug("Exit construction for {}", clazz.toString());
} }
} }

View File

@@ -4,8 +4,6 @@ import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import javax.sql.DataSource; import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactory;
@@ -50,7 +48,10 @@ public class SqliteMybatisConfig {
String filePath = hikariDataSource.getJdbcUrl(); String filePath = hikariDataSource.getJdbcUrl();
if (filePath == null || !filePath.startsWith("jdbc:sqlite:")) { 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; return;
} }
filePath = filePath.substring("jdbc:sqlite:".length()).trim(); filePath = filePath.substring("jdbc:sqlite:".length()).trim();

View File

@@ -52,7 +52,8 @@ public class IndexControllerV1 extends BaseController {
String username, String username,
String password, String password,
String newPassword, String newPassword,
String email) { String email,
String apiToken) {
if (EncryptUtils.passwordIsNotEmpty(newPassword)) { if (EncryptUtils.passwordIsNotEmpty(newPassword)) {
if (!platformConfig.getPassword().equals(password)) { if (!platformConfig.getPassword().equals(password)) {
@@ -66,7 +67,7 @@ public class IndexControllerV1 extends BaseController {
else { else {
throw RException.badRequest("用户名不能为空"); throw RException.badRequest("用户名不能为空");
} }
platformConfig.setEmail(email); platformConfig.setEmail(email).setApiToken(apiToken);
return R.judge(() -> { return R.judge(() -> {
if (configService.saveOrUpdate(platformConfig)) { if (configService.saveOrUpdate(platformConfig)) {
authService.setLogin(username, platformConfig.getPassword()); authService.setLogin(username, platformConfig.getPassword());

View File

@@ -18,6 +18,7 @@ import quant.rich.emoney.pojo.dto.R;
import quant.rich.emoney.service.AuthService; import quant.rich.emoney.service.AuthService;
import quant.rich.emoney.service.ConfigService; import quant.rich.emoney.service.ConfigService;
import quant.rich.emoney.util.EncryptUtils; import quant.rich.emoney.util.EncryptUtils;
import quant.rich.emoney.util.TextUtils;
@Controller @Controller
@RequestMapping("/admin/v1") @RequestMapping("/admin/v1")
@@ -84,7 +85,11 @@ public class LoginControllerV1 extends BaseController {
if (StringUtils.isAnyBlank(username) || !EncryptUtils.passwordIsNotEmpty(password)) { if (StringUtils.isAnyBlank(username) || !EncryptUtils.passwordIsNotEmpty(password)) {
throw new LoginException("用户名和密码不能为空"); 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); boolean success = configService.saveOrUpdate(platformConfig);
if (!success) { if (!success) {
throw new LoginException("无法配置用户名和密码,请检查"); throw new LoginException("无法配置用户名和密码,请检查");

View File

@@ -27,8 +27,8 @@ import jakarta.annotation.PostConstruct;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.reflections.Reflections; import org.reflections.Reflections;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import nano.BaseResponse.Base_Response; import nano.BaseResponse.Base_Response;
import quant.rich.emoney.annotation.ResponseDecodeExtension; 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.EmoneyConvertResult;
import quant.rich.emoney.pojo.dto.EmoneyProtobufBody; import quant.rich.emoney.pojo.dto.EmoneyProtobufBody;
import quant.rich.emoney.service.sqlite.ProtocolMatchService; 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.SpringBeanDetector;
import quant.rich.emoney.util.SpringContextHolder; import quant.rich.emoney.util.SpringContextHolder;
@@ -52,20 +51,18 @@ public class ProtoDecodeControllerV1 {
@Autowired @Autowired
ProtocolMatchService protocolMatchService; ProtocolMatchService protocolMatchService;
@Autowired
StrategyAndPoolService strategyAndPoolService;
@Autowired @Autowired
Reflections reflections; Reflections reflections;
Map<String, List<MethodInfo>> responseDecodeExtensions = new HashMap<String, List<MethodInfo>>(); Map<String, List<MethodInfo>> responseDecodeExtensions = new HashMap<String, List<MethodInfo>>();
@Data @Data
@AllArgsConstructor @RequiredArgsConstructor
private static class MethodInfo { private static class MethodInfo {
Method method; final Method method;
Class<?> declaringClass; final Class<?> declaringClass;
Integer order; final Integer order;
Object instance;
} }
@PostConstruct @PostConstruct
@@ -112,7 +109,7 @@ public class ProtoDecodeControllerV1 {
for (List<MethodInfo> list : responseDecodeExtensions.values()) { for (List<MethodInfo> list : responseDecodeExtensions.values()) {
list.sort(Comparator.comparingInt(info -> info.getOrder())); list.sort(Comparator.comparingInt(info -> info.getOrder()));
} }
log.debug("共载入 {} 个 ProtocolID 的 {} 个方法", log.debug("ResponseDecodeExtension: 共载入 {} 个 ProtocolID 的 {} 个方法",
responseDecodeExtensions.keySet().size(), responseDecodeExtensions.values().size()); responseDecodeExtensions.keySet().size(), responseDecodeExtensions.values().size());
} }
@@ -274,22 +271,24 @@ public class ProtoDecodeControllerV1 {
JsonNode jo = new ObjectMapper().valueToTree(nano); JsonNode jo = new ObjectMapper().valueToTree(nano);
// 协议 9400 则更新到 StrategyAndPool 里面去
if (protocolId == 9400) {
strategyAndPoolService.updateByQueryResponse(jo);
}
// 查找 ResponseDecodeExtension // 查找 ResponseDecodeExtension
List<MethodInfo> methodInfos = responseDecodeExtensions.get(protocolId.toString()); List<MethodInfo> methodInfos = responseDecodeExtensions.get(protocolId.toString());
if (methodInfos != null) { if (methodInfos != null) {
for (MethodInfo methodInfo : methodInfos) { for (MethodInfo methodInfo : methodInfos) {
Object instance = null; if (methodInfo.getInstance() != null) {
if (methodInfo.getDeclaringClass() != null) { // instance 不为 null 则说明是已经取到的 spring bean, 直接调用
// 获取 spring 管理的实例类 methodInfo.getMethod().invoke(methodInfo.getInstance(), jo);
instance = SpringContextHolder.getBean(methodInfo.getDeclaringClass()); }
else if (methodInfo.getDeclaringClass() != null) {
// 获取 spring 管理的实例类
Object instance = SpringContextHolder.getBean(methodInfo.getDeclaringClass());
methodInfo.getMethod().invoke(instance, jo);
methodInfo.setInstance(instance);
}
else {
// 静态方法直接 invoke
methodInfo.getMethod().invoke(null, jo);
} }
// invoke
methodInfo.getMethod().invoke(instance, jo);
} }
} }

View File

@@ -3,12 +3,9 @@ package quant.rich.emoney.controller.common;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import quant.rich.emoney.pojo.dto.R;
import quant.rich.emoney.service.AuthService; import quant.rich.emoney.service.AuthService;
@Controller @Controller

View File

@@ -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), "删除失败,是否已删除?");
}
}

View File

@@ -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);
}
}

View File

@@ -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&lt;Plan></code>
* @param <T> 实体类型
* @see #updateBool(String, String, Boolean)
*/
@Slf4j
public abstract class UpdateBoolServiceController<T> extends ServiceController<T> {
protected ObjectMapper mapper = new ObjectMapper();
/**
* 更新布尔值主方法,以 form 形式 POSTuri: /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 + " 错误");
}
}
}

View File

@@ -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());
}
}

View File

@@ -1,9 +1,9 @@
package quant.rich.emoney.controller.manage; package quant.rich.emoney.controller.manage;
import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -25,7 +25,7 @@ import quant.rich.emoney.service.IndexDetailService;
public class IndexInfoControllerV1 extends BaseController { public class IndexInfoControllerV1 extends BaseController {
@Autowired @Autowired
IndexInfoConfig indexInfo; IndexInfoConfig indexInfoConfig;
@Autowired @Autowired
IndexDetailService indexDetailService; IndexDetailService indexDetailService;
@@ -35,6 +35,11 @@ public class IndexInfoControllerV1 extends BaseController {
return "/admin/v1/manage/indexInfo/index"; return "/admin/v1/manage/indexInfo/index";
} }
/**
* 获取指标详情解释
* @param indexCode
* @return
*/
@GetMapping("/getIndexDetail") @GetMapping("/getIndexDetail")
@ResponseBody @ResponseBody
public R<?> getIndexDetail(String indexCode) { public R<?> getIndexDetail(String indexCode) {
@@ -43,6 +48,11 @@ public class IndexInfoControllerV1 extends BaseController {
indexDetailService.getIndexDetail(indexCode)); indexDetailService.getIndexDetail(indexCode));
} }
/**
* 强制刷新并获取指标详情解释
* @param indexCode
* @return
*/
@GetMapping("/forceRefreshAndGetIndexDetail") @GetMapping("/forceRefreshAndGetIndexDetail")
@ResponseBody @ResponseBody
public R<?> forceRefreshAndGetIndexDetail(String indexCode) { public R<?> forceRefreshAndGetIndexDetail(String indexCode) {
@@ -51,21 +61,14 @@ public class IndexInfoControllerV1 extends BaseController {
indexDetailService.forceRefreshAndGetIndexDetail(indexCode)); 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") @GetMapping("/getFields")
@ResponseBody @ResponseBody
public R<?> getFields(@RequestParam("fields") String[] fields) { public R<?> getFields(@RequestParam("fields") String[] fields) {
if (fields == null || fields.length == 0) { 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<>(); Map<String, Object> map = new HashMap<>();
for (String field : fields) { for (String field : fields) {
map.put(field, indexInfoJson.get(field)); map.put(field, indexInfoJson.get(field));
@@ -73,17 +76,21 @@ public class IndexInfoControllerV1 extends BaseController {
return R.ok(map); return R.ok(map);
} }
/**
* 根据给定 url 获取在线指标配置
* @param url
* @return
*/
@GetMapping("/getConfigIndOnlineByUrl") @GetMapping("/getConfigIndOnlineByUrl")
@ResponseBody @ResponseBody
public R<?> getConfigOnlineByUrl(String url) { public R<?> getConfigOnlineByUrl(String url) {
return R.judge(() -> indexInfo.getOnlineConfigByUrl()); return R.judge(() -> indexInfoConfig.getOnlineConfigByUrl(url));
} }
@GetMapping("/getIndexInfoConfig") @GetMapping("/getIndexInfoConfig")
@ResponseBody @ResponseBody
public R<?> getIndexInfoConfig() { public R<?> getIndexInfoConfig() {
return R.ok(indexInfo); return R.ok(indexInfoConfig);
} }
@GetMapping("/list") @GetMapping("/list")

View File

@@ -13,11 +13,8 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 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 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.entity.sqlite.Plan;
import quant.rich.emoney.exception.RException; import quant.rich.emoney.exception.RException;
import quant.rich.emoney.interfaces.IQueryableEnum; import quant.rich.emoney.interfaces.IQueryableEnum;
@@ -29,7 +26,7 @@ import quant.rich.emoney.service.sqlite.PlanService;
@Slf4j @Slf4j
@Controller @Controller
@RequestMapping("/admin/v1/manage/plan") @RequestMapping("/admin/v1/manage/plan")
public class PlanControllerV1 extends UpdateBoolController<Plan> { public class PlanControllerV1 extends UpdateBoolServiceController<Plan> {
@Autowired @Autowired
PlanService planService; PlanService planService;
@@ -42,35 +39,25 @@ public class PlanControllerV1 extends UpdateBoolController<Plan> {
@GetMapping("/list") @GetMapping("/list")
@ResponseBody @ResponseBody
public LayPageResp<?> list(LayPageReq<Plan> pageReq) { public LayPageResp<?> list(LayPageReq<Plan> pageReq) {
Page<Plan> planPage = planService.page(pageReq); return super.list(pageReq);
return new LayPageResp<>(planPage);
} }
@GetMapping("/getOne") @GetMapping("/getOne")
@ResponseBody @ResponseBody
public R<?> getOne(String planId) { public R<?> getOne(String planId) {
// 如果 planId 是空,说明可能希望新建一个 Plan需要返回默认实例化对象否则从数据库取 return super.getOne(planId);
return
planId == null ? R.ok(new Plan()) :
R.judgeNonNull(planService.getById(planId), "无法找到对应 ID 的 Plan");
} }
@PostMapping("/save") @PostMapping("/save")
@ResponseBody @ResponseBody
public R<?> save(@RequestBody Plan plan) { public R<?> save(@RequestBody Plan plan) {
if (StringUtils.isNotBlank(plan.getPlanId())) { return super.save(plan);
planService.updateById(plan);
}
else {
planService.save(plan.setPlanId(null));
}
return R.ok();
} }
@PostMapping("/delete") @PostMapping("/delete")
@ResponseBody @ResponseBody
public R<?> delete(String planId) { public R<?> delete(String planId) {
return R.judge(planService.removeById(planId), "删除失败,是否已删除?"); return super.delete(planId);
} }
@PostMapping("/batchOp") @PostMapping("/batchOp")

View File

@@ -1,11 +1,8 @@
package quant.rich.emoney.controller.manage; package quant.rich.emoney.controller.manage;
import java.lang.reflect.Field;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping; 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 org.springframework.web.bind.annotation.ResponseBody;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 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 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.entity.sqlite.ProxySetting;
import quant.rich.emoney.exception.RException; import quant.rich.emoney.exception.RException;
import quant.rich.emoney.pojo.dto.LayPageReq; import quant.rich.emoney.pojo.dto.LayPageReq;
@@ -34,7 +25,7 @@ import quant.rich.emoney.service.sqlite.ProxySettingService;
@Slf4j @Slf4j
@Controller @Controller
@RequestMapping("/admin/v1/manage/proxySetting") @RequestMapping("/admin/v1/manage/proxySetting")
public class ProxySettingControllerV1 extends BaseController { public class ProxySettingControllerV1 extends UpdateBoolServiceController<ProxySetting> {
@Autowired @Autowired
ProxySettingService proxySettingService; ProxySettingService proxySettingService;
@@ -47,63 +38,32 @@ public class ProxySettingControllerV1 extends BaseController {
@GetMapping("/list") @GetMapping("/list")
@ResponseBody @ResponseBody
public LayPageResp<?> list(LayPageReq<ProxySetting> pageReq) { public LayPageResp<?> list(LayPageReq<ProxySetting> pageReq) {
Page<ProxySetting> planPage = proxySettingService.page(pageReq); return super.list(pageReq);
return new LayPageResp<>(planPage);
} }
@GetMapping("/getOne") @GetMapping("/getOne")
@ResponseBody @ResponseBody
public R<?> getOne(String id) { public R<?> getOne(String id) {
return super.getOne(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();
} }
@PostMapping("/save") @PostMapping("/save")
@ResponseBody @ResponseBody
public R<?> save(@RequestBody ProxySetting proxySetting) { public R<?> save(@RequestBody ProxySetting proxySetting) {
if (!Objects.isNull(proxySetting.getId())) { return super.save(proxySetting);
proxySettingService.updateById(proxySetting);
}
else {
proxySettingService.save(proxySetting.setId(null));
}
return R.ok();
} }
@PostMapping("/delete") @PostMapping("/delete")
@ResponseBody @ResponseBody
public R<?> delete(String id) { 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") @PostMapping("/batchOp")
@ResponseBody @ResponseBody
@@ -149,5 +109,14 @@ public class ProxySettingControllerV1 extends BaseController {
DISABLE_HTTPS_VERIFY, DISABLE_HTTPS_VERIFY,
ENABLE_HTTP_VERIFY ENABLE_HTTP_VERIFY
} }
@GetMapping("/refreshIpThroughProxy")
@ResponseBody
public R<?> refreshIpThroughProxy() {
return R.ok(
proxySettingService
.refreshIpThroughProxy());
}
} }

View File

@@ -1,5 +1,8 @@
package quant.rich.emoney.controller.manage; package quant.rich.emoney.controller.manage;
import java.util.Arrays;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.stereotype.Controller; 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.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody; 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 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.entity.sqlite.RequestInfo;
import quant.rich.emoney.exception.RException;
import quant.rich.emoney.pojo.dto.LayPageReq; import quant.rich.emoney.pojo.dto.LayPageReq;
import quant.rich.emoney.pojo.dto.LayPageResp; import quant.rich.emoney.pojo.dto.LayPageResp;
import quant.rich.emoney.pojo.dto.R; import quant.rich.emoney.pojo.dto.R;
@@ -23,7 +29,7 @@ import quant.rich.emoney.service.sqlite.RequestInfoService;
@Slf4j @Slf4j
@Controller @Controller
@RequestMapping("/admin/v1/manage/requestInfo") @RequestMapping("/admin/v1/manage/requestInfo")
public class RequestInfoControllerV1 extends UpdateBoolController<RequestInfo> { public class RequestInfoControllerV1 extends UpdateBoolServiceController<RequestInfo> {
@Autowired @Autowired
RequestInfoService requestInfoService; RequestInfoService requestInfoService;
@@ -36,42 +42,57 @@ public class RequestInfoControllerV1 extends UpdateBoolController<RequestInfo> {
@GetMapping("/list") @GetMapping("/list")
@ResponseBody @ResponseBody
public LayPageResp<?> list(LayPageReq<RequestInfo> pageReq) { public LayPageResp<?> list(LayPageReq<RequestInfo> pageReq) {
Page<RequestInfo> planPage = requestInfoService.page(pageReq); return super.list(pageReq);
return new LayPageResp<>(planPage);
} }
@GetMapping("/getOne") @GetMapping("/getOne")
@ResponseBody @ResponseBody
public R<?> getOne(Integer id) { public R<?> getOne(Integer id) {
return super.getOne(id);
// 如果 id 是空,说明可能希望新建并返回默认实例化对象
if (id == null) {
return R.ok(new RequestInfo());
}
// 否则从数据库取
RequestInfo requestInfo = requestInfoService.getById(id);
return R.judgeNonNull(requestInfo, "无法找到对应 ID 的请求信息");
} }
@PostMapping("/save") @PostMapping("/save")
@ResponseBody @ResponseBody
public R<?> save(@RequestBody @NonNull RequestInfo plan) { public R<?> save(@RequestBody @NonNull RequestInfo requestInfo) {
return R.judge(() -> requestInfoService.saveOrUpdate(plan)); return super.save(requestInfo);
} }
@PostMapping("/delete") @PostMapping("/delete")
@ResponseBody @ResponseBody
public R<?> delete(String id) { 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") @PostMapping("/batchOp")
@ResponseBody @ResponseBody
public R<?> batchOp( public R<?> batchOp(
@RequestParam(value="ids[]", required=true) @RequestParam(value="ids[]", required=true)
String[] ids, String op) { @Valid @NotEmpty String[] ids, @NotNull RequestInfoBatchOp op) {
return null;
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
} }
} }

View File

@@ -19,6 +19,7 @@ public class AndroidSdkLevelConfig implements IConfig<AndroidSdkLevelConfig> {
public AndroidSdkLevelConfig() { public AndroidSdkLevelConfig() {
androidVerToSdk = new HashMap<>(); androidVerToSdk = new HashMap<>();
androidVerToSdk.put("15", 35);
androidVerToSdk.put("14", 34); androidVerToSdk.put("14", 34);
androidVerToSdk.put("13", 33); androidVerToSdk.put("13", 33);
androidVerToSdk.put("12L", 32); androidVerToSdk.put("12L", 32);

View File

@@ -49,6 +49,11 @@ public class DeviceInfoConfig implements IConfig<DeviceInfoConfig> {
@Slf4j @Slf4j
public static class DeviceInfo { public static class DeviceInfo {
// 持久化在本地的 DeviceInfo 只有三个字段:
// model、deviceType 和 fingerprint
// 其中除 model 和 deviceType 外,其他字段全部从 fingerprint 派生
// 也就是说只要提供 model、deviceType 和 fingerprint 就能创建一个 DeviceInfo 实例
@JsonView(IConfig.Views.Persistence.class) @JsonView(IConfig.Views.Persistence.class)
private String model; private String model;
private String brand; private String brand;
@@ -62,11 +67,12 @@ public class DeviceInfoConfig implements IConfig<DeviceInfoConfig> {
private String buildType; private String buildType;
private String buildTags; private String buildTags;
/**
* 用以匹配 fingerprint 的正则表达式
*/
public static final Pattern PATTERN = Pattern.compile("^(?<brand>.*?)/(?<product>.*?)/(?<device>.*?):(?<versionRelease>.*?)/(?<buildId>.*?)/(?<buildNumber>.*?):(?<buildType>.*?)/(?<buildTags>.*?)$"); public static final Pattern PATTERN = Pattern.compile("^(?<brand>.*?)/(?<product>.*?)/(?<device>.*?):(?<versionRelease>.*?)/(?<buildId>.*?)/(?<buildNumber>.*?):(?<buildType>.*?)/(?<buildTags>.*?)$");
private DeviceInfo() {}
private DeviceInfo() {
}
public DeviceInfo setFingerprint(String fingerprint) { public DeviceInfo setFingerprint(String fingerprint) {
Matcher m = PATTERN.matcher(fingerprint); Matcher m = PATTERN.matcher(fingerprint);
@@ -126,8 +132,8 @@ public class DeviceInfoConfig implements IConfig<DeviceInfoConfig> {
} }
public final String toString() { public final String toString() {
return String.format("Model: %s, Fingerprint: %s", return String.format("Model: %s, DeviceType: %s, Fingerprint: %s",
getModel(), getFingerprint() getModel(), getDeviceType(), getFingerprint()
); );
} }
@@ -139,7 +145,7 @@ public class DeviceInfoConfig implements IConfig<DeviceInfoConfig> {
} }
public int hashCode() { public int hashCode() {
return Objects.hash(getModel(), getFingerprint()); return Objects.hash(getModel(), getDeviceType(), getFingerprint());
} }
} }
} }

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -1,25 +1,21 @@
package quant.rich.emoney.entity.config; package quant.rich.emoney.entity.config;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import lombok.AccessLevel;
import lombok.Data; import lombok.Data;
import lombok.Getter;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import okhttp3.ConnectionPool;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import quant.rich.emoney.annotation.LockByCaller;
import quant.rich.emoney.client.OkHttpClientProvider; import quant.rich.emoney.client.OkHttpClientProvider;
import quant.rich.emoney.component.CallerLockAspect.LockByCaller;
import quant.rich.emoney.interfaces.ConfigInfo; import quant.rich.emoney.interfaces.ConfigInfo;
import quant.rich.emoney.interfaces.IConfig; 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) @JsonView(IConfig.Views.Persistence.class)
private JsonNode configIndOnline; private JsonNode configIndOnline;
@Autowired
@JsonIgnore
@Getter(AccessLevel.PRIVATE)
private EmoneyRequestConfig emoneyRequestConfig;
public IndexInfoConfig() {} public IndexInfoConfig() {}
public String getConfigIndOnlineStr() { public String getConfigIndOnlineStr() {
@@ -49,10 +40,13 @@ public class IndexInfoConfig implements IConfig<IndexInfoConfig> {
@LockByCaller @LockByCaller
@JsonIgnore @JsonIgnore
public String getOnlineConfigByUrl() throws IOException { public String getOnlineConfigByUrl(String url) throws IOException {
synchronized (this) { synchronized (this) {
if (SpringContextHolder.getBean(RequestInfoService.class).getDefaultRequestInfo() == null) {
throw new RuntimeException("请先新增请求配置并作为默认配置");
}
Request request = new Request.Builder() Request request = new Request.Builder()
.url(configIndOnlineUrl) .url(url)
.header("Cache-Control", "no-cache") .header("Cache-Control", "no-cache")
.get() .get()
.build(); .build();
@@ -68,7 +62,5 @@ public class IndexInfoConfig implements IConfig<IndexInfoConfig> {
} }
} }
} }
public static ConnectionPool connectionPool = new ConnectionPool(10, 5, TimeUnit.MINUTES);
} }

View File

@@ -15,6 +15,8 @@ public class PlatformConfig implements IConfig<PlatformConfig> {
private String password; private String password;
private String email; private String email;
private String apiToken;
private Boolean isInited; private Boolean isInited;

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -26,6 +26,8 @@ public class ProxySetting {
@TableId(value="id", type=IdType.AUTO) @TableId(value="id", type=IdType.AUTO)
private Integer id; private Integer id;
private Boolean isDefault;
@Nonnull @Nonnull
private String proxyName; private String proxyName;

View File

@@ -7,12 +7,14 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Setter; import lombok.Setter;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j; 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.ChromeVersionsConfig;
import quant.rich.emoney.entity.config.DeviceInfoConfig; import quant.rich.emoney.entity.config.DeviceInfoConfig;
import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo; 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.EncryptUtils;
import quant.rich.emoney.util.SpringContextHolder; import quant.rich.emoney.util.SpringContextHolder;
import quant.rich.emoney.util.TextUtils; import quant.rich.emoney.util.TextUtils;
import quant.rich.emoney.validator.RequestInfoValid;
/**
* 用于配置请求时的请求行为一般而言请求头与安卓系统的信息有关build.prop
* 虽然部分请求对应服务器可能不进行审核,但合理的请求头能尽可能模仿真机行为,避免风险
* @see DeviceInfoConfig
* @see AndroidSdkLevelConfig
* @see ChromeVersionsConfig
*/
@Data @Data
@EqualsAndHashCode(callSuper=false)
@Accessors(chain = true) @Accessors(chain = true)
@Slf4j @Slf4j
@RequestInfoValid
@TableName(value = "request_info") @TableName(value = "request_info")
public class RequestInfo { public class RequestInfo extends Model<RequestInfo> {
private static AndroidSdkLevelConfig androidSdkLevelConfig = SpringContextHolder.getBean(AndroidSdkLevelConfig.class); private static final long serialVersionUID = -3113053377999289627L;
private static DeviceInfoConfig deviceInfoConfig = SpringContextHolder.getBean(DeviceInfoConfig.class);
private static ChromeVersionsConfig chromeVersionsConfig = SpringContextHolder.getBean(ChromeVersionsConfig.class); 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() { public RequestInfo() {
DeviceInfo deviceInfo = deviceInfoConfig.getRandomDeviceInfo(); DeviceInfo deviceInfo = deviceInfoConfig.getRandomDeviceInfo();
setRelativeFieldsFromDeviceInfo(deviceInfo); setRelativeFieldsFromDeviceInfo(deviceInfo);
@@ -43,15 +60,15 @@ public class RequestInfo {
@TableId(value = "id", type = IdType.AUTO) @TableId(value = "id", type = IdType.AUTO)
private Integer id; private Integer id;
/**
* 该请求是否设为默认请求
*/
private Boolean isDefault = false;
/** /**
* 该请求信息配置的名称,助记用 * 该请求信息配置的名称,助记用
*/ */
private String name = ""; private String name = "";
/**
* 是否匿名登录
*/
private Boolean isAnonymous = true;
/** /**
* 非匿名登录时的用户名 * 非匿名登录时的用户名
@@ -88,8 +105,9 @@ public class RequestInfo {
* <li>Webview <code><i>User-Agent</i></li> * <li>Webview <code><i>User-Agent</i></li>
* <li>Non-Webview Image <code><i>User-Agent</i></code></li> * <li>Non-Webview Image <code><i>User-Agent</i></code></li>
* </ul> * </ul>
* <b>来源:</b><code><b>DeviceInfoConfig</b></code> * <b>来源:</b><code><b>DeviceInfo</b></code>
* @see DeviceInfoConfig * @see DeviceInfoConfig
* @see DeviceInfo
*/ */
@Setter(AccessLevel.PRIVATE) @Setter(AccessLevel.PRIVATE)
@TableField(exist=false) @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>X-Android-Agent</i> = EMAPP/{<b>emoneyVersion</b>}(Android;{<b>androidSdkLevel</b>})</code></li>
* <li>益盟登录接口 <code><i>osVersion</i> = <b>androidSdkLevel</b></code></li> * <li>益盟登录接口 <code><i>osVersion</i> = <b>androidSdkLevel</b></code></li>
* </ul> * </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 DeviceInfoConfig
* @see DeviceInfo
* @see AndroidSdkLevelConfig * @see AndroidSdkLevelConfig
*/ */
@Setter(AccessLevel.PRIVATE) @Setter(AccessLevel.PRIVATE)
@@ -112,8 +131,9 @@ public class RequestInfo {
* <b>用于:</b><ul> * <b>用于:</b><ul>
* <li>益盟登录接口 <code><i>softwareType</i> = <b>softwareType</b></code></li> * <li>益盟登录接口 <code><i>softwareType</i> = <b>softwareType</b></code></li>
* </ul> * </ul>
* <b>来源:</b><code><b>DeviceInfoConfig</b>,由本例代管</code> * <b>来源:</b><code><b>DeviceInfo</b>
* @see DeviceInfoConfig * @see DeviceInfoConfig
* @see DeviceInfo
*/ */
private String softwareType; 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>
* <li>非 WebView 图片<code><i>User-Agent</i></code></li> * <li>非 WebView 图片<code><i>User-Agent</i></code></li>
* </ul> * </ul>
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理 * <b>来源:</b><code><b>DeviceInfo</b>
* @see DeviceInfoConfig * @see DeviceInfoConfig
* @see DeviceInfo
*/ */
private String deviceName; private String deviceName;
@@ -177,7 +198,7 @@ public class RequestInfo {
* </ul> * </ul>
* 由程序版本决定<br> * 由程序版本决定<br>
* <b>来源:</b>本例管理 * <b>来源:</b>本例管理
* @see EmoneyRequestConfig.androidSdkLevel * @see #androidSdkLevel
*/ */
private String emoneyVersion = "5.8.1"; private String emoneyVersion = "5.8.1";
@@ -429,5 +450,9 @@ public class RequestInfo {
return node; return node;
} }
public boolean isAnonymous() {
return !StringUtils.isAnyBlank(getUsername(), getPassword());
}
} }

View File

@@ -21,6 +21,7 @@ public class StrategyAndPool implements Comparable<StrategyAndPool> {
private String strategyName; private String strategyName;
private Integer strategyId; private Integer strategyId;
private String poolName; private String poolName;
private String type;
@TableId @TableId
private Integer poolId; 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.strategyName = strategyName;
this.strategyId = strategyId; this.strategyId = strategyId;
this.poolName = poolName; this.poolName = poolName;
@@ -38,18 +40,20 @@ public class StrategyAndPool implements Comparable<StrategyAndPool> {
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null) return false;
if (!(o instanceof StrategyAndPool)) return false; if (!(o instanceof StrategyAndPool)) return false;
StrategyAndPool strategyAndPool = (StrategyAndPool) o; StrategyAndPool strategyAndPool = (StrategyAndPool) o;
return return
strategyName == strategyAndPool.strategyName && Objects.equals(strategyName, strategyAndPool.strategyName) &&
strategyId == strategyAndPool.strategyId && Objects.equals(strategyId, strategyAndPool.strategyId) &&
poolName == strategyAndPool.poolName && Objects.equals(poolName, strategyAndPool.poolName) &&
poolId == strategyAndPool.poolId; Objects.equals(poolId, strategyAndPool.poolId) &&
Objects.equals(type, strategyAndPool.type);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(strategyName, strategyId, poolName, poolId); return Objects.hash(strategyName, strategyId, poolName, poolId, type);
} }
/** /**

View File

@@ -21,7 +21,12 @@ import org.springframework.stereotype.Component;
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface ConfigInfo { 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 ""; String field() default "";

View File

@@ -41,10 +41,11 @@ public class PatchOkHttp {
randomIds[0] = random.nextInt(); randomIds[0] = random.nextInt();
} }
rule.setId(randomIds[0]); rule.setId(randomIds[0]);
log.debug("PatchOkHttp.apply(rule.id={})", randomIds[0]);
} }
rules.add(rule); rules.add(rule);
log.debug("PatchOkHttp.apply() 在 ClassLoader {} 中运行", PatchOkHttp.class.getClassLoader()); //log.debug("PatchOkHttp.apply() 在 ClassLoader {} 中运行", PatchOkHttp.class.getClassLoader());
if (!isHooked) hook(); if (!isHooked) hook();
return rule.getId(); return rule.getId();
} }
@@ -60,12 +61,10 @@ public class PatchOkHttp {
} }
public static void match(RequestContext ctx, String currentHeader, Consumer<String> consumer) { public static void match(RequestContext ctx, String currentHeader, Consumer<String> consumer) {
if (!logOnce) { // log.debug("PatchOkHttp.match() 在 ClassLoader {} 中运行", PatchOkHttp.class.getClassLoader());
log.debug("PatchOkHttp.match() 在 ClassLoader {} 中运行", PatchOkHttp.class.getClassLoader());
logOnce = true;
}
for (PatchOkHttpRule rule : PatchOkHttp.rules) { for (PatchOkHttpRule rule : PatchOkHttp.rules) {
if (rule.matches(ctx)) { if (rule.matches(ctx)) {
log.debug("PatchOkHttp.match() 匹配到规则 rule.id={}", rule.getId());
rule.apply(ctx, currentHeader, consumer); rule.apply(ctx, currentHeader, consumer);
} }
} }

View File

@@ -154,7 +154,7 @@ public class PatchOkHttpRule {
return this; return this;
} }
public Builder overrideIf(String headerName, String value) { public Builder overrideHeader(String headerName, String value) {
actions.add((ctx, curr, setter) -> { actions.add((ctx, curr, setter) -> {
if (curr.equalsIgnoreCase(headerName)) { if (curr.equalsIgnoreCase(headerName)) {
log.debug("matches and applying - host: {}, currHeader {}, targetHeader {}, value: {}, classLoader: {}", ctx.host, curr, headerName, log.debug("matches and applying - host: {}, currHeader {}, targetHeader {}, value: {}, classLoader: {}", ctx.host, curr, headerName,
@@ -164,6 +164,24 @@ public class PatchOkHttpRule {
}); });
return this; 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() { public PatchOkHttpRule build() {
return new PatchOkHttpRule(condition, actions); return new PatchOkHttpRule(condition, actions);

View File

@@ -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 { public static R<?> judgeThrow(ThrowingSupplier<?> supplier) throws Exception {
return R.ok(supplier.get()); return R.ok(supplier.get());

View File

@@ -197,21 +197,26 @@ public class ConfigService implements InitializingBean {
} }
/** /**
* 获取 Config * 获取给定类型的 Config 实例
* <p> * <p>
* 当缓存中有时从缓存取, 缓存没有时从数据库取并更新到缓存, 数据库也没有时, 如果指定的 Config 的 @ConfigInfo * 当缓存中有时从缓存取,缓存没有时从数据库取并更新到缓存,数据库也没有时,
* 注解开启了 initDefault = true 则尝试返回一个初始 Config否则返回 null * 如果指定的 Config 的 @ConfigInfo
* 注解开启了 initDefault = true 则尝试返回一个初始 Config否则返回 null.
* 原本不存在的 Config 如果 @ConfigInfo 开启了 save(),会持久化到本地。
* *
* @param <Config> * @param <Config>
* @param clazz * @param clazz 配置类
* @return * @return
* @see ConfigInfo
* @see #getOrCreateConfig(String)
*
*/ */
public <Config extends IConfig<Config>> Config getConfig(Class<Config> clazz) { public <Config extends IConfig<Config>> Config getConfig(Class<Config> clazz) {
if (classObjectCache.containsKey(clazz)) { if (classObjectCache.containsKey(clazz)) {
try { try {
return getCache(clazz); return getCache(clazz);
} catch (Exception e) { } 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); String field = fieldClassCache.inverse().get(clazz);
@@ -244,11 +249,6 @@ public class ConfigService implements InitializingBean {
try { try {
String filePath = getConfigFilePath(field, false); String filePath = getConfigFilePath(field, false);
SmartResourceResolver.saveText(filePath, configJoString); 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) { } catch (IOException e) {
log.error("Cannot write config to local file {}.json, error: {}", field, e.getMessage()); log.error("Cannot write config to local file {}.json, error: {}", field, e.getMessage());
return false; return false;
@@ -300,7 +300,7 @@ public class ConfigService implements InitializingBean {
Class<Config> configClass = (Class<Config>) fieldClassCache.get(field); Class<Config> configClass = (Class<Config>) fieldClassCache.get(field);
if (configClass == null) { 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; return null;
} }
ConfigInfo info = getConfigInfo(configClass); ConfigInfo info = getConfigInfo(configClass);
@@ -313,10 +313,12 @@ public class ConfigService implements InitializingBean {
// 也就是无论如何fallback 都不应由程序来写入 // 也就是无论如何fallback 都不应由程序来写入
String filePath = getConfigFilePath(field, false); String filePath = getConfigFilePath(field, false);
config = getFromFile(filePath, configClass); config = getFromFile(filePath, configClass);
boolean needSave = false;
if (config == null) { if (config == null) {
log.info("Cannot init config from local file of {}Config, try fallback", field); log.info("Cannot init config from local file of {}Config, try fallback", field);
// 走 fallback 流程 // 走 fallback 流程
config = getFromFile(getConfigFilePath(field, true), configClass); config = getFromFile(getConfigFilePath(field, true), configClass);
needSave = true;
} }
if (config == null) { if (config == null) {
@@ -328,6 +330,7 @@ public class ConfigService implements InitializingBean {
config = config =
configClass.getDeclaredConstructor() configClass.getDeclaredConstructor()
.newInstance(); .newInstance();
needSave = true;
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) { | InvocationTargetException | NoSuchMethodException | SecurityException e) {
// 一般是初始化方法内出现未被捕获的错误 // 一般是初始化方法内出现未被捕获的错误
@@ -340,7 +343,8 @@ public class ConfigService implements InitializingBean {
if (config == null) { if (config == null) {
log.warn("Cannot read or init from default/fallback for config {}Config, it will be null", field); log.warn("Cannot read or init from default/fallback for config {}Config, it will be null", field);
} }
else { else if (needSave) {
// 走了 fallback 或者初始化了则保存一份
saveOrUpdate(config); saveOrUpdate(config);
} }

View File

@@ -39,15 +39,17 @@ import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
import quant.rich.emoney.client.OkHttpClientProvider; 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.IndexInfoConfig;
import quant.rich.emoney.entity.config.SmartViewWriter; 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.IndexDetail;
import quant.rich.emoney.pojo.dto.NonParamsIndexDetail; import quant.rich.emoney.pojo.dto.NonParamsIndexDetail;
import quant.rich.emoney.pojo.dto.NonParamsIndexDetail.NonParamsIndexDetailData; import quant.rich.emoney.pojo.dto.NonParamsIndexDetail.NonParamsIndexDetailData;
import quant.rich.emoney.util.EncryptUtils; import quant.rich.emoney.util.EncryptUtils;
import quant.rich.emoney.util.SmartResourceResolver; import quant.rich.emoney.util.SmartResourceResolver;
import quant.rich.emoney.pojo.dto.ParamsIndexDetail; import quant.rich.emoney.pojo.dto.ParamsIndexDetail;
import quant.rich.emoney.service.sqlite.RequestInfoService;
/** /**
* 获取指标详情的服务 * 获取指标详情的服务
@@ -67,7 +69,7 @@ public class IndexDetailService {
IndexInfoConfig indexInfoConfig; IndexInfoConfig indexInfoConfig;
@Autowired @Autowired
EmoneyRequestConfig emoneyRequestConfig; RequestInfoService requestInfoService;
static final String filePath = "./conf/extra/indexDetail/"; static final String filePath = "./conf/extra/indexDetail/";
static final ObjectMapper mapper = new ObjectMapper(); static final ObjectMapper mapper = new ObjectMapper();
@@ -84,6 +86,7 @@ public class IndexDetailService {
* @return * @return
*/ */
@CacheEvict(cacheNames="@indexDetailService.getIndexDetail(Serializable)", key="#indexCode.toString()") @CacheEvict(cacheNames="@indexDetailService.getIndexDetail(Serializable)", key="#indexCode.toString()")
@RequireAuthAndProxy(autoLogin = true)
public IndexDetail forceRefreshAndGetIndexDetail(Serializable indexCode) { public IndexDetail forceRefreshAndGetIndexDetail(Serializable indexCode) {
// 刷新的本质就是从网络获取,因为此处已经清理了缓存,所以直接从网络获取后再 // 刷新的本质就是从网络获取,因为此处已经清理了缓存,所以直接从网络获取后再
@@ -101,6 +104,7 @@ public class IndexDetailService {
@Cacheable(cacheNames="@indexDetailService.getIndexDetail(Serializable)", key="#indexCode.toString()") @Cacheable(cacheNames="@indexDetailService.getIndexDetail(Serializable)", key="#indexCode.toString()")
@RequireAuthAndProxy(autoLogin = true)
public IndexDetail getIndexDetail(Serializable indexCode) { public IndexDetail getIndexDetail(Serializable indexCode) {
if (indexCode == null) { if (indexCode == null) {
@@ -137,11 +141,16 @@ public class IndexDetailService {
/** /**
* 从网络获取有参指标详情 * 从网络获取有参指标详情
* <p>本例用到的 requestInfo 涉及鉴权
* @param indexCode * @param indexCode
* @see RequestInfoService#getDefaultRequestInfo()
* @return * @return
*/ */
private ParamsIndexDetail getParamsIndexDetailOnline(Serializable indexCode) { private ParamsIndexDetail getParamsIndexDetailOnline(Serializable indexCode) {
RequestInfo requestInfo = requestInfoService.getDefaultRequestInfo();
if (requestInfo == null || StringUtils.isBlank(requestInfo.getAuthorization())) {
throw new RuntimeException("无法获取已鉴权的 RequestInfo");
}
try { try {
OkHttpClient client = OkHttpClientProvider.getInstance(); OkHttpClient client = OkHttpClientProvider.getInstance();
String url = "https://emapp.emoney.cn/Config/AppIndicator/Get"; 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-Protocol-Id", "Config%2FAppIndicator%2FGet")
.header("X-Request-Id", "null") .header("X-Request-Id", "null")
.header("EM-Sign", EncryptUtils.getEMSign(content, "POST", "Config%2FAppIndicator%2FGet")) .header("EM-Sign", EncryptUtils.getEMSign(content, "POST", "Config%2FAppIndicator%2FGet"))
.header("Authorization", emoneyRequestConfig.getAuthorization()) .header("Authorization", requestInfoService.getDefaultRequestInfo().getAuthorization())
.header("X-Android-Agent", emoneyRequestConfig.getXAndroidAgent()) .header("X-Android-Agent", requestInfoService.getDefaultRequestInfo().getXAndroidAgent())
.header("Emapp-ViewMode", emoneyRequestConfig.getEmappViewMode()) .header("Emapp-ViewMode", requestInfoService.getDefaultRequestInfo().getEmappViewMode())
.build(); .build();
final Response response = client.newCall(request).execute(); final Response response = client.newCall(request).execute();
String responseText = response.body().string(); String responseText = response.body().string();
@@ -223,8 +232,11 @@ public class IndexDetailService {
/** /**
* 从网络获取指定 indexCode 的无参指标详情 * 从网络获取指定 indexCode 的无参指标详情
* <p>本例用到的 requestInfo 不需要 PatchOkHttp 覆写,但要求鉴权参数拼接到 url 中,故要求鉴权
* <p>会一并尝试获取其他在本地未有的无参指标</p> * <p>会一并尝试获取其他在本地未有的无参指标</p>
* @param indexCode * @param indexCode
* @see RequestInfo#getWebviewUserAgent()
* @see RequestInfoService#getDefaultRequestInfo()
* @return * @return
*/ */
private NonParamsIndexDetail getNonParamsIndexDetailOnline(Serializable indexCode) { private NonParamsIndexDetail getNonParamsIndexDetailOnline(Serializable indexCode) {
@@ -234,7 +246,7 @@ public class IndexDetailService {
.header("Host", "appstatic.emoney.cn") .header("Host", "appstatic.emoney.cn")
.header("Connection", "keep-alive") .header("Connection", "keep-alive")
.header("Upgrade-Insecure-Requests", "1") .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("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("X-Request-With", "cn.emoney.emstock")
.header("Sec-Fetch-Site", "none") .header("Sec-Fetch-Site", "none")
@@ -259,7 +271,7 @@ public class IndexDetailService {
Document doc = Jsoup.parseBodyFragment(responseBody); Document doc = Jsoup.parseBodyFragment(responseBody);
doc.select("script[src]").forEach(el -> { doc.select("script[src]").forEach(el -> {
String absoluteURI = resolveUrl(url, el.attr("src")); String absoluteURI = resolveUrl(url, el.attr("src"));
log.info("script uri: {}", absoluteURI); log.debug("script uri: {}", absoluteURI);
if (absoluteURI != null) { if (absoluteURI != null) {
scripts.add(absoluteURI); scripts.add(absoluteURI);
} }
@@ -281,7 +293,7 @@ public class IndexDetailService {
Request.Builder scriptBuilder = new Request.Builder() Request.Builder scriptBuilder = new Request.Builder()
.header("Host", "appstatic.emoney.cn") .header("Host", "appstatic.emoney.cn")
.header("Connection", "keep-alive") .header("Connection", "keep-alive")
.header("User-Agent", emoneyRequestConfig.getWebviewUserAgent()) .header("User-Agent", requestInfoService.getDefaultRequestInfo().getWebviewUserAgent())
.header("Accept", "*/*") .header("Accept", "*/*")
.header("X-Request-With", "cn.emoney.emstock") .header("X-Request-With", "cn.emoney.emstock")
.header("Sec-Fetch-Site", "same-origin") .header("Sec-Fetch-Site", "same-origin")
@@ -381,7 +393,9 @@ public class IndexDetailService {
NonParamsIndexDetail existed; NonParamsIndexDetail existed;
try { try {
existed = mapper.readValue(inputStream, NonParamsIndexDetail.class); 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); saveIndexDetail(detail);
} }
} }
@@ -415,10 +429,10 @@ public class IndexDetailService {
/** /**
* 为指定 NonParamsIndexDetail 加载在线图片并转换成 base64 * 为指定 NonParamsIndexDetail 加载在线图片并转换成 base64
* <p>该方法涉及 authorization(token)/webviewUserAgent需确保 EmoneyRequestConfig 已正确注入并登录</p> * <p>该方法涉及 authorization(token)/webviewUserAgent需确保 RequestInfo 已正确配置并登录</p>
* @param detail * @param detail
* @return * @return
* @see EmoneyRequestConfig * @see RequestInfo
*/ */
private NonParamsIndexDetail loadImages(NonParamsIndexDetail detail) { private NonParamsIndexDetail loadImages(NonParamsIndexDetail detail) {
OkHttpClient client = OkHttpClientProvider.getInstance(); OkHttpClient client = OkHttpClientProvider.getInstance();
@@ -444,7 +458,7 @@ public class IndexDetailService {
.url(imageUrl) .url(imageUrl)
.header("Host", host) .header("Host", host)
.header("Connection", "keep-alive") .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("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
.header("X-Request-With", "cn.emoney.emstock") .header("X-Request-With", "cn.emoney.emstock")
.header("Sec-Fetch-Site", "same-origin") .header("Sec-Fetch-Site", "same-origin")
@@ -548,17 +562,23 @@ public class IndexDetailService {
/** /**
* 获取 NonParamsIndexDetail URL * 获取 NonParamsIndexDetail URL
* <p>该 Url 涉及 authorization(token),需确保 EmoneyRequestConfig 已正确注入并登录</p> * <p>该 Url 涉及 authorization(token),需确保 RequestInfo 已正确配置并登录</p>
* @param indexCode * @param indexCode
* @return * @return
* @see EmoneyRequestConfig * @see RequestInfo
*/ */
private String buildNonParamsIndexUrl(Serializable indexCode) { private String buildNonParamsIndexUrl(Serializable indexCode) {
RequestInfo requestInfo = requestInfoService.getDefaultRequestInfo();
if (requestInfo == null || StringUtils.isBlank(requestInfo.getAuthorization())) {
throw new RuntimeException("无法获取已鉴权的 RequestInfo");
}
StringBuilder urlBuilder = new StringBuilder(); StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append("https://appstatic.emoney.cn/html/emapp/stock/note/?name="); urlBuilder.append("https://appstatic.emoney.cn/html/emapp/stock/note/?name=");
urlBuilder.append(indexCode.toString()); urlBuilder.append(indexCode.toString());
urlBuilder.append("&emoneyScaleType=0&emoneyLandMode=0&token="); urlBuilder.append("&emoneyScaleType=0&emoneyLandMode=0&token=");
urlBuilder.append(emoneyRequestConfig.getAuthorization()); urlBuilder.append(requestInfoService.getDefaultRequestInfo().getAuthorization());
return urlBuilder.toString(); return urlBuilder.toString();
} }

View 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;
}
}
}

View File

@@ -2,11 +2,43 @@ package quant.rich.emoney.service.sqlite;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.baomidou.dynamic.datasource.annotation.DS; 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.entity.sqlite.ProxySetting;
import quant.rich.emoney.mapper.sqlite.ProxySettingMapper; import quant.rich.emoney.mapper.sqlite.ProxySettingMapper;
import quant.rich.emoney.pojo.dto.IpInfo;
import quant.rich.emoney.util.GeoIPUtil;
@DS("sqlite") @DS("sqlite")
@Service @Service
public class ProxySettingService extends SqliteServiceImpl<ProxySettingMapper, ProxySetting> { 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;
}
} }

View File

@@ -1,14 +1,52 @@
package quant.rich.emoney.service.sqlite; package quant.rich.emoney.service.sqlite;
import java.util.function.Supplier;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo; import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import quant.rich.emoney.entity.sqlite.RequestInfo; import quant.rich.emoney.entity.sqlite.RequestInfo;
import quant.rich.emoney.mapper.sqlite.RequestInfoMapper; import quant.rich.emoney.mapper.sqlite.RequestInfoMapper;
import quant.rich.emoney.patch.okhttp.PatchOkHttp;
import quant.rich.emoney.patch.okhttp.PatchOkHttpRule;
@DS("sqlite") @DS("sqlite")
@Service @Service
@Slf4j
@Lazy(false)
public class RequestInfoService extends SqliteServiceImpl<RequestInfoMapper, RequestInfo> { 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;
}
} }

View File

@@ -3,10 +3,14 @@ package quant.rich.emoney.service.sqlite;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.baomidou.dynamic.datasource.annotation.DS; 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.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
@@ -76,6 +80,7 @@ public class StrategyAndPoolService extends SqliteServiceImpl<StrategyAndPoolMap
* @param jsonNode * @param jsonNode
*/ */
@ResponseDecodeExtension(protocolId="9400") @ResponseDecodeExtension(protocolId="9400")
@Async
public void updateByQueryResponse(JsonNode jsonNode) { public void updateByQueryResponse(JsonNode jsonNode) {
// jsonNode.output[].band[]/.tech[] // jsonNode.output[].band[]/.tech[]
@@ -91,6 +96,7 @@ public class StrategyAndPoolService extends SqliteServiceImpl<StrategyAndPoolMap
} }
Set<StrategyAndPool> set = new HashSet<>(); Set<StrategyAndPool> set = new HashSet<>();
String[] types = new String[] {"band", "tech", "val"}; // 波段/技术/基本面 String[] types = new String[] {"band", "tech", "val"}; // 波段/技术/基本面
for (JsonNode node : output) { for (JsonNode node : output) {
for (String type : types) { for (String type : types) {
@@ -98,17 +104,23 @@ public class StrategyAndPoolService extends SqliteServiceImpl<StrategyAndPoolMap
if (strategies != null && strategies.getNodeType() == JsonNodeType.ARRAY) { if (strategies != null && strategies.getNodeType() == JsonNodeType.ARRAY) {
for (JsonNode simpleStrategy : strategies) { for (JsonNode simpleStrategy : strategies) {
StrategyAndPool strategyAndPool = new StrategyAndPool( StrategyAndPool strategyAndPool = new StrategyAndPool(
type,
simpleStrategy.get("strategyName").asText(), simpleStrategy.get("strategyName").asText(),
simpleStrategy.get("strategyId").asInt(), simpleStrategy.get("strategyId").asInt(),
simpleStrategy.get("poolName").asText(), simpleStrategy.get("poolName").asText(),
simpleStrategy.get("poolId").asInt() simpleStrategy.get("poolId").asInt()
); );
set.add(strategyAndPool); if (!set.contains(strategyAndPool) && !exists(
new LambdaQueryWrapper<StrategyAndPool>()
.eq(StrategyAndPool::getPoolId, strategyAndPool.getPoolId()))) {
set.add(strategyAndPool);
}
} }
} }
} }
} }
this.saveOrUpdateBatch(set); this.saveOrUpdateBatch(set);
log.info("新增 {} 条 StrategyAndPool", set.size());
} }
} }

View File

@@ -24,7 +24,6 @@ import org.bouncycastle.util.encoders.Hex;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
@Slf4j @Slf4j

View File

@@ -10,7 +10,7 @@ import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import quant.rich.emoney.client.OkHttpClientProvider; 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 quant.rich.emoney.pojo.dto.IpInfo;
import java.io.IOException; import java.io.IOException;
@@ -36,10 +36,13 @@ public class GeoIPUtil {
} }
@Async @Async
public static IpInfo getIpInfoThroughProxy(ProxyConfig proxyConfig) { public static IpInfo getIpInfoThroughProxy(ProxySetting proxySetting) {
if (proxySetting == null) {
throw new RuntimeException("代理为空");
}
return CallerLockUtil.tryCallWithCallerLock(() -> { return CallerLockUtil.tryCallWithCallerLock(() -> {
Proxy proxy = proxyConfig.getProxy(); Proxy proxy = proxySetting.getProxy();
boolean ignoreHttpsVerification = proxyConfig.getIgnoreHttpsVerification(); boolean ignoreHttpsVerification = proxySetting.getIgnoreHttpsVerification();
// OkHttp 客户端配置 // OkHttp 客户端配置
OkHttpClient client = OkHttpClientProvider.getInstance( OkHttpClient client = OkHttpClientProvider.getInstance(
proxy, ignoreHttpsVerification, proxy, ignoreHttpsVerification,
@@ -82,7 +85,7 @@ public class GeoIPUtil {
log.warn("Proxy ipv6 error {}", e.getMessage()); log.warn("Proxy ipv6 error {}", e.getMessage());
} }
return queryIpInfoGeoLite(ipInfo); return queryIpInfoGeoLite(ipInfo);
}, 100, proxyConfig).orElse(IpInfo.EMPTY); }, 100, proxySetting).orElse(IpInfo.EMPTY);
} }
/** /**

View File

@@ -48,25 +48,25 @@ public class SmartResourceResolver {
Path externalPath = resolveExternalPath(relativePath); Path externalPath = resolveExternalPath(relativePath);
if (externalPath != null && Files.exists(externalPath)) { if (externalPath != null && Files.exists(externalPath)) {
log.debug("从外部文件系统加载资源: {}", externalPath); log.debug("Load resource externally: {}", externalPath);
return Files.newInputStream(externalPath); return Files.newInputStream(externalPath);
} }
// 否则回退到 classpathJAR、WAR、IDE // 否则回退到 classpathJAR、WAR、IDE
InputStream in = SmartResourceResolver.class.getClassLoader().getResourceAsStream(relativePath); InputStream in = SmartResourceResolver.class.getClassLoader().getResourceAsStream(relativePath);
if (in != null) { if (in != null) {
log.debug(" classpath 内部加载资源: {}", relativePath); log.debug("Load resource within internal classpath: {}", relativePath);
return in; return in;
} }
throw new FileNotFoundException("无法找到资源: " + relativePath); throw new FileNotFoundException("Cannot find resources: " + relativePath);
} }
public static void saveText(String relativePath, String content) throws IOException { public static void saveText(String relativePath, String content) throws IOException {
Path outputPath = resolveExternalPath(relativePath); Path outputPath = resolveExternalPath(relativePath);
Files.createDirectories(outputPath.getParent()); // 确保目录存在 Files.createDirectories(outputPath.getParent()); // 确保目录存在
Files.writeString(outputPath, content, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); 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) { private static Path resolveExternalPath(String relativePath) {

View File

@@ -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 {};
}

View File

@@ -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;
}
}

View File

@@ -10,10 +10,10 @@ import jakarta.validation.Constraint;
import jakarta.validation.Payload; import jakarta.validation.Payload;
@Documented @Documented
@Constraint(validatedBy = EmoneyRequestConfigValidator.class) @Constraint(validatedBy = RequestInfoValidator.class)
@Target({ ElementType.TYPE }) @Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface EmoneyRequestConfigValid { public @interface RequestInfoValid {
String message() default "非法的 EmoneyRequestConfig"; String message() default "非法的 EmoneyRequestConfig";
Class<?>[] groups() default {}; Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {}; Class<? extends Payload>[] payload() default {};

View File

@@ -7,27 +7,21 @@ import org.apache.commons.lang3.StringUtils;
import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext; import jakarta.validation.ConstraintValidatorContext;
import quant.rich.emoney.entity.config.DeviceInfoConfig; import quant.rich.emoney.entity.config.DeviceInfoConfig;
import quant.rich.emoney.entity.config.EmoneyRequestConfig; import quant.rich.emoney.entity.sqlite.RequestInfo;
import quant.rich.emoney.interfaces.IConfig;
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}$"); static final Pattern androidIdPattern = Pattern.compile("^[0-9a-f]{16}$");
@Override @Override
public boolean isValid(IConfig<EmoneyRequestConfig> value, ConstraintValidatorContext context) { public boolean isValid(RequestInfo value, ConstraintValidatorContext context) {
if (value == null) return true; if (value == null) return true;
if (!(value instanceof EmoneyRequestConfig config)) return true; if (!(value instanceof RequestInfo config)) return true;
if (!config.getIsAnonymous()) { // 如果有用户名则必须设置密码
// 非匿名须判断用户名密码是否为空 if (!StringUtils.isBlank(config.getUsername()) && StringUtils.isBlank(config.getPassword())) {
if (StringUtils.isAnyBlank(config.getUsername(), config.getPassword())) { return invalid(context, "当设置了用户名时,必须提供密码");
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("配置非匿名时用户名和密码不能为空")
.addConstraintViolation();
return false;
}
} }
if (!androidIdPattern.matcher(config.getAndroidId()).matches()) { if (!androidIdPattern.matcher(config.getAndroidId()).matches()) {

View File

@@ -27,15 +27,3 @@ spring:
init: init:
mode: always mode: always
continue-on-error: true 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

View File

@@ -36,6 +36,21 @@ spring:
encoding: UTF-8 encoding: UTF-8
cache: false 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: kaptcha:
border: "no" border: "no"
image: image:

View File

@@ -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\"}"
}

View File

@@ -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\":[\"代表标的的中期趋势:\",\"曲线向上走时,代表中期趋势上涨;\",\"曲线向下走时,代表中期趋势下跌。\"]}]}"
}

View File

@@ -6,5 +6,6 @@
"title" : "量王精选(投教)", "title" : "量王精选(投教)",
"items" : [ "红色实心柱子是量王,黄色实心柱子是天量;量王出现说明近阶段的量能达到了一定的程度,呈现交易相对活跃状态。具体用法参考量王精选相关教程。" ], "items" : [ "红色实心柱子是量王,黄色实心柱子是天量;量王出现说明近阶段的量能达到了一定的程度,呈现交易相对活跃状态。具体用法参考量王精选相关教程。" ],
"image" : null "image" : null
} ] } ],
"original" : "{\"data\":[{\"title\":\"量王精选(投教)\",\"items\":[\"红色实心柱子是量王,黄色实心柱子是天量;量王出现说明近阶段的量能达到了一定的程度,呈现交易相对活跃状态。具体用法参考量王精选相关教程。\"]}],\"name\":\"量王精选(投教)\",\"nameCode\":\"10013500\"}"
} }

View File

@@ -2,5 +2,6 @@
"id" : "52", "id" : "52",
"name" : "ATR", "name" : "ATR",
"code" : "10010600", "code" : "10010600",
"descriptions" : [ "算法今日振幅、今日最高与昨收差价、今日最低与昨收差价中的最大值为真实波幅求真实波幅的N日移动平均", "参数N为天数一般取14" ] "descriptions" : [ "算法今日振幅、今日最高与昨收差价、今日最低与昨收差价中的最大值为真实波幅求真实波幅的N日移动平均", "参数N为天数一般取14" ],
"original" : "{\"id\":52,\"code\":\"10010600\",\"name\":\"ATR\",\"descriptions\":[\"算法今日振幅、今日最高与昨收差价、今日最低与昨收差价中的最大值为真实波幅求真实波幅的N日移动平均\",\"参数N为天数一般取14\"]}"
} }

View File

@@ -33,6 +33,7 @@
"12": 31, "12": 31,
"12L": 32, "12L": 32,
"13": 33, "13": 33,
"14": 34 "14": 34,
"15": 35
} }
} }

View File

@@ -1,38 +1,39 @@
{ {
"androidVerToSdk" : { "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.0" : 1,
"1.1" : 2, "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.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.2" : 17,
"4.3" : 18,
"4.4" : 19,
"4.4W" : 20,
"5.0" : 21,
"5.1" : 22, "5.1" : 22,
"6.0" : 23, "6.0" : 23,
"1.6" : 4,
"4.3" : 18,
"7.0" : 24, "7.0" : 24,
"9" : 28,
"4.4" : 19,
"7.1" : 25, "7.1" : 25,
"8.0" : 26, "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
} }
} }

View File

@@ -1,5 +0,0 @@
{
"emoneyLoginFormDataList" : null,
"selectedId" : "57de6ca2423e0f64d8626477e1f8a46b",
"isRandom" : false
}

View File

@@ -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"
}

View File

@@ -2,5 +2,6 @@
"username" : "admin", "username" : "admin",
"password" : "81667f60a8c11d4c8e9d2e0670ff24667e6c72d49b0b15562525bcbd", "password" : "81667f60a8c11d4c8e9d2e0670ff24667e6c72d49b0b15562525bcbd",
"email" : "huocaizhu@gmail.com", "email" : "huocaizhu@gmail.com",
"apiToken" : "vgb2IHmax9Mjji4R",
"isInited" : true "isInited" : true
} }

Binary file not shown.

View File

@@ -1,6 +1,6 @@
if (!window.Helper) { window.Helper = {} } if (!window.Helper) { 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], allEmoneyPeriods: [1, 5, 15, 30, 60, 120, 10000, 20000, 30000, 40000, 50000, 60000],
showIndexDetailLayer: async function(obj, forceRefresh) { showIndexDetailLayer: async function(obj, forceRefresh) {
// obj: {indexCode: _, indexName: _} // obj: {indexCode: _, indexName: _}
@@ -56,12 +56,12 @@ window.Helper = {
} }
layer.close(load); layer.close(load);
}, },
trimChars: function (str, chars) { trimChars: function(str, chars) {
const escaped = chars.split('').map(c => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join(''); const escaped = chars.split('').map(c => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('');
const pattern = new RegExp(`^[${escaped}]+|[${escaped}]+$`, 'g'); const pattern = new RegExp(`^[${escaped}]+|[${escaped}]+$`, 'g');
return str.replace(pattern, ''); return str.replace(pattern, '');
}, },
setLayerMainBtn: function (layero, index) { setLayerMainBtn: function(layero, index) {
var btns = layero.find('.layui-layer-btn>*'), j = 1; var btns = layero.find('.layui-layer-btn>*'), j = 1;
if (index < 0) index = btns.length + index; if (index < 0) index = btns.length + index;
for (let i = 0; i < btns.length; i++) { for (let i = 0; i < btns.length; i++) {
@@ -73,7 +73,7 @@ window.Helper = {
btn.setAttribute('class', filtered.join(' ')); btn.setAttribute('class', filtered.join(' '));
} }
}, },
openR: function (option) { openR: function(option) {
const defaultOption = { const defaultOption = {
type: 1, area: '500px', type: 1, area: '500px',
skin: 'layui-anim layui-anim-rl layui-layer-adminRight', skin: 'layui-anim layui-anim-rl layui-layer-adminRight',
@@ -84,11 +84,11 @@ window.Helper = {
}, },
/** /**
* 按照通用配置来渲染表格 * 按照通用配置来渲染表格
* option: 和 table.render 选项基本一致, 但需要额外提供: * @param option 和 table.render 选项基本一致, 但需要额外提供:
* idName: 该表格行对象 id 的名称 * @param option.idName 该表格行对象 id 的名称
* baseUrl: 当前基本 URL, 将在此基础上拼接 list/updateBool/batchOp 等内容 * @param option.baseUrl 当前基本 URL, 将在此基础上拼接 list/updateBool/batchOp 等内容
* batchOpEnum: 批量操作枚举名,若不提供则不渲染批量操作控件和回调逻辑,若为 true 则仅渲染批量删除逻辑 * @param option.batchOpEnum 批量操作枚举名,若不提供则不渲染批量操作控件和回调逻辑,若为 true 则仅渲染批量删除逻辑
* 除此以外cols 内列如果需要开关选项的, 在相应 col 内添加 switchTemplet: true * <br>除此以外cols 内列如果需要开关选项的, 在相应 col 内添加 switchTemplet: true
*/ */
renderTable: (option) => { renderTable: (option) => {
const defaultOption = { const defaultOption = {
@@ -99,29 +99,48 @@ window.Helper = {
if (!option.baseUrl) throw new Error('baseUrl 不允许为空'); if (!option.baseUrl) throw new Error('baseUrl 不允许为空');
if (!option.baseUrl.endsWith('/')) option.baseUrl += '/'; if (!option.baseUrl.endsWith('/')) option.baseUrl += '/';
option.url = option.baseUrl + 'list'; option.url = option.baseUrl + 'list';
let tableSwitchTemplet = function () { let tableSwitchTemplet = function() {
// 以 elem 选择器 + '.' + switchFilter 作为 filter // 以 elem 选择器 + '.' + switchFilter 作为 filter
const filter = `${option.elem}.switchFilter`; 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); console.log(obj, obj.elem.checked);
const data = { const data = {
field: obj.elem.dataset.field, field: obj.elem.dataset.field,
value: obj.elem.checked, value: obj.elem.checked,
id: obj.elem.dataset.id id: obj.elem.dataset.id
}; };
$.ajax({ $.ajax({
url: option.baseUrl + 'updateBool', method: 'POST', url: option.baseUrl + 'updateBool', method: 'POST',
data:data, data: data,
success: () => Dog.success({time: 1000}), success: () => {
error: function (res) { Dog.success({ time: 1000 });
Dog.error({msg: res}) let colOption = option.cols[0].find(x => x.field === obj.elem.dataset.field);
// 恢复 enabled 状态 let refresh = colOption.refresh, mutex = colOption.mutex;
obj.elem.checked = !obj.elem.checked; let tableId = obj.elem.closest('[lay-table-id]').getAttribute('lay-table-id');
layui.form.render('checkbox') let tableFilter = document.getElementById(tableId).getAttribute('lay-filter');
return if (refresh) {
} Dog.reloadTable(tableFilter);
}) }
}); else if (mutex) {
// 互斥, 把当前表格内当前列的所有开关都设为与当前状态相反的状态
// 先找到所有数据
layui.table.getData(tableFilter).forEach((v, i) => {
if (v[option.idName] != obj.elem.dataset.id) {
// 非我行类,全部置反
$(`[lay-table-id="${tableFilter}"] tr[data-index="${i}"] [data-field="${obj.elem.dataset.field}"][lay-filter="${filter}"]`).removeAttr('checked')
}
})
}
},
error: function(res) {
Dog.error({ msg: res })
// 恢复 enabled 状态
obj.elem.checked = !obj.elem.checked;
layui.form.render('checkbox')
return
}
})
});
return d => { return d => {
var fieldName = d.LAY_COL.field; var fieldName = d.LAY_COL.field;
return `<input type="checkbox" lay-skin="switch" lay-text="|" return `<input type="checkbox" lay-skin="switch" lay-text="|"
@@ -141,17 +160,17 @@ window.Helper = {
layui.form.on(`submit(${submitButtonFilter})`, _ => { layui.form.on(`submit(${submitButtonFilter})`, _ => {
const buttonEl = $(`[lay-filter="${submitButtonFilter}"]`); const buttonEl = $(`[lay-filter="${submitButtonFilter}"]`);
if (!buttonEl.length) { if (!buttonEl.length) {
Dog.error({msg: `找不到对应 filter 为 ${submitButtonFilter} 的 button`}); Dog.error({ msg: `找不到对应 filter 为 ${submitButtonFilter} 的 button` });
return return
} }
const form = $(buttonEl.parents('.layui-form')[0] || buttonEl.parents('form')[0]); const form = $(buttonEl.parents('.layui-form')[0] || buttonEl.parents('form')[0]);
if (!form.length) { if (!form.length) {
Dog.error({msg: `找不到对应 filter 为 ${submitButtonFilter} 的 button 对应的表单`}); Dog.error({ msg: `找不到对应 filter 为 ${submitButtonFilter} 的 button 对应的表单` });
return return
} }
// 获取 form 内所有表单 // 获取 form 内所有表单
const els = form.find('input[name], select[name], textarea[name], button[name]'); 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) => { $.each(els, (i, el) => {
const name = el.name; const name = el.name;
if (!name) return true if (!name) return true
@@ -193,12 +212,15 @@ window.Helper = {
method: 'POST', method: 'POST',
contentType: 'application/json', contentType: 'application/json',
data: JSON.stringify(obj.field), data: JSON.stringify(obj.field),
success: function (r) { success: function(r) {
Dog.success({onClose: () => { Dog.success({
if (window.editLayer) layui.layer.close(window.editLayer); onClose: () => {
Dog.reloadTable()}}) if (window.editLayer) layui.layer.close(window.editLayer);
Dog.reloadTable()
}
})
}, },
error: res => Dog.error({msg: res}), error: res => Dog.error({ msg: res }),
}); });
} }
}) })
@@ -213,20 +235,23 @@ window.Helper = {
const type = fieldEl.type; const type = fieldEl.type;
switch (type) { switch (type) {
case 'checkbox': 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'); const laySkin = fieldEl.getAttribute('lay-skin');
if (checked) fieldEl.setAttribute('checked', '');
else fieldEl.removeAttribute('checked');
if (laySkin) { if (laySkin) {
switchFuncs[key] = function (obj) { switchFuncs[key] = function(obj) {
obj.elem.value = obj.elem.checked; obj.elem.value = obj.elem.checked;
layui.form.render(); layui.form.render();
} }
layui.form.on(`switch(${key})`, function (obj) { layui.form.on(`switch(${key})`, function(obj) {
switchFuncs[obj.elem.name](obj); switchFuncs[obj.elem.name](obj);
}) });
layui.event.call(this, 'form', `switch(${key})`, { layui.form.render();
elem: fieldEl, layui.event.call(this, 'form', `switch(${key})`, {
value: checked elem: fieldEl,
}); value: checked
});
} }
break; break;
case 'radio': case 'radio':
@@ -252,19 +277,81 @@ window.Helper = {
switchFuncs = $.extend(switchFuncs, extraSwitchFuncs) switchFuncs = $.extend(switchFuncs, extraSwitchFuncs)
} }
}, },
tableSwitchTemplet: idName => { /**
layui.form.on('switch(switchFilter)', function (obj) { * @param option.elem dropdown 选择器
console.log(obj, obj.elem.checked); * @param option.tableFilter 数据表格 filter用以确定操作对哪个表格的数据生效
$.ajax({ * @param option.idName 数据表格 idName
*/
renderDropdown: option => {
const defaultOption = {
};
option = $.extend(defaultOption, option);
if (!option.elem) throw new Error('elem 选择器不允许为空');
if (!option.idName) throw new Error('idName 不允许为空');
if (!option.tableFilter) throw new Error('数据表格 filter 不允许为空');
if (!option.url) throw new Error('url 不允许为空');
let click = (data, othis) => {
var checked = layui.table.checkStatus(option.tableFilter), ids = [];
if (!checked.data.length) {
Dog.error('未选中任何项', { time: 1000 });
return;
}
$.each(checked.data, function(i, row) {
ids.push(row[option.idName]);
});
data = $.extend(data, { ids: ids });
var op = async function() {
$.ajax({
url: option.url,
method: 'POST',
data: data,
success: () =>
Dog.success({
msg: '批量操作成功',
time: 1000,
onClose: () => Dog.reloadTable(option.tableFilter)
}),
error: res => Dog.error({msg: res, time: 2000})
})
}
data.op != 'DELETE' ? op() : layer.confirm('确认批量删除吗?该操作不可恢复', function() {
op();
}) })
}) };
return d => { option.click = click;
var fieldName = d.LAY_COL.field; layui.dropdown.render(option);
return `<input type="checkbox" lay-skin="switch" lay-text="|"
data-field="${fieldName}" data-id="${d[idName]}"
${d[fieldName] ? 'checked' : ''} lay-filter="switchFilter">`;
}
}, },
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 }));
}
}
});
}
} }

View File

@@ -53,29 +53,29 @@
class="fa-fw fa-solid fa-screwdriver-wrench"></i> 管理 class="fa-fw fa-solid fa-screwdriver-wrench"></i> 管理
</a> </a>
<dl class="layui-nav-child"> <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> <dd>
<a th:href="@{/admin/v1/manage/requestInfo}"> <i <a th:href="@{/admin/v1/manage/requestInfo}"> <i
class="fas fa-fw fa-envelope-open-text"></i> 请求配置 class="fas fa-fw fa-envelope-open-text"></i> 请求配置
</a> </a>
</dd> </dd>
<dd>
<a th:href="@{/admin/v1/manage/indexInfo}"> <i
class="fa-fw fa-solid fa-chart-line "></i> 指标配置
</a>
</dd>
<dd> <dd>
<a th:href="@{/admin/v1/manage/proxySetting}"> <i <a th:href="@{/admin/v1/manage/proxySetting}"> <i
class="fa-fw fa-solid fa-network-wired"></i> 代理配置 class="fa-fw fa-solid fa-network-wired"></i> 代理配置
</a> </a>
</dd> </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> <dd>
<a th:href="@{/admin/v1/manage/protocolMatch}"> <i <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> </a>
</dd> </dd>
<dd> <dd>
@@ -88,16 +88,6 @@
class="fa-fw fa-solid fa-gears"></i> 设置 class="fa-fw fa-solid fa-gears"></i> 设置
</a> </a>
<dl class="layui-nav-child"> <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> </dl></li>
<li class="layui-nav-item" <li class="layui-nav-item"
style="float: right; margin-right: 1px;" lay-unselect=""><a style="float: right; margin-right: 1px;" lay-unselect=""><a
@@ -115,23 +105,31 @@
</dd> </dd>
</dl></li> </dl></li>
<li class="layui-nav-item ipInfo" <li class="layui-nav-item ipInfo"
th:with="ipInfo=${@proxySettingService.getDefaultProxySettingIpInfo()}"
style="float: right; margin-right: 1px" lay-unselect=""><a style="float: right; margin-right: 1px" lay-unselect=""><a
id="ipThroughProxy" href="javascript:manualRefreshIp()" id="ipThroughProxy" href="javascript:manualRefreshIp()"
title="立即刷新"> IP 属地: <span title="立即刷新"> IP 属地: <span
th:if="${@proxyConfig.ipInfo == null}" th:if="${ipInfo == null}"
class="layui-badge layui-bg-cyan">加载中...</span> <span class="layui-badge layui-bg-cyan">加载中...</span> <span
th:if="${@proxyConfig.ipInfo != null}" th:if="${ipInfo != null}"
class="layui-badge layui-bg-cyan">[[${@proxyConfig.ipInfo.geoString}]]</span> class="layui-badge layui-bg-cyan">[[${ipInfo.geoString}]]</span>
</a> <th:block th:if="${@proxyConfig.ipInfo != null}"> </a>
<th:block th:if="${ipInfo != null}">
<dl class="layui-nav-child"> <dl class="layui-nav-child">
<dd class="ip"> <dd class="ip">
<a title="点击复制">[[${@proxyConfig.ipInfo.ip}]]</a> <a title="点击复制">[[${ipInfo.ip}]]</a>
</dd> </dd>
<dd class="ipv6" th:if="${@proxyConfig.ipInfo.ipv6 != null}"> <dd class="ipv6" th:if="${ipInfo.ipv6 != null}">
<a title="点击复制">[[${@proxyConfig.ipInfo.ipv6}]]</a> <a title="点击复制">[[${ipInfo.ipv6}]]</a>
</dd> </dd>
</dl> </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> </ul>
<script type="text/html" id="editUser"> <script type="text/html" id="editUser">
@@ -161,6 +159,17 @@
<input type="password" name="newPassword" placeholder="" autocomplete="off" class="layui-input"/> <input type="password" name="newPassword" placeholder="" autocomplete="off" class="layui-input"/>
</div> </div>
</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 style="display:none" class="layui-form-item">
<div class="layui-input-block"> <div class="layui-input-block">
<button class="layui-btn" lay-submit="*" lay-filter="editUser">提交</button> <button class="layui-btn" lay-submit="*" lay-filter="editUser">提交</button>
@@ -186,11 +195,7 @@
document.querySelectorAll('dd.ip,dd.ipv6').forEach(el => { document.querySelectorAll('dd.ip,dd.ipv6').forEach(el => {
el.addEventListener('click', () => { el.addEventListener('click', () => {
let text = el.querySelector('a').textContent; let text = el.querySelector('a').textContent;
navigator.clipboard.writeText(text).then(() => { Helper.copyText(text);
Dog.success({msg: '复制成功'})
}).catch(err => {
Dog.error({msg: '复制失败'})
});
}) })
}); });
async function refreshIpThroughProxy() { async function refreshIpThroughProxy() {
@@ -198,13 +203,16 @@
let geoEl = let geoEl =
document.querySelector('#ipThroughProxy>span'); document.querySelector('#ipThroughProxy>span');
//geoEl.textContent = '加载中...'; //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) { if (res.ok) {
geoEl.classList.remove('layui-bg-red');
geoEl.classList.add('layui-bg-cyan');
geoEl.textContent = res.data.geoString || '获取失败'; geoEl.textContent = res.data.geoString || '获取失败';
let ipMenu = document.querySelector('.ipInfo>dl'); let ipMenu = document.querySelector('.ipInfo>dl');
let genIpEL = (clazz, title) => { let genIpEL = (clazz, title) => {
let el = ipMenu.querySelector('.' + clazz); let el = ipMenu.querySelector('.' + clazz);
if (!el) { if (!el) {
ipMenu.style.display = '';
el = document.createElement('dd'); el = document.createElement('dd');
let a = document.createElement('a'); let a = document.createElement('a');
a.setAttribute('title', title || '点击复制'); a.setAttribute('title', title || '点击复制');
@@ -225,6 +233,11 @@
ipv6El.remove(); ipv6El.remove();
} }
} }
else {
geoEl.textContent = res.message;
geoEl.classList.remove('layui-bg-cyan');
geoEl.classList.add('layui-bg-red');
}
} catch (e) { } catch (e) {
console.error('刷新失败:', e); console.error('刷新失败:', e);
} }
@@ -251,6 +264,7 @@
element.on('nav(demo)', function (elem) { element.on('nav(demo)', function (elem) {
layer.msg(elem.text()); layer.msg(elem.text());
}); });
$('.change-user-info').on('click', function (e) { $('.change-user-info').on('click', function (e) {
e.preventDefault(); e.preventDefault();
@@ -270,6 +284,7 @@
var el = $(layero), o = el.find('#old-pass'), n = el.find('#new-pass'); var el = $(layero), o = el.find('#old-pass'), n = el.find('#new-pass');
el.find('[name="username"]').val(r.data.username); el.find('[name="username"]').val(r.data.username);
el.find('[name="email"]').val(r.data.email); el.find('[name="email"]').val(r.data.email);
el.find('[name="apiToken"]').val(r.data.apiToken);
el.on('input propertychange', '#new-pass input', function () { el.on('input propertychange', '#new-pass input', function () {
var that = $(this); var that = $(this);
if (that.val()) { if (that.val()) {
@@ -287,6 +302,19 @@
n.find('input').removeAttr('lay-verify'); 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) { layui.form.on('submit(editUser)', function (data) {
if (data.field.password && data.field.newPassword) { if (data.field.password && data.field.newPassword) {
data.field['password'] = sha3_224(data.field.password); data.field['password'] = sha3_224(data.field.password);

View File

@@ -62,7 +62,7 @@
table.render({ table.render({
elem: '#indexInfos', elem: '#indexInfos',
url: '/admin/v1/manage/indexInfo/configIndOnline', url: '/admin/v1/manage/indexInfo/getFields',
skin: 'line', skin: 'line',
cols: [ [ cols: [ [
{field: 'indexCode', title: '指标代码'}, {field: 'indexCode', title: '指标代码'},
@@ -82,7 +82,7 @@
}, },
parseData: (json) => { parseData: (json) => {
console.log(json) console.log(json)
const jo = json.data, indMap = jo.indMap; const jo = json.data.configIndOnline, indMap = jo.indMap;
const data = []; const data = [];
Object.keys(indMap).forEach(key => { Object.keys(indMap).forEach(key => {
const indInfo = indMap[key]; const indInfo = indMap[key];

View File

@@ -215,11 +215,11 @@
}) })
const loadConfigLayer = layui.layer.load(2); 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) { if (json.ok) {
const indexNameSelectEl = document.querySelector('[name="indexCode"]'); const indexNameSelectEl = document.querySelector('[name="indexCode"]');
const jo = json.data, indMap = jo.indMap; const jo = json.data.configIndOnline, indMap = jo.indMap;
let selected = undefined; let selected = undefined;
const optionsFragment = document.createDocumentFragment(); const optionsFragment = document.createDocumentFragment();
Object.keys(indMap).forEach(key => { Object.keys(indMap).forEach(key => {

View File

@@ -67,7 +67,7 @@
{field:'operation', title: '操作', toolbar: '#operationTpl'} {field:'operation', title: '操作', toolbar: '#operationTpl'}
]] ]]
}) })
dropdown.render({ Helper.renderDropdown({
elem: '.operdown', elem: '.operdown',
data: [ data: [
{title: '删除', op: 'DELETE'}, {title: '删除', op: 'DELETE'},
@@ -75,51 +75,9 @@
{title: '停用', op: 'DISABLE'}, {title: '停用', op: 'DISABLE'},
{title: '开启交易日校验', op: 'ENABLE_OPEN_DAY_CHECK'}, {title: '开启交易日校验', op: 'ENABLE_OPEN_DAY_CHECK'},
{title: '关闭交易日校验', op: 'DISABLE_OPEN_DAY_CHECK'}], {title: '关闭交易日校验', op: 'DISABLE_OPEN_DAY_CHECK'}],
click: function (data, othis){ url: '/admin/v1/manage/plan/batchOp',
var checked = layui.table.checkStatus('plans'), planIds = []; tableFilter: 'plans',
if (!checked.data.length) { idName: 'planId'
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();
})
}
}); });
}) })
</script> </script>

View File

@@ -11,6 +11,13 @@
<input type="text" lay-verify="required" name="proxyName" placeholder="" autocomplete="off" class="layui-input"/> <input type="text" lay-verify="required" name="proxyName" placeholder="" autocomplete="off" class="layui-input"/>
</div> </div>
</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"> <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"> <div class="layui-input-block">

View File

@@ -10,7 +10,7 @@
<div class="manage-body"> <div class="manage-body">
<div> <div>
<h1 class="manage-title"> <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> <b>代理设置</b><a href="javascript:openNewForm()" class="operate">新增</a>
</h1> </h1>
@@ -40,95 +40,33 @@
}) })
.use(['table', 'form', 'dropdown', 'layer', 'xmSelect', 'cron'], function(){ .use(['table', 'form', 'dropdown', 'layer', 'xmSelect', 'cron'], function(){
var dropdown = layui.dropdown, table = layui.table, form = layui.form; var dropdown = layui.dropdown, table = layui.table, form = layui.form;
table.render({ Helper.renderTable({
elem: '#proxySettings', elem: '#proxySettings',
url:'/admin/v1/manage/proxySetting/list', baseUrl:'/admin/v1/manage/proxySetting',
page:true, skin:'line', page:true, skin:'line',
idName: 'id',
cols: [ [ cols: [ [
{type:'checkbox'}, {type:'checkbox'},
{field:'id', hide: true, width: 60, title: 'ID'}, {field:'id', hide: true, width: 60, title: 'ID'},
{field:'isDefault', title: '默认', width: 95, switchTemplet: true, mutex: true},
{field:'proxyName', title: '名称'}, {field:'proxyName', title: '名称'},
{field:'proxyType', title: '类型'}, {field:'proxyType', title: '类型'},
{field:'proxyHost', title: '主机'}, {field:'proxyHost', title: '主机'},
{field:'proxyPort', 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'} {field:'operation', title: '操作', toolbar: '#operationTpl'}
]] ]]
}); });
form.on('switch(switchFilter)', function(obj) { Helper.renderDropdown({
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({
elem: '.operdown', elem: '.operdown',
data: [ data: [
{title: '删除', op: 'DELETE'}, {title: '删除', op: 'DELETE'},
{title: '启用', op: 'ENABLE'}, {title: '连通性检查', op: 'CHECK'},
{title: '停用', op: 'DISABLE'}, {title: '停用 HTTPS 证书校验', op: 'DISABLE_HTTPS_VERIFY'},
{title: '开启交易日校验', op: 'ENABLE_OPEN_DAY_CHECK'}, {title: '启用 HTTPS 证书校验', op: 'ENABLE_HTTP_VERIFY'}],
{title: '关闭交易日校验', op: 'DISABLE_OPEN_DAY_CHECK'}], tableFilter: 'proxySettings',
click: function (data, othis){ url: '/admin/v1/manage/proxySetting/batchOp',
var checked = layui.table.checkStatus('proxySettings'), planIds = []; idName: 'id'
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();
})
}
}); });
}) })
</script> </script>

View File

@@ -6,15 +6,15 @@
<div class="layui-form" style="margin:10px 15px" id="editRequestInfoForm" lay-filter="editRequestInfoForm"> <div class="layui-form" style="margin:10px 15px" id="editRequestInfoForm" lay-filter="editRequestInfoForm">
<div class="layui-form-item"> <div class="layui-form-item">
<input type="hidden" name="id"/> <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"> <div class="layui-input-block">
<input type="text" lay-verify="required" name="name" placeholder="" autocomplete="off" class="layui-input"/> <input type="text" lay-verify="required" name="name" placeholder="" autocomplete="off" class="layui-input"/>
</div> </div>
</div> </div>
<div class="layui-form-item"> <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"> <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> </div>
<div class="layui-form-item non-anonymous"> <div class="layui-form-item non-anonymous">
@@ -98,6 +98,7 @@
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
Helper.onSubmitForm('submitRequestInfo', '/admin/v1/manage/requestInfo/save'); Helper.onSubmitForm('submitRequestInfo', '/admin/v1/manage/requestInfo/save');
Helper.listenAllInputChange();
function refreshAndroidId() { function refreshAndroidId() {
let androidIdEl = document.querySelector('[name="androidId"]'); let androidIdEl = document.querySelector('[name="androidId"]');
androidIdEl.value = Helper.randomHexString(16); androidIdEl.value = Helper.randomHexString(16);
@@ -140,12 +141,14 @@
success: async function (layero, layerIndex) { success: async function (layero, layerIndex) {
Helper.setLayerMainBtn(layero, -1); Helper.setLayerMainBtn(layero, -1);
var el = $(layero), extraSwitchFuncs = []; var el = $(layero), extraSwitchFuncs = [];
// 覆写 isAnonymous
extraSwitchFuncs.isAnonymous = function (obj) { $('#editRequestInfoForm [name="username"],#editRequestInfoForm [name="password"]').on('input', function(){
const checked = obj.elem.value = obj.elem.checked; const usernameEl = document.querySelector('#editRequestInfoForm [name="username"]');
const passwordEl = document.querySelector('#editRequestInfoForm [name="password"]');
const nonAnonymouses = document.querySelectorAll('.non-anonymous'); const nonAnonymouses = document.querySelectorAll('.non-anonymous');
const checked = usernameEl.value !== '' || passwordEl.value !== '';
nonAnonymouses.forEach(non => { nonAnonymouses.forEach(non => {
if (!checked) { if (checked) {
// 非匿名,那 username/password 等就需要选必选 // 非匿名,那 username/password 等就需要选必选
if (!non.querySelector('label>span')) { if (!non.querySelector('label>span')) {
$(non).children('label').append('<span>*</span>'); $(non).children('label').append('<span>*</span>');
@@ -158,7 +161,15 @@
$(non).find('lay-verify').removeAttr('lay-verify'); $(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); Helper.fillEditForm(r, layero, layerIndex, extraSwitchFuncs);
layui.event.call(this, 'form', 'switch(isAnonymous)', { layui.event.call(this, 'form', 'switch(isAnonymous)', {
elem: el[0].querySelector('[name="isAnonymous"]'), elem: el[0].querySelector('[name="isAnonymous"]'),

View File

@@ -47,9 +47,8 @@
idName: 'id', idName: 'id',
cols: [ [ cols: [ [
{type:'checkbox'}, {type:'checkbox'},
{field:'id', width: 60, title: 'ID'}, {field:'isDefault', title: '默认', width: 95, switchTemplet: true, mutex: true},
{field:'name', title: '名称'}, {field:'name', title: '名称'},
{field:'isAnonymous', title: '匿名', width: 95, switchTemplet: true},
{field:'username', title: '用户名', width: 95}, {field:'username', title: '用户名', width: 95},
{field:'uid', title: 'UID'}, {field:'uid', title: 'UID'},
{field:'androidVersion', title: '安卓版本'}, {field:'androidVersion', title: '安卓版本'},
@@ -57,37 +56,15 @@
{field:'operation', title: '操作', toolbar: '#operationTpl'} {field:'operation', title: '操作', toolbar: '#operationTpl'}
]] ]]
}) })
dropdown.render({ Helper.renderDropdown({
elem: '.operdown', elem: '.operdown',
data: [ data: [
{title: '删除', op: 'DELETE'}, {title: '删除', op: 'DELETE'},
{title: '启用', op: 'enable'}, {title: '开启匿名', op: 'ENABLE_ANONYMOUS'},
{title: '停用', op: 'disable'}, {title: '关闭匿名', op: 'DISABLE_ANONYMOUS'}],
{title: '开启交易日校验', op: 'enableOpenDayCheck'}, tableFilter: 'requestInfos',
{title: '关闭交易日校验', op: 'disableOpenDayCheck'}], url: '/admin/v1/manage/requestInfo/batchOp',
click: function (data, othis){ idName: 'id'
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({
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();
})
}
}); });
}) })
</script> </script>

View File

@@ -16,7 +16,7 @@ public class ByteBuddyTest {
PatchOkHttp.apply( PatchOkHttp.apply(
r -> r.not(a -> a.isHttps()) r -> r.not(a -> a.isHttps())
.overrideIf("User-Agent", "okhttp/3.12.2") .overrideHeader("User-Agent", "okhttp/3.12.2")
); );
OkHttpClient client = new OkHttpClient(); OkHttpClient client = new OkHttpClient();

View File

@@ -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();
}
}
}

View File

@@ -2,10 +2,8 @@ import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import quant.rich.emoney.entity.config.EmoneyRequestConfig;
import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo; import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo;
public class FingerprintsSpliter { public class FingerprintsSpliter {

View File

@@ -34,9 +34,9 @@ import okhttp3.Response;
import quant.rich.emoney.EmoneyAutoApplication; import quant.rich.emoney.EmoneyAutoApplication;
import quant.rich.emoney.client.EmoneyClient; import quant.rich.emoney.client.EmoneyClient;
import quant.rich.emoney.client.OkHttpClientProvider; 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;
import quant.rich.emoney.pojo.dto.NonParamsIndexDetail.NonParamsIndexDetailData; import quant.rich.emoney.pojo.dto.NonParamsIndexDetail.NonParamsIndexDetailData;
import quant.rich.emoney.service.sqlite.RequestInfoService;
@SpringBootTest @SpringBootTest
@ContextConfiguration(classes = EmoneyAutoApplication.class) @ContextConfiguration(classes = EmoneyAutoApplication.class)
@@ -47,7 +47,7 @@ public class EmoneyIndexScraper {
private static final ObjectMapper MAPPER = new ObjectMapper(); private static final ObjectMapper MAPPER = new ObjectMapper();
@Autowired @Autowired
EmoneyRequestConfig emoneyRequestConfig; RequestInfoService requestInfoService;
static { static {
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 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("https://appstatic.emoney.cn/html/emapp/stock/note/?name=");
urlBuilder.append(indexCode.toString()); urlBuilder.append(indexCode.toString());
urlBuilder.append("&emoneyScaleType=0&emoneyLandMode=0&token="); urlBuilder.append("&emoneyScaleType=0&emoneyLandMode=0&token=");
urlBuilder.append(emoneyRequestConfig.getAuthorization()); urlBuilder.append(requestInfoService.getDefaultRequestInfo().getAuthorization());
return urlBuilder.toString(); return urlBuilder.toString();
} }
@@ -75,7 +75,7 @@ public class EmoneyIndexScraper {
.header("Host", "appstatic.emoney.cn") .header("Host", "appstatic.emoney.cn")
.header("Connection", "keep-alive") .header("Connection", "keep-alive")
.header("Upgrade-Insecure-Requests", "1") .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("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("X-Request-With", "cn.emoney.emstock")
.header("Sec-Fetch-Site", "none") .header("Sec-Fetch-Site", "none")
@@ -124,7 +124,7 @@ public class EmoneyIndexScraper {
Request.Builder scriptBuilder = new Request.Builder() Request.Builder scriptBuilder = new Request.Builder()
.header("Host", "appstatic.emoney.cn") .header("Host", "appstatic.emoney.cn")
.header("Connection", "keep-alive") .header("Connection", "keep-alive")
.header("User-Agent", emoneyRequestConfig.getWebviewUserAgent()) .header("User-Agent", requestInfoService.getDefaultRequestInfo().getWebviewUserAgent())
.header("Accept", "*/*") .header("Accept", "*/*")
.header("X-Request-With", "cn.emoney.emstock") .header("X-Request-With", "cn.emoney.emstock")
.header("Sec-Fetch-Site", "same-origin") .header("Sec-Fetch-Site", "same-origin")
@@ -160,7 +160,6 @@ public class EmoneyIndexScraper {
// 将每个 jsonString 转换为 jsonArray进一步转换成 IndexDetail // 将每个 jsonString 转换为 jsonArray进一步转换成 IndexDetail
List<NonParamsIndexDetail> valid = new ArrayList<>(); List<NonParamsIndexDetail> valid = new ArrayList<>();
List<ArrayNode> arrayNodes = new ArrayList<>();
for (String jsonString : matchGroups) { for (String jsonString : matchGroups) {
try { try {
JsonNode root = MAPPER.readTree(jsonString); JsonNode root = MAPPER.readTree(jsonString);
@@ -189,7 +188,7 @@ public class EmoneyIndexScraper {
Request.Builder imageBuilder = new Request.Builder() Request.Builder imageBuilder = new Request.Builder()
.header("Host", "appstatic.emoney.cn") .header("Host", "appstatic.emoney.cn")
.header("Connection", "keep-alive") .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("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
.header("X-Request-With", "cn.emoney.emstock") .header("X-Request-With", "cn.emoney.emstock")
.header("Sec-Fetch-Site", "same-origin") .header("Sec-Fetch-Site", "same-origin")

View File

@@ -31,7 +31,7 @@ public class PatchOkHttpTest {
.or(c -> c.hostContains("localhost")) .or(c -> c.hostContains("localhost"))
.or(a -> a.hostContains("emapp")) .or(a -> a.hostContains("emapp"))
.or(b -> b.hasHeaderName("X-Protocol-Id")) .or(b -> b.hasHeaderName("X-Protocol-Id"))
.overrideIf("User-Agent", "okhttp/3.12.2") .overrideHeader("User-Agent", "okhttp/3.12.2")
.build(); .build();
context = new RequestContext(new Request.Builder() context = new RequestContext(new Request.Builder()