builderConsumer) {
- ProxyConfig proxyConfig = getProxyConfig();
+ ProxySetting proxySetting = getDefaultProxySetting();
return getInstance(
- proxyConfig.getProxy(),
- proxyConfig.getIgnoreHttpsVerification(),
+ proxySetting.getProxy(),
+ proxySetting.getIgnoreHttpsVerification(),
builderConsumer);
}
diff --git a/src/main/java/quant/rich/emoney/component/CallerLockAspect.java b/src/main/java/quant/rich/emoney/component/CallerLockAspect.java
index 1ca3752..c7bf8b0 100644
--- a/src/main/java/quant/rich/emoney/component/CallerLockAspect.java
+++ b/src/main/java/quant/rich/emoney/component/CallerLockAspect.java
@@ -8,9 +8,13 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
-import quant.rich.emoney.annotation.LockByCaller;
import quant.rich.emoney.util.CallerLockUtil;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.concurrent.locks.ReentrantLock;
@@ -20,7 +24,7 @@ public class CallerLockAspect {
private final SpelExpressionParser parser = new SpelExpressionParser();
- @Around("@annotation(me.qwq.emoney.annotation.LockByCaller)")
+ @Around("@annotation(quant.rich.emoney.component.CallerLockAspect.LockByCaller)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
@@ -52,4 +56,25 @@ public class CallerLockAspect {
lock.unlock();
}
}
+
+ /**
+ * 在方法上添加此注解,可针对调用方加锁,即:
+ * 调用方法为 A 的,多次从 A 调用则加锁,从 B 调用时不受影响
+ * 需要开启 AOP:在任意配置类上增加注解:@EnableAspectJAutoProxy
+ * @see CallerLockAspect
+ */
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.RUNTIME)
+ @Documented
+ public static @interface LockByCaller {
+ /**
+ * 可选参数,用于 SpEL 表达式获取 key
+ * 例如:
+ * @LockByCaller(key = "#userId")
+ * @LockByCaller(key = "#userId + ':' + #userName")
+ *
+ * 当不指定时,不校验参数,单纯校验 Caller
+ */
+ String key() default "";
+ }
}
\ No newline at end of file
diff --git a/src/main/java/quant/rich/emoney/component/EmoneyAutoPlatformExceptionHandler.java b/src/main/java/quant/rich/emoney/component/EmoneyAutoPlatformExceptionHandler.java
index 7f1e8cf..cba5cc7 100644
--- a/src/main/java/quant/rich/emoney/component/EmoneyAutoPlatformExceptionHandler.java
+++ b/src/main/java/quant/rich/emoney/component/EmoneyAutoPlatformExceptionHandler.java
@@ -19,6 +19,7 @@ import jakarta.validation.ConstraintViolationException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
+import org.springframework.jdbc.UncategorizedSQLException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
@@ -94,15 +95,7 @@ public class EmoneyAutoPlatformExceptionHandler {
if (ex instanceof PageNotFoundException) {
throw (PageNotFoundException) ex;
}
- String message = null;
- if (ex.getMessage() != null) {
- message = ex.getMessage();
- }
- else if (ex.getCause() != null) {
- message = ex.getCause().getMessage();
- }
- ex.printStackTrace();
- log.warn("Resolved exception {}", message);
+ log.warn("Resolved exception {}", ex);
log.warn(httpServletRequestToString(request));
return bodyOrPage(HttpStatus.INTERNAL_SERVER_ERROR, ex);
}
@@ -161,16 +154,9 @@ public class EmoneyAutoPlatformExceptionHandler {
}
private R> bodyOrPage(HttpStatus httpStatus, Exception ex) {
- boolean isPage = true;
- String message = null;
- if (ex instanceof RException ||
- ex instanceof LoginException) {
- isPage = false;
- message = ex.getMessage();
- }
- else {
- isPage = isPage();
- }
+ String message = getMessage(ex, ex instanceof UncategorizedSQLException);
+ boolean isPage = (ex instanceof RException || ex instanceof LoginException) ?
+ false : isPage();
if (isPage) {
if (ex instanceof NoResourceFoundException nrfe) {
if (StringUtils.isNotEmpty(nrfe.getMessage())
@@ -182,7 +168,7 @@ public class EmoneyAutoPlatformExceptionHandler {
}
throw ex == null ? new RuntimeException("Page exception raised") : new RuntimeException(ex);
}
- R> r = message != null ?
+ R> r = StringUtils.isNotEmpty(message) ?
R.status(httpStatus).setMessage(message).setData(message)
: R.status(httpStatus);
return r;
@@ -217,4 +203,19 @@ public class EmoneyAutoPlatformExceptionHandler {
return sb.toString();
}
+
+ public String getMessage(Throwable e, boolean causeMessageFirst) {
+ String causeMessage = null;
+ if (e.getCause() != null && e.getCause() != e) {
+ if (causeMessageFirst) {
+ return getMessage(e.getCause(), true);
+ }
+ else {
+ causeMessage = e.getCause().getMessage();
+ }
+ }
+ String mainMessage = e.getMessage();
+ if (mainMessage != null) return mainMessage;
+ return causeMessage;
+ }
}
diff --git a/src/main/java/quant/rich/emoney/component/RequireAuthAndProxyAspect.java b/src/main/java/quant/rich/emoney/component/RequireAuthAndProxyAspect.java
new file mode 100644
index 0000000..5769f83
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/component/RequireAuthAndProxyAspect.java
@@ -0,0 +1,81 @@
+package quant.rich.emoney.component;
+
+import org.apache.commons.lang3.StringUtils;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.*;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import quant.rich.emoney.client.EmoneyClient;
+import quant.rich.emoney.entity.sqlite.ProxySetting;
+import quant.rich.emoney.entity.sqlite.RequestInfo;
+import quant.rich.emoney.service.sqlite.ProxySettingService;
+import quant.rich.emoney.service.sqlite.RequestInfoService;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Method;
+
+
+@Aspect
+@Component
+public class RequireAuthAndProxyAspect {
+
+ @Autowired
+ RequestInfoService requestInfoService;
+
+ @Autowired
+ ProxySettingService proxySettingService;
+
+ @Around("@annotation(quant.rich.emoney.component.RequireAuthAndProxyAspect.RequireAuthAndProxy)")
+ public Object around(ProceedingJoinPoint pjp) throws Throwable {
+
+ ProxySetting defualtProxySetting = proxySettingService.getDefaultProxySetting();
+
+ if (defualtProxySetting == null) {
+ throw new RuntimeException("需要配置默认代理设置");
+ }
+
+ RequestInfo defaultRequestInfo = requestInfoService.getDefaultRequestInfo();
+
+ if (defaultRequestInfo == null) {
+ throw new RuntimeException("需要配置默认请求信息");
+ }
+
+ MethodSignature signature = (MethodSignature) pjp.getSignature();
+ Method method = signature.getMethod();
+ RequireAuthAndProxy annotation = method.getAnnotation(RequireAuthAndProxy.class);
+
+ if (StringUtils.isBlank(defaultRequestInfo.getAuthorization())) {
+ if (!annotation.autoLogin()) {
+ throw new RuntimeException("需要手动为请求信息鉴权");
+ }
+ if (!EmoneyClient.loginWithManaged()) {
+ throw new RuntimeException("鉴权登录失败");
+ }
+ }
+
+ return pjp.proceed();
+ }
+
+ /**
+ * 在方法上添加此注解,则进入该方法前先校验 defaultRequestInfo 已鉴权、代理已配置,否则不允许进入方法
+ * 需要开启 AOP:在任意配置类上增加注解:@EnableAspectJAutoProxy
+ * @see RequireAuthAndProxyAspect
+ */
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.RUNTIME)
+ @Documented
+ public static @interface RequireAuthAndProxy {
+ /**
+ * 当存在默认请求配置但未鉴权时,是否自动鉴权
+ * @return
+ */
+ boolean autoLogin() default false;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/quant/rich/emoney/config/ConfigAutoRegistrar.java b/src/main/java/quant/rich/emoney/config/ConfigAutoRegistrar.java
index c8302e8..eb21706 100644
--- a/src/main/java/quant/rich/emoney/config/ConfigAutoRegistrar.java
+++ b/src/main/java/quant/rich/emoney/config/ConfigAutoRegistrar.java
@@ -13,7 +13,9 @@ import quant.rich.emoney.interfaces.ConfigInfo;
import quant.rich.emoney.interfaces.IConfig;
/**
- * 实现自动化注册 Config
+ * 实现自动化注册 Config
+ * Config 放在 quant.rich.emoney.entity.config 包下并且必须实现 IConfig 接口
+ * @see quant.rich.emoney.interfaces.IConfig
*/
@Slf4j
@DependsOn("configService")
@@ -28,6 +30,7 @@ public class ConfigAutoRegistrar implements BeanDefinitionRegistryPostProcessor
scanner.findCandidateComponents("quant.rich.emoney.entity.config").forEach(beanDefinition -> {
String className = beanDefinition.getBeanClassName();
try {
+
// 确保其 field 规则与 configService 内 field 生成规则一致,即:
// 如果 @ConfigInfo 指定了 field 的,使用该 field + "Config"
// 作为 beanName,否则使用首字母小写的 simpleClassName 作为
@@ -37,18 +40,18 @@ public class ConfigAutoRegistrar implements BeanDefinitionRegistryPostProcessor
+ clazz.getSimpleName().substring(1);
if (!IConfig.class.isAssignableFrom(clazz)) {
- log.warn("Config {} does not implement IConfig, ignore", beanName);
+ log.error("Ignore config class {} which is not implemented IConfig interface", beanName);
return;
}
if (!beanName.endsWith("Config")) {
- log.warn("Config {}'s simple class name does not end with \"Config\", ignore", beanName);
+ log.error("Ignore config class {} which class name is not end with \"Config\"", beanName);
return;
}
ConfigInfo info = clazz.getAnnotation(ConfigInfo.class);
if (info == null) {
- log.warn("Config {} does not have @ConfigInfo annotation, ignore", clazz.getName());
+ log.error("Ignore config class {} which is not annotated with @ConfigInfo", clazz.getName());
return;
}
if (StringUtils.isNotBlank(info.field())) {
@@ -58,15 +61,16 @@ public class ConfigAutoRegistrar implements BeanDefinitionRegistryPostProcessor
BeanDefinitionBuilder factoryBean = BeanDefinitionBuilder
.genericBeanDefinition(ConfigServiceFactoryBean.class)
.addConstructorArgValue(clazz);
- // 注意此处注册 factoryBean 不意味着 FactoryBean.getObject() 方法立即被执行,
- // Spring 管理的 Bean 默认在其被使用时才创建,所以如果 getObject() 调用一些方
- // 法,这些方法会在初次使用 Bean 时才被创建。如果这些方法对于启动过程很重要,
- // 需要在对应 Config(Bean) 上加上 @Bean 和 @Lazy(false) 注解,确保一旦准备好
- // 相应的 Bean 就会被创建。
+ /**
+ * 注意此处通过 factoryBean 创建 bean 不意味着 FactoryBean.getObject() 方法
+ * 会被立即执行。Spring 默认会在 bean 被使用时才会创建。如果该 bean 对程序
+ * 启动很重要,需要立即创建的,需在其类上添加 @Bean 及 @Lazy(false) 注解,
+ * 确保一旦准备好,相应的 bean 就会被创建
+ */
registry.registerBeanDefinition(beanName, factoryBean.getBeanDefinition());
- log.info("Add config {} to bean register", beanName);
+ log.info("Add config class {} to bean register", beanName);
} catch (ClassNotFoundException e) {
- throw new RuntimeException("Failed to load class: " + className, e);
+ throw new RuntimeException("Cannot found specific config class: " + className, e);
}
});
}
diff --git a/src/main/java/quant/rich/emoney/config/ConfigServiceFactoryBean.java b/src/main/java/quant/rich/emoney/config/ConfigServiceFactoryBean.java
index 9ecb298..88b7fa1 100644
--- a/src/main/java/quant/rich/emoney/config/ConfigServiceFactoryBean.java
+++ b/src/main/java/quant/rich/emoney/config/ConfigServiceFactoryBean.java
@@ -9,8 +9,9 @@ import quant.rich.emoney.interfaces.IConfig;
import quant.rich.emoney.service.ConfigService;
/**
- * 实现配置项自动载入
- * @param
+ * 配置类工厂
+ * @param 配置类
+ *
*/
@Slf4j
public class ConfigServiceFactoryBean> implements FactoryBean, BeanNameAware {
@@ -37,24 +38,19 @@ public class ConfigServiceFactoryBean> implements FactoryBe
@Override
public T getObject() throws Exception {
ConstructionGuard.enter(targetClass);
- boolean success = true;
try {
T bean = configService.getConfig(targetClass);
beanFactory.autowireBean(bean);
beanFactory.initializeBean(bean, beanName);
- configService.saveOrUpdate(bean);
+ //configService.saveOrUpdate(bean);
return bean;
}
catch (Exception e) {
- log.error("Fail to load config: " + targetClass.getName(), e);
- success = false;
+ log.error("无法载入配置类: " + targetClass.getName(), e);
throw e;
}
finally {
ConstructionGuard.exit(targetClass);
- if (success) {
- log.debug("getObject() for {} success", targetClass.toString());
- }
}
}
diff --git a/src/main/java/quant/rich/emoney/config/ConstructionGuard.java b/src/main/java/quant/rich/emoney/config/ConstructionGuard.java
index ce7de19..876f851 100644
--- a/src/main/java/quant/rich/emoney/config/ConstructionGuard.java
+++ b/src/main/java/quant/rich/emoney/config/ConstructionGuard.java
@@ -7,6 +7,25 @@ import org.springframework.beans.factory.BeanCreationException;
import lombok.extern.slf4j.Slf4j;
+/**
+ * To prevent bean cyclic instantiation through BeanFactory:
+ * The reason is that some configuration classes are deserialized via json.
+ * During this process, the default behavior is to call their no-argument
+ * constructors. Inside these constructors, it’s likely that static methods
+ * from SpringContextHolder are invoked to obtain other configuration classes.
+ * However, those other configuration classes may in turn call the same static
+ * methods of SpringContextHolder to obtain yet other configuration classes.
+ * At this point, since the configuration class is still {@code null}, Spring attempts
+ * once again to produce the instance of this class via BeanFactory, leading to
+ * a cyclic instantiation process and eventually causing a stack overflow.
+ * Since SpringContextHolder actually operates outside of Spring’s management lifecycle,
+ * it is difficult to detect this issue at runtime. Therefore, this class should
+ * be used within the Factory’s getObject method, where an exception is thrown
+ * if cyclic instantiation occurs. The implementation of this class is based on ThreadLocal.
+ *
+ * @author Doghole
+ * @see ConfigServiceFactoryBean
+ */
@Slf4j
public class ConstructionGuard {
private static final ThreadLocal>> constructing = ThreadLocal.withInitial(HashSet::new);
@@ -16,7 +35,6 @@ public class ConstructionGuard {
}
public static void enter(Class> clazz) {
- log.debug("Enter construction for {}", clazz.toString());
if (isConstructing(clazz)) {
StringBuilder sb = new StringBuilder();
sb.append("Class ")
@@ -31,6 +49,5 @@ public class ConstructionGuard {
public static void exit(Class> clazz) {
constructing.get().remove(clazz);
- log.debug("Exit construction for {}", clazz.toString());
}
}
\ No newline at end of file
diff --git a/src/main/java/quant/rich/emoney/config/SqliteMybatisConfig.java b/src/main/java/quant/rich/emoney/config/SqliteMybatisConfig.java
index 6192d25..ffb6489 100644
--- a/src/main/java/quant/rich/emoney/config/SqliteMybatisConfig.java
+++ b/src/main/java/quant/rich/emoney/config/SqliteMybatisConfig.java
@@ -4,8 +4,6 @@ import java.io.File;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
-
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
@@ -50,7 +48,10 @@ public class SqliteMybatisConfig {
String filePath = hikariDataSource.getJdbcUrl();
if (filePath == null || !filePath.startsWith("jdbc:sqlite:")) {
- log.warn("无法在 SQLite HikariDataSource 中找到合法 SQLite JDBC url, 数据库可能会加载失败。获取到的 jdbc-url: {}", filePath);
+ log.warn(
+ "无法在 SQLite HikariDataSource 中找到合法 SQLite JDBC url, "
+ + "数据库可能会加载失败。合法的 url 需在 application.yml(properties) "
+ + "中配置,以 jdbc:sqlite: 开头。当前获取到的 jdbc-url: {}", filePath);
return;
}
filePath = filePath.substring("jdbc:sqlite:".length()).trim();
diff --git a/src/main/java/quant/rich/emoney/controller/IndexControllerV1.java b/src/main/java/quant/rich/emoney/controller/IndexControllerV1.java
index c856ae7..1c532be 100644
--- a/src/main/java/quant/rich/emoney/controller/IndexControllerV1.java
+++ b/src/main/java/quant/rich/emoney/controller/IndexControllerV1.java
@@ -52,7 +52,8 @@ public class IndexControllerV1 extends BaseController {
String username,
String password,
String newPassword,
- String email) {
+ String email,
+ String apiToken) {
if (EncryptUtils.passwordIsNotEmpty(newPassword)) {
if (!platformConfig.getPassword().equals(password)) {
@@ -66,7 +67,7 @@ public class IndexControllerV1 extends BaseController {
else {
throw RException.badRequest("用户名不能为空");
}
- platformConfig.setEmail(email);
+ platformConfig.setEmail(email).setApiToken(apiToken);
return R.judge(() -> {
if (configService.saveOrUpdate(platformConfig)) {
authService.setLogin(username, platformConfig.getPassword());
diff --git a/src/main/java/quant/rich/emoney/controller/LoginControllerV1.java b/src/main/java/quant/rich/emoney/controller/LoginControllerV1.java
index 84e48eb..a56518d 100644
--- a/src/main/java/quant/rich/emoney/controller/LoginControllerV1.java
+++ b/src/main/java/quant/rich/emoney/controller/LoginControllerV1.java
@@ -18,6 +18,7 @@ import quant.rich.emoney.pojo.dto.R;
import quant.rich.emoney.service.AuthService;
import quant.rich.emoney.service.ConfigService;
import quant.rich.emoney.util.EncryptUtils;
+import quant.rich.emoney.util.TextUtils;
@Controller
@RequestMapping("/admin/v1")
@@ -84,7 +85,11 @@ public class LoginControllerV1 extends BaseController {
if (StringUtils.isAnyBlank(username) || !EncryptUtils.passwordIsNotEmpty(password)) {
throw new LoginException("用户名和密码不能为空");
}
- platformConfig.setUsername(username).setPassword(password).setIsInited(true);
+ platformConfig
+ .setUsername(username)
+ .setPassword(password)
+ .setIsInited(true)
+ .setApiToken(TextUtils.randomString(16));
boolean success = configService.saveOrUpdate(platformConfig);
if (!success) {
throw new LoginException("无法配置用户名和密码,请检查");
diff --git a/src/main/java/quant/rich/emoney/controller/api/ProtoDecodeControllerV1.java b/src/main/java/quant/rich/emoney/controller/api/ProtoDecodeControllerV1.java
index fd1bfe0..bfec8f9 100644
--- a/src/main/java/quant/rich/emoney/controller/api/ProtoDecodeControllerV1.java
+++ b/src/main/java/quant/rich/emoney/controller/api/ProtoDecodeControllerV1.java
@@ -27,8 +27,8 @@ import jakarta.annotation.PostConstruct;
import org.apache.commons.lang3.StringUtils;
import org.reflections.Reflections;
-import lombok.AllArgsConstructor;
import lombok.Data;
+import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import nano.BaseResponse.Base_Response;
import quant.rich.emoney.annotation.ResponseDecodeExtension;
@@ -37,7 +37,6 @@ import quant.rich.emoney.exception.RException;
import quant.rich.emoney.pojo.dto.EmoneyConvertResult;
import quant.rich.emoney.pojo.dto.EmoneyProtobufBody;
import quant.rich.emoney.service.sqlite.ProtocolMatchService;
-import quant.rich.emoney.service.sqlite.StrategyAndPoolService;
import quant.rich.emoney.util.SpringBeanDetector;
import quant.rich.emoney.util.SpringContextHolder;
@@ -52,20 +51,18 @@ public class ProtoDecodeControllerV1 {
@Autowired
ProtocolMatchService protocolMatchService;
- @Autowired
- StrategyAndPoolService strategyAndPoolService;
-
@Autowired
Reflections reflections;
Map> responseDecodeExtensions = new HashMap>();
@Data
- @AllArgsConstructor
+ @RequiredArgsConstructor
private static class MethodInfo {
- Method method;
- Class> declaringClass;
- Integer order;
+ final Method method;
+ final Class> declaringClass;
+ final Integer order;
+ Object instance;
}
@PostConstruct
@@ -112,7 +109,7 @@ public class ProtoDecodeControllerV1 {
for (List list : responseDecodeExtensions.values()) {
list.sort(Comparator.comparingInt(info -> info.getOrder()));
}
- log.debug("共载入 {} 个 ProtocolID 的 {} 个方法",
+ log.debug("ResponseDecodeExtension: 共载入 {} 个 ProtocolID 的 {} 个方法",
responseDecodeExtensions.keySet().size(), responseDecodeExtensions.values().size());
}
@@ -274,22 +271,24 @@ public class ProtoDecodeControllerV1 {
JsonNode jo = new ObjectMapper().valueToTree(nano);
- // 协议 9400 则更新到 StrategyAndPool 里面去
- if (protocolId == 9400) {
- strategyAndPoolService.updateByQueryResponse(jo);
- }
-
// 查找 ResponseDecodeExtension
List methodInfos = responseDecodeExtensions.get(protocolId.toString());
if (methodInfos != null) {
for (MethodInfo methodInfo : methodInfos) {
- Object instance = null;
- if (methodInfo.getDeclaringClass() != null) {
- // 获取 spring 管理的实例类
- instance = SpringContextHolder.getBean(methodInfo.getDeclaringClass());
+ if (methodInfo.getInstance() != null) {
+ // instance 不为 null 则说明是已经取到的 spring bean, 直接调用
+ methodInfo.getMethod().invoke(methodInfo.getInstance(), jo);
+ }
+ else if (methodInfo.getDeclaringClass() != null) {
+ // 获取 spring 管理的实例类
+ Object instance = SpringContextHolder.getBean(methodInfo.getDeclaringClass());
+ methodInfo.getMethod().invoke(instance, jo);
+ methodInfo.setInstance(instance);
+ }
+ else {
+ // 静态方法直接 invoke
+ methodInfo.getMethod().invoke(null, jo);
}
- // invoke
- methodInfo.getMethod().invoke(instance, jo);
}
}
diff --git a/src/main/java/quant/rich/emoney/controller/common/BaseController.java b/src/main/java/quant/rich/emoney/controller/common/BaseController.java
index affe3b4..46d12bb 100644
--- a/src/main/java/quant/rich/emoney/controller/common/BaseController.java
+++ b/src/main/java/quant/rich/emoney/controller/common/BaseController.java
@@ -3,12 +3,9 @@ package quant.rich.emoney.controller.common;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
-import quant.rich.emoney.pojo.dto.R;
import quant.rich.emoney.service.AuthService;
@Controller
diff --git a/src/main/java/quant/rich/emoney/controller/common/ServiceController.java b/src/main/java/quant/rich/emoney/controller/common/ServiceController.java
new file mode 100644
index 0000000..eabec95
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/controller/common/ServiceController.java
@@ -0,0 +1,122 @@
+package quant.rich.emoney.controller.common;
+
+import java.io.Serializable;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.ResolvableType;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import jakarta.annotation.PostConstruct;
+import lombok.extern.slf4j.Slf4j;
+import quant.rich.emoney.exception.RException;
+import quant.rich.emoney.pojo.dto.LayPageReq;
+import quant.rich.emoney.pojo.dto.LayPageResp;
+import quant.rich.emoney.pojo.dto.R;
+
+/**
+ * 在控制器中提供实体类的 service
+ *
+ * 继承后,可直接通过 {@code thisType} 和 {@code thisService} 获取实体类型和对应的服务实例
+ * 也获得部分能力,但控制方法及 Mapping 路径要继承后自己写。
+ * 可获得的能力:
+ *
+ * list
+ * getOne
+ * delete
+ *
+ *
+ * @param 实体类型
+ */
+@Slf4j
+public abstract class ServiceController extends BaseController {
+
+ @Autowired
+ private ApplicationContext ctx;
+
+ protected IService> thisService;
+
+ protected Class> thisType;
+
+ @PostConstruct
+ void init() {
+ @SuppressWarnings("rawtypes")
+ Map beans = ctx.getBeansOfType(IService.class);
+ ResolvableType thisType = ResolvableType.forClass(this.getClass()).as(ServiceController.class);
+ @SuppressWarnings("unchecked")
+ // 获取本类的实体类
+ Class clazz = (Class) thisType.getGeneric(0).resolve();
+ this.thisType = clazz;
+ for (IService> service : beans.values()) {
+ ResolvableType type = ResolvableType.forClass(service.getClass()).as(IService.class);
+ Class> entityType = type.getGeneric(0).resolve();
+ if (entityType == clazz) {
+ this.thisService = service;
+ }
+ }
+ if (thisService == null) {
+ log.error("获取本例实体类服务失败,请检查");
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ protected IService getThisService() {
+ return (IService)this.thisService;
+ }
+
+ /**
+ * 返回实例类列表
+ * @param pageReq
+ * @return
+ */
+ protected LayPageResp> list(LayPageReq pageReq) {
+ Page planPage = getThisService().page(pageReq);
+ return new LayPageResp<>(planPage);
+ }
+
+ /**
+ * 根据 id 获取实例化对象。如果 id 为空则返回通过默认无参构造器构造的新实例化对象
+ * @param id
+ * @return
+ */
+ protected R> getOne(Serializable id) {
+ // id 为空,返回一个新实例化对象
+ if (id == null) {
+ try {
+ return R.ok(thisType.getConstructor().newInstance());
+ } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
+ | InvocationTargetException | NoSuchMethodException | SecurityException e) {
+ final String s = "根据默认构造器创建新实例化对象失败";
+ log.error(s, e);
+ throw RException.internalServerError(s);
+ }
+ }
+
+ // 否则从数据库取
+ T exist = getThisService().getById(id);
+ return R.judge(exist != null, exist, "无法找到对应 ID 的 ProxySetting");
+ }
+
+ /**
+ * 保存
+ * @param object
+ * @return
+ */
+ protected R> save(T object) {
+ return
+ R.judge(
+ () -> getThisService().saveOrUpdate(object),
+ "新增或保存失败");
+ }
+
+ /**
+ * 删除
+ * @param id
+ * @return
+ */
+ protected R> delete(Serializable id) {
+ return R.judge(getThisService().removeById(id), "删除失败,是否已删除?");
+ }
+
+}
diff --git a/src/main/java/quant/rich/emoney/controller/common/UpdateBoolController.java b/src/main/java/quant/rich/emoney/controller/common/UpdateBoolController.java
deleted file mode 100644
index 03174ee..0000000
--- a/src/main/java/quant/rich/emoney/controller/common/UpdateBoolController.java
+++ /dev/null
@@ -1,91 +0,0 @@
-package quant.rich.emoney.controller.common;
-
-import java.lang.reflect.Field;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.ApplicationContext;
-import org.springframework.core.ResolvableType;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.ResponseBody;
-
-import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
-import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
-import com.baomidou.mybatisplus.core.metadata.TableInfo;
-import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
-import com.baomidou.mybatisplus.extension.service.IService;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import jakarta.annotation.PostConstruct;
-import lombok.extern.slf4j.Slf4j;
-import quant.rich.emoney.exception.RException;
-import quant.rich.emoney.pojo.dto.R;
-
-@Slf4j
-public abstract class UpdateBoolController extends BaseController {
-
- ObjectMapper mapper = new ObjectMapper();
-
- @Autowired
- private ApplicationContext ctx;
-
- private Map, IService>> serviceMap;
-
- @PostConstruct
- void init() {
- serviceMap = new HashMap<>();
- @SuppressWarnings("rawtypes")
- Map beans = ctx.getBeansOfType(IService.class);
- for (IService> service : beans.values()) {
- ResolvableType type = ResolvableType.forClass(service.getClass()).as(IService.class);
- Class> entityType = type.getGeneric(0).resolve();
- if (entityType != null) {
- serviceMap.put(entityType, service);
- }
- }
- }
-
- @SuppressWarnings("unchecked")
- public IService getService(Class entityClass) {
- return (IService) serviceMap.get(entityClass);
- }
-
- @PostMapping("/updateBool")
- @ResponseBody
- protected
- R> updateBool(String id, String field, Boolean value) {
-
-
- ResolvableType type = ResolvableType.forClass(this.getClass()).as(UpdateBoolController.class);
- @SuppressWarnings("unchecked")
- Class clazz = (Class) type.getGeneric(0).resolve();
-
- TableInfo tableInfo = TableInfoHelper.getTableInfo(clazz);
- Object converted = mapper.convertValue(id, tableInfo.getKeyType());
-
- // 获取 Service
- IService s = getService((Class) clazz);
-
- try {
- String idField = tableInfo.getKeyColumn();
- Field declaredField = clazz.getDeclaredField(field);
- Optional fieldInfo = tableInfo.getFieldList().stream()
- .filter(f -> f.getProperty().equals(field))
- .findFirst();
- if (declaredField.getType().equals(Boolean.class)) {
- return R.judge(s.update(
- new UpdateWrapper()
- .set(fieldInfo.get().getColumn(), value)
- .eq(idField, converted)
- ), "更新失败,请查看日志");
- }
- }
- catch (Exception e) {
- log.error("update bool failed", e);
- }
- throw RException.badRequest().setLogRequest(true);
- }
-
-}
diff --git a/src/main/java/quant/rich/emoney/controller/common/UpdateBoolServiceController.java b/src/main/java/quant/rich/emoney/controller/common/UpdateBoolServiceController.java
new file mode 100644
index 0000000..dd3a34a
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/controller/common/UpdateBoolServiceController.java
@@ -0,0 +1,86 @@
+package quant.rich.emoney.controller.common;
+
+import java.lang.reflect.Field;
+import java.util.Optional;
+
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
+import com.baomidou.mybatisplus.core.metadata.TableInfo;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import lombok.extern.slf4j.Slf4j;
+import quant.rich.emoney.exception.RException;
+import quant.rich.emoney.pojo.dto.R;
+
+/**
+ * 更新实体类中 Boolean 字段的抽象控制器,一般用于实体类中包含 Boolean 字段的前端更新
+ *
+ * 前端实体类数据列表中,常有 CheckBox 或 Switch 等控件,希望通过点击数据表中的控件直接修改行对象
+ * Boolean 值的,可用该方法。需要引入功能的需 extends 本类,如对 {@code Plan} 生效,则可在其控制器
+ * {@code PlanController} 中:
+ *
+ * PlanController extends UpdateBoolController<Plan>
+ * @param 实体类型
+ * @see #updateBool(String, String, Boolean)
+ */
+@Slf4j
+public abstract class UpdateBoolServiceController extends ServiceController {
+
+ protected ObjectMapper mapper = new ObjectMapper();
+
+ /**
+ * 更新布尔值主方法,以 form 形式 POST,uri: /updateBool,表单字段名需与该方法参数名一致
+ * @param id 欲修改的实体类的实例化对象的主键值
+ * @param field 欲修改的实体类的实例化对象的布尔字段名
+ * @param value 需要修改为的布尔值
+ * @return
+ */
+ @PostMapping("/updateBool")
+ @ResponseBody
+ protected
+ R> updateBool(String id, String field, Boolean value) {
+
+
+ // 获取表信息
+ TableInfo tableInfo = TableInfoHelper.getTableInfo(thisType);
+ Object converted = mapper.convertValue(id, tableInfo.getKeyType());
+
+ // 获取 Service
+
+ try {
+ // 获取主键名
+ String idField = tableInfo.getKeyColumn();
+ // 获取指定布尔字段的字段信息
+ Field declaredField = thisType.getDeclaredField(field);
+ // 获取指定布尔字段在数据表中的映射字段信息
+ Optional fieldInfo = tableInfo.getFieldList().stream()
+ .filter(f -> f.getProperty().equals(field))
+ .findFirst();
+
+ if (fieldInfo.isEmpty()) {
+ throw RException.badRequest("无法根据 field: " + field + " 找到类内字段信息");
+ }
+
+ if (declaredField.getType().equals(Boolean.class)
+ || declaredField.getType().equals(boolean.class)
+ ) {
+ return R.judge(getThisService().update(
+ new UpdateWrapper()
+ .set(fieldInfo.get().getColumn(), value)
+ .eq(idField, converted)
+ ), "更新失败,请查看日志");
+ }
+ else {
+ throw RException.badRequest("field: " + field + " 不为布尔值类型字段");
+ }
+ }
+ catch (NoSuchFieldException | SecurityException e) {
+ throw RException.badRequest("获取字段 " + field + " 错误");
+ }
+ }
+
+}
diff --git a/src/main/java/quant/rich/emoney/controller/config/ProxyConfigControllerV1.java b/src/main/java/quant/rich/emoney/controller/config/ProxyConfigControllerV1.java
deleted file mode 100644
index a573dc2..0000000
--- a/src/main/java/quant/rich/emoney/controller/config/ProxyConfigControllerV1.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package quant.rich.emoney.controller.config;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.ResponseBody;
-
-import lombok.extern.slf4j.Slf4j;
-import quant.rich.emoney.controller.common.BaseController;
-import quant.rich.emoney.entity.config.ProxyConfig;
-import quant.rich.emoney.pojo.dto.R;
-
-@Slf4j
-@Controller
-@RequestMapping("/admin/v1/config/proxy")
-public class ProxyConfigControllerV1 extends BaseController {
-
- @Autowired
- ProxyConfig proxyConfig;
-
-
- @GetMapping("/refreshIpThroughProxy")
- @ResponseBody
- public R> refreshIpThroughProxy() {
- return R.ok(proxyConfig.refreshIpThroughProxy());
- }
-}
diff --git a/src/main/java/quant/rich/emoney/controller/manage/IndexInfoControllerV1.java b/src/main/java/quant/rich/emoney/controller/manage/IndexInfoControllerV1.java
index 301dc82..d702513 100644
--- a/src/main/java/quant/rich/emoney/controller/manage/IndexInfoControllerV1.java
+++ b/src/main/java/quant/rich/emoney/controller/manage/IndexInfoControllerV1.java
@@ -1,9 +1,9 @@
package quant.rich.emoney.controller.manage;
-import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
+import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@@ -25,7 +25,7 @@ import quant.rich.emoney.service.IndexDetailService;
public class IndexInfoControllerV1 extends BaseController {
@Autowired
- IndexInfoConfig indexInfo;
+ IndexInfoConfig indexInfoConfig;
@Autowired
IndexDetailService indexDetailService;
@@ -35,6 +35,11 @@ public class IndexInfoControllerV1 extends BaseController {
return "/admin/v1/manage/indexInfo/index";
}
+ /**
+ * 获取指标详情解释
+ * @param indexCode
+ * @return
+ */
@GetMapping("/getIndexDetail")
@ResponseBody
public R> getIndexDetail(String indexCode) {
@@ -43,6 +48,11 @@ public class IndexInfoControllerV1 extends BaseController {
indexDetailService.getIndexDetail(indexCode));
}
+ /**
+ * 强制刷新并获取指标详情解释
+ * @param indexCode
+ * @return
+ */
@GetMapping("/forceRefreshAndGetIndexDetail")
@ResponseBody
public R> forceRefreshAndGetIndexDetail(String indexCode) {
@@ -51,21 +61,14 @@ public class IndexInfoControllerV1 extends BaseController {
indexDetailService.forceRefreshAndGetIndexDetail(indexCode));
}
- @GetMapping("/configIndOnline")
- @ResponseBody
- public R> configIndOnline(String url) throws IOException {
-
- //return R.judge(() -> indexInfo.getOnlineConfigByUrl(url));
- return R.ok(indexInfo.getConfigIndOnline());
- }
-
@GetMapping("/getFields")
@ResponseBody
public R> getFields(@RequestParam("fields") String[] fields) {
if (fields == null || fields.length == 0) {
- return R.ok(indexInfo);
+ return R.ok(indexInfoConfig);
}
- ObjectNode indexInfoJson = new ObjectMapper().valueToTree(indexInfo);
+ Object indexInfoConfigWithoutProxy = AopProxyUtils.getSingletonTarget(indexInfoConfig);
+ ObjectNode indexInfoJson = new ObjectMapper().valueToTree(indexInfoConfigWithoutProxy);
Map map = new HashMap<>();
for (String field : fields) {
map.put(field, indexInfoJson.get(field));
@@ -73,17 +76,21 @@ public class IndexInfoControllerV1 extends BaseController {
return R.ok(map);
}
-
+ /**
+ * 根据给定 url 获取在线指标配置
+ * @param url
+ * @return
+ */
@GetMapping("/getConfigIndOnlineByUrl")
@ResponseBody
public R> getConfigOnlineByUrl(String url) {
- return R.judge(() -> indexInfo.getOnlineConfigByUrl());
+ return R.judge(() -> indexInfoConfig.getOnlineConfigByUrl(url));
}
@GetMapping("/getIndexInfoConfig")
@ResponseBody
public R> getIndexInfoConfig() {
- return R.ok(indexInfo);
+ return R.ok(indexInfoConfig);
}
@GetMapping("/list")
diff --git a/src/main/java/quant/rich/emoney/controller/manage/PlanControllerV1.java b/src/main/java/quant/rich/emoney/controller/manage/PlanControllerV1.java
index fc0c0f4..179cd70 100644
--- a/src/main/java/quant/rich/emoney/controller/manage/PlanControllerV1.java
+++ b/src/main/java/quant/rich/emoney/controller/manage/PlanControllerV1.java
@@ -13,11 +13,8 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
-import com.baomidou.mybatisplus.core.toolkit.StringUtils;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-
import lombok.extern.slf4j.Slf4j;
-import quant.rich.emoney.controller.common.UpdateBoolController;
+import quant.rich.emoney.controller.common.UpdateBoolServiceController;
import quant.rich.emoney.entity.sqlite.Plan;
import quant.rich.emoney.exception.RException;
import quant.rich.emoney.interfaces.IQueryableEnum;
@@ -29,7 +26,7 @@ import quant.rich.emoney.service.sqlite.PlanService;
@Slf4j
@Controller
@RequestMapping("/admin/v1/manage/plan")
-public class PlanControllerV1 extends UpdateBoolController {
+public class PlanControllerV1 extends UpdateBoolServiceController {
@Autowired
PlanService planService;
@@ -42,35 +39,25 @@ public class PlanControllerV1 extends UpdateBoolController {
@GetMapping("/list")
@ResponseBody
public LayPageResp> list(LayPageReq pageReq) {
- Page planPage = planService.page(pageReq);
- return new LayPageResp<>(planPage);
+ return super.list(pageReq);
}
@GetMapping("/getOne")
@ResponseBody
public R> getOne(String planId) {
- // 如果 planId 是空,说明可能希望新建一个 Plan,需要返回默认实例化对象,否则从数据库取
- return
- planId == null ? R.ok(new Plan()) :
- R.judgeNonNull(planService.getById(planId), "无法找到对应 ID 的 Plan");
+ return super.getOne(planId);
}
@PostMapping("/save")
@ResponseBody
public R> save(@RequestBody Plan plan) {
- if (StringUtils.isNotBlank(plan.getPlanId())) {
- planService.updateById(plan);
- }
- else {
- planService.save(plan.setPlanId(null));
- }
- return R.ok();
+ return super.save(plan);
}
@PostMapping("/delete")
@ResponseBody
public R> delete(String planId) {
- return R.judge(planService.removeById(planId), "删除失败,是否已删除?");
+ return super.delete(planId);
}
@PostMapping("/batchOp")
diff --git a/src/main/java/quant/rich/emoney/controller/manage/ProxySettingControllerV1.java b/src/main/java/quant/rich/emoney/controller/manage/ProxySettingControllerV1.java
index 4ac7827..6e73a3b 100644
--- a/src/main/java/quant/rich/emoney/controller/manage/ProxySettingControllerV1.java
+++ b/src/main/java/quant/rich/emoney/controller/manage/ProxySettingControllerV1.java
@@ -1,11 +1,8 @@
package quant.rich.emoney.controller.manage;
-import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
-import java.util.Optional;
-
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@@ -16,14 +13,8 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
-import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
-import com.baomidou.mybatisplus.core.metadata.TableInfo;
-import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-
import lombok.extern.slf4j.Slf4j;
-import quant.rich.emoney.controller.common.BaseController;
+import quant.rich.emoney.controller.common.UpdateBoolServiceController;
import quant.rich.emoney.entity.sqlite.ProxySetting;
import quant.rich.emoney.exception.RException;
import quant.rich.emoney.pojo.dto.LayPageReq;
@@ -34,7 +25,7 @@ import quant.rich.emoney.service.sqlite.ProxySettingService;
@Slf4j
@Controller
@RequestMapping("/admin/v1/manage/proxySetting")
-public class ProxySettingControllerV1 extends BaseController {
+public class ProxySettingControllerV1 extends UpdateBoolServiceController {
@Autowired
ProxySettingService proxySettingService;
@@ -47,63 +38,32 @@ public class ProxySettingControllerV1 extends BaseController {
@GetMapping("/list")
@ResponseBody
public LayPageResp> list(LayPageReq pageReq) {
- Page planPage = proxySettingService.page(pageReq);
- return new LayPageResp<>(planPage);
+ return super.list(pageReq);
}
@GetMapping("/getOne")
@ResponseBody
public R> getOne(String id) {
-
- // 如果 planId 是空,说明可能希望新建一个 ProxySetting,需要返回默认实例化对象
- if (id == null) {
- return R.ok(new ProxySetting());
- }
-
- // 否则从数据库取
- ProxySetting proxy = proxySettingService.getById(id);
- return R.judge(proxy != null, proxy, "无法找到对应 ID 的 ProxySetting");
- }
-
- @PostMapping("/updateBool")
- @ResponseBody
- public R> updateBool(String id, String field, Boolean value) {
- TableInfo tableInfo = TableInfoHelper.getTableInfo(ProxySetting.class);
- try {
- Field declaredField = ProxySetting.class.getDeclaredField(field);
-
- Optional fieldInfo = tableInfo.getFieldList().stream()
- .filter(f -> f.getProperty().equals(field))
- .findFirst();
- if (declaredField.getType().equals(Boolean.class)) {
- proxySettingService.update(
- new UpdateWrapper()
- .eq("id", id)
- .set(fieldInfo.get().getColumn(), value));
- return R.ok();
- }
- }
- catch (Exception e) {}
- throw RException.badRequest();
+ return super.getOne(id);
}
@PostMapping("/save")
@ResponseBody
public R> save(@RequestBody ProxySetting proxySetting) {
- if (!Objects.isNull(proxySetting.getId())) {
- proxySettingService.updateById(proxySetting);
- }
- else {
- proxySettingService.save(proxySetting.setId(null));
- }
- return R.ok();
+ return super.save(proxySetting);
}
@PostMapping("/delete")
@ResponseBody
public R> delete(String id) {
- return R.judge(proxySettingService.removeById(id), "删除失败,是否已删除?");
+ return super.delete(id);
}
+
+ @Override
+ protected
+ R> updateBool(String id, String field, Boolean value) {
+ return super.updateBool(id, field, value);
+ }
@PostMapping("/batchOp")
@ResponseBody
@@ -149,5 +109,14 @@ public class ProxySettingControllerV1 extends BaseController {
DISABLE_HTTPS_VERIFY,
ENABLE_HTTP_VERIFY
}
+
+
+ @GetMapping("/refreshIpThroughProxy")
+ @ResponseBody
+ public R> refreshIpThroughProxy() {
+ return R.ok(
+ proxySettingService
+ .refreshIpThroughProxy());
+ }
}
diff --git a/src/main/java/quant/rich/emoney/controller/manage/RequestInfoControllerV1.java b/src/main/java/quant/rich/emoney/controller/manage/RequestInfoControllerV1.java
index 7d820fa..a5f5375 100644
--- a/src/main/java/quant/rich/emoney/controller/manage/RequestInfoControllerV1.java
+++ b/src/main/java/quant/rich/emoney/controller/manage/RequestInfoControllerV1.java
@@ -1,5 +1,8 @@
package quant.rich.emoney.controller.manage;
+import java.util.Arrays;
+import java.util.List;
+
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Controller;
@@ -10,11 +13,14 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
-import quant.rich.emoney.controller.common.UpdateBoolController;
+import quant.rich.emoney.controller.common.UpdateBoolServiceController;
import quant.rich.emoney.entity.sqlite.RequestInfo;
+import quant.rich.emoney.exception.RException;
import quant.rich.emoney.pojo.dto.LayPageReq;
import quant.rich.emoney.pojo.dto.LayPageResp;
import quant.rich.emoney.pojo.dto.R;
@@ -23,7 +29,7 @@ import quant.rich.emoney.service.sqlite.RequestInfoService;
@Slf4j
@Controller
@RequestMapping("/admin/v1/manage/requestInfo")
-public class RequestInfoControllerV1 extends UpdateBoolController {
+public class RequestInfoControllerV1 extends UpdateBoolServiceController {
@Autowired
RequestInfoService requestInfoService;
@@ -36,42 +42,57 @@ public class RequestInfoControllerV1 extends UpdateBoolController {
@GetMapping("/list")
@ResponseBody
public LayPageResp> list(LayPageReq pageReq) {
- Page planPage = requestInfoService.page(pageReq);
- return new LayPageResp<>(planPage);
+ return super.list(pageReq);
}
@GetMapping("/getOne")
@ResponseBody
public R> getOne(Integer id) {
-
- // 如果 id 是空,说明可能希望新建并返回默认实例化对象
- if (id == null) {
- return R.ok(new RequestInfo());
- }
-
- // 否则从数据库取
- RequestInfo requestInfo = requestInfoService.getById(id);
- return R.judgeNonNull(requestInfo, "无法找到对应 ID 的请求信息");
+ return super.getOne(id);
}
@PostMapping("/save")
@ResponseBody
- public R> save(@RequestBody @NonNull RequestInfo plan) {
- return R.judge(() -> requestInfoService.saveOrUpdate(plan));
+ public R> save(@RequestBody @NonNull RequestInfo requestInfo) {
+ return super.save(requestInfo);
}
@PostMapping("/delete")
@ResponseBody
public R> delete(String id) {
- return R.judge(requestInfoService.removeById(id), "删除失败,是否已删除?");
+ return super.delete(id);
+ }
+
+ @Override
+ protected
+ R> updateBool(String id, String field, Boolean value) {
+ return super.updateBool(id, field, value);
}
@PostMapping("/batchOp")
@ResponseBody
public R> batchOp(
@RequestParam(value="ids[]", required=true)
- String[] ids, String op) {
- return null;
+ @Valid @NotEmpty String[] ids, @NotNull RequestInfoBatchOp op) {
+
+ List idArray = Arrays.asList(ids);
+
+ if (op == RequestInfoBatchOp.DELETE) {
+ return R.judge(getThisService().removeByIds(idArray));
+ }
+
+ LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>();
+ uw.in(RequestInfo::getId, idArray);
+ switch (op) {
+ default:
+ throw RException.badRequest("未知操作");
+ }
+ }
+
+ private static enum RequestInfoBatchOp {
+ DELETE,
+ ENABLE_ANONYMOUS,
+ DISABLE_ANONYMOUS
}
}
diff --git a/src/main/java/quant/rich/emoney/entity/config/AndroidSdkLevelConfig.java b/src/main/java/quant/rich/emoney/entity/config/AndroidSdkLevelConfig.java
index 0608187..f0d315f 100644
--- a/src/main/java/quant/rich/emoney/entity/config/AndroidSdkLevelConfig.java
+++ b/src/main/java/quant/rich/emoney/entity/config/AndroidSdkLevelConfig.java
@@ -19,6 +19,7 @@ public class AndroidSdkLevelConfig implements IConfig {
public AndroidSdkLevelConfig() {
androidVerToSdk = new HashMap<>();
+ androidVerToSdk.put("15", 35);
androidVerToSdk.put("14", 34);
androidVerToSdk.put("13", 33);
androidVerToSdk.put("12L", 32);
diff --git a/src/main/java/quant/rich/emoney/entity/config/DeviceInfoConfig.java b/src/main/java/quant/rich/emoney/entity/config/DeviceInfoConfig.java
index b90757f..f8b32fe 100644
--- a/src/main/java/quant/rich/emoney/entity/config/DeviceInfoConfig.java
+++ b/src/main/java/quant/rich/emoney/entity/config/DeviceInfoConfig.java
@@ -49,6 +49,11 @@ public class DeviceInfoConfig implements IConfig {
@Slf4j
public static class DeviceInfo {
+ // 持久化在本地的 DeviceInfo 只有三个字段:
+ // model、deviceType 和 fingerprint
+ // 其中除 model 和 deviceType 外,其他字段全部从 fingerprint 派生
+ // 也就是说只要提供 model、deviceType 和 fingerprint 就能创建一个 DeviceInfo 实例
+
@JsonView(IConfig.Views.Persistence.class)
private String model;
private String brand;
@@ -62,11 +67,12 @@ public class DeviceInfoConfig implements IConfig {
private String buildType;
private String buildTags;
+ /**
+ * 用以匹配 fingerprint 的正则表达式
+ */
public static final Pattern PATTERN = Pattern.compile("^(?.*?)/(?.*?)/(?.*?):(?.*?)/(?.*?)/(?.*?):(?.*?)/(?.*?)$");
-
- private DeviceInfo() {
- }
+ private DeviceInfo() {}
public DeviceInfo setFingerprint(String fingerprint) {
Matcher m = PATTERN.matcher(fingerprint);
@@ -126,8 +132,8 @@ public class DeviceInfoConfig implements IConfig {
}
public final String toString() {
- return String.format("Model: %s, Fingerprint: %s",
- getModel(), getFingerprint()
+ return String.format("Model: %s, DeviceType: %s, Fingerprint: %s",
+ getModel(), getDeviceType(), getFingerprint()
);
}
@@ -139,7 +145,7 @@ public class DeviceInfoConfig implements IConfig {
}
public int hashCode() {
- return Objects.hash(getModel(), getFingerprint());
+ return Objects.hash(getModel(), getDeviceType(), getFingerprint());
}
}
}
diff --git a/src/main/java/quant/rich/emoney/entity/config/EmoneyRequestConfig.java b/src/main/java/quant/rich/emoney/entity/config/EmoneyRequestConfig.java
deleted file mode 100644
index d7cbaa9..0000000
--- a/src/main/java/quant/rich/emoney/entity/config/EmoneyRequestConfig.java
+++ /dev/null
@@ -1,539 +0,0 @@
-package quant.rich.emoney.entity.config;
-
-import java.io.Serializable;
-import java.util.Objects;
-
-import org.apache.commons.lang3.ObjectUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.Validate;
-import org.springframework.beans.factory.annotation.Autowired;
-
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-
-import lombok.AccessLevel;
-import lombok.Data;
-import lombok.Getter;
-import lombok.Setter;
-import lombok.experimental.Accessors;
-import lombok.extern.slf4j.Slf4j;
-import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo;
-import quant.rich.emoney.interfaces.ConfigInfo;
-import quant.rich.emoney.interfaces.IConfig;
-import quant.rich.emoney.patch.okhttp.PatchOkHttp;
-import quant.rich.emoney.patch.okhttp.PatchOkHttpRule;
-import quant.rich.emoney.util.EncryptUtils;
-import quant.rich.emoney.util.SpringContextHolder;
-import quant.rich.emoney.util.TextUtils;
-import quant.rich.emoney.validator.EmoneyRequestConfigValid;
-
-/**
- * 用于配置请求时的请求行为,一般而言,请求头与安卓系统的信息有关(build.prop)
- * 虽然部分请求对应服务器可能不进行审核,但合理的请求头能尽可能模仿真机行为,避免风险
- * @see DeviceInfoConfig
- * @see AndroidSdkLevelConfig
- * @see ChromeVersionsConfig
- */
-@Data
-@Accessors(chain = true)
-@Slf4j
-@EmoneyRequestConfigValid
-@ConfigInfo(field = "emoneyRequest", name = "益盟请求设置", initDefault = true)
-public class EmoneyRequestConfig implements IConfig {
-
- /**
- * 是否匿名登录
- */
- private Boolean isAnonymous = true;
-
- /**
- * 非匿名登录时的用户名
- */
- private String username = "";
-
- /**
- * 非匿名登录时的密码
- */
- private String password = "";
-
- /**
- * 鉴权信息
- */
- private String authorization;
-
- /**
- * UID
- */
- private Integer uid;
-
- /**
- * 用于:
- * 益盟登录接口 guid = MD5(androidId )
- * 益盟登录接口 exIdentify.AndroidID = androidId
- *
- * 来源: 本例随机生成并管理,需要符合 16 位
- *
- */
- private String androidId = TextUtils.randomString("abcdef0123456789", 16);
-
- /**
- * 用于:
- * Webview User-Agent
- * Non-Webview Image User-Agent
- *
- * 来源: DeviceInfoConfig
- * @see DeviceInfoConfig
- */
- @Setter(AccessLevel.PRIVATE)
- private String androidVersion;
-
- /**
- * 用于:
- * 益盟通讯接口请求头 X-Android-Agent = EMAPP/{emoneyVersion }(Android;{androidSdkLevel })
- * 益盟登录接口 osVersion = androidSdkLevel
- *
- * 来源: DeviceInfoConfig , 经由 AndroidSdkLevelConfig 转换,由本例代管
- * @see DeviceInfoConfig
- * @see AndroidSdkLevelConfig
- */
- @Setter(AccessLevel.PRIVATE)
- private String androidSdkLevel;
-
- /**
- * 用于:
- * 益盟登录接口 softwareType = softwareType
- *
- * 来源: DeviceInfoConfig ,由本例代管
- * @see DeviceInfoConfig
- */
- private String softwareType;
-
- /**
- * 用于:
- * 益盟通讯接口请求头 User-Agent = okHttpUserAgent
- *
- * 一般由程序所使用的 OkHttp 版本决定
- * 来源: 本例管理
- */
- private String okHttpUserAgent = "okhttp/3.12.2";
-
- /**
- * 对应 build.prop 中 Build.MODEL, 用于:
- * WebView User-Agent
- * 非 WebView 图片User-Agent
- *
- * 来源: DeviceInfoConfig , 由本例代为管理
- * @see DeviceInfoConfig
- */
- private String deviceName;
-
- /**
- * 对应 build.prop 中 Build.FINGERPRINT, 用于:
- * 益盟登录接口 hardware = MD5(fingerprint )
- * 益盟登录接口 exIdentify.OSFingerPrint = fingerprint
- *
- * 注意最终生成的 exIdentify 是 jsonString, 且有对斜杠的转义
- * 来源: DeviceInfoConfig , 由本例代为管理
- * @see DeviceInfoConfig
- *
- */
- private String fingerprint;
-
-
- /**
- * 对应 build.prop 中 Build.ID, 用于:
- * WebView User-Agent
- * 非 WebView 图片User-Agent
- *
- * 来源: DeviceInfoConfig , 由本例代为管理
- * @see DeviceInfoConfig
- *
- */
- private String buildId;
-
- /**
- * 用于:
- * WebView User-Agent
- *
- * 来源: ChromeVersionsConfig , 由本例代为管理
- * @see ChromeVersionsConfig
- */
- private String chromeVersion;
-
- /**
- * 用于:
- * 益盟通讯接口请求头 X-Android-Agent =
- * EMAPP/{emoneyVersion }(Android;{androidSdkLevel})
- *
- * 由程序版本决定
- * 来源: 本例管理
- * @see EmoneyRequestConfig.androidSdkLevel
- */
- private String emoneyVersion = "5.8.1";
-
- /**
- * 用于:
- * 益盟通讯接口请求头 Emapp-ViewMode = emappViewMode
- *
- * 由程序决定, 一般默认为 "1"
- * 来源: 本例管理
- */
- private String emappViewMode = "1";
-
- /**
- * OkHttp 用于注入 User-Agent 规则的 id
- */
- @JsonIgnore
- private Integer userAgentPatchRuleId;
-
- @Getter(AccessLevel.PRIVATE)
- @Autowired
- private AndroidSdkLevelConfig androidSdkLevelConfig;
-
- @Getter(AccessLevel.PRIVATE)
- @Autowired
- private DeviceInfoConfig deviceInfoConfig;
-
- @Getter(AccessLevel.PRIVATE)
- @Autowired
- private ChromeVersionsConfig chromeVersionsConfig;
-
- public void afterBeanInit() {
-
- try {
- androidSdkLevelConfig = Objects.requireNonNullElseGet(androidSdkLevelConfig, () -> SpringContextHolder.getBean(AndroidSdkLevelConfig.class));
- deviceInfoConfig = Objects.requireNonNullElseGet(deviceInfoConfig, () -> SpringContextHolder.getBean(DeviceInfoConfig.class));
- chromeVersionsConfig = Objects.requireNonNullElseGet(chromeVersionsConfig, () -> SpringContextHolder.getBean(ChromeVersionsConfig.class));
- }
- catch (IllegalStateException e) {
- log.debug("试图从 SpringContextHolder 初始化 androidSdkLevelConfig, deviceInfoConfig 和 chromeVersionConfig, 但 SpringContextHolder 未准备好");
- }
-
- if (ObjectUtils.anyNull(fingerprint, buildId, deviceName, androidVersion, androidSdkLevel, softwareType)) {
- // 任意是 null 的都要统一由 deviceInfo 进行设置
- initFromRandomDeviceInfo();
- }
- else {
- // 都不是 null,则由 fingerprint 来检查各项。
- // model 和 softwareType 本应交由 deviceInfoConfig 检查以
- // 应对可能的通过修改本地 json 来进行攻击的方式,可是本身
- // deviceInfoConfig 对 model 和 softwareType 的信息也来源
- // 于本地,万一本地的 deviceInfo(.fallback).json 也不值得信任?
- // 所以只检查 fingerprint
-
- DeviceInfo deviceInfo;
- boolean valid = true;
- try {
- deviceInfo = DeviceInfo.from(null, fingerprint);
- Validate.validState(androidVersion.equals(
- deviceInfo.getVersionRelease()),
- "androidVersion(versionRelease) 与预设 fingerprint 不匹配");
- Validate.validState(androidSdkLevel.equals(
- String.valueOf(androidSdkLevelConfig.getSdkLevel(deviceInfo.getVersionRelease()))),
- "androidSdkLevel 与预设 fingerprint 不匹配");
- Validate.validState(buildId.equals(deviceInfo.getBuildId()),
- "buildId 与预设 fingerprint 不匹配");
- }
- catch (Exception e) {
- valid = false;
- }
- if (!valid) {
- initFromRandomDeviceInfo();
- }
- }
-
- if (chromeVersion == null) {
- chromeVersion = chromeVersionsConfig.getRandomChromeVersion();
- }
-
- // 注入 OkHttp
- patchOkHttp();
- }
-
- /**
- * 注入 User-Agent patch 规则
- */
- private EmoneyRequestConfig patchOkHttp() {
- userAgentPatchRuleId = PatchOkHttp.apply(
- PatchOkHttpRule.when()
- .hostEndsWith("emoney.cn")
- .not(r -> r.hostMatches("appstatic"))
- .or(a -> a.hostContains("emapp"))
- .or(b -> b.hasHeaderName("X-Protocol-Id"))
- .overrideIf("User-Agent", getOkHttpUserAgent()).build()
- .setId(userAgentPatchRuleId));
- return this;
- }
-
- /**
- * 从随机 deviceInfo 填充本例相关字段
- * @return
- */
- private EmoneyRequestConfig initFromRandomDeviceInfo() {
- DeviceInfo deviceInfo = deviceInfoConfig.getRandomDeviceInfo();
- // 更新 deviceInfo 后对应 androidId 也要修改,哪怕原来非空
- androidId = TextUtils.randomString("abcdef0123456789", 16);
- return initFromDeviceInfo(deviceInfo);
- }
-
- /**
- * 从指定 deviceInfo 填充本例相关字段
- * @param deviceInfo
- * @return
- */
- private EmoneyRequestConfig initFromDeviceInfo(DeviceInfo deviceInfo) {
- if (deviceInfo == null) {
- log.error("deviceInfo is null");
- RuntimeException e = new RuntimeException("deviceInfo is null");
- e.printStackTrace();
- throw e;
- }
- deviceName = deviceInfo.getModel();
- androidVersion = deviceInfo.getVersionRelease();
- androidSdkLevel = String.valueOf(androidSdkLevelConfig.getSdkLevel(androidVersion));
- softwareType = deviceInfo.getDeviceType();
- fingerprint = deviceInfo.getFingerprint();
- buildId = deviceInfo.getBuildId();
- return this;
- }
-
- public EmoneyRequestConfig() {}
-
- public EmoneyRequestConfig setFingerprint(String fingerprint) {
- // 进入前即便 androidSdkLevelConfig 为 null 也要尝试获取一下
- // 因为为 null 时不一定是程序初始化时,也有可能是从前端 Post 而来的
- try {
- androidSdkLevelConfig = Objects.requireNonNullElseGet(androidSdkLevelConfig, () -> SpringContextHolder.getBean(AndroidSdkLevelConfig.class));
- }
- catch (IllegalStateException e) {
- log.debug("SpringContext not ready");
- }
-
- if (ObjectUtils.allNotNull(deviceName, softwareType, fingerprint, androidSdkLevelConfig)) {
- DeviceInfo deviceInfo = DeviceInfo.from(deviceName, fingerprint, softwareType);
- initFromDeviceInfo(deviceInfo);
- }
- else {
- this.fingerprint = fingerprint;
- }
- return this;
- }
-
- /**
- * 根据当前配置获取 guid,用于益盟登录接口
- * @return
- */
- @JsonIgnore
- public String getGuid() {
- return EncryptUtils.toMD5String(androidId);
- }
-
- /**
- * 一般 Protobuf 请求 X-Android-Agent 头,由 emoneyVersion 和 androidSdkLevel 组成
- * @return
- */
- @JsonIgnore
- public String getXAndroidAgent() {
- // EMAPP/{emoneyVersion}(Android;{androidSdkLevel})
- return
- new StringBuilder()
- .append("EMAPP/")
- .append(getEmoneyVersion())
- .append("(Android;")
- .append(getAndroidSdkLevel())
- .append(")").toString();
- }
-
- /**
- * 用于 App 内用到 Webview 的地方
- * @return
- */
- @JsonIgnore
- public String getWebviewUserAgent() {
- return new StringBuilder()
- .append("Mozilla/5.0 (Linux; Android ")
- .append(getAndroidVersion())
- .append("; ")
- .append(getDeviceName())
- .append(" Build/")
- .append(getBuildId())
- .append("; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/")
- .append(getChromeVersion())
- .append(" Mobile Safari/537.36")
- .toString();
- }
-
- /**
- * 用于 App 内少量未用到 Webview 的地方,如首页获取图片等
- * @return
- */
- @JsonIgnore
- public String getNonWebviewResourceUserAgent() {
- // Dalvik/2.1.0 (Linux; U; Android {安卓版本};{Build.DEVICE} Build/{Build.ID})
- return new StringBuilder()
- .append("Dalvik/2.1.0 (Linux; U; Android ")
- .append(getAndroidVersion())
- .append(";")
- .append(getDeviceName())
- .append(" Build/")
- .append(getBuildId())
- .append(")")
- .toString();
- }
- /**
- * 根据当前配置获取 hardware,用于益盟登录接口
- * @return
- */
- @JsonIgnore
- public String getHardware() {
- return EncryptUtils.toMD5String(getFingerprint());
- }
-
- /**
- * 根据本例信息(包括保存的用户名和密码)生成一个用于登录的 ObjectNode
- * @return
- */
- @JsonIgnore
- public ObjectNode getUsernamePasswordLoginObject() {
- return getUsernamePasswordLoginObject(username, password);
- }
-
- /**
- * 根据指定用户名、密码和本例信息生成一个用于登录的 ObjectNode
- * @param username 用户名
- * @param password 密码(可以是加密过的,也可以是明文)
- * @return
- */
- public ObjectNode getUsernamePasswordLoginObject(String username, String password) {
-
- if (StringUtils.isAnyBlank(username, password)) {
- throw new RuntimeException("Try to generate a emoney login object but username and/or password is blank");
- }
-
- ObjectNode node = getAnonymousLoginObject();
- node.put("accId", username);
- node.put("accType", 1);
-
- // 尝试解密 password 看是否成功,如果成功说明原本就已经是加密了的
- String tryDecryptPassword = EncryptUtils.decryptAesForEmoneyPassword(password);
-
- node.put("pwd",
- tryDecryptPassword != null ? password :
- EncryptUtils.encryptAesForEmoneyPassword(password)
- );
-
- return node;
- }
-
- /**
- * 根据本例信息生成一个用于匿名登录的 ObjectNode
- * @return
- */
- @JsonIgnore
- public ObjectNode getAnonymousLoginObject() {
- ObjectMapper mapper = new ObjectMapper();
- ObjectNode node = mapper.createObjectNode();
- ObjectNode exIdentify = mapper.createObjectNode();
- exIdentify.put("IMEI", "");
- exIdentify.put("AndroidID", getAndroidId());
- exIdentify.put("MAC", "");
- exIdentify.put("OSFingerPrint", getFingerprint());
- String exIdentifyString = exIdentify.toString().replace("/", "\\/");
-
- // 这玩意最好按照顺序来,当前这个顺序是 5.8.1 的顺序
- String guid = getGuid();
- node.put("appVersion", getEmoneyVersion());
- node.put("productId", 4);
- node.put("softwareType", getSoftwareType());
- node.put("deviceName", getDeviceName());
- node.put("ssid", "0");
- node.put("platform", "android");
- node.put("exIdentify", exIdentifyString);
- node.put("osVersion", getAndroidSdkLevel());
- node.put("accId", guid);
- node.put("guid", guid);
- node.put("accType", 4);
- node.put("pwd", "");
- node.put("channelId", "1711");
- node.put("hardware", getHardware());
-
- return node;
- }
-
- /**
- * 根据本例信息获取 Relogin ObjectNode
- * @return 如果 authorization 和 uid 任意 null 则本例返回 null
- */
- @JsonIgnore
- public ObjectNode getReloginObject() {
-
- if (getUid() == null || StringUtils.isBlank(getAuthorization())) {
- return null;
- }
-
- ObjectMapper mapper = new ObjectMapper();
- ObjectNode node = mapper.createObjectNode();
- ObjectNode exIdentify = mapper.createObjectNode();
- exIdentify.put("IMEI", "");
- exIdentify.put("AndroidID", getAndroidId());
- exIdentify.put("MAC", "");
- exIdentify.put("OSFingerPrint", getFingerprint());
- String exIdentifyString = exIdentify.toString().replace("/", "\\/");
-
- // 这玩意最好按照顺序来,当前这个顺序是 5.8.1 的顺序
- String guid = getGuid();
- node.put("appVersion", getEmoneyVersion());
- node.put("productId", 4);
- node.put("softwareType", getSoftwareType());
- node.put("deviceName", getDeviceName());
- node.put("ssid", "0");
- node.put("platform", "android");
- node.put("token", getAuthorization()); // 和登录不同的地方: token
- node.put("exIdentify", exIdentifyString);
- node.put("uid", getUid()); // 和登录不同的地方: uid
- node.put("osVersion", getAndroidSdkLevel());
- node.put("guid", guid);
- node.put("channelId", "1711");
- node.put("hardware", getHardware());
-
- return node;
- }
-
- /**
- * 设置密码:
- * null or empty,保存空字符串
- * 尝试解密成功,说明是密文,直接保存
- * 尝试解密失败,说明是明文,加密保存
- *
- * @param password
- * @return
- */
- public EmoneyRequestConfig setPassword(String password) {
- if (StringUtils.isEmpty(password)) {
- this.password = "";
- return this;
- }
- String tryDecryptPassword = EncryptUtils.decryptAesForEmoneyPassword(password);
- if (tryDecryptPassword != null) {
- this.password = password;
- }
- else {
- this.password = EncryptUtils.encryptAesForEmoneyPassword(password);
- }
- return this;
- }
-
- /**
- * 确保 androidVersion/androidSdkLevel 不为 null
- */
- public EmoneyRequestConfig beforeSaving() {
- setFingerprint(this.fingerprint);
- return this;
- }
-
- public EmoneyRequestConfig afterSaving() {
- patchOkHttp();
- return this;
- }
-}
diff --git a/src/main/java/quant/rich/emoney/entity/config/IndexInfo.java b/src/main/java/quant/rich/emoney/entity/config/IndexInfo.java
deleted file mode 100644
index 4b9437b..0000000
--- a/src/main/java/quant/rich/emoney/entity/config/IndexInfo.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package quant.rich.emoney.entity.config;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import lombok.Data;
-import lombok.experimental.Accessors;
-import quant.rich.emoney.enums.StockSpan;
-
-@Data
-@Accessors(chain=true)
-public class IndexInfo {
-
- private List paramInfoList = new ArrayList<>();
-
- private String code;
-
- private String name;
-
- private Boolean isCalc;
-
- private List supportPeriod = new ArrayList<>();
-
- @Data
- @Accessors(chain=true)
- public static class ParamInfo {
-
- private String name;
-
- private Integer max;
-
- private Integer min;
-
- private Integer defaultValue;
-
- }
-
-}
diff --git a/src/main/java/quant/rich/emoney/entity/config/IndexInfoConfig.java b/src/main/java/quant/rich/emoney/entity/config/IndexInfoConfig.java
index d4aeccb..e7f17ad 100644
--- a/src/main/java/quant/rich/emoney/entity/config/IndexInfoConfig.java
+++ b/src/main/java/quant/rich/emoney/entity/config/IndexInfoConfig.java
@@ -1,25 +1,21 @@
package quant.rich.emoney.entity.config;
import java.io.IOException;
-import java.util.concurrent.TimeUnit;
-
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.JsonNode;
-import lombok.AccessLevel;
import lombok.Data;
-import lombok.Getter;
import lombok.experimental.Accessors;
-import okhttp3.ConnectionPool;
import okhttp3.Request;
import okhttp3.Response;
-import quant.rich.emoney.annotation.LockByCaller;
import quant.rich.emoney.client.OkHttpClientProvider;
+import quant.rich.emoney.component.CallerLockAspect.LockByCaller;
import quant.rich.emoney.interfaces.ConfigInfo;
import quant.rich.emoney.interfaces.IConfig;
+import quant.rich.emoney.service.sqlite.RequestInfoService;
+import quant.rich.emoney.util.SpringContextHolder;
/**
* 指标信息配置,只做运行时管理,不做保存
@@ -36,11 +32,6 @@ public class IndexInfoConfig implements IConfig {
@JsonView(IConfig.Views.Persistence.class)
private JsonNode configIndOnline;
- @Autowired
- @JsonIgnore
- @Getter(AccessLevel.PRIVATE)
- private EmoneyRequestConfig emoneyRequestConfig;
-
public IndexInfoConfig() {}
public String getConfigIndOnlineStr() {
@@ -49,10 +40,13 @@ public class IndexInfoConfig implements IConfig {
@LockByCaller
@JsonIgnore
- public String getOnlineConfigByUrl() throws IOException {
+ public String getOnlineConfigByUrl(String url) throws IOException {
synchronized (this) {
+ if (SpringContextHolder.getBean(RequestInfoService.class).getDefaultRequestInfo() == null) {
+ throw new RuntimeException("请先新增请求配置并作为默认配置");
+ }
Request request = new Request.Builder()
- .url(configIndOnlineUrl)
+ .url(url)
.header("Cache-Control", "no-cache")
.get()
.build();
@@ -68,7 +62,5 @@ public class IndexInfoConfig implements IConfig {
}
}
}
-
- public static ConnectionPool connectionPool = new ConnectionPool(10, 5, TimeUnit.MINUTES);
}
diff --git a/src/main/java/quant/rich/emoney/entity/config/PlatformConfig.java b/src/main/java/quant/rich/emoney/entity/config/PlatformConfig.java
index 3dc8238..236f360 100644
--- a/src/main/java/quant/rich/emoney/entity/config/PlatformConfig.java
+++ b/src/main/java/quant/rich/emoney/entity/config/PlatformConfig.java
@@ -15,6 +15,8 @@ public class PlatformConfig implements IConfig {
private String password;
private String email;
+
+ private String apiToken;
private Boolean isInited;
diff --git a/src/main/java/quant/rich/emoney/entity/config/ProxyConfig.java b/src/main/java/quant/rich/emoney/entity/config/ProxyConfig.java
deleted file mode 100644
index 34119ed..0000000
--- a/src/main/java/quant/rich/emoney/entity/config/ProxyConfig.java
+++ /dev/null
@@ -1,101 +0,0 @@
-package quant.rich.emoney.entity.config;
-
-import java.net.InetSocketAddress;
-import java.net.Proxy;
-
-import org.apache.commons.lang3.ObjectUtils;
-
-import com.fasterxml.jackson.annotation.JsonView;
-import lombok.Data;
-import lombok.experimental.Accessors;
-import lombok.extern.slf4j.Slf4j;
-import quant.rich.emoney.interceptor.EnumOptionsInterceptor.EnumOptions;
-import quant.rich.emoney.interfaces.ConfigInfo;
-import quant.rich.emoney.interfaces.IConfig;
-import quant.rich.emoney.pojo.dto.IpInfo;
-import quant.rich.emoney.util.GeoIPUtil;
-import quant.rich.emoney.validator.ProxyConfigValid;
-
-/**
- * 独立出来一个代理设置的原因是后续可能需要做一个代理池,这样的话独立配置比较适合后续扩展
- */
-@Data
-@Accessors(chain = true)
-@Slf4j
-@ProxyConfigValid
-@ConfigInfo(field = "proxy", name = "代理设置", initDefault = true)
-public class ProxyConfig implements IConfig {
-
- /**
- * 代理类型
- */
- @EnumOptions("ProxyTypeEnum")
- @JsonView(IConfig.Views.Persistence.class)
- private Proxy.Type proxyType = Proxy.Type.DIRECT;
-
- /**
- * 代理主机
- */
- @JsonView(IConfig.Views.Persistence.class)
- private String proxyHost = "";
-
- /**
- * 代理端口
- */
- @JsonView(IConfig.Views.Persistence.class)
- private Integer proxyPort = 1;
-
- /**
- * 是否忽略 HTTPS 证书校验
- */
- @JsonView(IConfig.Views.Persistence.class)
- private Boolean ignoreHttpsVerification = false;
-
- /**
- * 通过代理后的 IP,不做存储,只做呈现
- */
- private IpInfo ipInfo;
-
- public void afterBeanInit() {
- //refreshIpThroughProxy();
- }
-
- public synchronized IpInfo refreshIpThroughProxy() {
- ipInfo = GeoIPUtil.getIpInfoThroughProxy(this);
- return ipInfo;
- }
-
-
- public ProxyConfig() {}
-
- /**
- * 根据配置获取代理
- * @return
- */
- public Proxy getProxy() {
- if (getProxyType() != null && getProxyType() != Proxy.Type.DIRECT) {
- return new Proxy(getProxyType(),
- new InetSocketAddress(getProxyHost(), getProxyPort()));
- }
- return Proxy.NO_PROXY;
- }
-
- public String getProxyUrl() {
- if (ObjectUtils.anyNull(getProxyType(), getProxyHost(), getProxyPort())) {
- return null;
- }
- StringBuilder sb = new StringBuilder();
- if (getProxyType() == Proxy.Type.SOCKS) {
- sb.append("socks5://");
- }
- else if (getProxyType() == Proxy.Type.HTTP) {
- sb.append("http://");
- }
- else {
- return null;
- }
- sb.append(getProxyHost()).append(':').append(getProxyPort());
- return sb.toString();
- }
-
-}
diff --git a/src/main/java/quant/rich/emoney/entity/postgre/StockStrategy.java b/src/main/java/quant/rich/emoney/entity/postgre/StockStrategy.java
new file mode 100644
index 0000000..00078dc
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/entity/postgre/StockStrategy.java
@@ -0,0 +1,42 @@
+package quant.rich.emoney.entity.postgre;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ * 益盟个股策略信息,包含策略类型和日期
+ */
+@Data
+@Accessors(chain=true)
+public class StockStrategy {
+
+ private String tsCode;
+
+ public StockStrategy setTsCodeFromGoodsId(Integer goodsId) {
+ // 自动将益盟 goodsId 转换成 tsCode
+ // 1301325 -> 301325.SZ
+ // 600325 -> 600325.SH
+ // 1920009 -> 920009.BJ
+ String goodsIdStr = goodsId.toString();
+ RuntimeException e = new RuntimeException("无法将 goodsId " + goodsIdStr + " 转换为 tsCode");
+ if (goodsIdStr.length() == 6) {
+ // SH
+ return setTsCode(goodsIdStr + ".SH");
+ }
+ else if (goodsIdStr.length() == 7) {
+ if (goodsIdStr.charAt(0) != '1') {
+ throw e;
+ }
+ if (goodsIdStr.charAt(1) == '9') {
+ // BJ
+ return setTsCode(goodsIdStr.substring(1) + ".BJ");
+ }
+ // SZ
+ return setTsCode(goodsIdStr.substring(1) + ".SZ");
+ }
+ throw e;
+ }
+
+
+
+}
diff --git a/src/main/java/quant/rich/emoney/entity/sqlite/ProxySetting.java b/src/main/java/quant/rich/emoney/entity/sqlite/ProxySetting.java
index 292be50..992e30f 100644
--- a/src/main/java/quant/rich/emoney/entity/sqlite/ProxySetting.java
+++ b/src/main/java/quant/rich/emoney/entity/sqlite/ProxySetting.java
@@ -26,6 +26,8 @@ public class ProxySetting {
@TableId(value="id", type=IdType.AUTO)
private Integer id;
+ private Boolean isDefault;
+
@Nonnull
private String proxyName;
diff --git a/src/main/java/quant/rich/emoney/entity/sqlite/RequestInfo.java b/src/main/java/quant/rich/emoney/entity/sqlite/RequestInfo.java
index 0b0c7e4..9e024d6 100644
--- a/src/main/java/quant/rich/emoney/entity/sqlite/RequestInfo.java
+++ b/src/main/java/quant/rich/emoney/entity/sqlite/RequestInfo.java
@@ -7,12 +7,14 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.AccessLevel;
import lombok.Data;
+import lombok.EqualsAndHashCode;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
@@ -20,21 +22,36 @@ import quant.rich.emoney.entity.config.AndroidSdkLevelConfig;
import quant.rich.emoney.entity.config.ChromeVersionsConfig;
import quant.rich.emoney.entity.config.DeviceInfoConfig;
import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo;
-import quant.rich.emoney.entity.config.EmoneyRequestConfig;
import quant.rich.emoney.util.EncryptUtils;
import quant.rich.emoney.util.SpringContextHolder;
import quant.rich.emoney.util.TextUtils;
+import quant.rich.emoney.validator.RequestInfoValid;
+/**
+ * 用于配置请求时的请求行为,一般而言,请求头与安卓系统的信息有关(build.prop)
+ * 虽然部分请求对应服务器可能不进行审核,但合理的请求头能尽可能模仿真机行为,避免风险
+ * @see DeviceInfoConfig
+ * @see AndroidSdkLevelConfig
+ * @see ChromeVersionsConfig
+ */
@Data
+@EqualsAndHashCode(callSuper=false)
@Accessors(chain = true)
@Slf4j
+@RequestInfoValid
@TableName(value = "request_info")
-public class RequestInfo {
+public class RequestInfo extends Model {
- private static AndroidSdkLevelConfig androidSdkLevelConfig = SpringContextHolder.getBean(AndroidSdkLevelConfig.class);
- private static DeviceInfoConfig deviceInfoConfig = SpringContextHolder.getBean(DeviceInfoConfig.class);
- private static ChromeVersionsConfig chromeVersionsConfig = SpringContextHolder.getBean(ChromeVersionsConfig.class);
+ private static final long serialVersionUID = -3113053377999289627L;
+
+ private volatile static AndroidSdkLevelConfig androidSdkLevelConfig = SpringContextHolder.getBean(AndroidSdkLevelConfig.class);
+ private volatile static DeviceInfoConfig deviceInfoConfig = SpringContextHolder.getBean(DeviceInfoConfig.class);
+ private volatile static ChromeVersionsConfig chromeVersionsConfig = SpringContextHolder.getBean(ChromeVersionsConfig.class);
+ /**
+ * 使用随机设备信息(DeviceInfo)初始化对象
+ * @see DeviceInfo
+ */
public RequestInfo() {
DeviceInfo deviceInfo = deviceInfoConfig.getRandomDeviceInfo();
setRelativeFieldsFromDeviceInfo(deviceInfo);
@@ -43,15 +60,15 @@ public class RequestInfo {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
+ /**
+ * 该请求是否设为默认请求
+ */
+ private Boolean isDefault = false;
+
/**
* 该请求信息配置的名称,助记用
*/
private String name = "";
-
- /**
- * 是否匿名登录
- */
- private Boolean isAnonymous = true;
/**
* 非匿名登录时的用户名
@@ -88,8 +105,9 @@ public class RequestInfo {
*
Webview User-Agent
* Non-Webview Image User-Agent
*
- * 来源: DeviceInfoConfig
+ * 来源: DeviceInfo
* @see DeviceInfoConfig
+ * @see DeviceInfo
*/
@Setter(AccessLevel.PRIVATE)
@TableField(exist=false)
@@ -100,8 +118,9 @@ public class RequestInfo {
* 益盟通讯接口请求头 X-Android-Agent = EMAPP/{emoneyVersion }(Android;{androidSdkLevel })
* 益盟登录接口 osVersion = androidSdkLevel
*
- * 来源: DeviceInfoConfig , 经由 AndroidSdkLevelConfig 转换,由本例代管
+ * 来源: DeviceInfo , 经由 AndroidSdkLevelConfig 转换,由本例代管
* @see DeviceInfoConfig
+ * @see DeviceInfo
* @see AndroidSdkLevelConfig
*/
@Setter(AccessLevel.PRIVATE)
@@ -112,8 +131,9 @@ public class RequestInfo {
* 用于:
* 益盟登录接口 softwareType = softwareType
*
- * 来源: DeviceInfoConfig ,由本例代管
+ * 来源: DeviceInfo
* @see DeviceInfoConfig
+ * @see DeviceInfo
*/
private String softwareType;
@@ -131,8 +151,9 @@ public class RequestInfo {
* WebView User-Agent
* 非 WebView 图片User-Agent
*
- * 来源: DeviceInfoConfig , 由本例代为管理
+ * 来源: DeviceInfo
* @see DeviceInfoConfig
+ * @see DeviceInfo
*/
private String deviceName;
@@ -177,7 +198,7 @@ public class RequestInfo {
*
* 由程序版本决定
* 来源: 本例管理
- * @see EmoneyRequestConfig.androidSdkLevel
+ * @see #androidSdkLevel
*/
private String emoneyVersion = "5.8.1";
@@ -429,5 +450,9 @@ public class RequestInfo {
return node;
}
+
+ public boolean isAnonymous() {
+ return !StringUtils.isAnyBlank(getUsername(), getPassword());
+ }
}
diff --git a/src/main/java/quant/rich/emoney/entity/sqlite/StrategyAndPool.java b/src/main/java/quant/rich/emoney/entity/sqlite/StrategyAndPool.java
index 9f4ebad..5f5e78d 100644
--- a/src/main/java/quant/rich/emoney/entity/sqlite/StrategyAndPool.java
+++ b/src/main/java/quant/rich/emoney/entity/sqlite/StrategyAndPool.java
@@ -21,6 +21,7 @@ public class StrategyAndPool implements Comparable {
private String strategyName;
private Integer strategyId;
private String poolName;
+ private String type;
@TableId
private Integer poolId;
@@ -28,7 +29,8 @@ public class StrategyAndPool implements Comparable {
}
- public StrategyAndPool(String strategyName, Integer strategyId, String poolName, Integer poolId) {
+ public StrategyAndPool(String type, String strategyName, Integer strategyId, String poolName, Integer poolId) {
+ this.type = type;
this.strategyName = strategyName;
this.strategyId = strategyId;
this.poolName = poolName;
@@ -38,18 +40,20 @@ public class StrategyAndPool implements Comparable {
@Override
public boolean equals(Object o) {
if (this == o) return true;
+ if (o == null) return false;
if (!(o instanceof StrategyAndPool)) return false;
StrategyAndPool strategyAndPool = (StrategyAndPool) o;
return
- strategyName == strategyAndPool.strategyName &&
- strategyId == strategyAndPool.strategyId &&
- poolName == strategyAndPool.poolName &&
- poolId == strategyAndPool.poolId;
+ Objects.equals(strategyName, strategyAndPool.strategyName) &&
+ Objects.equals(strategyId, strategyAndPool.strategyId) &&
+ Objects.equals(poolName, strategyAndPool.poolName) &&
+ Objects.equals(poolId, strategyAndPool.poolId) &&
+ Objects.equals(type, strategyAndPool.type);
}
@Override
public int hashCode() {
- return Objects.hash(strategyName, strategyId, poolName, poolId);
+ return Objects.hash(strategyName, strategyId, poolName, poolId, type);
}
/**
diff --git a/src/main/java/quant/rich/emoney/interfaces/ConfigInfo.java b/src/main/java/quant/rich/emoney/interfaces/ConfigInfo.java
index 0f9694d..d09feda 100644
--- a/src/main/java/quant/rich/emoney/interfaces/ConfigInfo.java
+++ b/src/main/java/quant/rich/emoney/interfaces/ConfigInfo.java
@@ -21,7 +21,12 @@ import org.springframework.stereotype.Component;
@Retention(RetentionPolicy.RUNTIME)
public @interface ConfigInfo {
/**
- * @return 配置 field 标识,用以自动注入、持久化配置文件名
+ * 配置 field 标识,用以自动注入、持久化配置文件名。
+ * 例:
+ *
+ * 指定 field = "website", 则 bean 名为 websiteConfig 持久化文件名为 websiteConfig.json
+ * 未指定 field, 类名为 ProxyConfig, 则 bean 名为 proxyConfig, 持久化文件名为 proxyConfig.json
+ *
*/
String field() default "";
diff --git a/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttp.java b/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttp.java
index 511b3c2..f41c655 100644
--- a/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttp.java
+++ b/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttp.java
@@ -41,10 +41,11 @@ public class PatchOkHttp {
randomIds[0] = random.nextInt();
}
rule.setId(randomIds[0]);
+ log.debug("PatchOkHttp.apply(rule.id={})", randomIds[0]);
}
rules.add(rule);
- log.debug("PatchOkHttp.apply() 在 ClassLoader {} 中运行", PatchOkHttp.class.getClassLoader());
+ //log.debug("PatchOkHttp.apply() 在 ClassLoader {} 中运行", PatchOkHttp.class.getClassLoader());
if (!isHooked) hook();
return rule.getId();
}
@@ -60,12 +61,10 @@ public class PatchOkHttp {
}
public static void match(RequestContext ctx, String currentHeader, Consumer consumer) {
- if (!logOnce) {
- log.debug("PatchOkHttp.match() 在 ClassLoader {} 中运行", PatchOkHttp.class.getClassLoader());
- logOnce = true;
- }
+ // log.debug("PatchOkHttp.match() 在 ClassLoader {} 中运行", PatchOkHttp.class.getClassLoader());
for (PatchOkHttpRule rule : PatchOkHttp.rules) {
if (rule.matches(ctx)) {
+ log.debug("PatchOkHttp.match() 匹配到规则 rule.id={}", rule.getId());
rule.apply(ctx, currentHeader, consumer);
}
}
diff --git a/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttpRule.java b/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttpRule.java
index f2c4d1d..924e52a 100644
--- a/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttpRule.java
+++ b/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttpRule.java
@@ -154,7 +154,7 @@ public class PatchOkHttpRule {
return this;
}
- public Builder overrideIf(String headerName, String value) {
+ public Builder overrideHeader(String headerName, String value) {
actions.add((ctx, curr, setter) -> {
if (curr.equalsIgnoreCase(headerName)) {
log.debug("matches and applying - host: {}, currHeader {}, targetHeader {}, value: {}, classLoader: {}", ctx.host, curr, headerName,
@@ -164,6 +164,24 @@ public class PatchOkHttpRule {
});
return this;
}
+
+ /**
+ * 如果满足条件则覆写指定 Header。当覆写值可能动态变化时,使用本方法提供 supplier
+ * @param headerName
+ * @param valueSupplier
+ * @return
+ */
+ public Builder overrideHeader(String headerName, Supplier valueSupplier) {
+ actions.add((ctx, curr, setter) -> {
+ if (curr.equalsIgnoreCase(headerName)) {
+ String value = valueSupplier.get();
+ log.debug("matches and applying - host: {}, currHeader {}, targetHeader {}, value: {}, classLoader: {}", ctx.host, curr, headerName,
+ value, this.getClass().getClassLoader());
+ setter.accept(value);
+ }
+ });
+ return this;
+ }
public PatchOkHttpRule build() {
return new PatchOkHttpRule(condition, actions);
diff --git a/src/main/java/quant/rich/emoney/pojo/dto/R.java b/src/main/java/quant/rich/emoney/pojo/dto/R.java
index 21e644a..28e3a2e 100644
--- a/src/main/java/quant/rich/emoney/pojo/dto/R.java
+++ b/src/main/java/quant/rich/emoney/pojo/dto/R.java
@@ -206,6 +206,22 @@ public class R implements Serializable {
}
}
+ /**
+ * 提供一返回值为 boolean 的 supplier,如果成功则返回 R.ok(), 失败则抛出 RException.badRequest(defaltMessage),
+ * 抛出错误则抛出 RException.badRequest(e.getMessage())
+ * @param supplier
+ * @param defaultMessage
+ * @return
+ */
+ public static R> judge(ThrowingSupplier supplier, String defaultMessage) {
+ try {
+ return R.judge(supplier.get(), defaultMessage);
+ }
+ catch (Exception e) {
+ throw RException.badRequest(e.getMessage());
+ }
+ }
+
public static R> judgeThrow(ThrowingSupplier> supplier) throws Exception {
return R.ok(supplier.get());
diff --git a/src/main/java/quant/rich/emoney/service/ConfigService.java b/src/main/java/quant/rich/emoney/service/ConfigService.java
index 66bd3c9..77f263c 100644
--- a/src/main/java/quant/rich/emoney/service/ConfigService.java
+++ b/src/main/java/quant/rich/emoney/service/ConfigService.java
@@ -197,21 +197,26 @@ public class ConfigService implements InitializingBean {
}
/**
- * 获取 Config
+ * 获取给定类型的 Config 实例
*
- * 当缓存中有时从缓存取, 缓存没有时从数据库取并更新到缓存, 数据库也没有时, 如果指定的 Config 的 @ConfigInfo
- * 注解开启了 initDefault = true, 则尝试返回一个初始 Config,否则返回 null
+ * 当缓存中有时从缓存取,缓存没有时从数据库取并更新到缓存,数据库也没有时,
+ * 如果指定的 Config 的 @ConfigInfo
+ * 注解开启了 initDefault = true, 则尝试返回一个初始 Config,否则返回 null.
+ * 原本不存在的 Config 如果 @ConfigInfo 开启了 save(),会持久化到本地。
*
* @param
- * @param clazz
+ * @param clazz 配置类
* @return
+ * @see ConfigInfo
+ * @see #getOrCreateConfig(String)
+ *
*/
public > Config getConfig(Class clazz) {
if (classObjectCache.containsKey(clazz)) {
try {
return getCache(clazz);
} catch (Exception e) {
- log.warn("Cannot get config info of " + clazz.toString() + " from cache, try to read from database", e);
+ log.warn("Cannot get config info of {} from cache, try to read from database", clazz.toString(), e);
}
}
String field = fieldClassCache.inverse().get(clazz);
@@ -244,11 +249,6 @@ public class ConfigService implements InitializingBean {
try {
String filePath = getConfigFilePath(field, false);
SmartResourceResolver.saveText(filePath, configJoString);
- //Path dirPath = Paths.get(filePath).getParent();
- //if (Files.notExists(dirPath)) {
- // Files.createDirectories(dirPath);
- //}
- //Files.writeString(Path.of(filePath), configJoString);
} catch (IOException e) {
log.error("Cannot write config to local file {}.json, error: {}", field, e.getMessage());
return false;
@@ -300,7 +300,7 @@ public class ConfigService implements InitializingBean {
Class configClass = (Class) fieldClassCache.get(field);
if (configClass == null) {
- log.warn("Cannot get class info from fieldClassCache, field name: {}", field);
+ log.warn("Cannot get config class from fieldClassCache, field name {} not exist", field);
return null;
}
ConfigInfo info = getConfigInfo(configClass);
@@ -313,10 +313,12 @@ public class ConfigService implements InitializingBean {
// 也就是无论如何,fallback 都不应由程序来写入
String filePath = getConfigFilePath(field, false);
config = getFromFile(filePath, configClass);
+ boolean needSave = false;
if (config == null) {
log.info("Cannot init config from local file of {}Config, try fallback", field);
// 走 fallback 流程
config = getFromFile(getConfigFilePath(field, true), configClass);
+ needSave = true;
}
if (config == null) {
@@ -328,6 +330,7 @@ public class ConfigService implements InitializingBean {
config =
configClass.getDeclaredConstructor()
.newInstance();
+ needSave = true;
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
// 一般是初始化方法内出现未被捕获的错误
@@ -340,7 +343,8 @@ public class ConfigService implements InitializingBean {
if (config == null) {
log.warn("Cannot read or init from default/fallback for config {}Config, it will be null", field);
}
- else {
+ else if (needSave) {
+ // 走了 fallback 或者初始化了则保存一份
saveOrUpdate(config);
}
diff --git a/src/main/java/quant/rich/emoney/service/IndexDetailService.java b/src/main/java/quant/rich/emoney/service/IndexDetailService.java
index fbf23bd..207298e 100644
--- a/src/main/java/quant/rich/emoney/service/IndexDetailService.java
+++ b/src/main/java/quant/rich/emoney/service/IndexDetailService.java
@@ -39,15 +39,17 @@ import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import quant.rich.emoney.client.OkHttpClientProvider;
-import quant.rich.emoney.entity.config.EmoneyRequestConfig;
+import quant.rich.emoney.component.RequireAuthAndProxyAspect.RequireAuthAndProxy;
import quant.rich.emoney.entity.config.IndexInfoConfig;
import quant.rich.emoney.entity.config.SmartViewWriter;
+import quant.rich.emoney.entity.sqlite.RequestInfo;
import quant.rich.emoney.pojo.dto.IndexDetail;
import quant.rich.emoney.pojo.dto.NonParamsIndexDetail;
import quant.rich.emoney.pojo.dto.NonParamsIndexDetail.NonParamsIndexDetailData;
import quant.rich.emoney.util.EncryptUtils;
import quant.rich.emoney.util.SmartResourceResolver;
import quant.rich.emoney.pojo.dto.ParamsIndexDetail;
+import quant.rich.emoney.service.sqlite.RequestInfoService;
/**
* 获取指标详情的服务
@@ -67,7 +69,7 @@ public class IndexDetailService {
IndexInfoConfig indexInfoConfig;
@Autowired
- EmoneyRequestConfig emoneyRequestConfig;
+ RequestInfoService requestInfoService;
static final String filePath = "./conf/extra/indexDetail/";
static final ObjectMapper mapper = new ObjectMapper();
@@ -84,6 +86,7 @@ public class IndexDetailService {
* @return
*/
@CacheEvict(cacheNames="@indexDetailService.getIndexDetail(Serializable)", key="#indexCode.toString()")
+ @RequireAuthAndProxy(autoLogin = true)
public IndexDetail forceRefreshAndGetIndexDetail(Serializable indexCode) {
// 刷新的本质就是从网络获取,因为此处已经清理了缓存,所以直接从网络获取后再
@@ -101,6 +104,7 @@ public class IndexDetailService {
@Cacheable(cacheNames="@indexDetailService.getIndexDetail(Serializable)", key="#indexCode.toString()")
+ @RequireAuthAndProxy(autoLogin = true)
public IndexDetail getIndexDetail(Serializable indexCode) {
if (indexCode == null) {
@@ -137,11 +141,16 @@ public class IndexDetailService {
/**
* 从网络获取有参指标详情
+ * 本例用到的 requestInfo 涉及鉴权
* @param indexCode
+ * @see RequestInfoService#getDefaultRequestInfo()
* @return
*/
private ParamsIndexDetail getParamsIndexDetailOnline(Serializable indexCode) {
-
+ RequestInfo requestInfo = requestInfoService.getDefaultRequestInfo();
+ if (requestInfo == null || StringUtils.isBlank(requestInfo.getAuthorization())) {
+ throw new RuntimeException("无法获取已鉴权的 RequestInfo");
+ }
try {
OkHttpClient client = OkHttpClientProvider.getInstance();
String url = "https://emapp.emoney.cn/Config/AppIndicator/Get";
@@ -154,9 +163,9 @@ public class IndexDetailService {
.header("X-Protocol-Id", "Config%2FAppIndicator%2FGet")
.header("X-Request-Id", "null")
.header("EM-Sign", EncryptUtils.getEMSign(content, "POST", "Config%2FAppIndicator%2FGet"))
- .header("Authorization", emoneyRequestConfig.getAuthorization())
- .header("X-Android-Agent", emoneyRequestConfig.getXAndroidAgent())
- .header("Emapp-ViewMode", emoneyRequestConfig.getEmappViewMode())
+ .header("Authorization", requestInfoService.getDefaultRequestInfo().getAuthorization())
+ .header("X-Android-Agent", requestInfoService.getDefaultRequestInfo().getXAndroidAgent())
+ .header("Emapp-ViewMode", requestInfoService.getDefaultRequestInfo().getEmappViewMode())
.build();
final Response response = client.newCall(request).execute();
String responseText = response.body().string();
@@ -223,8 +232,11 @@ public class IndexDetailService {
/**
* 从网络获取指定 indexCode 的无参指标详情
+ *
本例用到的 requestInfo 不需要 PatchOkHttp 覆写,但要求鉴权参数拼接到 url 中,故要求鉴权
*
会一并尝试获取其他在本地未有的无参指标
* @param indexCode
+ * @see RequestInfo#getWebviewUserAgent()
+ * @see RequestInfoService#getDefaultRequestInfo()
* @return
*/
private NonParamsIndexDetail getNonParamsIndexDetailOnline(Serializable indexCode) {
@@ -234,7 +246,7 @@ public class IndexDetailService {
.header("Host", "appstatic.emoney.cn")
.header("Connection", "keep-alive")
.header("Upgrade-Insecure-Requests", "1")
- .header("User-Agent", emoneyRequestConfig.getWebviewUserAgent())
+ .header("User-Agent", requestInfoService.getDefaultRequestInfo().getWebviewUserAgent())
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
.header("X-Request-With", "cn.emoney.emstock")
.header("Sec-Fetch-Site", "none")
@@ -259,7 +271,7 @@ public class IndexDetailService {
Document doc = Jsoup.parseBodyFragment(responseBody);
doc.select("script[src]").forEach(el -> {
String absoluteURI = resolveUrl(url, el.attr("src"));
- log.info("script uri: {}", absoluteURI);
+ log.debug("script uri: {}", absoluteURI);
if (absoluteURI != null) {
scripts.add(absoluteURI);
}
@@ -281,7 +293,7 @@ public class IndexDetailService {
Request.Builder scriptBuilder = new Request.Builder()
.header("Host", "appstatic.emoney.cn")
.header("Connection", "keep-alive")
- .header("User-Agent", emoneyRequestConfig.getWebviewUserAgent())
+ .header("User-Agent", requestInfoService.getDefaultRequestInfo().getWebviewUserAgent())
.header("Accept", "*/*")
.header("X-Request-With", "cn.emoney.emstock")
.header("Sec-Fetch-Site", "same-origin")
@@ -381,7 +393,9 @@ public class IndexDetailService {
NonParamsIndexDetail existed;
try {
existed = mapper.readValue(inputStream, NonParamsIndexDetail.class);
- if (!existed.getOriginal().equals(detail.getOriginal())) {
+ if (existed.getOriginal() == null ||
+ !existed.getOriginal().equals(detail.getOriginal())) {
+ log.debug("本地 NonParamsIndexDetail {} 原始数据与最新数据不一致,更新", indexCode);
saveIndexDetail(detail);
}
}
@@ -415,10 +429,10 @@ public class IndexDetailService {
/**
* 为指定 NonParamsIndexDetail 加载在线图片并转换成 base64
- * 该方法涉及 authorization(token)/webviewUserAgent,需确保 EmoneyRequestConfig 已正确注入并登录
+ * 该方法涉及 authorization(token)/webviewUserAgent,需确保 RequestInfo 已正确配置并登录
* @param detail
* @return
- * @see EmoneyRequestConfig
+ * @see RequestInfo
*/
private NonParamsIndexDetail loadImages(NonParamsIndexDetail detail) {
OkHttpClient client = OkHttpClientProvider.getInstance();
@@ -444,7 +458,7 @@ public class IndexDetailService {
.url(imageUrl)
.header("Host", host)
.header("Connection", "keep-alive")
- .header("User-Agent", emoneyRequestConfig.getWebviewUserAgent())
+ .header("User-Agent", requestInfoService.getDefaultRequestInfo().getWebviewUserAgent())
.header("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
.header("X-Request-With", "cn.emoney.emstock")
.header("Sec-Fetch-Site", "same-origin")
@@ -548,17 +562,23 @@ public class IndexDetailService {
/**
* 获取 NonParamsIndexDetail URL
- * 该 Url 涉及 authorization(token),需确保 EmoneyRequestConfig 已正确注入并登录
+ * 该 Url 涉及 authorization(token),需确保 RequestInfo 已正确配置并登录
* @param indexCode
* @return
- * @see EmoneyRequestConfig
+ * @see RequestInfo
*/
private String buildNonParamsIndexUrl(Serializable indexCode) {
+
+ RequestInfo requestInfo = requestInfoService.getDefaultRequestInfo();
+ if (requestInfo == null || StringUtils.isBlank(requestInfo.getAuthorization())) {
+ throw new RuntimeException("无法获取已鉴权的 RequestInfo");
+ }
+
StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append("https://appstatic.emoney.cn/html/emapp/stock/note/?name=");
urlBuilder.append(indexCode.toString());
urlBuilder.append("&emoneyScaleType=0&emoneyLandMode=0&token=");
- urlBuilder.append(emoneyRequestConfig.getAuthorization());
+ urlBuilder.append(requestInfoService.getDefaultRequestInfo().getAuthorization());
return urlBuilder.toString();
}
diff --git a/src/main/java/quant/rich/emoney/service/WarnService.java b/src/main/java/quant/rich/emoney/service/WarnService.java
new file mode 100644
index 0000000..6e9ff9c
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/service/WarnService.java
@@ -0,0 +1,27 @@
+package quant.rich.emoney.service;
+
+import org.springframework.stereotype.Service;
+
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+@Service
+@Slf4j
+public class WarnService {
+
+
+
+ public static class Warn {
+ }
+
+ public static enum WarnType {
+ NO_SPECIFIC_REQUEST_INFO("未设置指定的请求信息");
+
+ @Getter
+ private String info;
+ WarnType(String info) {
+ this.info = info;
+ }
+ }
+
+}
diff --git a/src/main/java/quant/rich/emoney/service/sqlite/ProxySettingService.java b/src/main/java/quant/rich/emoney/service/sqlite/ProxySettingService.java
index 8672b2a..4955dca 100644
--- a/src/main/java/quant/rich/emoney/service/sqlite/ProxySettingService.java
+++ b/src/main/java/quant/rich/emoney/service/sqlite/ProxySettingService.java
@@ -2,11 +2,43 @@ package quant.rich.emoney.service.sqlite;
import org.springframework.stereotype.Service;
import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import quant.rich.emoney.entity.sqlite.ProxySetting;
import quant.rich.emoney.mapper.sqlite.ProxySettingMapper;
+import quant.rich.emoney.pojo.dto.IpInfo;
+import quant.rich.emoney.util.GeoIPUtil;
@DS("sqlite")
@Service
public class ProxySettingService extends SqliteServiceImpl {
+
+ private volatile IpInfo ipInfo;
+
+ /**
+ * 获取默认代理配置
+ * @return
+ */
+ public ProxySetting getDefaultProxySetting() {
+ return getOne(
+ new LambdaQueryWrapper()
+ .eq(ProxySetting::getIsDefault, true));
+ }
+
+ /**
+ * 获取默认代理配置的 IP 信息
+ * @return
+ */
+ public IpInfo getDefaultProxySettingIpInfo() {
+ ProxySetting proxySetting = getDefaultProxySetting();
+ if (proxySetting == null) return null;
+ if (ipInfo != null) return ipInfo;
+ return refreshIpThroughProxy();
+ }
+
+
+ public synchronized IpInfo refreshIpThroughProxy() {
+ ipInfo = GeoIPUtil.getIpInfoThroughProxy(getDefaultProxySetting());
+ return ipInfo;
+ }
}
diff --git a/src/main/java/quant/rich/emoney/service/sqlite/RequestInfoService.java b/src/main/java/quant/rich/emoney/service/sqlite/RequestInfoService.java
index 8fcb10d..3cd6efb 100644
--- a/src/main/java/quant/rich/emoney/service/sqlite/RequestInfoService.java
+++ b/src/main/java/quant/rich/emoney/service/sqlite/RequestInfoService.java
@@ -1,14 +1,52 @@
package quant.rich.emoney.service.sqlite;
+import java.util.function.Supplier;
+
+import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import com.baomidou.dynamic.datasource.annotation.DS;
-
-import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import jakarta.annotation.PostConstruct;
+import lombok.extern.slf4j.Slf4j;
import quant.rich.emoney.entity.sqlite.RequestInfo;
import quant.rich.emoney.mapper.sqlite.RequestInfoMapper;
+import quant.rich.emoney.patch.okhttp.PatchOkHttp;
+import quant.rich.emoney.patch.okhttp.PatchOkHttpRule;
@DS("sqlite")
@Service
+@Slf4j
+@Lazy(false)
public class RequestInfoService extends SqliteServiceImpl {
-
+
+ private volatile Integer userAgentPatchRuleId;
+
+ @PostConstruct
+ void postConstruct() {
+ userAgentPatchRuleId = PatchOkHttp.apply(
+ PatchOkHttpRule.when()
+ .hostEndsWith("emoney.cn")
+ .not(r -> r.hostMatches("appstatic"))
+ .or(a -> a.hostContains("emapp"))
+ .or(b -> b.hasHeaderName("X-Protocol-Id"))
+ .overrideHeader("User-Agent", new Supplier() {
+ @Override
+ public String get() {
+ log.debug("触发获取请求配置的 OkHttpUserAgent");
+ return getDefaultRequestInfo().getOkHttpUserAgent();
+ }
+ }).build().setId(userAgentPatchRuleId));
+ }
+
+ public RequestInfo getDefaultRequestInfo() {
+ RequestInfo requestInfo = getOne(
+ new LambdaQueryWrapper().eq(RequestInfo::getIsDefault, true)
+ );
+ if (requestInfo == null) {
+ requestInfo = new RequestInfo();
+ save(requestInfo);
+ }
+ return requestInfo;
+ }
+
}
diff --git a/src/main/java/quant/rich/emoney/service/sqlite/StrategyAndPoolService.java b/src/main/java/quant/rich/emoney/service/sqlite/StrategyAndPoolService.java
index 13f5b04..a3aa210 100644
--- a/src/main/java/quant/rich/emoney/service/sqlite/StrategyAndPoolService.java
+++ b/src/main/java/quant/rich/emoney/service/sqlite/StrategyAndPoolService.java
@@ -3,10 +3,14 @@ package quant.rich.emoney.service.sqlite;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
+import java.util.stream.Collectors;
+import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
@@ -76,6 +80,7 @@ public class StrategyAndPoolService extends SqliteServiceImpl set = new HashSet<>();
+
String[] types = new String[] {"band", "tech", "val"}; // 波段/技术/基本面
for (JsonNode node : output) {
for (String type : types) {
@@ -98,17 +104,23 @@ public class StrategyAndPoolService extends SqliteServiceImpl()
+ .eq(StrategyAndPool::getPoolId, strategyAndPool.getPoolId()))) {
+ set.add(strategyAndPool);
+ }
}
}
}
}
this.saveOrUpdateBatch(set);
+ log.info("新增 {} 条 StrategyAndPool", set.size());
}
}
diff --git a/src/main/java/quant/rich/emoney/util/EncryptUtils.java b/src/main/java/quant/rich/emoney/util/EncryptUtils.java
index 7c595ad..6a06637 100644
--- a/src/main/java/quant/rich/emoney/util/EncryptUtils.java
+++ b/src/main/java/quant/rich/emoney/util/EncryptUtils.java
@@ -24,7 +24,6 @@ import org.bouncycastle.util.encoders.Hex;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
-import okhttp3.Request;
import okhttp3.RequestBody;
@Slf4j
diff --git a/src/main/java/quant/rich/emoney/util/GeoIPUtil.java b/src/main/java/quant/rich/emoney/util/GeoIPUtil.java
index 725af2f..76cf0a2 100644
--- a/src/main/java/quant/rich/emoney/util/GeoIPUtil.java
+++ b/src/main/java/quant/rich/emoney/util/GeoIPUtil.java
@@ -10,7 +10,7 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import quant.rich.emoney.client.OkHttpClientProvider;
-import quant.rich.emoney.entity.config.ProxyConfig;
+import quant.rich.emoney.entity.sqlite.ProxySetting;
import quant.rich.emoney.pojo.dto.IpInfo;
import java.io.IOException;
@@ -36,10 +36,13 @@ public class GeoIPUtil {
}
@Async
- public static IpInfo getIpInfoThroughProxy(ProxyConfig proxyConfig) {
+ public static IpInfo getIpInfoThroughProxy(ProxySetting proxySetting) {
+ if (proxySetting == null) {
+ throw new RuntimeException("代理为空");
+ }
return CallerLockUtil.tryCallWithCallerLock(() -> {
- Proxy proxy = proxyConfig.getProxy();
- boolean ignoreHttpsVerification = proxyConfig.getIgnoreHttpsVerification();
+ Proxy proxy = proxySetting.getProxy();
+ boolean ignoreHttpsVerification = proxySetting.getIgnoreHttpsVerification();
// OkHttp 客户端配置
OkHttpClient client = OkHttpClientProvider.getInstance(
proxy, ignoreHttpsVerification,
@@ -82,7 +85,7 @@ public class GeoIPUtil {
log.warn("Proxy ipv6 error {}", e.getMessage());
}
return queryIpInfoGeoLite(ipInfo);
- }, 100, proxyConfig).orElse(IpInfo.EMPTY);
+ }, 100, proxySetting).orElse(IpInfo.EMPTY);
}
/**
diff --git a/src/main/java/quant/rich/emoney/util/SmartResourceResolver.java b/src/main/java/quant/rich/emoney/util/SmartResourceResolver.java
index e667443..3458301 100644
--- a/src/main/java/quant/rich/emoney/util/SmartResourceResolver.java
+++ b/src/main/java/quant/rich/emoney/util/SmartResourceResolver.java
@@ -48,25 +48,25 @@ public class SmartResourceResolver {
Path externalPath = resolveExternalPath(relativePath);
if (externalPath != null && Files.exists(externalPath)) {
- log.debug("从外部文件系统加载资源: {}", externalPath);
+ log.debug("Load resource externally: {}", externalPath);
return Files.newInputStream(externalPath);
}
// 否则回退到 classpath(JAR、WAR、IDE)
InputStream in = SmartResourceResolver.class.getClassLoader().getResourceAsStream(relativePath);
if (in != null) {
- log.debug("从 classpath 内部加载资源: {}", relativePath);
+ log.debug("Load resource within internal classpath: {}", relativePath);
return in;
}
- throw new FileNotFoundException("无法找到资源: " + relativePath);
+ throw new FileNotFoundException("Cannot find resources: " + relativePath);
}
public static void saveText(String relativePath, String content) throws IOException {
Path outputPath = resolveExternalPath(relativePath);
Files.createDirectories(outputPath.getParent()); // 确保目录存在
Files.writeString(outputPath, content, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
- log.debug("写入外部资源文件成功: {}", outputPath);
+ log.debug("Write resources externally success: {}", outputPath);
}
private static Path resolveExternalPath(String relativePath) {
diff --git a/src/main/java/quant/rich/emoney/validator/ProxyConfigValid.java b/src/main/java/quant/rich/emoney/validator/ProxyConfigValid.java
deleted file mode 100644
index 0c982fa..0000000
--- a/src/main/java/quant/rich/emoney/validator/ProxyConfigValid.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package quant.rich.emoney.validator;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.Target;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-import jakarta.validation.Constraint;
-import jakarta.validation.Payload;
-
-@Documented
-@Constraint(validatedBy = ProxyConfigValidator.class)
-@Target({ ElementType.TYPE })
-@Retention(RetentionPolicy.RUNTIME)
-public @interface ProxyConfigValid {
- String message() default "非法的 ProxyConfig";
- Class>[] groups() default {};
- Class extends Payload>[] payload() default {};
-}
diff --git a/src/main/java/quant/rich/emoney/validator/ProxyConfigValidator.java b/src/main/java/quant/rich/emoney/validator/ProxyConfigValidator.java
deleted file mode 100644
index 81038d6..0000000
--- a/src/main/java/quant/rich/emoney/validator/ProxyConfigValidator.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package quant.rich.emoney.validator;
-
-import java.net.Proxy;
-import java.util.Objects;
-import org.apache.commons.lang3.StringUtils;
-
-import jakarta.validation.ConstraintValidator;
-import jakarta.validation.ConstraintValidatorContext;
-import quant.rich.emoney.entity.config.ProxyConfig;
-import quant.rich.emoney.interfaces.IConfig;
-import quant.rich.emoney.pojo.dto.IpInfo;
-
-public class ProxyConfigValidator implements IValidator, ConstraintValidator> {
-
- @Override
- public boolean isValid(IConfig value, ConstraintValidatorContext context) {
-
- if (value == null) return true;
- if (!(value instanceof ProxyConfig config)) return true;
-
- if (config.getProxyType() != null && config.getProxyType() != Proxy.Type.DIRECT) {
- if (StringUtils.isBlank(config.getProxyHost())) {
- return invalid(context, "设置代理为 HTTP 或 SOCKS 时,代理地址不允许为空");
- }
- if (Objects.isNull(config.getProxyPort()) || config.getProxyPort() <= 0 || config.getProxyPort() > 65535) {
- return invalid(context, "端口不合法");
- }
- // 非匿名须判断用户名密码是否为空
- IpInfo ipInfo = config.refreshIpThroughProxy();
- return !ipInfo.isEmpty();
- }
-
- return true;
-
- }
-
-}
diff --git a/src/main/java/quant/rich/emoney/validator/EmoneyRequestConfigValid.java b/src/main/java/quant/rich/emoney/validator/RequestInfoValid.java
similarity index 81%
rename from src/main/java/quant/rich/emoney/validator/EmoneyRequestConfigValid.java
rename to src/main/java/quant/rich/emoney/validator/RequestInfoValid.java
index e995e2b..8f29f0a 100644
--- a/src/main/java/quant/rich/emoney/validator/EmoneyRequestConfigValid.java
+++ b/src/main/java/quant/rich/emoney/validator/RequestInfoValid.java
@@ -10,10 +10,10 @@ import jakarta.validation.Constraint;
import jakarta.validation.Payload;
@Documented
-@Constraint(validatedBy = EmoneyRequestConfigValidator.class)
+@Constraint(validatedBy = RequestInfoValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
-public @interface EmoneyRequestConfigValid {
+public @interface RequestInfoValid {
String message() default "非法的 EmoneyRequestConfig";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
diff --git a/src/main/java/quant/rich/emoney/validator/EmoneyRequestConfigValidator.java b/src/main/java/quant/rich/emoney/validator/RequestInfoValidator.java
similarity index 56%
rename from src/main/java/quant/rich/emoney/validator/EmoneyRequestConfigValidator.java
rename to src/main/java/quant/rich/emoney/validator/RequestInfoValidator.java
index 2861ff8..8d579a9 100644
--- a/src/main/java/quant/rich/emoney/validator/EmoneyRequestConfigValidator.java
+++ b/src/main/java/quant/rich/emoney/validator/RequestInfoValidator.java
@@ -7,27 +7,21 @@ import org.apache.commons.lang3.StringUtils;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import quant.rich.emoney.entity.config.DeviceInfoConfig;
-import quant.rich.emoney.entity.config.EmoneyRequestConfig;
-import quant.rich.emoney.interfaces.IConfig;
+import quant.rich.emoney.entity.sqlite.RequestInfo;
-public class EmoneyRequestConfigValidator implements IValidator, ConstraintValidator> {
+public class RequestInfoValidator implements IValidator, ConstraintValidator {
static final Pattern androidIdPattern = Pattern.compile("^[0-9a-f]{16}$");
@Override
- public boolean isValid(IConfig value, ConstraintValidatorContext context) {
+ public boolean isValid(RequestInfo value, ConstraintValidatorContext context) {
if (value == null) return true;
- if (!(value instanceof EmoneyRequestConfig config)) return true;
+ if (!(value instanceof RequestInfo config)) return true;
- if (!config.getIsAnonymous()) {
- // 非匿名须判断用户名密码是否为空
- if (StringUtils.isAnyBlank(config.getUsername(), config.getPassword())) {
- context.disableDefaultConstraintViolation();
- context.buildConstraintViolationWithTemplate("配置非匿名时用户名和密码不能为空")
- .addConstraintViolation();
- return false;
- }
+ // 如果有用户名则必须设置密码
+ if (!StringUtils.isBlank(config.getUsername()) && StringUtils.isBlank(config.getPassword())) {
+ return invalid(context, "当设置了用户名时,必须提供密码");
}
if (!androidIdPattern.matcher(config.getAndroidId()).matches()) {
diff --git a/src/main/resources/application-remote.yml b/src/main/resources/application-remote.yml
index 07d0b81..aa16fff 100644
--- a/src/main/resources/application-remote.yml
+++ b/src/main/resources/application-remote.yml
@@ -27,15 +27,3 @@ spring:
init:
mode: always
continue-on-error: true
-mybatis-plus:
- banner: false
- mapper-locations:
- - classpath*:mapper/postgre/*.xml
- - classpath*:mapper/sqlite/*.xml
- type-aliases-package:
- - quant.rich.emoney.entity.postgre
- - quant.rich.emoney.entity.sqlite
- type-handlers-package: quant.rich.emoney.mybatis.typehandler
- configuration:
- map-underscore-to-camel-case: true
- default-enum-type-handler: org.apache.ibatis.type.EnumOrdinalTypeHandler
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 7262c31..c2d7885 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -36,6 +36,21 @@ spring:
encoding: UTF-8
cache: false
+mybatis-plus-join.banner: false
+mybatis-plus:
+ global-config:
+ banner: false
+ mapper-locations:
+ - classpath*:mapper/postgre/*.xml
+ - classpath*:mapper/sqlite/*.xml
+ type-aliases-package:
+ - quant.rich.emoney.entity.postgre
+ - quant.rich.emoney.entity.sqlite
+ type-handlers-package: quant.rich.emoney.mybatis.typehandler
+ configuration:
+ map-underscore-to-camel-case: true
+ default-enum-type-handler: org.apache.ibatis.type.EnumOrdinalTypeHandler
+
kaptcha:
border: "no"
image:
diff --git a/src/main/resources/conf/extra/indexDetail/nonParams/10002700.json b/src/main/resources/conf/extra/indexDetail/nonParams/10002700.json
index bc54d37..e1f06dd 100644
--- a/src/main/resources/conf/extra/indexDetail/nonParams/10002700.json
+++ b/src/main/resources/conf/extra/indexDetail/nonParams/10002700.json
@@ -1 +1,11 @@
-{"id":"45","name":"FUNDCPX","nameCode":"10002700","data":[{"title":"基金操盘线:","items":["基金操盘线是一款可以识别趋势的指标。","操作原则:B点买入,S点卖出;红色持有标的,蓝色抛出标的。","趋势分为上涨趋势和下跌趋势,由于是趋势指标所以只在趋势明朗后才会出现B点买入信号或者S点卖出信号,一般不会在拐点位置出现信号,因此B点不是最早的买入点,S点是最后的卖出点。"],"image":null}],"original":"{\"id\":45,\"data\":[{\"title\":\"基金操盘线:\",\"items\":[\"基金操盘线是一款可以识别趋势的指标。\",\"操作原则:B点买入,S点卖出;红色持有标的,蓝色抛出标的。\",\"趋势分为上涨趋势和下跌趋势,由于是趋势指标所以只在趋势明朗后才会出现B点买入信号或者S点卖出信号,一般不会在拐点位置出现信号,因此B点不是最早的买入点,S点是最后的卖出点。\"]}],\"name\":\"FUNDCPX\",\"nameCode\":\"10002700\"}","indexCode":"10002700","indexName":"FUNDCPX","details":[{"content":"基金操盘线:","type":"TITLE"},{"content":"基金操盘线是一款可以识别趋势的指标。","type":"TEXT"},{"content":"操作原则:B点买入,S点卖出;红色持有标的,蓝色抛出标的。","type":"TEXT"},{"content":"趋势分为上涨趋势和下跌趋势,由于是趋势指标所以只在趋势明朗后才会出现B点买入信号或者S点卖出信号,一般不会在拐点位置出现信号,因此B点不是最早的买入点,S点是最后的卖出点。","type":"TEXT"}]}
\ No newline at end of file
+{
+ "id" : "45",
+ "name" : "FUNDCPX",
+ "nameCode" : "10002700",
+ "data" : [ {
+ "title" : "基金操盘线:",
+ "items" : [ "基金操盘线是一款可以识别趋势的指标。", "操作原则:B点买入,S点卖出;红色持有标的,蓝色抛出标的。", "趋势分为上涨趋势和下跌趋势,由于是趋势指标所以只在趋势明朗后才会出现B点买入信号或者S点卖出信号,一般不会在拐点位置出现信号,因此B点不是最早的买入点,S点是最后的卖出点。" ],
+ "image" : null
+ } ],
+ "original" : "{\"id\":45,\"data\":[{\"title\":\"基金操盘线:\",\"items\":[\"基金操盘线是一款可以识别趋势的指标。\",\"操作原则:B点买入,S点卖出;红色持有标的,蓝色抛出标的。\",\"趋势分为上涨趋势和下跌趋势,由于是趋势指标所以只在趋势明朗后才会出现B点买入信号或者S点卖出信号,一般不会在拐点位置出现信号,因此B点不是最早的买入点,S点是最后的卖出点。\"]}],\"name\":\"FUNDCPX\",\"nameCode\":\"10002700\"}"
+}
\ No newline at end of file
diff --git a/src/main/resources/conf/extra/indexDetail/nonParams/10012100.json b/src/main/resources/conf/extra/indexDetail/nonParams/10012100.json
index 4abf7e0..8e85bd5 100644
--- a/src/main/resources/conf/extra/indexDetail/nonParams/10012100.json
+++ b/src/main/resources/conf/extra/indexDetail/nonParams/10012100.json
@@ -1 +1,11 @@
-{"id":null,"name":"中期线","nameCode":"10012100","data":[{"title":"中期线:","items":["代表标的的中期趋势:","曲线向上走时,代表中期趋势上涨;","曲线向下走时,代表中期趋势下跌。"],"image":null}],"original":"{\"name\":\"中期线\",\"nameCode\":\"10012100\",\"data\":[{\"title\":\"中期线:\",\"items\":[\"代表标的的中期趋势:\",\"曲线向上走时,代表中期趋势上涨;\",\"曲线向下走时,代表中期趋势下跌。\"]}]}","indexCode":"10012100","indexName":"中期线","details":[{"content":"中期线:","type":"TITLE"},{"content":"代表标的的中期趋势:","type":"TEXT"},{"content":"曲线向上走时,代表中期趋势上涨;","type":"TEXT"},{"content":"曲线向下走时,代表中期趋势下跌。","type":"TEXT"}]}
\ No newline at end of file
+{
+ "id" : null,
+ "name" : "中期线",
+ "nameCode" : "10012100",
+ "data" : [ {
+ "title" : "中期线:",
+ "items" : [ "代表标的的中期趋势:", "曲线向上走时,代表中期趋势上涨;", "曲线向下走时,代表中期趋势下跌。" ],
+ "image" : null
+ } ],
+ "original" : "{\"name\":\"中期线\",\"nameCode\":\"10012100\",\"data\":[{\"title\":\"中期线:\",\"items\":[\"代表标的的中期趋势:\",\"曲线向上走时,代表中期趋势上涨;\",\"曲线向下走时,代表中期趋势下跌。\"]}]}"
+}
\ No newline at end of file
diff --git a/src/main/resources/conf/extra/indexDetail/nonParams/10013500.json b/src/main/resources/conf/extra/indexDetail/nonParams/10013500.json
index 4016a69..d412e3e 100644
--- a/src/main/resources/conf/extra/indexDetail/nonParams/10013500.json
+++ b/src/main/resources/conf/extra/indexDetail/nonParams/10013500.json
@@ -6,5 +6,6 @@
"title" : "量王精选(投教):",
"items" : [ "红色实心柱子是量王,黄色实心柱子是天量;量王出现说明近阶段的量能达到了一定的程度,呈现交易相对活跃状态。具体用法参考量王精选相关教程。" ],
"image" : null
- } ]
+ } ],
+ "original" : "{\"data\":[{\"title\":\"量王精选(投教):\",\"items\":[\"红色实心柱子是量王,黄色实心柱子是天量;量王出现说明近阶段的量能达到了一定的程度,呈现交易相对活跃状态。具体用法参考量王精选相关教程。\"]}],\"name\":\"量王精选(投教)\",\"nameCode\":\"10013500\"}"
}
\ No newline at end of file
diff --git a/src/main/resources/conf/extra/indexDetail/params/10010600.json b/src/main/resources/conf/extra/indexDetail/params/10010600.json
index 3e18939..206430e 100644
--- a/src/main/resources/conf/extra/indexDetail/params/10010600.json
+++ b/src/main/resources/conf/extra/indexDetail/params/10010600.json
@@ -2,5 +2,6 @@
"id" : "52",
"name" : "ATR",
"code" : "10010600",
- "descriptions" : [ "算法:今日振幅、今日最高与昨收差价、今日最低与昨收差价中的最大值,为真实波幅,求真实波幅的N日移动平均", "参数:N为天数,一般取14" ]
+ "descriptions" : [ "算法:今日振幅、今日最高与昨收差价、今日最低与昨收差价中的最大值,为真实波幅,求真实波幅的N日移动平均", "参数:N为天数,一般取14" ],
+ "original" : "{\"id\":52,\"code\":\"10010600\",\"name\":\"ATR\",\"descriptions\":[\"算法:今日振幅、今日最高与昨收差价、今日最低与昨收差价中的最大值,为真实波幅,求真实波幅的N日移动平均\",\"参数:N为天数,一般取14\"]}"
}
\ No newline at end of file
diff --git a/src/main/resources/conf/system/androidSdkLevel.fallback.json b/src/main/resources/conf/system/androidSdkLevel.fallback.json
index ff2dfb3..f6f606f 100644
--- a/src/main/resources/conf/system/androidSdkLevel.fallback.json
+++ b/src/main/resources/conf/system/androidSdkLevel.fallback.json
@@ -33,6 +33,7 @@
"12": 31,
"12L": 32,
"13": 33,
- "14": 34
+ "14": 34,
+ "15": 35
}
}
\ No newline at end of file
diff --git a/src/main/resources/conf/system/androidSdkLevel.json b/src/main/resources/conf/system/androidSdkLevel.json
index 35d6b44..f46fdb2 100644
--- a/src/main/resources/conf/system/androidSdkLevel.json
+++ b/src/main/resources/conf/system/androidSdkLevel.json
@@ -1,38 +1,39 @@
{
"androidVerToSdk" : {
- "4.4W" : 20,
- "4.0.1" : 15,
- "12L" : 32,
- "10" : 29,
- "11" : 30,
- "2.0.1" : 6,
- "12" : 31,
- "13" : 33,
- "14" : 34,
- "2.3.3" : 10,
"1.0" : 1,
"1.1" : 2,
- "2.0" : 5,
- "2.1" : 7,
- "3.0" : 11,
- "2.2" : 8,
- "3.1" : 12,
- "4.0" : 14,
- "2.3" : 9,
- "3.2" : 13,
- "4.1" : 16,
- "5.0" : 21,
"1.5" : 3,
+ "1.6" : 4,
+ "2.0" : 5,
+ "2.0.1" : 6,
+ "2.1" : 7,
+ "2.2" : 8,
+ "2.3" : 9,
+ "2.3.3" : 10,
+ "3.0" : 11,
+ "3.1" : 12,
+ "3.2" : 13,
+ "4.0" : 14,
+ "4.0.1" : 15,
+ "4.1" : 16,
"4.2" : 17,
+ "4.3" : 18,
+ "4.4" : 19,
+ "4.4W" : 20,
+ "5.0" : 21,
"5.1" : 22,
"6.0" : 23,
- "1.6" : 4,
- "4.3" : 18,
"7.0" : 24,
- "9" : 28,
- "4.4" : 19,
"7.1" : 25,
"8.0" : 26,
- "8.1" : 27
+ "8.1" : 27,
+ "9" : 28,
+ "10" : 29,
+ "11" : 30,
+ "12" : 31,
+ "12L" : 32,
+ "13" : 33,
+ "14" : 34,
+ "15" : 35
}
}
\ No newline at end of file
diff --git a/src/main/resources/conf/system/emoneyLoginForm.json b/src/main/resources/conf/system/emoneyLoginForm.json
deleted file mode 100644
index 5082c14..0000000
--- a/src/main/resources/conf/system/emoneyLoginForm.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "emoneyLoginFormDataList" : null,
- "selectedId" : "57de6ca2423e0f64d8626477e1f8a46b",
- "isRandom" : false
-}
\ No newline at end of file
diff --git a/src/main/resources/conf/system/emoneyRequest.json b/src/main/resources/conf/system/emoneyRequest.json
deleted file mode 100644
index 148444b..0000000
--- a/src/main/resources/conf/system/emoneyRequest.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "isAnonymous" : true,
- "username" : "",
- "password" : "",
- "authorization" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkpXVCJ9.eyJ1dWQiOjEwMTUyNzk3MzEsInVpZCI6MjkyNTE0NDEsImRpZCI6IjM5N2RmZjEwOWEwOWFmOGY2NGJhNWMzYmYxNmE0ODA2IiwidHlwIjo0LCJhY2MiOiIzOTdkZmYxMDlhMDlhZjhmNjRiYTVjM2JmMTZhNDgwNiIsInN3dCI6MSwibGd0IjoxNzU4MjY0NjYwNzU0LCJuYmYiOjE3NTgyNjQ2NjAsImV4cCI6MTc1OTk5MjY2MCwiaWF0IjoxNzU4MjY0NjYwfQ.Y1aU7PlyuhGauY9aJCgkdqYC5gqcS4SioiHlPX2sSNc",
- "uid" : 29251441,
- "androidId" : "2aa9eb6eea32a4c3",
- "androidVersion" : "13",
- "androidSdkLevel" : "33",
- "softwareType" : "Mobile",
- "okHttpUserAgent" : "okhttp/3.12.2",
- "deviceName" : "2112123AG",
- "fingerprint" : "Xiaomi/psyche_global/psyche:13/RKQ1.211001.001/V14.0.6.0.TLDTWXM:user/release-keys",
- "buildId" : "RKQ1.211001.001",
- "chromeVersion" : "87.0.4280.141",
- "emoneyVersion" : "5.8.1",
- "emappViewMode" : "1"
-}
\ No newline at end of file
diff --git a/src/main/resources/conf/system/platform.json b/src/main/resources/conf/system/platform.json
index 4df3bb7..af394c5 100644
--- a/src/main/resources/conf/system/platform.json
+++ b/src/main/resources/conf/system/platform.json
@@ -2,5 +2,6 @@
"username" : "admin",
"password" : "81667f60a8c11d4c8e9d2e0670ff24667e6c72d49b0b15562525bcbd",
"email" : "huocaizhu@gmail.com",
+ "apiToken" : "vgb2IHmax9Mjji4R",
"isInited" : true
}
\ No newline at end of file
diff --git a/src/main/resources/database.db b/src/main/resources/database.db
index aa37305..90418f3 100644
Binary files a/src/main/resources/database.db and b/src/main/resources/database.db differ
diff --git a/src/main/resources/static/admin/v1/static/js/helper.js b/src/main/resources/static/admin/v1/static/js/helper.js
index b68453c..4a91b50 100644
--- a/src/main/resources/static/admin/v1/static/js/helper.js
+++ b/src/main/resources/static/admin/v1/static/js/helper.js
@@ -1,6 +1,6 @@
if (!window.Helper) { window.Helper = {} }
window.Helper = {
- emoneyPeriodToName: x => x < 10000 && `${x} 分钟线` || [null, '日线', '周线', '月线', '季线', '半年线', '年线'][x/10000],
+ emoneyPeriodToName: x => x < 10000 && `${x} 分钟线` || [null, '日线', '周线', '月线', '季线', '半年线', '年线'][x / 10000],
allEmoneyPeriods: [1, 5, 15, 30, 60, 120, 10000, 20000, 30000, 40000, 50000, 60000],
showIndexDetailLayer: async function(obj, forceRefresh) {
// obj: {indexCode: _, indexName: _}
@@ -56,12 +56,12 @@ window.Helper = {
}
layer.close(load);
},
- trimChars: function (str, chars) {
- const escaped = chars.split('').map(c => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('');
- const pattern = new RegExp(`^[${escaped}]+|[${escaped}]+$`, 'g');
- return str.replace(pattern, '');
+ trimChars: function(str, chars) {
+ const escaped = chars.split('').map(c => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('');
+ const pattern = new RegExp(`^[${escaped}]+|[${escaped}]+$`, 'g');
+ return str.replace(pattern, '');
},
- setLayerMainBtn: function (layero, index) {
+ setLayerMainBtn: function(layero, index) {
var btns = layero.find('.layui-layer-btn>*'), j = 1;
if (index < 0) index = btns.length + index;
for (let i = 0; i < btns.length; i++) {
@@ -73,7 +73,7 @@ window.Helper = {
btn.setAttribute('class', filtered.join(' '));
}
},
- openR: function (option) {
+ openR: function(option) {
const defaultOption = {
type: 1, area: '500px',
skin: 'layui-anim layui-anim-rl layui-layer-adminRight',
@@ -84,11 +84,11 @@ window.Helper = {
},
/**
* 按照通用配置来渲染表格
- * option: 和 table.render 选项基本一致, 但需要额外提供:
- * idName: 该表格行对象 id 的名称
- * baseUrl: 当前基本 URL, 将在此基础上拼接 list/updateBool/batchOp 等内容
- * batchOpEnum: 批量操作枚举名,若不提供则不渲染批量操作控件和回调逻辑,若为 true 则仅渲染批量删除逻辑
- * 除此以外,cols 内列如果需要开关选项的, 在相应 col 内添加 switchTemplet: true
+ * @param option 和 table.render 选项基本一致, 但需要额外提供:
+ * @param option.idName 该表格行对象 id 的名称
+ * @param option.baseUrl 当前基本 URL, 将在此基础上拼接 list/updateBool/batchOp 等内容
+ * @param option.batchOpEnum 批量操作枚举名,若不提供则不渲染批量操作控件和回调逻辑,若为 true 则仅渲染批量删除逻辑
+ * 除此以外,cols 内列如果需要开关选项的, 在相应 col 内添加 switchTemplet: true
*/
renderTable: (option) => {
const defaultOption = {
@@ -99,29 +99,48 @@ window.Helper = {
if (!option.baseUrl) throw new Error('baseUrl 不允许为空');
if (!option.baseUrl.endsWith('/')) option.baseUrl += '/';
option.url = option.baseUrl + 'list';
- let tableSwitchTemplet = function () {
- // 以 elem 选择器 + '.' + switchFilter 作为 filter
- const filter = `${option.elem}.switchFilter`;
- layui.form.on(`switch(${filter})`, function (obj) {
- console.log(obj, obj.elem.checked);
- const data = {
- field: obj.elem.dataset.field,
- value: obj.elem.checked,
- id: obj.elem.dataset.id
- };
- $.ajax({
- url: option.baseUrl + 'updateBool', method: 'POST',
- data:data,
- success: () => Dog.success({time: 1000}),
- error: function (res) {
- Dog.error({msg: res})
- // 恢复 enabled 状态
- obj.elem.checked = !obj.elem.checked;
- layui.form.render('checkbox')
- return
- }
- })
- });
+ let tableSwitchTemplet = function() {
+ // 以 elem 选择器 + '.' + switchFilter 作为 filter
+ const filter = `${option.elem}.switchFilter`;
+ layui.form.on(`switch(${filter})`, function(obj) {
+ console.log(obj, obj.elem.checked);
+ const data = {
+ field: obj.elem.dataset.field,
+ value: obj.elem.checked,
+ id: obj.elem.dataset.id
+ };
+ $.ajax({
+ url: option.baseUrl + 'updateBool', method: 'POST',
+ data: data,
+ success: () => {
+ Dog.success({ time: 1000 });
+ let colOption = option.cols[0].find(x => x.field === obj.elem.dataset.field);
+ let refresh = colOption.refresh, mutex = colOption.mutex;
+ let tableId = obj.elem.closest('[lay-table-id]').getAttribute('lay-table-id');
+ let tableFilter = document.getElementById(tableId).getAttribute('lay-filter');
+ if (refresh) {
+ Dog.reloadTable(tableFilter);
+ }
+ else if (mutex) {
+ // 互斥, 把当前表格内当前列的所有开关都设为与当前状态相反的状态
+ // 先找到所有数据
+ layui.table.getData(tableFilter).forEach((v, i) => {
+ if (v[option.idName] != obj.elem.dataset.id) {
+ // 非我行类,全部置反
+ $(`[lay-table-id="${tableFilter}"] tr[data-index="${i}"] [data-field="${obj.elem.dataset.field}"][lay-filter="${filter}"]`).removeAttr('checked')
+ }
+ })
+ }
+ },
+ error: function(res) {
+ Dog.error({ msg: res })
+ // 恢复 enabled 状态
+ obj.elem.checked = !obj.elem.checked;
+ layui.form.render('checkbox')
+ return
+ }
+ })
+ });
return d => {
var fieldName = d.LAY_COL.field;
return ` {
const buttonEl = $(`[lay-filter="${submitButtonFilter}"]`);
if (!buttonEl.length) {
- Dog.error({msg: `找不到对应 filter 为 ${submitButtonFilter} 的 button`});
+ Dog.error({ msg: `找不到对应 filter 为 ${submitButtonFilter} 的 button` });
return
}
const form = $(buttonEl.parents('.layui-form')[0] || buttonEl.parents('form')[0]);
if (!form.length) {
- Dog.error({msg: `找不到对应 filter 为 ${submitButtonFilter} 的 button 对应的表单`});
+ Dog.error({ msg: `找不到对应 filter 为 ${submitButtonFilter} 的 button 对应的表单` });
return
}
// 获取 form 内所有表单
const els = form.find('input[name], select[name], textarea[name], button[name]');
- let obj = {form: form[0], field: {}};
+ let obj = { form: form[0], field: {} };
$.each(els, (i, el) => {
const name = el.name;
if (!name) return true
@@ -193,12 +212,15 @@ window.Helper = {
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(obj.field),
- success: function (r) {
- Dog.success({onClose: () => {
- if (window.editLayer) layui.layer.close(window.editLayer);
- Dog.reloadTable()}})
+ success: function(r) {
+ Dog.success({
+ onClose: () => {
+ if (window.editLayer) layui.layer.close(window.editLayer);
+ Dog.reloadTable()
+ }
+ })
},
- error: res => Dog.error({msg: res}),
+ error: res => Dog.error({ msg: res }),
});
}
})
@@ -213,20 +235,23 @@ window.Helper = {
const type = fieldEl.type;
switch (type) {
case 'checkbox':
- const checked = fieldEl.value = fieldEl.chceked = val == 'true' || val == true;
+ const checked = fieldEl.value = val == 'true' || val == true;
const laySkin = fieldEl.getAttribute('lay-skin');
+ if (checked) fieldEl.setAttribute('checked', '');
+ else fieldEl.removeAttribute('checked');
if (laySkin) {
- switchFuncs[key] = function (obj) {
- obj.elem.value = obj.elem.checked;
- layui.form.render();
- }
- layui.form.on(`switch(${key})`, function (obj) {
- switchFuncs[obj.elem.name](obj);
- })
- layui.event.call(this, 'form', `switch(${key})`, {
- elem: fieldEl,
- value: checked
- });
+ switchFuncs[key] = function(obj) {
+ obj.elem.value = obj.elem.checked;
+ layui.form.render();
+ }
+ layui.form.on(`switch(${key})`, function(obj) {
+ switchFuncs[obj.elem.name](obj);
+ });
+ layui.form.render();
+ layui.event.call(this, 'form', `switch(${key})`, {
+ elem: fieldEl,
+ value: checked
+ });
}
break;
case 'radio':
@@ -252,19 +277,81 @@ window.Helper = {
switchFuncs = $.extend(switchFuncs, extraSwitchFuncs)
}
},
- tableSwitchTemplet: idName => {
- layui.form.on('switch(switchFilter)', function (obj) {
- console.log(obj, obj.elem.checked);
- $.ajax({
-
+ /**
+ * @param option.elem dropdown 选择器
+ * @param option.tableFilter 数据表格 filter,用以确定操作对哪个表格的数据生效
+ * @param option.idName 数据表格 idName
+ */
+ renderDropdown: option => {
+ const defaultOption = {
+ };
+ option = $.extend(defaultOption, option);
+ if (!option.elem) throw new Error('elem 选择器不允许为空');
+ if (!option.idName) throw new Error('idName 不允许为空');
+ if (!option.tableFilter) throw new Error('数据表格 filter 不允许为空');
+ if (!option.url) throw new Error('url 不允许为空');
+ let click = (data, othis) => {
+ var checked = layui.table.checkStatus(option.tableFilter), ids = [];
+ if (!checked.data.length) {
+ Dog.error('未选中任何项', { time: 1000 });
+ return;
+ }
+ $.each(checked.data, function(i, row) {
+ ids.push(row[option.idName]);
+ });
+ data = $.extend(data, { ids: ids });
+ var op = async function() {
+ $.ajax({
+ url: option.url,
+ method: 'POST',
+ data: data,
+ success: () =>
+ Dog.success({
+ msg: '批量操作成功',
+ time: 1000,
+ onClose: () => Dog.reloadTable(option.tableFilter)
+ }),
+ error: res => Dog.error({msg: res, time: 2000})
+ })
+ }
+ data.op != 'DELETE' ? op() : layer.confirm('确认批量删除吗?该操作不可恢复', function() {
+ op();
})
- })
- return d => {
- var fieldName = d.LAY_COL.field;
- return ` `;
- }
+ };
+ option.click = click;
+ layui.dropdown.render(option);
},
- randomHexString: n => [...Array(n)].map(() => 'abcdef0123456789'[~~(Math.random() * 16)]).join('')
+ /**
+ * 复制文本
+ * @param text 欲复制的文本
+ */
+ copyText: text => {
+ navigator.clipboard.writeText(text).then(() => {
+ Dog.success({ msg: '复制成功' })
+ }).catch(err => {
+ console.error(err);
+ Dog.error({ msg: '复制失败' })
+ });
+ },
+ randomHexString: n => [...Array(n)].map(() => 'abcdef0123456789'[~~(Math.random() * 16)]).join(''),
+ randomWordString: n => [...Array(n)].map(() => 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'[~~(Math.random() * 62)]).join(''),
+ /**
+ * 监听所有的 input change 事件,包括未来生成的,通过脚本设置值的。只需要运行一次
+ */
+ listenAllInputChange: function() {
+ if (window.Helper && window.Helper.__listenAllInputChangeFlag) return;
+ window.Helper.__listenAllInputChangeFlag = !0;
+ const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
+ Object.defineProperty(HTMLInputElement.prototype, 'value', {
+ get: descriptor.get,
+ set: function(val) {
+ const old = descriptor.get.call(this);
+ descriptor.set.call(this, val);
+
+ if (val !== old) {
+ this.dispatchEvent(new Event('input', { bubbles: true }));
+ }
+ }
+ });
+ }
}
\ No newline at end of file
diff --git a/src/main/resources/webpage/admin/v1/include.html b/src/main/resources/webpage/admin/v1/include.html
index ece2c68..3f8e813 100644
--- a/src/main/resources/webpage/admin/v1/include.html
+++ b/src/main/resources/webpage/admin/v1/include.html
@@ -53,29 +53,29 @@
class="fa-fw fa-solid fa-screwdriver-wrench"> 管理
-
- 计划任务
-
-
请求配置
-
- 指标配置
-
-
代理配置
+
+ 计划任务
+
+
+
+ 指标信息
+
+
Protocol 配置
+ class="fa-fw fa-regular fa-handshake"> 协议信息
@@ -88,16 +88,6 @@
class="fa-fw fa-solid fa-gears"> 设置
-
- 请求头设置
-
-
-
- 代理设置
-
-
IP 属地: 加载中... [[${@proxyConfig.ipInfo.geoString}]]
-
+ th:if="${ipInfo != null}"
+ class="layui-badge layui-bg-cyan">[[${ipInfo.geoString}]]
+
+
- [[${@proxyConfig.ipInfo.ip}]]
+ [[${ipInfo.ip}]]
-
- [[${@proxyConfig.ipInfo.ipv6}]]
+
+ [[${ipInfo.ipv6}]]
-
+
+
+
+
+
+
+
diff --git a/src/main/resources/webpage/admin/v1/manage/proxySetting/include.html b/src/main/resources/webpage/admin/v1/manage/proxySetting/include.html
index 7dd5093..2fed639 100644
--- a/src/main/resources/webpage/admin/v1/manage/proxySetting/include.html
+++ b/src/main/resources/webpage/admin/v1/manage/proxySetting/include.html
@@ -11,6 +11,13 @@
+