First Commit
This commit is contained in:
17
src/main/java/quant/rich/emoney/EmoneyAutoApplication.java
Normal file
17
src/main/java/quant/rich/emoney/EmoneyAutoApplication.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package quant.rich.emoney;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
@SpringBootApplication
|
||||
public class EmoneyAutoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(EmoneyAutoApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
264
src/main/java/quant/rich/emoney/client/EmoneyClient.java
Normal file
264
src/main/java/quant/rich/emoney/client/EmoneyClient.java
Normal file
@@ -0,0 +1,264 @@
|
||||
package quant.rich.emoney.client;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.Proxy.Type;
|
||||
import java.net.SocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
|
||||
import com.google.protobuf.nano.MessageNano;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import nano.BaseResponse;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttp;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import quant.rich.emoney.entity.config.EmoneyRequestConfig;
|
||||
import quant.rich.emoney.entity.config.IndexInfoConfig;
|
||||
import quant.rich.emoney.exception.EmoneyDecodeException;
|
||||
import quant.rich.emoney.exception.EmoneyIllegalRequestParamException;
|
||||
import quant.rich.emoney.exception.EmoneyRequestException;
|
||||
import quant.rich.emoney.exception.EmoneyResponseException;
|
||||
import quant.rich.emoney.patch.okhttp.PatchOkHttp;
|
||||
import quant.rich.emoney.patch.okhttp.PatchOkHttpRule;
|
||||
import quant.rich.emoney.util.EncryptUtils;
|
||||
import quant.rich.emoney.util.SpringContextHolder;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
@Data
|
||||
@Slf4j
|
||||
@Accessors(chain = true)
|
||||
public class EmoneyClient implements Cloneable {
|
||||
|
||||
private static final String MBS_URL = "https://mbs.emoney.cn/";
|
||||
private static final String LOGIN_URL = "https://emapp.emoney.cn/user/auth/login";
|
||||
// private static final String LOGIN_URL = "http://localhost:7790/user/auth/login";
|
||||
private static final String LOGIN_X_PROTOCOL_ID = "user%2Fauth%2Flogin";
|
||||
|
||||
private static volatile EmoneyRequestConfig emoneyRequestConfig;
|
||||
|
||||
private static EmoneyRequestConfig getEmoneyRequestConfig() {
|
||||
if (emoneyRequestConfig == null) {
|
||||
synchronized (EmoneyClient.class) {
|
||||
if (emoneyRequestConfig == null) {
|
||||
emoneyRequestConfig = SpringContextHolder.getBean(EmoneyRequestConfig.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
return emoneyRequestConfig;
|
||||
}
|
||||
|
||||
private EmoneyClient() {}
|
||||
|
||||
/**
|
||||
* 根据系统配置自动选择登录方式
|
||||
* @return
|
||||
* @see EmoneyRequestConfig
|
||||
*/
|
||||
public static Boolean loginWithManaged() {
|
||||
if (getEmoneyRequestConfig().getIsAnonymous()) {
|
||||
return loginWithAnonymous();
|
||||
}
|
||||
else {
|
||||
return loginWithUsernamePassword();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 使用系统管理的用户名密码登录
|
||||
* @return
|
||||
*/
|
||||
public static Boolean loginWithUsernamePassword() {
|
||||
ObjectNode formObject = getEmoneyRequestConfig().getUsernamePasswordLoginObject();
|
||||
return login(formObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用给定的用户名密码登录
|
||||
* @param username 用户名
|
||||
* @param password 密码,可以是明文,也可是密文
|
||||
* @return
|
||||
*/
|
||||
public static Boolean loginWithUsernamePassword(String username, String password) {
|
||||
ObjectNode formObject = getEmoneyRequestConfig().getUsernamePasswordLoginObject(username, password);
|
||||
return login(formObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* 匿名登录
|
||||
* @return
|
||||
*/
|
||||
public static Boolean loginWithAnonymous() {
|
||||
ObjectNode formObject = getEmoneyRequestConfig().getAnonymousLoginObject();
|
||||
return login(formObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录总方法,唯一要控制的只有 formObject
|
||||
* @param formObject
|
||||
* @return
|
||||
*/
|
||||
private static Boolean login(ObjectNode formObject) {
|
||||
try {
|
||||
//OkHttpClient okHttpClient = new OkHttpClient();
|
||||
|
||||
OkHttpClient okHttpClient =
|
||||
IndexInfoConfig.getInstance().newBuilder()
|
||||
.proxy(new Proxy(
|
||||
Type.HTTP,
|
||||
new InetSocketAddress("127.0.0.1", 8888)
|
||||
)).build();
|
||||
MediaType type = MediaType.parse("application/json");
|
||||
//type.charset(StandardCharsets.UTF_8);
|
||||
byte[] content = formObject.toString().getBytes("utf-8");
|
||||
RequestBody body = RequestBody.create(
|
||||
content, type);
|
||||
|
||||
Request.Builder requestBuilder = new Request.Builder()
|
||||
.url(LOGIN_URL)
|
||||
.post(body)
|
||||
// 这玩意可能也有顺序
|
||||
// 按照 Fiddler HexView 顺序如下:
|
||||
// X-Protocol-Id > X-Request-Id > EM-Sign > Authorization >
|
||||
// X-Android-Agent > Emapp-ViewMode > Content-Type > Content-Length >
|
||||
// Host > Connection: "Keep-Alive" > Accept-Encoding: "gzip" > User-Agent
|
||||
.header("X-Protocol-Id", LOGIN_X_PROTOCOL_ID)
|
||||
.header("X-Request-Id", "null")
|
||||
.header("EM-Sign", EncryptUtils.getEMSign(content, "POST", LOGIN_X_PROTOCOL_ID))
|
||||
.header("Authorization", "")
|
||||
.header("X-Android-Agent", getEmoneyRequestConfig().getXAndroidAgent())
|
||||
.header("Emapp-ViewMode", getEmoneyRequestConfig().getEmappViewMode());
|
||||
//.header("User-Agent", getEmoneyRequestConfig().getOkHttpUserAgent())
|
||||
|
||||
Request request = requestBuilder.build();
|
||||
|
||||
final Call call = okHttpClient.newCall(request);
|
||||
Response response = call.execute();
|
||||
ObjectNode loginResult = (ObjectNode) new ObjectMapper().readTree(response.body().string());
|
||||
|
||||
Integer code = loginResult.get("result").get("code").asInt();
|
||||
|
||||
if (code == 0) {
|
||||
getEmoneyRequestConfig().setAuthorization(
|
||||
loginResult.get("detail").get("token").asText())
|
||||
.updateAuthorizationTime().saveOrUpdate();
|
||||
log.info("执行 emoney LOGIN 成功");
|
||||
return true;
|
||||
} else {
|
||||
String msg = loginResult.get("result").get("msg").asText();
|
||||
throw new EmoneyResponseException("执行 emoney LOGIN 请求返回错误,code: " + code + ", msg: " + msg, code);
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new EmoneyDecodeException("试图将返回数据解析成 JSON 时失败", e);
|
||||
} catch (Exception e) {
|
||||
throw new EmoneyRequestException("执行 emoney LOGIN 请求/返回时出现错误", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取基本返回 Base_Response
|
||||
*
|
||||
* @param <T>
|
||||
* @param nanoRequest
|
||||
* @return
|
||||
*/
|
||||
public static <T extends MessageNano> BaseResponse.Base_Response post(
|
||||
T nanoRequest,
|
||||
Serializable xProtocolId,
|
||||
Serializable xRequestId) {
|
||||
|
||||
if (xProtocolId == null) {
|
||||
throw new EmoneyIllegalRequestParamException("执行 emoney 请求错误,Protocol id 不能为 null!",
|
||||
new IllegalArgumentException());
|
||||
}
|
||||
if (StringUtils.isBlank(getEmoneyRequestConfig().getAuthorization())) {
|
||||
throw new EmoneyIllegalRequestParamException("执行 emoney 请求错误,Authorization 为空,是否未登录?",
|
||||
new IllegalArgumentException());
|
||||
}
|
||||
|
||||
try {
|
||||
OkHttpClient okHttpClient = new OkHttpClient();
|
||||
|
||||
byte[] content = MessageNano.toByteArray(nanoRequest);
|
||||
RequestBody body = RequestBody.create(
|
||||
content,
|
||||
MediaType.parse("application/x-protobuf-v3"));
|
||||
|
||||
Request.Builder requestBuilder = new Request.Builder()
|
||||
.url(MBS_URL)
|
||||
.post(body)
|
||||
// 这玩意可能也有顺序
|
||||
// 按照 Fiddler HexView 顺序如下:
|
||||
// X-Protocol-Id > X-Request-Id > EM-Sign > Authorization > X-Android-Agent > Emapp-ViewMode > Content-Type > Content-Length > Host ...
|
||||
.header("X-Protocol-Id", xProtocolId.toString())
|
||||
.header("X-Request-Id", xRequestId == null ? "null" : xRequestId.toString())
|
||||
.header("EM-Sign", EncryptUtils.getEMSign(content, "POST", xProtocolId.toString()))
|
||||
.header("Authorization", getEmoneyRequestConfig().getAuthorization())
|
||||
.header("X-Android-Agent", getEmoneyRequestConfig().getXAndroidAgent())
|
||||
.header("Emapp-ViewMode", getEmoneyRequestConfig().getEmappViewMode())
|
||||
;
|
||||
|
||||
Request request = requestBuilder.build();
|
||||
|
||||
final Call call = okHttpClient.newCall(request);
|
||||
Response response = call.execute();
|
||||
|
||||
BaseResponse.Base_Response baseResponse = BaseResponse.Base_Response.parseFrom(response.body().bytes());
|
||||
return baseResponse;
|
||||
} catch (InvalidProtocolBufferNanoException e) {
|
||||
throw new EmoneyDecodeException("试图将返回数据解析成 Base_Response 时失败", e);
|
||||
} catch (Exception e) {
|
||||
throw new EmoneyRequestException("执行 emoney 请求/返回时出现错误", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定 clazz 获取返回
|
||||
*
|
||||
* @param <T>
|
||||
* @param <U>
|
||||
* @param nanoRequest
|
||||
* @param clazz
|
||||
* @return
|
||||
*/
|
||||
public static <T extends MessageNano, U extends MessageNano> U post(T nanoRequest, Class<U> clazz, Serializable xProtocolId, Serializable xRequestId) {
|
||||
|
||||
BaseResponse.Base_Response baseResponse;
|
||||
try {
|
||||
baseResponse = post(nanoRequest, xProtocolId, xRequestId);
|
||||
if (baseResponse.result.getCode() == 0) {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
U nanoResponse = (U) MessageNano.mergeFrom(
|
||||
(MessageNano) clazz.getDeclaredConstructor().newInstance(), baseResponse.detail.getValue());
|
||||
log.debug("执行 emoney 请求成功");
|
||||
return nanoResponse;
|
||||
} catch (Exception e) {
|
||||
throw new EmoneyDecodeException("试图将返回数据解析成 " + clazz.getSimpleName() + " 时失败", e);
|
||||
}
|
||||
} else {
|
||||
Integer code = baseResponse.result.getCode();
|
||||
String msg = baseResponse.result.getMsg();
|
||||
throw new EmoneyResponseException("执行 emoney 请求返回错误,code: " + code + ", msg: " + msg, code);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new EmoneyRequestException("执行 emoney " + nanoRequest.getClass().getSimpleName() + " 请求/返回时出现错误", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package quant.rich.emoney.component;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.exception.PageNotFoundException;
|
||||
import quant.rich.emoney.exception.RException;
|
||||
import quant.rich.emoney.pojo.dto.LayPageResp;
|
||||
import quant.rich.emoney.pojo.dto.R;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.security.auth.login.LoginException;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
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.validation.BindException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/**
|
||||
* 异常处理
|
||||
*/
|
||||
@ControllerAdvice
|
||||
@Slf4j
|
||||
public class EmoneyAutoPlatformExceptionHandler {
|
||||
|
||||
@Autowired
|
||||
HttpServletResponse response;
|
||||
|
||||
@Autowired
|
||||
HttpServletRequest request;
|
||||
|
||||
@ExceptionHandler({
|
||||
BindException.class,
|
||||
ConstraintViolationException.class,
|
||||
MethodArgumentNotValidException.class })
|
||||
@ResponseBody
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <Ex extends BindException> R<?> handleBindException(Ex ex) {
|
||||
StringBuilder messageSb = new StringBuilder();
|
||||
ex.getBindingResult().getAllErrors().forEach(error -> messageSb.append(error.getDefaultMessage()).append("\n"));
|
||||
log.warn("Resolved Exception {}", messageSb.substring(0, messageSb.length() - 1));
|
||||
log.warn(httpServletRequestToString(request));
|
||||
return bodyOrPage(HttpStatus.BAD_REQUEST, messageSb.substring(0, messageSb.length() - 1));
|
||||
}
|
||||
|
||||
@ExceptionHandler(LoginException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
|
||||
public R<?> handleLoginException(LoginException ex) {
|
||||
return bodyOrPage(HttpStatus.NOT_ACCEPTABLE, ex);
|
||||
}
|
||||
|
||||
@ExceptionHandler(RException.class)
|
||||
@ResponseBody
|
||||
public R<?> handleRException(RException ex) {
|
||||
response.setStatus(ex.getHttpStatus().value());
|
||||
if (ex.getLogRequest()) {
|
||||
log.warn(httpServletRequestToString(request));
|
||||
}
|
||||
return bodyOrPage(ex.getHttpStatus(), ex);
|
||||
}
|
||||
|
||||
@ExceptionHandler(ServletException.class)
|
||||
@ResponseBody
|
||||
public R<?> handleServletException(ServletException ex) {
|
||||
HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
if (ex instanceof HttpRequestMethodNotSupportedException)
|
||||
httpStatus = HttpStatus.METHOD_NOT_ALLOWED;
|
||||
response.setStatus(httpStatus.value());
|
||||
return bodyOrPage(httpStatus, ex);
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = Exception.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R<?> handleException(Exception ex) {
|
||||
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(httpServletRequestToString(request));
|
||||
return bodyOrPage(HttpStatus.INTERNAL_SERVER_ERROR, ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 @Autowired request 等自动判断当前请求是 text/html 还是 application/json
|
||||
* @return
|
||||
*/
|
||||
private boolean isPage() {
|
||||
boolean isPage = true;
|
||||
String accept;
|
||||
// 匹配当前 request 在控制层的方法,根据是否是
|
||||
// @ResponseBody 注解、@RestController 注解、
|
||||
// 已知的返回类型来判断返回的是否是页面
|
||||
Object handler = request.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE);
|
||||
|
||||
if (handler instanceof HandlerMethod handlerMethod) {
|
||||
Method method = handlerMethod.getMethod();
|
||||
Class<?> controllerClass = handlerMethod.getBeanType();
|
||||
|
||||
boolean hasResponseBody =
|
||||
method.isAnnotationPresent(ResponseBody.class) ||
|
||||
controllerClass.isAnnotationPresent(ResponseBody.class) ||
|
||||
controllerClass.isAnnotationPresent(RestController.class);
|
||||
|
||||
Class<?> returnType = method.getReturnType();
|
||||
boolean isDtoReturnType =
|
||||
returnType.equals(R.class) ||
|
||||
returnType.equals(LayPageResp.class) ||
|
||||
JsonNode.class.isAssignableFrom(returnType);
|
||||
|
||||
if (hasResponseBody || isDtoReturnType) {
|
||||
isPage = false;
|
||||
}
|
||||
}
|
||||
if (isPage && (accept = request.getHeader("Accept")) != null) {
|
||||
int indexOfHtml = accept.indexOf("text/html"), indexOfJson = accept.indexOf("application/json");
|
||||
if (indexOfHtml == -1 && indexOfJson != -1) {
|
||||
isPage = false;
|
||||
} else if (indexOfHtml != -1 && indexOfJson != -1) {
|
||||
isPage = indexOfHtml < indexOfJson;
|
||||
}
|
||||
}
|
||||
return isPage;
|
||||
}
|
||||
|
||||
private R<?> bodyOrPage(HttpStatus httpStatus, String message) {
|
||||
boolean isPage = isPage();
|
||||
if (isPage) {
|
||||
throw message == null ? new RuntimeException("Page exception raised") : new RuntimeException(message);
|
||||
}
|
||||
R<?> r = message != null ?
|
||||
R.status(httpStatus).setMessage(message).setData(message)
|
||||
: R.status(httpStatus);
|
||||
return r;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
if (isPage) {
|
||||
if (ex instanceof NoResourceFoundException nrfe) {
|
||||
if (StringUtils.isNotEmpty(nrfe.getMessage())
|
||||
&& nrfe.getMessage().endsWith(" .well-known/appspecific/com.chrome.devtools.json.")) {
|
||||
// 傻逼 Chrome 开发工具默认调用该地址
|
||||
return null;
|
||||
}
|
||||
}
|
||||
throw ex == null ? new RuntimeException("Page exception raised") : new RuntimeException(ex);
|
||||
}
|
||||
R<?> r = message != null ?
|
||||
R.status(httpStatus).setMessage(message).setData(message)
|
||||
: R.status(httpStatus);
|
||||
return r;
|
||||
}
|
||||
|
||||
private static String httpServletRequestToString(HttpServletRequest request) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Request Method = \"" + request.getMethod() + "\", ");
|
||||
sb.append("Request URL Path = \"" + request.getRequestURL() + "\", ");
|
||||
|
||||
String headers = Collections.list(request.getHeaderNames()).stream()
|
||||
.map(headerName -> headerName + " : " + Collections.list(request.getHeaders(headerName)))
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
if (headers.isEmpty()) {
|
||||
sb.append("Request headers: NONE,");
|
||||
} else {
|
||||
sb.append("Request headers: [" + headers + "],");
|
||||
}
|
||||
|
||||
String parameters = Collections.list(request.getParameterNames()).stream().map(p -> {
|
||||
String[] values = request.getParameterValues(p);
|
||||
return p + ": [" + StringUtils.join(values, ", ") + "]";
|
||||
}).collect(Collectors.joining(", "));
|
||||
|
||||
if (parameters.isEmpty()) {
|
||||
sb.append("Request parameters: NONE.");
|
||||
} else {
|
||||
sb.append("Request parameters: [" + parameters + "].");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package quant.rich.emoney.config;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.core.type.filter.AssignableTypeFilter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
|
||||
/**
|
||||
* 实现自动化注册 Config
|
||||
*/
|
||||
@Slf4j
|
||||
@DependsOn("configService")
|
||||
public class ConfigAutoRegistrar implements BeanDefinitionRegistryPostProcessor {
|
||||
|
||||
@Override
|
||||
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
|
||||
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
|
||||
scanner.addIncludeFilter(new AssignableTypeFilter(IConfig.class));
|
||||
scanner.addIncludeFilter(new AnnotationTypeFilter(ConfigInfo.class));
|
||||
|
||||
scanner.findCandidateComponents("quant.rich.emoney.entity.config").forEach(beanDefinition -> {
|
||||
String className = beanDefinition.getBeanClassName();
|
||||
try {
|
||||
// 确保其 field 规则与 configService 内 field 生成规则一致,即:
|
||||
// 如果 @ConfigInfo 指定了 field 的,使用该 field + "Config"
|
||||
// 作为 beanName,否则使用首字母小写的 simpleClassName 作为
|
||||
// beanName,且 simpleClassName 无论如何必须以 Config 作为结尾。
|
||||
Class<?> clazz = Class.forName(className);
|
||||
String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase()
|
||||
+ clazz.getSimpleName().substring(1);
|
||||
|
||||
if (!IConfig.class.isAssignableFrom(clazz)) {
|
||||
log.warn("Config {} does not implement IConfig, ignore", beanName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!beanName.endsWith("Config")) {
|
||||
log.warn("Config {}'s simple class name does not end with \"Config\", ignore", beanName);
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigInfo info = clazz.getAnnotation(ConfigInfo.class);
|
||||
if (info == null) {
|
||||
log.warn("Config {} does not have @ConfigInfo annotation, ignore", clazz.getName());
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isNotBlank(info.field())) {
|
||||
beanName = info.field() + "Config";
|
||||
}
|
||||
|
||||
BeanDefinitionBuilder factoryBean = BeanDefinitionBuilder
|
||||
.genericBeanDefinition(ConfigServiceFactoryBean.class)
|
||||
.addConstructorArgValue(clazz);
|
||||
// 注意此处注册 factoryBean 不意味着 FactoryBean.getObject() 方法立即被执行,
|
||||
// Spring 管理的 Bean 默认在其被使用时才创建,所以如果 getObject() 调用一些方
|
||||
// 法,这些方法会在初次使用 Bean 时才被创建。如果这些方法对于启动过程很重要,
|
||||
// 需要在对应 Config(Bean) 上加上 @Bean 和 @Lazy(false) 注解,确保一旦准备好
|
||||
// 相应的 Bean 就会被创建。
|
||||
registry.registerBeanDefinition(beanName, factoryBean.getBeanDefinition());
|
||||
log.info("Add config {} to bean register", beanName);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException("Failed to load class: " + className, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package quant.rich.emoney.config;
|
||||
|
||||
import org.springframework.beans.factory.BeanNameAware;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
import quant.rich.emoney.service.ConfigService;
|
||||
|
||||
/**
|
||||
* 实现配置项自动载入
|
||||
* @param <T>
|
||||
*/
|
||||
@Slf4j
|
||||
public class ConfigServiceFactoryBean<T extends IConfig<T>> implements FactoryBean<T>, BeanNameAware {
|
||||
|
||||
private final Class<T> targetClass;
|
||||
|
||||
private String beanName;
|
||||
|
||||
@Autowired
|
||||
private AutowireCapableBeanFactory beanFactory;
|
||||
|
||||
@Autowired
|
||||
private ConfigService configService;
|
||||
|
||||
public ConfigServiceFactoryBean(Class<T> targetClass) {
|
||||
this.targetClass = targetClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanName(String name) {
|
||||
this.beanName = name;
|
||||
}
|
||||
|
||||
@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);
|
||||
return bean;
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error("Fail to load config: " + targetClass.getName(), e);
|
||||
success = false;
|
||||
throw e;
|
||||
}
|
||||
finally {
|
||||
ConstructionGuard.exit(targetClass);
|
||||
if (success) {
|
||||
log.debug("getObject() for {} success", targetClass.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return targetClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingleton() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package quant.rich.emoney.config;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class ConstructionGuard {
|
||||
private static final ThreadLocal<Set<Class<?>>> constructing = ThreadLocal.withInitial(HashSet::new);
|
||||
|
||||
public static boolean isConstructing(Class<?> clazz) {
|
||||
return constructing.get().contains(clazz);
|
||||
}
|
||||
|
||||
public static void enter(Class<?> clazz) {
|
||||
log.debug("Enter construction for {}", clazz.toString());
|
||||
if (isConstructing(clazz)) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Class ")
|
||||
.append(clazz.getName())
|
||||
.append(" is being constructed but is seems like circular involving.");
|
||||
String msg = sb.toString();
|
||||
log.error(msg);
|
||||
throw new BeanCreationException(msg);
|
||||
}
|
||||
constructing.get().add(clazz);
|
||||
}
|
||||
|
||||
public static void exit(Class<?> clazz) {
|
||||
constructing.get().remove(clazz);
|
||||
log.debug("Exit construction for {}", clazz.toString());
|
||||
}
|
||||
}
|
||||
25
src/main/java/quant/rich/emoney/config/DataSourceConfig.java
Normal file
25
src/main/java/quant/rich/emoney/config/DataSourceConfig.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package quant.rich.emoney.config;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.jdbc.DataSourceBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class DataSourceConfig {
|
||||
|
||||
@Bean(name = "postgreDataSource")
|
||||
@ConfigurationProperties(prefix = "spring.datasource.postgre")
|
||||
public DataSource postgreDataSource() {
|
||||
return DataSourceBuilder.create().build();
|
||||
}
|
||||
|
||||
@Bean(name = "sqliteDataSource")
|
||||
@ConfigurationProperties(prefix = "spring.datasource.sqlite")
|
||||
public DataSource sqliteDataSource() {
|
||||
DataSource dataSource = DataSourceBuilder.create().build();
|
||||
return dataSource;
|
||||
}
|
||||
}
|
||||
38
src/main/java/quant/rich/emoney/config/EmoneyAutoConfig.java
Normal file
38
src/main/java/quant/rich/emoney/config/EmoneyAutoConfig.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package quant.rich.emoney.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
|
||||
import quant.rich.emoney.interceptor.BaseInterceptor;
|
||||
import quant.rich.emoney.service.ConfigService;
|
||||
|
||||
/**
|
||||
* Emoney-Auto 配置项
|
||||
*
|
||||
* @author Doghole
|
||||
*
|
||||
*/
|
||||
@Configuration
|
||||
@Import(ConfigAutoRegistrar.class)
|
||||
public class EmoneyAutoConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
BaseInterceptor baseInterceptor;
|
||||
|
||||
@Autowired
|
||||
ConfigService configService;
|
||||
|
||||
/**
|
||||
* 简单鉴权/配置项注入拦截器
|
||||
*
|
||||
* @param registry
|
||||
*/
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(baseInterceptor).addPathPatterns("/**");
|
||||
}
|
||||
}
|
||||
31
src/main/java/quant/rich/emoney/config/KaptchaConfig.java
Normal file
31
src/main/java/quant/rich/emoney/config/KaptchaConfig.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package quant.rich.emoney.config;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import com.google.code.kaptcha.impl.DefaultKaptcha;
|
||||
import com.google.code.kaptcha.util.Config;
|
||||
|
||||
@Configuration
|
||||
public class KaptchaConfig {
|
||||
|
||||
@Bean(name = "kaptchaProperties")
|
||||
@ConfigurationProperties
|
||||
Properties getKaptchaProperties() {
|
||||
return new Properties();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@DependsOn("kaptchaProperties")
|
||||
DefaultKaptcha getDefaultKaptcha(@Autowired Properties kaptchaProperties) {
|
||||
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
|
||||
Config config = new Config(kaptchaProperties);
|
||||
defaultKaptcha.setConfig(config);
|
||||
|
||||
return defaultKaptcha;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package quant.rich.emoney.config;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
|
||||
@Configuration
|
||||
@MapperScan(basePackages = "quant.rich.emoney.mapper.postgre", sqlSessionTemplateRef = "postgreSqlSessionTemplate")
|
||||
public class PostgreMybatisConfig {
|
||||
|
||||
@Bean("postgreSqlSessionFactory")
|
||||
public SqlSessionFactory postgreSqlSessionFactory(
|
||||
@Qualifier("postgreDataSource") DataSource dataSource) throws Exception {
|
||||
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
|
||||
factory.setDataSource(dataSource);
|
||||
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
|
||||
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
|
||||
factory.setPlugins(interceptor);
|
||||
|
||||
return factory.getObject();
|
||||
|
||||
}
|
||||
|
||||
@Bean("postgreSqlSessionTemplate")
|
||||
public SqlSessionTemplate sqliteSqlSessionTemplate(@Qualifier("postgreSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
|
||||
return new SqlSessionTemplate(sqlSessionFactory);
|
||||
}
|
||||
|
||||
@Bean("postgreTransactionManager")
|
||||
public DataSourceTransactionManager postgreTransacetionManager(@Qualifier("postgreDataSource") DataSource dataSource) {
|
||||
return new DataSourceTransactionManager(dataSource);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package quant.rich.emoney.config;
|
||||
|
||||
import org.reflections.Reflections;
|
||||
import org.reflections.scanners.Scanners;
|
||||
import org.reflections.util.ConfigurationBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Reflections configuration, support autowired
|
||||
*
|
||||
* @author Bakwat
|
||||
*
|
||||
*/
|
||||
@Configuration
|
||||
public class ReflectionsConfig {
|
||||
|
||||
@Bean("reflections")
|
||||
Reflections reflections() {
|
||||
return new Reflections(new ConfigurationBuilder()
|
||||
.addScanners(Scanners.MethodsAnnotated, Scanners.SubTypes, Scanners.TypesAnnotated)
|
||||
.forPackages("quant.rich.emoney"));
|
||||
}
|
||||
|
||||
}
|
||||
71
src/main/java/quant/rich/emoney/config/SecurityConfig.java
Normal file
71
src/main/java/quant/rich/emoney/config/SecurityConfig.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package quant.rich.emoney.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import quant.rich.emoney.service.AuthService;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final AuthService userDetailsService;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.headers(headers -> headers.cacheControl(cache -> cache.disable()))
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/admin/*/login").permitAll()
|
||||
.requestMatchers("/admin/*/static/**").permitAll()
|
||||
.requestMatchers("/public/**").permitAll()
|
||||
.requestMatchers("/captcha/**").permitAll()
|
||||
.requestMatchers("/api/**").permitAll()
|
||||
.requestMatchers("/img/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(form -> form // 开启表单登录,并指定登录页
|
||||
.loginPage("/admin/v1/login") // 指定登录页
|
||||
.loginProcessingUrl("/admin/v1/doLogin") // 处理登录请求的 URL
|
||||
.defaultSuccessUrl("/admin/v1/", false) // 登录成功后默认跳转
|
||||
.permitAll())
|
||||
.logout(logout -> logout
|
||||
.logoutUrl("/admin/v1/logout")
|
||||
.logoutSuccessUrl("/admin/v1/login")
|
||||
.invalidateHttpSession(true)
|
||||
.permitAll())
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||
);
|
||||
;
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(@Autowired PasswordEncoder passwordEncoder) {
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||
provider.setUserDetailsService(userDetailsService);
|
||||
provider.setPasswordEncoder(passwordEncoder);
|
||||
return new ProviderManager(provider);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return NoOpPasswordEncoder.getInstance();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package quant.rich.emoney.config;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
|
||||
@Configuration
|
||||
@MapperScan(basePackages = "quant.rich.emoney.mapper.sqlite", sqlSessionTemplateRef = "sqliteSqlSessionTemplate")
|
||||
public class SqliteMybatisConfig {
|
||||
|
||||
public static final String SQLITE_TRANSACTION_MANAGER = "sqliteTransactionManager";
|
||||
|
||||
@Bean("sqliteSqlSessionFactory")
|
||||
public SqlSessionFactory sqliteSqlSessionFactory(
|
||||
@Qualifier("sqliteDataSource") DataSource dataSource) throws Exception {
|
||||
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
|
||||
factory.setDataSource(dataSource);
|
||||
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.SQLITE));
|
||||
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
|
||||
factory.setPlugins(interceptor);
|
||||
|
||||
return factory.getObject();
|
||||
|
||||
}
|
||||
|
||||
@Bean("sqliteSqlSessionTemplate")
|
||||
public SqlSessionTemplate sqliteSqlSessionTemplate(@Qualifier("sqliteSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
|
||||
return new SqlSessionTemplate(sqlSessionFactory);
|
||||
}
|
||||
|
||||
@Bean("sqliteTransactionManager")
|
||||
public DataSourceTransactionManager postgreTransacetionManager(@Qualifier("sqliteDataSource") DataSource dataSource) {
|
||||
return new DataSourceTransactionManager(dataSource);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package quant.rich.emoney.controller;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.validation.BeanPropertyBindingResult;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.Errors;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import quant.rich.emoney.controller.common.BaseController;
|
||||
import quant.rich.emoney.exception.PageNotFoundException;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
import quant.rich.emoney.pojo.dto.R;
|
||||
import quant.rich.emoney.service.ConfigService;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/admin/v1/config")
|
||||
public class ConfigControllerV1 extends BaseController {
|
||||
|
||||
@Autowired
|
||||
Validator validator;
|
||||
|
||||
@Autowired
|
||||
ConfigService configService;
|
||||
|
||||
@GetMapping({ "/{configField}", "/{configField}/", "/{configField}/index" })
|
||||
public String configPage(@PathVariable String configField) {
|
||||
ConfigInfo info = configService.getConfigInfoByField(configField);
|
||||
if (info == null || !info.managed()) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
request.setAttribute("title", info.name());
|
||||
return "admin/v1/config/" + configField + "/index";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件重载
|
||||
* @param configField
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@GetMapping("/reload/{configField}")
|
||||
@ResponseBody
|
||||
public <T extends IConfig<T>> R<?> reload(@PathVariable String configField) {
|
||||
return R.judge(() ->
|
||||
configService.saveOrUpdate((T)configService.getOrCreateConfig(configField))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置项统一接口
|
||||
* @throws Exception
|
||||
*/
|
||||
@PostMapping({ "/{configField}", "/{configField}/", "/{configField}/index" })
|
||||
@ResponseBody
|
||||
public <T extends IConfig<T>> R<?> saveConfig(@PathVariable String configField, @Validated @RequestBody JsonNode config) throws Exception {
|
||||
|
||||
Class<T> clazz = configService.getConfigClassByField(configField);
|
||||
if (clazz == null) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
|
||||
return R.judgeThrow(() -> {
|
||||
T newConfig = (T)new ObjectMapper().treeToValue(config, clazz);
|
||||
|
||||
Method method = this.getClass().getMethod("saveConfig", String.class, JsonNode.class);
|
||||
MethodParameter methodParameter = new MethodParameter(method, 1);
|
||||
|
||||
// 执行校验
|
||||
BindingResult bindingResult = new BeanPropertyBindingResult(newConfig, clazz.getSimpleName());
|
||||
validator.validate(newConfig, bindingResult);
|
||||
|
||||
if (bindingResult.hasErrors()) {
|
||||
throw new MethodArgumentNotValidException(methodParameter, bindingResult);
|
||||
}
|
||||
|
||||
T oldConfig = configService.getConfig(clazz);
|
||||
oldConfig.mergeFrom(newConfig);
|
||||
return oldConfig.saveOrUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package quant.rich.emoney.controller;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import quant.rich.emoney.controller.common.BaseController;
|
||||
import quant.rich.emoney.entity.config.PlatformConfig;
|
||||
import quant.rich.emoney.exception.RException;
|
||||
import quant.rich.emoney.pojo.dto.R;
|
||||
import quant.rich.emoney.service.ConfigService;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/admin/v1")
|
||||
public class IndexControllerV1 extends BaseController {
|
||||
|
||||
@Autowired
|
||||
PlatformConfig platformConfig;
|
||||
|
||||
@Autowired
|
||||
ConfigService configService;
|
||||
|
||||
@GetMapping({ "", "/", "/index" })
|
||||
public String index() {
|
||||
return "admin/v1/index";
|
||||
}
|
||||
|
||||
@PostMapping("/changeUserInfo")
|
||||
@ResponseBody
|
||||
public R<?> changeUserInfo(String newUsername, String oldPassword, String newPassword) {
|
||||
if (!platformConfig.getPassword().equals(oldPassword)) {
|
||||
throw RException.badRequest("密码错误");
|
||||
}
|
||||
if (StringUtils.isAllEmpty(newUsername, newPassword)) {
|
||||
throw RException.badRequest("未更改任何信息");
|
||||
}
|
||||
if (StringUtils.isNotEmpty(newUsername)) {
|
||||
platformConfig.setUsername(newUsername);
|
||||
}
|
||||
if (StringUtils.isNotEmpty(newPassword)) {
|
||||
platformConfig.setPassword(newPassword);
|
||||
}
|
||||
configService.saveOrUpdate(platformConfig);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package quant.rich.emoney.controller;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.security.auth.login.LoginException;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import jodd.util.Base64;
|
||||
import quant.rich.emoney.controller.common.BaseController;
|
||||
import quant.rich.emoney.entity.config.PlatformConfig;
|
||||
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;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/admin/v1")
|
||||
public class LoginControllerV1 extends BaseController {
|
||||
|
||||
@Autowired
|
||||
ConfigService configService;
|
||||
|
||||
@Autowired
|
||||
PlatformConfig platformConfig;
|
||||
|
||||
@Autowired
|
||||
AuthService authService;
|
||||
|
||||
@GetMapping("/login")
|
||||
public String login() {
|
||||
|
||||
if (isLogin()) {
|
||||
return "redirect:/admin/v1/index";
|
||||
}
|
||||
|
||||
if (!platformConfig.getIsInited()) {
|
||||
return "admin/v1/init";
|
||||
}
|
||||
|
||||
return "admin/v1/login";
|
||||
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
@ResponseBody
|
||||
public R<?> login(String username, String password, String captcha, String redirect) throws LoginException {
|
||||
|
||||
// 登录流程
|
||||
if (platformConfig.getIsInited()) {
|
||||
|
||||
if (StringUtils.isBlank(captcha)) {
|
||||
throw new LoginException("验证码不能为空");
|
||||
}
|
||||
Object sessionCaptcha = session.getAttribute(AuthService.CAPTCHA);
|
||||
if (Objects.isNull(sessionCaptcha) || !captcha.equalsIgnoreCase(sessionCaptcha.toString())) {
|
||||
throw new LoginException("验证码错误");
|
||||
}
|
||||
if (StringUtils.isAnyBlank(username) || !passwordIsNotEmpty(password)) {
|
||||
throw new LoginException("用户名和密码不能为空");
|
||||
}
|
||||
if (!username.equals(platformConfig.getUsername())
|
||||
|| !password.equals(platformConfig.getPassword())) {
|
||||
session.removeAttribute(AuthService.CAPTCHA);
|
||||
throw new LoginException("用户名或密码错误");
|
||||
}
|
||||
String to = "/admin/v1";
|
||||
if (StringUtils.isNotEmpty(redirect)) {
|
||||
to = Base64.decodeToString(redirect);
|
||||
}
|
||||
|
||||
authService.setLogin(username, password);
|
||||
|
||||
session.removeAttribute(AuthService.CAPTCHA);
|
||||
return R.ok(to);
|
||||
}
|
||||
// 初始化流程
|
||||
|
||||
if (StringUtils.isAnyBlank(username) || !passwordIsNotEmpty(password)) {
|
||||
throw new LoginException("用户名和密码不能为空");
|
||||
}
|
||||
platformConfig.setUsername(username).setPassword(password).setIsInited(true);
|
||||
boolean success = configService.saveOrUpdate(platformConfig);
|
||||
if (!success) {
|
||||
throw new LoginException("无法配置用户名和密码,请检查");
|
||||
}
|
||||
|
||||
String to = Base64.decodeToString("/admin/v1/login");
|
||||
return R.ok(to);
|
||||
}
|
||||
|
||||
@GetMapping("/logout")
|
||||
public String logout() {
|
||||
return "redirect:/admin/v1/login";
|
||||
}
|
||||
|
||||
static final String EMPTY_PASSWORD = EncryptUtils.sha3("", 224);
|
||||
|
||||
static boolean passwordIsNotEmpty(String password) {
|
||||
return StringUtils.isNotEmpty(password) && !password.equalsIgnoreCase(EMPTY_PASSWORD);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package quant.rich.emoney.controller.api;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.protobuf.nano.MessageNano;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import nano.BaseResponse.Base_Response;
|
||||
import quant.rich.emoney.entity.sqlite.ProtocolMatch;
|
||||
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;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/proto")
|
||||
@Slf4j
|
||||
public class ProtoDecodeControllerV1 {
|
||||
|
||||
@Autowired
|
||||
private ProtocolMatchService protocolMatchService;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@PostMapping("/request/decode")
|
||||
public <U extends MessageNano> EmoneyConvertResult requestDecode(
|
||||
@RequestBody(required=true)
|
||||
@NonNull
|
||||
EmoneyProtobufBody body) {
|
||||
|
||||
Integer protocolId = body.getProtocolId();
|
||||
if (Objects.isNull(protocolId)) {
|
||||
throw RException.badRequest("protocolId cannot be null");
|
||||
}
|
||||
|
||||
ProtocolMatch match = protocolMatchService.getById(protocolId);
|
||||
|
||||
if (Objects.isNull(match) || StringUtils.isBlank(match.getClassName())) {
|
||||
throw RException.badRequest("暂无对应 protocolId = " + protocolId + " 的记录,可等 response decoder 搜集到后重试");
|
||||
}
|
||||
|
||||
String className = new StringBuilder()
|
||||
.append("nano.")
|
||||
.append(match.getClassName())
|
||||
.append("Request$")
|
||||
.append(match.getClassName())
|
||||
.append("_Request")
|
||||
.toString();
|
||||
// IndexInflow -> nano.IndexInflowRequest$IndexInflow_Request
|
||||
|
||||
Class<U> clazz;
|
||||
try {
|
||||
clazz = (Class<U>)Class.forName(className);
|
||||
}
|
||||
catch (Exception e) {
|
||||
String msg = new StringBuilder()
|
||||
.append("无法根据给定的 protocolId = ")
|
||||
.append(protocolId)
|
||||
.append(", className = ")
|
||||
.append(className)
|
||||
.append("找到对应类").toString();
|
||||
log.warn(msg, e);
|
||||
throw RException.internalServerError(msg);
|
||||
}
|
||||
|
||||
byte[] buf;
|
||||
try {
|
||||
buf = body.protocolBodyToByte();
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw RException.badRequest("转换 protocolBody 错误");
|
||||
}
|
||||
|
||||
try {
|
||||
U nano = (U)MessageNano.mergeFrom((MessageNano)
|
||||
clazz.getDeclaredConstructor().newInstance(), buf);
|
||||
return EmoneyConvertResult
|
||||
.ok(new ObjectMapper().valueToTree(nano))
|
||||
.setProtocolId(protocolId)
|
||||
.setSupposedClassName(className);
|
||||
}
|
||||
catch (Exception e) {
|
||||
String msg = new StringBuilder()
|
||||
.append("转换为类 ")
|
||||
.append(className)
|
||||
.append(" 时错误").toString();
|
||||
log.warn(msg, e);
|
||||
return EmoneyConvertResult.error(msg)
|
||||
.setProtocolId(protocolId)
|
||||
.setSupposedClassName(className);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@PostMapping("/response/decode")
|
||||
public <U extends MessageNano> EmoneyConvertResult responseDecode(
|
||||
@RequestBody(required=false)
|
||||
@NonNull
|
||||
EmoneyProtobufBody body) {
|
||||
|
||||
Integer protocolId = body.getProtocolId();
|
||||
ProtocolMatch match = null;
|
||||
if (Objects.isNull(protocolId)) {
|
||||
log.warn("protocolId is null, cannot update protocolMatch");
|
||||
}
|
||||
else {
|
||||
match = protocolMatchService.getById(protocolId);
|
||||
if (Objects.isNull(match)) {
|
||||
match = new ProtocolMatch().setProtocolId(protocolId);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] buf;
|
||||
try {
|
||||
buf = body.protocolBodyToByte();
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw RException.badRequest("转换 protocolBody 错误");
|
||||
}
|
||||
|
||||
Base_Response baseResponse;
|
||||
try {
|
||||
baseResponse = Base_Response.parseFrom(buf);
|
||||
}
|
||||
catch (Exception e) {
|
||||
String msg = new StringBuilder()
|
||||
.append("转换 BaseResponse 发生错误")
|
||||
.toString();
|
||||
log.warn(msg, e);
|
||||
return EmoneyConvertResult
|
||||
.error(msg)
|
||||
.setProtocolId(protocolId);
|
||||
}
|
||||
|
||||
Class<U> clazz;
|
||||
String className =
|
||||
baseResponse.detail.getTypeUrl()
|
||||
.replace("type.googleapis.com/", "");
|
||||
|
||||
String rawClassName = className.substring(0, className.lastIndexOf('_'));
|
||||
if (Objects.nonNull(match) && StringUtils.isBlank(match.getClassName())) {
|
||||
match.setClassName(rawClassName);
|
||||
protocolMatchService.saveOrUpdate(match);
|
||||
}
|
||||
|
||||
className = new StringBuilder()
|
||||
.append("nano.")
|
||||
.append(rawClassName)
|
||||
.append("Response$")
|
||||
.append(className)
|
||||
.toString();
|
||||
|
||||
try {
|
||||
clazz = (Class<U>)Class.forName(className);
|
||||
}
|
||||
catch (Exception e) {
|
||||
String msg = new StringBuilder()
|
||||
.append("无法根据给定的 protocolId = ")
|
||||
.append(protocolId)
|
||||
.append(", className = ")
|
||||
.append(className)
|
||||
.append("找到对应类").toString();
|
||||
log.warn(msg, e);
|
||||
throw RException.internalServerError(msg);
|
||||
}
|
||||
|
||||
try {
|
||||
U nano = (U)MessageNano.mergeFrom(
|
||||
(MessageNano)clazz.getDeclaredConstructor().newInstance(),
|
||||
baseResponse.detail.getValue());
|
||||
return EmoneyConvertResult
|
||||
.ok(new ObjectMapper().valueToTree(nano))
|
||||
.setProtocolId(protocolId)
|
||||
.setSupposedClassName(className);
|
||||
}
|
||||
catch (Exception e) {
|
||||
String msg = new StringBuilder()
|
||||
.append("转换为类 ")
|
||||
.append(className)
|
||||
.append(" 时错误").toString();
|
||||
log.warn(msg, e);
|
||||
return EmoneyConvertResult.error(msg)
|
||||
.setProtocolId(protocolId)
|
||||
.setSupposedClassName(className);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package quant.rich.emoney.controller.common;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import quant.rich.emoney.service.AuthService;
|
||||
|
||||
@Controller
|
||||
public abstract class BaseController {
|
||||
|
||||
@Autowired
|
||||
protected HttpServletRequest request;
|
||||
|
||||
@Autowired
|
||||
protected HttpServletResponse response;
|
||||
|
||||
@Autowired
|
||||
protected HttpSession session;
|
||||
|
||||
@Autowired
|
||||
AuthService authService;
|
||||
|
||||
protected Boolean isLogin() {
|
||||
return authService.isLogin();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package quant.rich.emoney.controller.common;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.web.error.ErrorAttributeOptions;
|
||||
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
|
||||
import org.springframework.boot.web.servlet.error.ErrorAttributes;
|
||||
import org.springframework.boot.web.servlet.error.ErrorController;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.context.request.ServletWebRequest;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
public class ErrorPageController implements ErrorController {
|
||||
|
||||
@Autowired
|
||||
ErrorAttributes errorAttributes;
|
||||
|
||||
public final static String ERROR_PATH = "/error";
|
||||
|
||||
@GetMapping(value = ERROR_PATH)
|
||||
@PostMapping(value = ERROR_PATH)
|
||||
public String errorHtml(HttpServletRequest request) {
|
||||
HttpStatus status = getStatus(request);
|
||||
String prefix = "error/";
|
||||
|
||||
return prefix + "error_400";
|
||||
|
||||
// switch (status) {
|
||||
// case BAD_REQUEST:
|
||||
// return prefix + "error_400";
|
||||
// case NOT_FOUND:
|
||||
// return prefix + "error_404";
|
||||
// case METHOD_NOT_ALLOWED:
|
||||
// return prefix + "error_405";
|
||||
// default:
|
||||
// return prefix + "error_5xx";
|
||||
// }
|
||||
}
|
||||
|
||||
@GetMapping(value = ERROR_PATH, produces = "application/json")
|
||||
@PostMapping(value = ERROR_PATH, produces = "application/json")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
|
||||
Map<String, Object> body = getErrorAttributes(request, getTraceParameter(request));
|
||||
HttpStatus status = getStatus(request);
|
||||
return new ResponseEntity<Map<String, Object>>(body, status);
|
||||
}
|
||||
|
||||
private boolean getTraceParameter(HttpServletRequest request) {
|
||||
String parameter = request.getParameter("trace");
|
||||
if (parameter == null) {
|
||||
return false;
|
||||
}
|
||||
return !"false".equals(parameter.toLowerCase());
|
||||
}
|
||||
|
||||
protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
|
||||
WebRequest webRequest = new ServletWebRequest(request);
|
||||
if (includeStackTrace) {
|
||||
return this.errorAttributes.getErrorAttributes(webRequest,
|
||||
ErrorAttributeOptions.defaults().including(Include.STACK_TRACE));
|
||||
}
|
||||
return this.errorAttributes.getErrorAttributes(webRequest, ErrorAttributeOptions.defaults());
|
||||
}
|
||||
|
||||
private HttpStatus getStatus(HttpServletRequest request) {
|
||||
Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
|
||||
if (statusCode != null) {
|
||||
try {
|
||||
return HttpStatus.valueOf(statusCode);
|
||||
} catch (Exception ex) {
|
||||
}
|
||||
}
|
||||
return HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package quant.rich.emoney.controller.common;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.google.code.kaptcha.impl.DefaultKaptcha;
|
||||
|
||||
import quant.rich.emoney.service.AuthService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/captcha")
|
||||
public class KaptchaController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
DefaultKaptcha kaptcha;
|
||||
|
||||
@GetMapping(value = "/get", produces = MediaType.IMAGE_JPEG_VALUE)
|
||||
public byte[] getCaptcha() throws Exception {
|
||||
String createText = kaptcha.createText();
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
session.setAttribute(AuthService.CAPTCHA, createText);
|
||||
ImageIO.write(kaptcha.createImage(createText), "jpg", os);
|
||||
byte[] result = os.toByteArray();
|
||||
os.close();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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.ChromeVersionsConfig;
|
||||
import quant.rich.emoney.entity.config.DeviceInfoConfig;
|
||||
import quant.rich.emoney.pojo.dto.R;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/admin/v1/config/emoneyRequest")
|
||||
public class EmoneyRequestConfigControllerV1 extends BaseController {
|
||||
|
||||
@Autowired
|
||||
DeviceInfoConfig deviceInfoConfig;
|
||||
|
||||
@Autowired
|
||||
ChromeVersionsConfig chromeVersionsConfig;
|
||||
|
||||
@GetMapping("/getRandomDeviceInfo")
|
||||
@ResponseBody
|
||||
public R<?> getRandomDeviceInfo() {
|
||||
return R.judgeNonNull(deviceInfoConfig.getRandomDeviceInfo());
|
||||
}
|
||||
|
||||
@GetMapping("/getRandomChromeVersion")
|
||||
@ResponseBody
|
||||
public R<?> getRandomChromeVersion() {
|
||||
return R.judgeNonNull(chromeVersionsConfig.getRandomChromeVersion());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package quant.rich.emoney.controller.manage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import quant.rich.emoney.controller.common.BaseController;
|
||||
import quant.rich.emoney.entity.config.IndexInfoConfig;
|
||||
import quant.rich.emoney.pojo.dto.LayPageResp;
|
||||
import quant.rich.emoney.pojo.dto.R;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/admin/v1/manage/indexInfo")
|
||||
public class IndexInfoControllerV1 extends BaseController {
|
||||
|
||||
@Autowired
|
||||
IndexInfoConfig indexInfo;
|
||||
|
||||
@GetMapping({"", "/", "/index"})
|
||||
public String index() {
|
||||
return "/admin/v1/manage/indexInfo/index";
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
ObjectNode indexInfoJson = new ObjectMapper().valueToTree(indexInfo);
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
for (String field : fields) {
|
||||
map.put(field, indexInfoJson.get(field));
|
||||
}
|
||||
return R.ok(map);
|
||||
}
|
||||
|
||||
@GetMapping("/getIndexDetail")
|
||||
@ResponseBody
|
||||
public R<?> getIndexDetail(String code) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/getConfigIndOnlineByUrl")
|
||||
@ResponseBody
|
||||
public R<?> getConfigOnlineByUrl(String url) {
|
||||
return R.judge(() -> indexInfo.getOnlineConfigByUrl(url));
|
||||
}
|
||||
|
||||
@GetMapping("/getIndexInfoConfig")
|
||||
@ResponseBody
|
||||
public R<?> getIndexInfoConfig() {
|
||||
return R.ok(indexInfo);
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@ResponseBody
|
||||
public LayPageResp<?> list() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
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;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
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.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.core.toolkit.StringUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.controller.common.BaseController;
|
||||
import quant.rich.emoney.entity.sqlite.Plan;
|
||||
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;
|
||||
import quant.rich.emoney.service.sqlite.PlanService;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/admin/v1/manage/plan")
|
||||
public class PlanControllerV1 extends BaseController {
|
||||
|
||||
@Autowired
|
||||
PlanService planService;
|
||||
|
||||
@GetMapping({"", "/", "/index"})
|
||||
public String index() {
|
||||
return "/admin/v1/manage/plan/index";
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@ResponseBody
|
||||
public LayPageResp<?> list(LayPageReq<Plan> pageReq) {
|
||||
Page<Plan> planPage = planService.page(pageReq);
|
||||
return new LayPageResp<>(planPage);
|
||||
}
|
||||
|
||||
@GetMapping("/getOne")
|
||||
@ResponseBody
|
||||
public R<?> getOne(String planId) {
|
||||
|
||||
// 如果 planId 是空,说明可能希望新建一个 Plan,需要返回默认实例化对象
|
||||
if (planId == null) {
|
||||
return R.ok(new Plan());
|
||||
}
|
||||
|
||||
// 否则从数据库取
|
||||
Plan plan = planService.getById(planId);
|
||||
return R.judge(plan != null, plan, "无法找到对应 ID 的 Plan");
|
||||
}
|
||||
|
||||
@PostMapping("/updateEnabledStatus")
|
||||
@ResponseBody
|
||||
public R<?> updateEnabledStatus(String planId, Boolean enabled) {
|
||||
if (planService.update(new LambdaUpdateWrapper<Plan>()
|
||||
.eq(Plan::getPlanId, planId)
|
||||
.set(Plan::getEnabled, enabled))) {
|
||||
return R.ok();
|
||||
}
|
||||
throw RException.badRequest();
|
||||
}
|
||||
|
||||
@PostMapping("/updateBool")
|
||||
@ResponseBody
|
||||
public R<?> updateBool(String planId, String field, Boolean value) {
|
||||
TableInfo tableInfo = TableInfoHelper.getTableInfo(Plan.class);
|
||||
try {
|
||||
Field declaredField = Plan.class.getDeclaredField(field);
|
||||
|
||||
Optional<TableFieldInfo> fieldInfo = tableInfo.getFieldList().stream()
|
||||
.filter(f -> f.getProperty().equals(field))
|
||||
.findFirst();
|
||||
if (declaredField.getType().equals(Boolean.class)) {
|
||||
planService.update(
|
||||
new UpdateWrapper<Plan>()
|
||||
.eq("plan_id", planId)
|
||||
.set(fieldInfo.get().getColumn(), value));
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
catch (Exception e) {}
|
||||
throw RException.badRequest();
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@ResponseBody
|
||||
public R<?> delete(String planId) {
|
||||
return R.judge(planService.removeById(planId), "删除失败,是否已删除?");
|
||||
}
|
||||
|
||||
@PostMapping("/batchOp")
|
||||
@ResponseBody
|
||||
public R<?> batchOp(
|
||||
@RequestParam(value="ids[]", required=true)
|
||||
String[] ids, String op) {
|
||||
if (Objects.isNull(ids) || ids.length == 0) {
|
||||
throw RException.badRequest("提供的计划 ID 不能为空");
|
||||
}
|
||||
List<String> idArray = Arrays.asList(ids);
|
||||
|
||||
if (StringUtils.isBlank(op)) {
|
||||
// op 为空是删除
|
||||
return R.judge(
|
||||
planService.removeBatchByIds(idArray));
|
||||
}
|
||||
|
||||
LambdaUpdateWrapper<Plan> uw = new LambdaUpdateWrapper<>();
|
||||
uw.in(Plan::getPlanId, idArray);
|
||||
|
||||
if ("enable".equals(op)) {
|
||||
uw.set(Plan::getEnabled, true);
|
||||
}
|
||||
else if ("disable".equals(op)) {
|
||||
uw.set(Plan::getEnabled, false);
|
||||
}
|
||||
else if ("enableOpenDayCheck".equals(op)) {
|
||||
uw.set(Plan::getOpenDayCheck, true);
|
||||
}
|
||||
else if ("disableOpenDayCheck".equals(op)) {
|
||||
uw.set(Plan::getOpenDayCheck, false);
|
||||
}
|
||||
else {
|
||||
throw RException.badRequest("未识别的操作");
|
||||
}
|
||||
|
||||
return R.judge(planService.update(uw));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package quant.rich.emoney.controller.manage;
|
||||
|
||||
import java.util.Objects;
|
||||
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.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.controller.common.BaseController;
|
||||
import quant.rich.emoney.entity.sqlite.Plan;
|
||||
import quant.rich.emoney.entity.sqlite.ProtocolMatch;
|
||||
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;
|
||||
import quant.rich.emoney.service.sqlite.ProtocolMatchService;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/admin/v1/manage/protocolMatch")
|
||||
public class ProtocolMatchControllerV1 extends BaseController {
|
||||
|
||||
@Autowired
|
||||
ProtocolMatchService protocolMatchService;
|
||||
|
||||
@GetMapping({"", "/", "/index"})
|
||||
public String index() {
|
||||
return "/admin/v1/manage/protocolMatch/index";
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@ResponseBody
|
||||
public LayPageResp<?> list(LayPageReq<ProtocolMatch> pageReq) {
|
||||
Page<ProtocolMatch> planPage = protocolMatchService.page(pageReq);
|
||||
return new LayPageResp<>(planPage);
|
||||
}
|
||||
|
||||
@GetMapping("/getOne")
|
||||
@ResponseBody
|
||||
public R<?> getOne(Integer protocolId) {
|
||||
|
||||
// 如果 planId 是空,说明可能希望新建一个 Plan,需要返回默认实例化对象
|
||||
if (protocolId == null) {
|
||||
return R.ok(new ProtocolMatch());
|
||||
}
|
||||
|
||||
// 否则从数据库取
|
||||
ProtocolMatch protocolMatch = protocolMatchService.getById(protocolId);
|
||||
return R.judge(protocolMatch != null, protocolMatch, "无法找到对应 ID 的 ProtocolMatch");
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
@ResponseBody
|
||||
public R<?> save(@RequestBody ProtocolMatch protocolMatch) {
|
||||
if (Objects.nonNull(protocolMatch.getProtocolId())) {
|
||||
return R.judge(
|
||||
protocolMatchService.saveOrUpdate(protocolMatch), "保存失败");
|
||||
}
|
||||
throw RException.badRequest("protocolId 不允许为空");
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@ResponseBody
|
||||
public R<?> delete(Integer protocolMatchId) {
|
||||
return R.judge(protocolMatchService.removeById(protocolMatchId), "删除失败,是否已删除?");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Slf4j
|
||||
@ConfigInfo(name = "安卓 SDK Level 设置", initDefault = true, managed = false)
|
||||
public class AndroidSdkLevelConfig implements IConfig<AndroidSdkLevelConfig> {
|
||||
|
||||
Map<String, Integer> androidVerToSdk;
|
||||
|
||||
public AndroidSdkLevelConfig() {
|
||||
androidVerToSdk = new HashMap<>();
|
||||
androidVerToSdk.put("14", 34);
|
||||
androidVerToSdk.put("13", 33);
|
||||
androidVerToSdk.put("12L", 32);
|
||||
androidVerToSdk.put("12", 31);
|
||||
androidVerToSdk.put("11", 30);
|
||||
androidVerToSdk.put("10", 29);
|
||||
androidVerToSdk.put("9", 28);
|
||||
androidVerToSdk.put("8.1", 27);
|
||||
androidVerToSdk.put("8.0", 26);
|
||||
androidVerToSdk.put("7.1", 25);
|
||||
androidVerToSdk.put("7.0", 24);
|
||||
androidVerToSdk.put("6.0", 23);
|
||||
androidVerToSdk.put("5.1", 22);
|
||||
androidVerToSdk.put("5.0", 21);
|
||||
androidVerToSdk.put("4.4W", 20);
|
||||
androidVerToSdk.put("4.4", 19);
|
||||
androidVerToSdk.put("4.3", 18);
|
||||
androidVerToSdk.put("4.2", 17);
|
||||
androidVerToSdk.put("4.1", 16);
|
||||
androidVerToSdk.put("4.0.3", 15);
|
||||
androidVerToSdk.put("4.0.1", 14);
|
||||
androidVerToSdk.put("3.2", 13);
|
||||
androidVerToSdk.put("3.1", 12);
|
||||
androidVerToSdk.put("3.0", 11);
|
||||
androidVerToSdk.put("2.3.3", 10);
|
||||
androidVerToSdk.put("2.3", 9);
|
||||
androidVerToSdk.put("2.2", 8);
|
||||
androidVerToSdk.put("2.1", 7);
|
||||
androidVerToSdk.put("2.0.1", 6);
|
||||
androidVerToSdk.put("2.0", 5);
|
||||
androidVerToSdk.put("1.6", 4);
|
||||
androidVerToSdk.put("1.5", 3);
|
||||
androidVerToSdk.put("1.1", 2);
|
||||
androidVerToSdk.put("1.0", 1);
|
||||
}
|
||||
|
||||
public int getSdkLevel(String androidVersion) {
|
||||
if (androidVerToSdk.containsKey(androidVersion)) {
|
||||
return androidVerToSdk.get(androidVersion);
|
||||
}
|
||||
|
||||
// 模糊匹配前缀
|
||||
for (String key : androidVerToSdk.keySet()) {
|
||||
if (androidVersion.startsWith(key)) {
|
||||
return androidVerToSdk.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("未知的 Android 版本: " + androidVersion);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
|
||||
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.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
import quant.rich.emoney.util.VersionComparator;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Slf4j
|
||||
@ConfigInfo(name = "Chrome 版本设置", initDefault = true, managed = false)
|
||||
public class ChromeVersionsConfig implements IConfig<ChromeVersionsConfig> {
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
private List<String> chromeVersions;
|
||||
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
@Setter(AccessLevel.PRIVATE)
|
||||
private Integer chromeVersionsHashCode = null;
|
||||
|
||||
public ChromeVersionsConfig setChromeVersions(List<String> chromeVersions) {
|
||||
this.chromeVersions = chromeVersions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getChromeVersions() {
|
||||
int chromeVersionsHashCode = chromeVersions.hashCode();
|
||||
if (this.chromeVersionsHashCode == null || chromeVersionsHashCode != this.chromeVersionsHashCode) {
|
||||
Set<String> set = new HashSet<>(chromeVersions);
|
||||
chromeVersions = new ArrayList<>(set);
|
||||
chromeVersions.sort(VersionComparator.INSTANCE.reversed());
|
||||
this.chromeVersionsHashCode = chromeVersions.hashCode();
|
||||
}
|
||||
return chromeVersions;
|
||||
}
|
||||
|
||||
public ChromeVersionsConfig() {
|
||||
chromeVersions = new ArrayList<>(
|
||||
List.of("135.0.7049.100","135.0.7049.79","135.0.7049.38","134.0.6998.136","134.0.6998.135","134.0.6998.108","134.0.6998.95","133.0.6943.121","133.0.6943.50","133.0.6943.49","133.0.6943.39","132.0.6834.165","132.0.6834.163","132.0.6834.122","132.0.6834.79","131.0.6778.200","131.0.6778.136","131.0.6778.105","131.0.6778.81","131.0.6778.39","130.0.6723.103","130.0.6723.102","130.0.6723.86","130.0.6723.59","130.0.6723.58","130.0.6723.40","129.0.6668.100","129.0.6668.81","129.0.6668.71","128.0.6613.146","128.0.6613.127","128.0.6613.99","127.0.6533.106","127.0.6533.103","127.0.6533.84","127.0.6533.64","126.0.6478.186","126.0.6478.122","126.0.6478.72","126.0.6478.71","126.0.6478.50","125.0.6422.165","125.0.6422.164","125.0.6422.147","125.0.6422.146","125.0.6422.113","125.0.6422.72","125.0.6422.53","125.0.6422.52","124.0.6367.179","124.0.6367.172","124.0.6367.171","124.0.6367.114","124.0.6367.113","124.0.6367.83","124.0.6367.82","124.0.6367.54","123.0.6312.121","123.0.6312.120","123.0.6312.119","123.0.6312.118","123.0.6312.99","123.0.6312.80","123.0.6312.41","123.0.6312.40","122.0.6261.119","122.0.6261.106","122.0.6261.105","122.0.6261.91","122.0.6261.90","122.0.6261.64","122.0.6261.43","121.0.6167.178","121.0.6167.165","121.0.6167.164","121.0.6167.144","121.0.6167.143","121.0.6167.101","120.0.6099.230","120.0.6099.210","120.0.6099.194","120.0.6099.193","120.0.6099.145","120.0.6099.144","120.0.6099.116","120.0.6099.115","120.0.6099.44","120.0.6099.43","119.0.6045.194","119.0.6045.193","119.0.6045.164","119.0.6045.163","119.0.6045.134","119.0.6045.66","119.0.6045.53","118.0.5993.112","118.0.5993.111","118.0.5993.80","118.0.5993.65","118.0.5993.48","117.0.5938.154","117.0.5938.141","117.0.5938.140","117.0.5938.61","117.0.5938.60","116.0.5845.172","116.0.5845.164","116.0.5845.163","116.0.5845.114","116.0.5845.92","115.0.5790.136","114.0.5735.60","114.0.5735.53","113.0.5672.77","113.0.5672.76","112.0.5615.136","112.0.5615.101","112.0.5615.100","112.0.5615.48","111.0.5563.116","111.0.5563.115","111.0.5563.58","111.0.5563.49","110.0.5481.154","110.0.5481.153","110.0.5481.65","110.0.5481.64","110.0.5481.63","110.0.5481.61","109.0.5414.118","109.0.5414.117","109.0.5414.86","108.0.5359.128","108.0.5359.61","107.0.5304.141","107.0.5304.105","107.0.5304.91","106.0.5249.126","106.0.5249.79","106.0.5249.65","105.0.5195.136","105.0.5195.124","105.0.5195.79","105.0.5195.77","105.0.5195.68","104.0.5112.97","104.0.5112.69","103.0.5060.129","103.0.5060.71","103.0.5060.70","103.0.5060.53","102.0.5005.125","102.0.5005.99","102.0.5005.78","102.0.5005.59","101.0.4951.61","101.0.4951.41","100.0.4896.127","100.0.4896.88","100.0.4896.79","100.0.4896.58","99.0.4844.73","99.0.4844.58","99.0.4844.48","98.0.4758.101","98.0.4758.87","97.0.4692.98","97.0.4692.87","97.0.4692.70","96.0.4664.104","96.0.4664.92","95.0.4638.74","95.0.4638.50","94.0.4606.85","94.0.4606.80","94.0.4606.71","94.0.4606.61","94.0.4606.50","93.0.4577.82","93.0.4577.62","92.0.4515.166","92.0.4515.159","92.0.4515.131","92.0.4515.115","92.0.4515.105","91.0.4472.164","91.0.4472.134","91.0.4472.120","91.0.4472.114","91.0.4472.101","91.0.4472.88","91.0.4472.77","91.0.4472.16","90.0.4430.210","90.0.4430.91","90.0.4430.82","90.0.4430.66","89.0.4389.105","89.0.4389.90","89.0.4389.86","89.0.4389.72","88.0.4324.181","88.0.4324.155","88.0.4324.152","88.0.4324.141","88.0.4324.93","87.0.4280.141","87.0.4280.101","87.0.4280.86","87.0.4280.66","86.0.4240.198","86.0.4240.185","86.0.4240.114","86.0.4240.110","86.0.4240.99","86.0.4240.75","85.0.4183.127","85.0.4183.101","85.0.4183.81","84.0.4147.125","84.0.4147.105","84.0.4147.89","83.0.4103.106","83.0.4103.101","83.0.4103.96","83.0.4103.83","81.0.4044.138","81.0.4044.117","80.0.3987.162","80.0.3987.149","80.0.3987.132","80.0.3987.119","80.0.3987.117","80.0.3987.87","79.0.3945.136","79.0.3945.116","79.0.3945.79","78.0.3904.108","78.0.3904.96","78.0.3904.90","78.0.3904.62","77.0.3865.116","77.0.3865.92","77.0.3865.73","76.0.3809.132","76.0.3809.111","76.0.3809.89","75.0.3770.143","75.0.3770.101","75.0.3770.89","75.0.3770.67","74.0.3729.157","74.0.3729.149","74.0.3729.136","74.0.3729.112","73.0.3683.90","73.0.3683.75")
|
||||
);
|
||||
// 立即去重、排序、更新 hashCode
|
||||
getChromeVersions();
|
||||
}
|
||||
|
||||
public String getRandomChromeVersion() {
|
||||
int index = RANDOM.nextInt(chromeVersions.size());
|
||||
return getChromeVersions().get(index);
|
||||
}
|
||||
|
||||
public String getLatest() {
|
||||
return getChromeVersions().get(0);
|
||||
}
|
||||
|
||||
public String getOldest() {
|
||||
List<String> chromeVersions = getChromeVersions();
|
||||
return chromeVersions.get(chromeVersions.size() - 1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Slf4j
|
||||
@ConfigInfo(
|
||||
field = "deviceInfo",
|
||||
name = "设备信息设置",
|
||||
initDefault = true,
|
||||
managed = false)
|
||||
public class DeviceInfoConfig implements IConfig<DeviceInfoConfig> {
|
||||
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
List<DeviceInfo> deviceInfos;
|
||||
|
||||
public DeviceInfoConfig() {
|
||||
deviceInfos = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取随机设备信息
|
||||
* @return
|
||||
*/
|
||||
public DeviceInfo getRandomDeviceInfo() {
|
||||
if (deviceInfos.isEmpty()) return null;
|
||||
return deviceInfos.get(RANDOM.nextInt(deviceInfos.size()));
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@Slf4j
|
||||
public static class DeviceInfo {
|
||||
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
private String model;
|
||||
private String brand;
|
||||
private String product;
|
||||
private String device;
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
private String deviceType;
|
||||
private String versionRelease;
|
||||
private String buildId;
|
||||
private String buildNumber;
|
||||
private String buildType;
|
||||
private String buildTags;
|
||||
|
||||
public static final Pattern PATTERN = Pattern.compile("^(?<brand>.*?)/(?<product>.*?)/(?<device>.*?):(?<versionRelease>.*?)/(?<buildId>.*?)/(?<buildNumber>.*?):(?<buildType>.*?)/(?<buildTags>.*?)$");
|
||||
|
||||
|
||||
private DeviceInfo() {
|
||||
}
|
||||
|
||||
public DeviceInfo setFingerprint(String fingerprint) {
|
||||
Matcher m = PATTERN.matcher(fingerprint);
|
||||
if (!m.matches()) {
|
||||
throw new IllegalArgumentException("Fingerprint not match the pattern: " + fingerprint);
|
||||
}
|
||||
this.setBrand(m.group("brand"));
|
||||
this.setBuildId(m.group("buildId"));
|
||||
this.setBuildNumber(m.group("buildNumber"));
|
||||
this.setBuildTags(m.group("buildTags"));
|
||||
this.setBuildType(m.group("buildType"));
|
||||
this.setProduct(m.group("product"));
|
||||
this.setVersionRelease(m.group("versionRelease"));
|
||||
this.setDevice(m.group("device"));
|
||||
return this;
|
||||
}
|
||||
|
||||
public DeviceInfo setDeviceType(String deviceType) {
|
||||
if (!"Mobile".equals(deviceType) && !"Pad".equals(deviceType)) {
|
||||
throw new IllegalArgumentException("DeviceType must be \"Mobile\" or \"Pad\", but got \"" + deviceType + "\"");
|
||||
}
|
||||
this.deviceType = deviceType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定 model、fingerprint 和 deviceType 返回 DeviceInfo
|
||||
* @param model
|
||||
* @param fingerprint
|
||||
* @param deviceType
|
||||
* @return
|
||||
*/
|
||||
public static DeviceInfo from(String model, String fingerprint, String deviceType) {
|
||||
|
||||
DeviceInfo deviceInfo = new DeviceInfo();
|
||||
deviceInfo.setModel(model);
|
||||
deviceInfo.setFingerprint(fingerprint);
|
||||
deviceInfo.setDeviceType(deviceType);
|
||||
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
public static DeviceInfo from(String model, String fingerprint) {
|
||||
return from(model, fingerprint, "Mobile");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从设备信息中还原 fingerprint
|
||||
* @return
|
||||
*/
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
public String getFingerprint() {
|
||||
return String.format("%s/%s/%s:%s/%s/%s:%s/%s",
|
||||
getBrand(), getProduct(), getDevice(), getVersionRelease(),
|
||||
getBuildId(), getBuildNumber(), getBuildType(), getBuildTags()
|
||||
);
|
||||
}
|
||||
|
||||
public final String toString() {
|
||||
return String.format("Model: %s, Fingerprint: %s",
|
||||
getModel(), getFingerprint()
|
||||
);
|
||||
}
|
||||
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null || getClass() != obj.getClass()) return false;
|
||||
DeviceInfo other = (DeviceInfo)obj;
|
||||
return hashCode() == other.hashCode();
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return Objects.hash(getModel(), getFingerprint());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import java.net.Proxy;
|
||||
import java.time.LocalDateTime;
|
||||
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.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
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.interfaces.ValidEmoneyRequestConfig;
|
||||
import quant.rich.emoney.patch.okhttp.PatchOkHttp;
|
||||
import quant.rich.emoney.util.EncryptUtils;
|
||||
import quant.rich.emoney.util.SpringContextHolder;
|
||||
import quant.rich.emoney.util.TextUtils;
|
||||
|
||||
/**
|
||||
* 用于配置请求时的请求行为,一般而言,请求头与安卓系统的信息有关(build.prop)
|
||||
* 虽然部分请求对应服务器可能不进行审核,但合理的请求头能尽可能模仿真机行为,避免风险
|
||||
* @see DeviceInfoConfig
|
||||
* @see AndroidSdkLevelConfig
|
||||
* @see ChromeVersionsConfig
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Slf4j
|
||||
@ValidEmoneyRequestConfig
|
||||
@ConfigInfo(field = "emoneyRequest", name = "益盟请求设置", initDefault = true)
|
||||
public class EmoneyRequestConfig implements IConfig<EmoneyRequestConfig> {
|
||||
|
||||
/**
|
||||
* 代理类型
|
||||
*/
|
||||
private Proxy.Type proxyType = Proxy.Type.DIRECT;
|
||||
|
||||
/**
|
||||
* 代理主机
|
||||
*/
|
||||
private String proxyHost;
|
||||
|
||||
/**
|
||||
* 代理端口
|
||||
*/
|
||||
private Integer proxyPort;
|
||||
|
||||
/**
|
||||
* 是否忽略 HTTPS 证书校验
|
||||
*/
|
||||
private Boolean ignoreHttpsVerification = false;
|
||||
|
||||
/**
|
||||
* 是否匿名登录
|
||||
*/
|
||||
private Boolean isAnonymous = true;
|
||||
|
||||
/**
|
||||
* 非匿名登录时的用户名
|
||||
*/
|
||||
private String username = "";
|
||||
|
||||
/**
|
||||
* 非匿名登录时的密码
|
||||
*/
|
||||
private String password = "";
|
||||
|
||||
/**
|
||||
* 鉴权信息
|
||||
*/
|
||||
private String authorization;
|
||||
|
||||
/**
|
||||
* 当前 authorization 更新时间
|
||||
*/
|
||||
@JsonSerialize(using = LocalDateTimeSerializer.class)
|
||||
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
|
||||
private LocalDateTime authorizationUpdateTime;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟登录接口 <code><i>guid</i> = MD5(<b>androidId</b>)</code></li>
|
||||
* <li>益盟登录接口 <code><i>exIdentify.AndroidID</i> = <b>androidId</b></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><br>本例随机生成并管理,需要符合 16 位
|
||||
*
|
||||
*/
|
||||
private String androidId = TextUtils.randomString("abcdef0123456789", 16);
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>Webview <code><i>User-Agent</i></li>
|
||||
* <li>Non-Webview Image <code><i>User-Agent</i></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b></code>
|
||||
* @see DeviceInfoConfig
|
||||
*/
|
||||
@Setter(AccessLevel.PRIVATE)
|
||||
private String androidVersion;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟通讯接口请求头 <code><i>X-Android-Agent</i> = EMAPP/{<b>emoneyVersion</b>}(Android;{<b>androidSdkLevel</b>})</code></li>
|
||||
* <li>益盟登录接口 <code><i>osVersion</i> = <b>androidSdkLevel</b></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 经由 <code><b>AndroidSdkLevelConfig</b></code> 转换,由本例代管</code>
|
||||
* @see DeviceInfoConfig
|
||||
* @see AndroidSdkLevelConfig
|
||||
*/
|
||||
@Setter(AccessLevel.PRIVATE)
|
||||
private String androidSdkLevel;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟登录接口 <code><i>softwareType</i> = <b>softwareType</b></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>,由本例代管</code>
|
||||
* @see DeviceInfoConfig
|
||||
*/
|
||||
private String softwareType;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟通讯接口请求头 <code><i>User-Agent</i> = <b>okHttpUserAgent</b></code></li>
|
||||
* </ul>
|
||||
* 一般由程序所使用的 OkHttp 版本决定<br>
|
||||
* <b>来源:</b>本例管理
|
||||
*/
|
||||
private String okHttpUserAgent = "okhttp/3.12.2";
|
||||
|
||||
/**
|
||||
* 对应 build.prop 中 Build.MODEL, <b>用于:</b><ul>
|
||||
* <li>WebView <code><i>User-Agent</i></code></li>
|
||||
* <li>非 WebView 图片<code><i>User-Agent</i></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
|
||||
* @see DeviceInfoConfig
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* 对应 build.prop 中 Build.FINGERPRINT, <b>用于:</b><ul>
|
||||
* <li>益盟登录接口 <code><i>hardware</i> = MD5(<b>fingerprint</b>)</code></li>
|
||||
* <li>益盟登录接口 <code><i>exIdentify.OSFingerPrint</i> = <b>fingerprint</b></code></li>
|
||||
* </ul>
|
||||
* <font color="red">注意最终生成的 exIdentify 是 jsonString, 且有对斜杠的转义</font><br>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
|
||||
* @see DeviceInfoConfig
|
||||
*
|
||||
*/
|
||||
private String fingerprint;
|
||||
|
||||
|
||||
/**
|
||||
* 对应 build.prop 中 Build.ID, <b>用于:</b><ul>
|
||||
* <li>WebView <code><i>User-Agent</i></code></li>
|
||||
* <li>非 WebView 图片<code><i>User-Agent</i></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
|
||||
* @see DeviceInfoConfig
|
||||
*
|
||||
*/
|
||||
private String buildId;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>WebView <code><i>User-Agent</i></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>ChromeVersionsConfig</b>, 由本例代为管理
|
||||
* @see ChromeVersionsConfig
|
||||
*/
|
||||
private String chromeVersion;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟通讯接口请求头 <code><i>X-Android-Agent</i> =
|
||||
* EMAPP/{<b>emoneyVersion</b>}(Android;{androidSdkLevel})</code></li>
|
||||
* </ul>
|
||||
* 由程序版本决定<br>
|
||||
* <b>来源:</b>本例管理
|
||||
* @see EmoneyRequestConfig.androidSdkLevel
|
||||
*/
|
||||
private String emoneyVersion = "5.8.1";
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟通讯接口请求头 <code><i>Emapp-ViewMode</i> = <b>emappViewMode</b></code></li>
|
||||
* </ul>
|
||||
* 由程序决定, 一般默认为 "1"<br>
|
||||
* <b>来源:</b>本例管理
|
||||
*/
|
||||
private String emappViewMode = "1";
|
||||
|
||||
@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("SpringContext not ready");
|
||||
}
|
||||
|
||||
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) doesn't match");
|
||||
Validate.validState(androidSdkLevel.equals(
|
||||
String.valueOf(androidSdkLevelConfig.getSdkLevel(deviceInfo.getVersionRelease()))),
|
||||
"androidSdkLevel doesn't match");
|
||||
Validate.validState(buildId.equals(deviceInfo.getBuildId()),
|
||||
"buildId doesn't match");
|
||||
}
|
||||
catch (Exception e) {
|
||||
valid = false;
|
||||
}
|
||||
if (!valid) {
|
||||
initFromRandomDeviceInfo();
|
||||
}
|
||||
}
|
||||
|
||||
if (chromeVersion == null) {
|
||||
chromeVersion = chromeVersionsConfig.getRandomChromeVersion();
|
||||
}
|
||||
|
||||
// 注入 OkHttp
|
||||
PatchOkHttp.apply(
|
||||
r -> r
|
||||
.hostEndsWith("emoney.cn")
|
||||
.or(a -> a.hostContains("emapp"))
|
||||
.or(b -> b.hasHeaderName("X-Protocol-Id"))
|
||||
.overrideIf("User-Agent", getOkHttpUserAgent()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从随机 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
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置密码:<ul>
|
||||
* <li>null or empty,保存空字符串</li>
|
||||
* <li>尝试解密成功,说明是密文,直接保存</li>
|
||||
* <li>尝试解密失败,说明是明文,加密保存</li>
|
||||
* </ul>
|
||||
* @param password
|
||||
* @return
|
||||
*/
|
||||
public EmoneyRequestConfig setPassword(String password) {
|
||||
if (StringUtils.isEmpty(password)) {
|
||||
this.password = "";
|
||||
return this;
|
||||
}
|
||||
String tryDecryptPassword = EncryptUtils.decryptAesForEmoneyPassword(password);
|
||||
if (tryDecryptPassword != null) {
|
||||
this.password = password;
|
||||
}
|
||||
else {
|
||||
this.password = EncryptUtils.encryptAesForEmoneyPassword(password);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保 androidVersion/androidSdkLevel 不为 null
|
||||
*/
|
||||
public EmoneyRequestConfig beforeSaving() {
|
||||
setFingerprint(this.fingerprint);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发更新 Authorization 的更新时间为现在
|
||||
* @return
|
||||
*/
|
||||
public EmoneyRequestConfig updateAuthorizationTime() {
|
||||
this.authorizationUpdateTime = LocalDateTime.now();
|
||||
return this;
|
||||
}
|
||||
|
||||
public EmoneyRequestConfig mergeTo(EmoneyRequestConfig other) {
|
||||
boolean authorizationUpdated = !Objects.equals(other.authorization, this.authorization);
|
||||
IConfig.super.mergeTo(other);
|
||||
if (authorizationUpdated) {
|
||||
other.updateAuthorizationTime();
|
||||
}
|
||||
return other;
|
||||
}
|
||||
|
||||
public EmoneyRequestConfig mergeFrom(EmoneyRequestConfig other) {
|
||||
boolean authorizationUpdated = !Objects.equals(other.authorization, this.authorization);
|
||||
IConfig.super.mergeFrom(other);
|
||||
if (authorizationUpdated) {
|
||||
this.updateAuthorizationTime();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
38
src/main/java/quant/rich/emoney/entity/config/IndexInfo.java
Normal file
38
src/main/java/quant/rich/emoney/entity/config/IndexInfo.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import quant.rich.emoney.enums.StockSpan;
|
||||
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class IndexInfo {
|
||||
|
||||
private List<ParamInfo> paramInfoList = new ArrayList<>();
|
||||
|
||||
private String code;
|
||||
|
||||
private String name;
|
||||
|
||||
private Boolean isCalc;
|
||||
|
||||
private List<StockSpan> supportPeriod = new ArrayList<>();
|
||||
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public static class ParamInfo {
|
||||
|
||||
private String name;
|
||||
|
||||
private Integer max;
|
||||
|
||||
private Integer min;
|
||||
|
||||
private Integer defaultValue;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
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 com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jodd.io.FileUtil;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.Accessors;
|
||||
import okhttp3.ConnectionPool;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.BufferedSource;
|
||||
import okio.GzipSource;
|
||||
import okio.Okio;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
|
||||
/**
|
||||
* 指标信息配置,只做运行时管理,不做保存
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@Lazy(false)
|
||||
@ConfigInfo(field = "indexInfo", name =" 指标信息配置", initDefault = true, managed = false)
|
||||
public class IndexInfoConfig implements IConfig<IndexInfoConfig> {
|
||||
|
||||
private static final String CONFIG_IND_ONLINE_PATH = "./conf/extra/config_ind_online.json";
|
||||
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
private String configIndOnlineUrl;
|
||||
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
private JsonNode configIndOnline;
|
||||
|
||||
@Autowired
|
||||
@JsonIgnore
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
private EmoneyRequestConfig emoneyRequestConfig;
|
||||
|
||||
public IndexInfoConfig() {
|
||||
try {
|
||||
String configStr = FileUtil.readString(CONFIG_IND_ONLINE_PATH);
|
||||
configIndOnline = new ObjectMapper().readTree(configStr);
|
||||
}
|
||||
catch (Exception e) {}
|
||||
configIndOnlineUrl = "https://emapp-static.oss-cn-shanghai.aliyuncs.com/down13/emstock/config/ind_config/v1000003/config_ind_online.json";
|
||||
}
|
||||
|
||||
public String getConfigIndOnlineStr() {
|
||||
return getConfigIndOnline().toPrettyString();
|
||||
}
|
||||
|
||||
public String getOnlineConfigByUrl(String url) throws IOException {
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.header("User-Agent", emoneyRequestConfig.getOkHttpUserAgent())
|
||||
.header("Accept-Encoding", "gzip")
|
||||
.header("Connection", "Keep-Alive")
|
||||
.header("Cache-Control", "no-cache")
|
||||
.get()
|
||||
// Host 不能在 OkHttp 中直接设置(由 URL 控制)
|
||||
.build();
|
||||
|
||||
// 发出请求
|
||||
Response backendResponse = getInstance().newCall(request).execute();
|
||||
if (backendResponse.body() != null) {
|
||||
// 将内容复制给前端
|
||||
return
|
||||
new String(backendResponse.body().bytes(), "UTF-8");
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static OkHttpClient okHttpClient;
|
||||
|
||||
public static ConnectionPool connectionPool = new ConnectionPool(10, 5, TimeUnit.MINUTES);
|
||||
|
||||
public static OkHttpClient getInstance() {
|
||||
if (okHttpClient == null) { //加同步安全
|
||||
synchronized (OkHttpClient.class) {
|
||||
if (okHttpClient == null) { //okhttp可以缓存数据....指定缓存路径
|
||||
okHttpClient = new OkHttpClient.Builder()//构建器
|
||||
.proxy(Proxy.NO_PROXY) //来屏蔽系统代理
|
||||
.connectionPool(connectionPool)
|
||||
.sslSocketFactory(getSSLSocketFactory(), getX509TrustManager())
|
||||
.hostnameVerifier(getHostnameVerifier())
|
||||
.connectTimeout(600, TimeUnit.SECONDS)//连接超时
|
||||
.writeTimeout(600, TimeUnit.SECONDS)//写入超时
|
||||
.readTimeout(600, TimeUnit.SECONDS)//读取超时
|
||||
.addNetworkInterceptor(new GzipResponseInterceptor())
|
||||
.build();
|
||||
okHttpClient.dispatcher().setMaxRequestsPerHost(200);
|
||||
okHttpClient.dispatcher().setMaxRequests(200);
|
||||
}
|
||||
}
|
||||
}
|
||||
return okHttpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* description 忽略https证书验证
|
||||
*/
|
||||
private static HostnameVerifier getHostnameVerifier() {
|
||||
HostnameVerifier hostnameVerifier = new HostnameVerifier() {
|
||||
@Override
|
||||
public boolean verify(String s, SSLSession sslSession) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
return hostnameVerifier;
|
||||
}
|
||||
/**
|
||||
* description 忽略https证书验证
|
||||
*/
|
||||
private static SSLSocketFactory getSSLSocketFactory() {
|
||||
try {
|
||||
SSLContext sslContext = SSLContext.getInstance("SSL");
|
||||
sslContext.init(null, getTrustManager(), new SecureRandom());
|
||||
return sslContext.getSocketFactory();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static X509TrustManager getX509TrustManager() {
|
||||
X509TrustManager trustManager = null;
|
||||
try {
|
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
trustManagerFactory.init((KeyStore) null);
|
||||
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
|
||||
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
|
||||
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
|
||||
}
|
||||
trustManager = (X509TrustManager) trustManagers[0];
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return trustManager;
|
||||
}
|
||||
|
||||
private static TrustManager[] getTrustManager() {
|
||||
TrustManager[] trustAllCerts = new TrustManager[]{
|
||||
new X509TrustManager() {
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[]{};
|
||||
}
|
||||
}
|
||||
};
|
||||
return trustAllCerts;
|
||||
}
|
||||
|
||||
public static class GzipResponseInterceptor implements Interceptor {
|
||||
@Override
|
||||
public Response intercept(Chain chain) throws IOException {
|
||||
Request request = chain.request();
|
||||
Response response = chain.proceed(request);
|
||||
|
||||
// 只有服务器返回了 gzip 编码才处理
|
||||
if ("gzip".equalsIgnoreCase(response.header("Content-Encoding"))) {
|
||||
// 原始响应体
|
||||
ResponseBody body = response.body();
|
||||
if (body == null) return response;
|
||||
|
||||
// 用 GzipSource 包装原始流,并缓冲
|
||||
GzipSource gzippedResponseBody = new GzipSource(body.source());
|
||||
BufferedSource unzippedSource = Okio.buffer(gzippedResponseBody);
|
||||
|
||||
// 构造一个新的 ResponseBody,不再带 Content-Encoding/Length
|
||||
ResponseBody newBody = ResponseBody.create(
|
||||
unzippedSource,
|
||||
body.contentType(),
|
||||
-1L
|
||||
);
|
||||
|
||||
// 去掉 Content-Encoding/Length,让后续调用 body().string() 时拿到解压后的内容
|
||||
return response.newBuilder()
|
||||
.removeHeader("Content-Encoding")
|
||||
.removeHeader("Content-Length")
|
||||
.body(newBody)
|
||||
.build();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@ConfigInfo(field = "platform", name = "平台设置", initDefault = true)
|
||||
public class PlatformConfig implements IConfig<PlatformConfig> {
|
||||
|
||||
private String username;
|
||||
|
||||
private String password;
|
||||
|
||||
private Boolean isInited;
|
||||
|
||||
public PlatformConfig() {
|
||||
username = "admin";
|
||||
password = "";
|
||||
isInited = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
|
||||
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
|
||||
public class SmartViewWriter {
|
||||
private static final ObjectMapper normalMapper = new ObjectMapper();
|
||||
private static final ObjectMapper withViewMapper = JsonMapper.builder()
|
||||
.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false)
|
||||
.build();
|
||||
|
||||
public SmartViewWriter() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能序列化:如果对象包含 @JsonView 注解字段,则只输出视图字段;否则输出全部字段
|
||||
*/
|
||||
public String writeWithSmartView(Object obj, Class<?> viewClass) {
|
||||
if (hasJsonViewRecursive(obj.getClass(), new HashSet<>())) {
|
||||
try {
|
||||
ObjectWriter writer = withViewMapper.writerWithView(viewClass);
|
||||
writer = writer.with(SerializationFeature.INDENT_OUTPUT);
|
||||
return writer.writeValueAsString(obj);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("Serialization failed", e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
ObjectWriter writer = normalMapper.writer();
|
||||
writer = writer.with(SerializationFeature.INDENT_OUTPUT);
|
||||
return writer.writeValueAsString(obj); // 无视图注解,走默认序列化
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("Serialization failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasJsonViewRecursive(Class<?> clazz, Set<Class<?>> visited) {
|
||||
if (clazz == null || clazz.getName().startsWith("java.") || visited.contains(clazz)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
visited.add(clazz);
|
||||
|
||||
JavaType javaType = normalMapper.getTypeFactory().constructType(clazz);
|
||||
BeanDescription beanDesc = normalMapper.getSerializationConfig().introspect(javaType);
|
||||
|
||||
for (BeanPropertyDefinition prop : beanDesc.findProperties()) {
|
||||
AnnotatedMember accessor = prop.getAccessor();
|
||||
if (accessor != null && accessor.hasAnnotation(JsonView.class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
JavaType fieldType = prop.getPrimaryType();
|
||||
|
||||
if (fieldType == null) continue;
|
||||
|
||||
if (fieldType.isCollectionLikeType()) {
|
||||
// 处理 List<T>, Set<T>
|
||||
JavaType contentType = fieldType.getContentType();
|
||||
if (hasJsonViewRecursive(contentType.getRawClass(), visited)) {
|
||||
return true;
|
||||
}
|
||||
} else if (fieldType.isArrayType()) {
|
||||
JavaType elemType = fieldType.getContentType();
|
||||
if (hasJsonViewRecursive(elemType.getRawClass(), visited)) {
|
||||
return true;
|
||||
}
|
||||
} else if (fieldType.isMapLikeType()) {
|
||||
JavaType valueType = fieldType.getContentType();
|
||||
if (hasJsonViewRecursive(valueType.getRawClass(), visited)) {
|
||||
return true;
|
||||
}
|
||||
} else if (!fieldType.isPrimitive() && !fieldType.isEnumType()) {
|
||||
// 普通对象类型递归
|
||||
if (hasJsonViewRecursive(fieldType.getRawClass(), visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
110
src/main/java/quant/rich/emoney/entity/postgre/EmoneyIndex.java
Normal file
110
src/main/java/quant/rich/emoney/entity/postgre/EmoneyIndex.java
Normal file
@@ -0,0 +1,110 @@
|
||||
package quant.rich.emoney.entity.postgre;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
import quant.rich.emoney.enums.StockSpan;
|
||||
|
||||
/**
|
||||
* 益盟指标
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class EmoneyIndex {
|
||||
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
@Setter(AccessLevel.PRIVATE)
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 股票代码
|
||||
*/
|
||||
private String tsCode;
|
||||
/**
|
||||
* 时间
|
||||
*/
|
||||
@TableField("trade_date")
|
||||
private Date date;
|
||||
/**
|
||||
* 指标值
|
||||
*/
|
||||
private Long value;
|
||||
/**
|
||||
* 指标名称
|
||||
*/
|
||||
private String indexName;
|
||||
/**
|
||||
* 指标线名称
|
||||
*/
|
||||
private String lineName;
|
||||
/**
|
||||
* 指标参数
|
||||
*/
|
||||
private JsonNode indexParam;
|
||||
/**
|
||||
* 指标形状
|
||||
*/
|
||||
private Integer lineShape;
|
||||
/**
|
||||
* 数据粒度
|
||||
*/
|
||||
private Integer dataPeriod;
|
||||
|
||||
public StockSpan getStockSpan() {
|
||||
return StockSpan.fromEmoneyCode(dataPeriod);
|
||||
}
|
||||
|
||||
public EmoneyIndex setStockSpan(StockSpan stockSpan) {
|
||||
|
||||
if (dataPeriod == null) {
|
||||
dataPeriod = stockSpan.getEmoneyCode();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public EmoneyIndex setIndexParam(@SuppressWarnings("rawtypes") Map map) {
|
||||
if (map == null) {
|
||||
indexParam = mapper.createObjectNode();
|
||||
}
|
||||
else {
|
||||
indexParam = mapper.valueToTree(map);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonNode getIndexParam() {
|
||||
if (indexParam == null) {
|
||||
indexParam = mapper.createObjectNode();
|
||||
}
|
||||
return indexParam;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return mapper.valueToTree(this).toString();
|
||||
}
|
||||
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) return true;
|
||||
if (other == null || getClass() != other.getClass()) return false;
|
||||
EmoneyIndex index = (EmoneyIndex) other;
|
||||
return this.toString().equals(index.toString());
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return Objects.hash(tsCode, date, value, indexName, lineName, indexParam, lineShape, dataPeriod);
|
||||
}
|
||||
|
||||
}
|
||||
67
src/main/java/quant/rich/emoney/entity/sqlite/Plan.java
Normal file
67
src/main/java/quant/rich/emoney/entity/sqlite/Plan.java
Normal file
@@ -0,0 +1,67 @@
|
||||
package quant.rich.emoney.entity.sqlite;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
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.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import quant.rich.emoney.mybatis.typehandler.CommaListTypeHandler;
|
||||
import quant.rich.emoney.mybatis.typehandler.JsonStringTypeHandler;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@TableName(value = "plan", autoResultMap = true)
|
||||
public class Plan {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private String planId;
|
||||
|
||||
private String cronExpression;
|
||||
|
||||
private String planName;
|
||||
|
||||
private String indexCode;
|
||||
|
||||
@TableField(typeHandler = CommaListTypeHandler.class)
|
||||
private List<String> periods;
|
||||
|
||||
@TableField(typeHandler = JsonStringTypeHandler.class)
|
||||
private JsonNode params;
|
||||
|
||||
private Boolean enabled;
|
||||
|
||||
private Boolean openDayCheck;
|
||||
|
||||
public Plan setPeriods(List<String> periods) {
|
||||
if (CollectionUtils.isEmpty(periods)) {
|
||||
this.periods = Collections.emptyList();
|
||||
return this;
|
||||
}
|
||||
Set<String> hashSet = new HashSet<>();
|
||||
periods.forEach(s -> {
|
||||
if (StringUtils.isNotBlank(s)) {
|
||||
hashSet.add(s.trim());
|
||||
}
|
||||
});
|
||||
this.periods = new ArrayList<>(hashSet);
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getPeriods() {
|
||||
setPeriods(periods);
|
||||
return periods;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package quant.rich.emoney.entity.sqlite;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@TableName(value = "protocol_match", autoResultMap = true)
|
||||
public class ProtocolMatch {
|
||||
|
||||
@TableId
|
||||
private Integer protocolId;
|
||||
|
||||
private String className;
|
||||
|
||||
}
|
||||
229
src/main/java/quant/rich/emoney/enums/StockSpan.java
Normal file
229
src/main/java/quant/rich/emoney/enums/StockSpan.java
Normal file
@@ -0,0 +1,229 @@
|
||||
package quant.rich.emoney.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 股价数据粒度
|
||||
* @author Administrator
|
||||
*
|
||||
*/
|
||||
public enum StockSpan {
|
||||
|
||||
Tick("7", null),
|
||||
Minute("8", 1),
|
||||
Minute5("0", 5),
|
||||
Minute15("1", 15),
|
||||
Minute30("2", 30),
|
||||
Minute60("3", 60),
|
||||
Minute120(null, 120),
|
||||
Daily("4", null),
|
||||
Weekly("5", null),
|
||||
Monthly("6", null),
|
||||
Seasonly("10", null),
|
||||
HalfYearly(null, null),
|
||||
Yearly("11", null);
|
||||
|
||||
private String tdxCode;
|
||||
@Getter
|
||||
private Integer min;
|
||||
|
||||
private StockSpan(String tdxCode, Integer min) {
|
||||
this.tdxCode = tdxCode;
|
||||
this.min = min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取东财对应的时间粒度表示
|
||||
* @return
|
||||
*/
|
||||
public String getEastMoneyCode() {
|
||||
|
||||
if (this.min != null) {
|
||||
return String.format("%d", 0);
|
||||
}
|
||||
|
||||
switch(this) {
|
||||
case Daily:
|
||||
return "101";
|
||||
case Weekly:
|
||||
return "102";
|
||||
case Monthly:
|
||||
return "103";
|
||||
case Seasonly:
|
||||
return "104";
|
||||
case HalfYearly:
|
||||
return "105";
|
||||
case Yearly:
|
||||
return "106";
|
||||
default:
|
||||
throw new RuntimeException("东财时间粒度不支持 " + this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从东财时间粒度转换为 StockSpan
|
||||
* @param eastMoneyCode
|
||||
* @return
|
||||
*/
|
||||
public static StockSpan fromEastMoneyCode(String eastMoneyCode) {
|
||||
|
||||
switch (eastMoneyCode) {
|
||||
case "1":
|
||||
return Minute;
|
||||
case "5":
|
||||
return Minute5;
|
||||
case "15":
|
||||
return Minute15;
|
||||
case "30":
|
||||
return Minute30;
|
||||
case "60":
|
||||
return Minute60;
|
||||
case "120":
|
||||
return Minute120;
|
||||
case "101":
|
||||
return Daily;
|
||||
case "102":
|
||||
return Weekly;
|
||||
case "103":
|
||||
return Monthly;
|
||||
case "104":
|
||||
return Seasonly;
|
||||
case "105":
|
||||
return HalfYearly;
|
||||
case "106":
|
||||
return Yearly;
|
||||
default:
|
||||
throw new RuntimeException("未知的东财时间粒度 " + eastMoneyCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为益盟时间粒度
|
||||
* @return
|
||||
*/
|
||||
public Integer getEmoneyCode() {
|
||||
|
||||
if (this.min != null) {
|
||||
return this.min;
|
||||
}
|
||||
|
||||
switch(this) {
|
||||
case Daily:
|
||||
return 10000;
|
||||
case Weekly:
|
||||
return 20000;
|
||||
case Monthly:
|
||||
return 30000;
|
||||
case Seasonly:
|
||||
return 40000;
|
||||
case HalfYearly:
|
||||
return 50000;
|
||||
case Yearly:
|
||||
return 60000;
|
||||
default:
|
||||
throw new RuntimeException("益盟时间粒度不支持:" + this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 从益盟时间粒度转换
|
||||
* @param emoneyDataPeriod
|
||||
* @return
|
||||
*/
|
||||
public static StockSpan fromEmoneyCode(Integer emoneyDataPeriod) {
|
||||
switch (emoneyDataPeriod) {
|
||||
case 1:
|
||||
return Minute;
|
||||
case 5:
|
||||
return Minute5;
|
||||
case 15:
|
||||
return Minute15;
|
||||
case 30:
|
||||
return Minute30;
|
||||
case 60:
|
||||
return Minute60;
|
||||
case 120:
|
||||
return Minute120;
|
||||
case 10000:
|
||||
return Daily;
|
||||
case 20000:
|
||||
return Weekly;
|
||||
case 30000:
|
||||
return Monthly;
|
||||
case 40000:
|
||||
return Seasonly;
|
||||
case 50000:
|
||||
return HalfYearly;
|
||||
case 60000:
|
||||
return Yearly;
|
||||
default:
|
||||
throw new RuntimeException("未知的益盟时间粒度 " + emoneyDataPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换成开盘啦数据粒度
|
||||
* @return
|
||||
*/
|
||||
public String getKaipanlaType() {
|
||||
String type;
|
||||
if (this == StockSpan.Yearly) {
|
||||
type = "y";
|
||||
}
|
||||
else if (this == StockSpan.Monthly) {
|
||||
type = "m";
|
||||
}
|
||||
else if (this == StockSpan.Weekly) {
|
||||
type = "w";
|
||||
}
|
||||
else if (this == StockSpan.Minute60) {
|
||||
type = "60";
|
||||
}
|
||||
else if (this == StockSpan.Minute30) {
|
||||
type = "30";
|
||||
}
|
||||
else if (this == StockSpan.Minute15) {
|
||||
type = "15";
|
||||
}
|
||||
else if (this == StockSpan.Minute5) {
|
||||
type = "5";
|
||||
}
|
||||
else if (this == StockSpan.Daily){
|
||||
type = "d";
|
||||
}
|
||||
else {
|
||||
throw new RuntimeException("开盘啦指标不支持该数据粒度 " + this);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从开盘啦时间粒度转换
|
||||
* @param kaipanlaType
|
||||
* @return
|
||||
*/
|
||||
public static StockSpan fromKaipanlaType(String kaipanlaType) {
|
||||
|
||||
switch (kaipanlaType) {
|
||||
case "5":
|
||||
return Minute5;
|
||||
case "15":
|
||||
return Minute15;
|
||||
case "30":
|
||||
return Minute30;
|
||||
case "60":
|
||||
return Minute60;
|
||||
case "d":
|
||||
return Daily;
|
||||
case "w":
|
||||
return Weekly;
|
||||
case "n":
|
||||
return Monthly;
|
||||
case "y":
|
||||
return Yearly;
|
||||
default:
|
||||
throw new RuntimeException("未知的开盘啦时间粒度 " + kaipanlaType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package quant.rich.emoney.exception;
|
||||
|
||||
public class EmoneyDecodeException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public EmoneyDecodeException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public EmoneyDecodeException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public EmoneyDecodeException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public EmoneyDecodeException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package quant.rich.emoney.exception;
|
||||
|
||||
public class EmoneyIllegalRequestParamException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public EmoneyIllegalRequestParamException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public EmoneyIllegalRequestParamException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public EmoneyIllegalRequestParamException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public EmoneyIllegalRequestParamException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package quant.rich.emoney.exception;
|
||||
|
||||
public class EmoneyRequestException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public EmoneyRequestException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public EmoneyRequestException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public EmoneyRequestException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public EmoneyRequestException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package quant.rich.emoney.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class EmoneyResponseException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Getter
|
||||
@Setter
|
||||
private Integer code;
|
||||
|
||||
public EmoneyResponseException(String message, Integer code) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package quant.rich.emoney.exception;
|
||||
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Page not found")
|
||||
public class PageNotFoundException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = -4080133008166027751L;
|
||||
|
||||
public PageNotFoundException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public PageNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
64
src/main/java/quant/rich/emoney/exception/RException.java
Normal file
64
src/main/java/quant/rich/emoney/exception/RException.java
Normal file
@@ -0,0 +1,64 @@
|
||||
package quant.rich.emoney.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 抛出该异常,返回带状态码的 R
|
||||
*
|
||||
* @author Doghole
|
||||
* @see me.qwq.doghouse.pojo.dto.R
|
||||
* @see EmoneyAutoPlatformExceptionHandler.qwq.doghouse.component.DoghouseExceptionHandler
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@AllArgsConstructor
|
||||
public class RException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = -4140809043470089887L;
|
||||
|
||||
private HttpStatus httpStatus;
|
||||
|
||||
private Boolean logRequest = false;
|
||||
|
||||
public RException(HttpStatus status) {
|
||||
super((String) null);
|
||||
this.httpStatus = status;
|
||||
}
|
||||
|
||||
public RException(HttpStatus status, String message) {
|
||||
super(message);
|
||||
this.httpStatus = status;
|
||||
}
|
||||
|
||||
public static RException badRequest() {
|
||||
return new RException(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
public static RException badRequest(String message) {
|
||||
return new RException(HttpStatus.BAD_REQUEST, message);
|
||||
}
|
||||
|
||||
public static RException unauthorized() {
|
||||
return new RException(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
public static RException unauthorized(String message) {
|
||||
return new RException(HttpStatus.UNAUTHORIZED, message);
|
||||
}
|
||||
|
||||
public static RException internalServerError() {
|
||||
return new RException(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
public static RException internalServerError(String message) {
|
||||
return new RException(HttpStatus.INTERNAL_SERVER_ERROR, message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package quant.rich.emoney.interceptor;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 后台系统身份验证拦截器 Modify by Doghole 2025/3/11
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class BaseInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
53
src/main/java/quant/rich/emoney/interfaces/ConfigInfo.java
Normal file
53
src/main/java/quant/rich/emoney/interfaces/ConfigInfo.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package quant.rich.emoney.interfaces;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 将该注解用在配置上,给后台 ConfigController 自动渲染配置页用
|
||||
*
|
||||
* @author Doghole
|
||||
*
|
||||
*/
|
||||
@Bean
|
||||
@Lazy(false)
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ConfigInfo {
|
||||
/**
|
||||
* @return 配置 field 标识,用以自动注入、持久化配置文件名
|
||||
*/
|
||||
String field() default "";
|
||||
|
||||
/**
|
||||
* @return 名称
|
||||
*/
|
||||
String name() default "配置";
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 为 true 时,最终会调用其无参构造方法,若需要填充默认值的,请在无参构造器中填充。
|
||||
* 当为 true 且无法载入配置文件而触发初始化时,若存在 ./conf/system/{field}.fallback.json 文件时,从 fallback
|
||||
* 文件中初始化。fallback 仅参与初始化,不参与持久化
|
||||
* </p>
|
||||
*
|
||||
* @return 是否初始化默认值
|
||||
*/
|
||||
boolean initDefault() default false;
|
||||
|
||||
/**
|
||||
* @return 是否提供后台控制
|
||||
*/
|
||||
boolean managed() default true;
|
||||
|
||||
/**
|
||||
* 是否持久化
|
||||
*/
|
||||
boolean save() default true;
|
||||
}
|
||||
84
src/main/java/quant/rich/emoney/interfaces/IConfig.java
Normal file
84
src/main/java/quant/rich/emoney/interfaces/IConfig.java
Normal file
@@ -0,0 +1,84 @@
|
||||
package quant.rich.emoney.interfaces;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.bean.copier.CopyOptions;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import quant.rich.emoney.service.ConfigService;
|
||||
import quant.rich.emoney.util.SpringContextHolder;
|
||||
|
||||
/**
|
||||
* 配置项必须实现该接口
|
||||
*
|
||||
* @author Doghole
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
public interface IConfig<T extends IConfig<T>> {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public default boolean saveOrUpdate() {
|
||||
return SpringContextHolder.getBean(ConfigService.class).saveOrUpdate((T)this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前处理。无论 @ConfigInfo 是否设置 save = true 都会调用
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public default T afterSaving() {
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存后处理。无论 @ConfigInfo 是否设置 save = true 都会调用
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public default T beforeSaving() {
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并到 other,且返回合并后的 other
|
||||
* @return
|
||||
*/
|
||||
public default T mergeTo(T other) {
|
||||
if (!Objects.equals(this, other)) {
|
||||
BeanUtil.copyProperties(this, other,
|
||||
CopyOptions.create().setIgnoreNullValue(true));
|
||||
}
|
||||
return other;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并其他,并返回本身
|
||||
* @param other
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public default T mergeFrom(T other) {
|
||||
if (!Objects.equals(this, other)) {
|
||||
BeanUtil.copyProperties(other, this,
|
||||
CopyOptions.create().setIgnoreNullValue(true));
|
||||
}
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化完成之后的方法,会在
|
||||
* <code><i>beanFactory</i>.autowireBean(bean)</code>
|
||||
* 和
|
||||
* <code><i>beanFactory</i>.initializeBean(bean, beanName)</code>
|
||||
* 后执行。<br>
|
||||
* @see quant.rich.emoney.config.ConfigServiceFactoryBean
|
||||
*/
|
||||
@PostConstruct
|
||||
public default void afterBeanInit() {}
|
||||
|
||||
public static class Views {
|
||||
public static class Persistence {}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package quant.rich.emoney.interfaces;
|
||||
|
||||
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;
|
||||
import quant.rich.emoney.validator.EmoneyRequestConfigValidator;
|
||||
|
||||
@Documented
|
||||
@Constraint(validatedBy = EmoneyRequestConfigValidator.class)
|
||||
@Target({ ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ValidEmoneyRequestConfig {
|
||||
String message() default "非法的 EmoneyRequestConfig";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package quant.rich.emoney.mapper.postgre;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import quant.rich.emoney.entity.postgre.EmoneyIndex;
|
||||
|
||||
@Component
|
||||
@Mapper
|
||||
@DS("postgre")
|
||||
public interface EmoneyIndexMapper extends BaseMapper<EmoneyIndex> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package quant.rich.emoney.mapper.sqlite;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import quant.rich.emoney.entity.sqlite.Plan;
|
||||
|
||||
@Component
|
||||
@Mapper
|
||||
@DS("sqlite")
|
||||
public interface PlanMapper extends BaseMapper<Plan> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package quant.rich.emoney.mapper.sqlite;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import quant.rich.emoney.entity.sqlite.ProtocolMatch;
|
||||
|
||||
@Component
|
||||
@Mapper
|
||||
@DS("sqlite")
|
||||
public interface ProtocolMatchMapper extends BaseMapper<ProtocolMatch> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package quant.rich.emoney.mybatis.typehandler;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||
import org.apache.ibatis.type.MappedTypes;
|
||||
import org.apache.ibatis.type.TypeHandler;
|
||||
|
||||
@MappedJdbcTypes(JdbcType.VARCHAR)
|
||||
@MappedTypes({List.class})
|
||||
public class CommaListTypeHandler implements TypeHandler<List<String>> {
|
||||
|
||||
@Override
|
||||
public void setParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
|
||||
String items = StringUtils.join(parameter, ',');
|
||||
try {
|
||||
ps.setString(i, items);
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getResult(ResultSet rs, String columnName) throws SQLException {
|
||||
String result = rs.getString(columnName);
|
||||
if (StringUtils.isNotBlank(result)) {
|
||||
return new ArrayList<>(Arrays.asList(result.split(",")));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
String result = rs.getString(columnIndex);
|
||||
if (StringUtils.isNotBlank(result)) {
|
||||
return new ArrayList<>(Arrays.asList(result.split(",")));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
String result = cs.getString(columnIndex);
|
||||
if (StringUtils.isNotBlank(result)) {
|
||||
return new ArrayList<>(Arrays.asList(result.split(",")));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package quant.rich.emoney.mybatis.typehandler;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||
import org.apache.ibatis.type.MappedTypes;
|
||||
import org.apache.ibatis.type.TypeHandler;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@MappedTypes(JsonNode.class)
|
||||
@MappedJdbcTypes(JdbcType.VARCHAR)
|
||||
public class JsonStringTypeHandler implements TypeHandler<JsonNode> {
|
||||
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public void setParameter(PreparedStatement ps, int i, JsonNode parameter, JdbcType jdbcType) throws SQLException {
|
||||
try {
|
||||
ps.setString(i, parameter.toString());
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode getResult(ResultSet rs, String columnName) throws SQLException {
|
||||
String result = rs.getString(columnName);
|
||||
if (StringUtils.isNotBlank(result)) {
|
||||
try {
|
||||
return mapper.readTree(result);
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode getResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
String result = rs.getString(columnIndex);
|
||||
if (StringUtils.isNotBlank(result)) {
|
||||
try {
|
||||
return mapper.readTree(result);
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode getResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
String result = cs.getString(columnIndex);
|
||||
if (StringUtils.isNotBlank(result)) {
|
||||
try {
|
||||
return mapper.readTree(result);
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
110
src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttp.java
Normal file
110
src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttp.java
Normal file
@@ -0,0 +1,110 @@
|
||||
package quant.rich.emoney.patch.okhttp;
|
||||
|
||||
import net.bytebuddy.ByteBuddy;
|
||||
import net.bytebuddy.agent.ByteBuddyAgent;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import okhttp3.Request;
|
||||
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
|
||||
import static net.bytebuddy.matcher.ElementMatchers.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
public class PatchOkHttp {
|
||||
private static final Logger log = LoggerFactory.getLogger(PatchOkHttp.class);
|
||||
private static final List<PatchOkHttpRule> rules = new CopyOnWriteArrayList<>();
|
||||
private static boolean isHooked = false;
|
||||
|
||||
public static void apply(PatchOkHttpRule.Builder builder) {
|
||||
rules.add(builder.build());
|
||||
if (!isHooked) hook();
|
||||
}
|
||||
|
||||
public static void apply(Consumer<PatchOkHttpRule.Builder> r) {
|
||||
PatchOkHttpRule.Builder builder = PatchOkHttpRule.when();
|
||||
r.accept(builder);
|
||||
rules.add(builder.build());
|
||||
if (!isHooked) hook();
|
||||
}
|
||||
|
||||
public static void match(RequestContext ctx, String currentHeader, Consumer<String> consumer) {
|
||||
for (PatchOkHttpRule rule : PatchOkHttp.rules) {
|
||||
if (rule.matches(ctx)) {
|
||||
rule.apply(ctx, currentHeader, consumer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void hook() {
|
||||
try {
|
||||
ByteBuddyAgent.install();
|
||||
|
||||
new ByteBuddy()
|
||||
.redefine(Class.forName("okhttp3.Request$Builder"))
|
||||
.visit(Advice.to(HeaderInterceptor.class)
|
||||
.on(named("header").and(takesArguments(String.class, String.class))))
|
||||
.make()
|
||||
.load(Class.forName("okhttp3.Request$Builder").getClassLoader(),
|
||||
ClassReloadingStrategy.fromInstalledAgent());
|
||||
|
||||
|
||||
isHooked = true;
|
||||
log.debug("[PatchOkHttp] Delegation hook installed.");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static class HeaderInterceptor {
|
||||
|
||||
|
||||
public static final Logger log = LoggerFactory.getLogger(HeaderInterceptor.class);
|
||||
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||
public static boolean intercept(
|
||||
@Advice.This Request.Builder builder,
|
||||
@Advice.Argument(0) String name,
|
||||
@Advice.Argument(value = 1, readOnly = false) String value,
|
||||
@Advice.Local("call") Callable<okhttp3.Request.Builder> superCall
|
||||
) throws Exception {
|
||||
|
||||
// print调试
|
||||
log.debug("[intercept] name = {}, value = {}", name, value);
|
||||
|
||||
try {
|
||||
// 获取 headers map
|
||||
|
||||
|
||||
RequestContext ctx = new RequestContext(builder);
|
||||
|
||||
String[] modified = new String[]{value};
|
||||
Consumer<String> setter = new ModifiedHeaderValueSetter(modified);
|
||||
PatchOkHttp.match(ctx, name, setter);
|
||||
|
||||
// 替换原参数值
|
||||
value = modified[0];
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ModifiedHeaderValueSetter implements Consumer<String> {
|
||||
private final String[] holder;
|
||||
public ModifiedHeaderValueSetter(String[] holder) {
|
||||
this.holder = holder;
|
||||
}
|
||||
@Override
|
||||
public void accept(String newVal) {
|
||||
holder[0] = newVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package quant.rich.emoney.patch.okhttp;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class PatchOkHttpRule {
|
||||
private final Predicate<RequestContext> condition;
|
||||
private final List<HeaderAction> actions;
|
||||
|
||||
public PatchOkHttpRule(Predicate<RequestContext> condition, List<HeaderAction> actions) {
|
||||
this.condition = condition;
|
||||
this.actions = actions;
|
||||
}
|
||||
|
||||
public boolean matches(RequestContext ctx) {
|
||||
return condition.test(ctx);
|
||||
}
|
||||
|
||||
public void apply(RequestContext ctx, String currentHeader, Consumer<String> overrideSetter) {
|
||||
for (HeaderAction action : actions) {
|
||||
action.apply(ctx, currentHeader, overrideSetter);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface HeaderAction {
|
||||
void apply(RequestContext ctx, String currentHeader, Consumer<String> overrideValueSetter);
|
||||
}
|
||||
|
||||
/**
|
||||
* 起始语句,创建一个 Builder
|
||||
* @return
|
||||
*/
|
||||
public static Builder when() {
|
||||
return new Builder(ctx -> true);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private Predicate<RequestContext> condition;
|
||||
private final List<HeaderAction> actions = new ArrayList<>();
|
||||
|
||||
public Builder(Predicate<RequestContext> initial) {
|
||||
this.condition = initial;
|
||||
}
|
||||
|
||||
public Builder hasHeaderPair(String name, String value) {
|
||||
return and(ctx -> value.equals(ctx.headers.get(name)));
|
||||
}
|
||||
|
||||
public Builder hasHeaderName(String name) {
|
||||
return and(ctx -> ctx.headers.containsKey(name));
|
||||
}
|
||||
|
||||
public Builder hasHeaderValueMatch(String name, String regex) {
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
return and(ctx -> {
|
||||
String val = ctx.headers.get(name);
|
||||
return val != null && pattern.matcher(val).matches();
|
||||
});
|
||||
}
|
||||
|
||||
public Builder hostEndsWith(String suffix) {
|
||||
return and(ctx -> ctx.host != null && ctx.host.endsWith(suffix));
|
||||
}
|
||||
|
||||
public Builder hostContains(String keyword) {
|
||||
return and(ctx -> ctx.host != null && ctx.host.contains(keyword));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param pattern
|
||||
* @return
|
||||
*/
|
||||
public Builder hostMatches(String pattern) {
|
||||
return and(ctx -> ctx.host != null && Pattern.matches(pattern, ctx.host));
|
||||
}
|
||||
|
||||
public Builder isHttp() {
|
||||
return and(ctx -> ctx.scheme.equalsIgnoreCase("http"));
|
||||
}
|
||||
|
||||
public Builder isHttps() {
|
||||
return and(ctx -> ctx.scheme.equalsIgnoreCase("https"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收一个 not 条件<br>
|
||||
* 例:Rule.when().not(r -> r.isHttps()).build()
|
||||
*
|
||||
* @param block
|
||||
* @return
|
||||
*/
|
||||
public Builder not(Consumer<Builder> block) {
|
||||
Builder sub = new Builder(ctx -> true); // starts with true
|
||||
block.accept(sub);
|
||||
this.condition = this.condition
|
||||
.and(sub.condition.negate());
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder or(Consumer<Builder> block) {
|
||||
Builder sub = new Builder(ctx -> true);
|
||||
block.accept(sub);
|
||||
this.condition = this.condition.or(sub.condition);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder or(Builder other) {
|
||||
this.condition = this.condition.or(other.condition);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder or(PatchOkHttpRule rule) {
|
||||
this.condition = this.condition.or(rule::matches);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder and() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder and(Predicate<RequestContext> other) {
|
||||
this.condition = this.condition.and(other);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder then() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setHeader(String name, String value) {
|
||||
actions.add((ctx, curr, setter) -> ctx.headers.put(name, value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder overrideIf(String headerName, String value) {
|
||||
actions.add((ctx, curr, setter) -> {
|
||||
if (curr.equalsIgnoreCase(headerName)) {
|
||||
setter.accept(value);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public PatchOkHttpRule build() {
|
||||
return new PatchOkHttpRule(condition, actions);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package quant.rich.emoney.patch.okhttp;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import okhttp3.Request;
|
||||
|
||||
public class RequestContext {
|
||||
public final Map<String, String> headers;
|
||||
public final String scheme;
|
||||
public final String host;
|
||||
public final Request.Builder builder;
|
||||
|
||||
public RequestContext(Request.Builder builder) {
|
||||
this.builder = builder;
|
||||
List<String> nvList = builder.getHeaders$okhttp().getNamesAndValues$okhttp();
|
||||
|
||||
Map<String, String> headerMap = new HashMap<>();
|
||||
for (int i = 0; i < nvList.size(); i += 2) {
|
||||
headerMap.put(nvList.get(i), nvList.get(i + 1));
|
||||
}
|
||||
this.headers = headerMap;
|
||||
|
||||
String url = builder.getUrl$okhttp().toString();
|
||||
this.scheme = url.startsWith("https") ? "https" : "http";
|
||||
this.host = url.replaceFirst("https?://", "").split("/")[0];
|
||||
}
|
||||
|
||||
public RequestContext(Map<String, String> headers, String scheme, String host) {
|
||||
this.builder = null;
|
||||
this.headers = headers;
|
||||
this.scheme = scheme;
|
||||
this.host = host;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package quant.rich.emoney.pojo.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@EqualsAndHashCode(callSuper=false)
|
||||
public class EmoneyConvertResult extends R<Object> {
|
||||
|
||||
private Integer protocolId;
|
||||
private String supposedClassName;
|
||||
|
||||
public static EmoneyConvertResult error(String msg) {
|
||||
return (EmoneyConvertResult) new EmoneyConvertResult()
|
||||
.setResultCode(HttpStatus.BAD_REQUEST.value())
|
||||
.setMessage(msg)
|
||||
.setData(null);
|
||||
}
|
||||
|
||||
public static EmoneyConvertResult ok(Serializable result) {
|
||||
return (EmoneyConvertResult) new EmoneyConvertResult()
|
||||
.setResultCode(HttpStatus.OK.value())
|
||||
.setMessage("")
|
||||
.setData(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package quant.rich.emoney.pojo.dto;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class EmoneyProtobufBody {
|
||||
|
||||
private Integer protocolId = null;
|
||||
private String protocolBody = null;
|
||||
private String className = null;
|
||||
|
||||
public byte[] protocolBodyToByte() {
|
||||
if (protocolBody == null) {
|
||||
return null;
|
||||
}
|
||||
return Base64.getDecoder().decode(protocolBody);
|
||||
}
|
||||
|
||||
}
|
||||
43
src/main/java/quant/rich/emoney/pojo/dto/LayPageReq.java
Normal file
43
src/main/java/quant/rich/emoney/pojo/dto/LayPageReq.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package quant.rich.emoney.pojo.dto;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 后台管理分页查询,适配 layui
|
||||
* @author Doghole
|
||||
*
|
||||
* @param <T>
|
||||
*/
|
||||
@Accessors(chain = true)
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper=false)
|
||||
public class LayPageReq<T> extends Page<T> {
|
||||
|
||||
private static final long serialVersionUID = 3471995935637905622L;
|
||||
|
||||
public LayPageReq<T> setPage(Long page) {
|
||||
current = page;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Long getPage() {
|
||||
return current;
|
||||
}
|
||||
|
||||
public LayPageReq<T> setLimit(Long limit) {
|
||||
size = limit;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Long getLimit() {
|
||||
return size;
|
||||
}
|
||||
|
||||
Integer start;
|
||||
|
||||
T condition;
|
||||
}
|
||||
41
src/main/java/quant/rich/emoney/pojo/dto/LayPageResp.java
Normal file
41
src/main/java/quant/rich/emoney/pojo/dto/LayPageResp.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package quant.rich.emoney.pojo.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 后台查询结果,适配 layui
|
||||
* @author Doghole
|
||||
**/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class LayPageResp<T> implements Serializable {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 3706125181722544872L;
|
||||
|
||||
//状态码
|
||||
private int code;
|
||||
|
||||
//提示消息
|
||||
private String msg;
|
||||
|
||||
//总条数
|
||||
private long count;
|
||||
|
||||
//表格数据
|
||||
private List<T> data;
|
||||
|
||||
public LayPageResp() {}
|
||||
|
||||
public LayPageResp(IPage<T> page) {
|
||||
this.data = page.getRecords();
|
||||
this.count = page.getTotal();
|
||||
}
|
||||
}
|
||||
223
src/main/java/quant/rich/emoney/pojo/dto/R.java
Normal file
223
src/main/java/quant/rich/emoney/pojo/dto/R.java
Normal file
@@ -0,0 +1,223 @@
|
||||
package quant.rich.emoney.pojo.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.exception.RException;
|
||||
|
||||
/**
|
||||
* application/json 返回
|
||||
* <p>
|
||||
* 此处返回若非提前指定,虽然 resultCode 可以设置对应 HttpStatus, 但 Response 的 HttpStatus 依然会是
|
||||
* 200.<br>
|
||||
* 需要同时指定 HttpStatus 的,见 {@code RException}
|
||||
* </p>
|
||||
*
|
||||
* @see me.qwq.doghouse.exception.RException
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Slf4j
|
||||
public class R<T> implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -7658884488696622015L;
|
||||
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
private int resultCode;
|
||||
private String message;
|
||||
private boolean ok;
|
||||
private T data;
|
||||
|
||||
public R() {
|
||||
}
|
||||
|
||||
public R(int resultCode, String message) {
|
||||
this.resultCode = resultCode;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否成功
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean isOk() {
|
||||
return HttpStatus.OK.value() == getResultCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对当前 R<T> 类型的 T 数据调用 toString(),并将其结果作为生成的 R<String> 的 data, 返回一个相应的
|
||||
* R<String> 对象
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public R<String> toStringifyR() {
|
||||
return new R<String>().setResultCode(resultCode).setMessage(message).setData(data.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回指定状态且 data 为空(类型为 String)的 R
|
||||
*
|
||||
* @param constants
|
||||
* @return
|
||||
*/
|
||||
public static R<String> status(HttpStatus constants) {
|
||||
return new R<String>().setResultCode(constants.value()).setMessage(constants.getReasonPhrase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回指定状态和 data 的 R
|
||||
*
|
||||
* @param constants
|
||||
* @return
|
||||
*/
|
||||
public static <T> R<T> status(HttpStatus constants, T data) {
|
||||
return new R<T>().setResultCode(constants.value()).setMessage(constants.getReasonPhrase()).setData(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 HttpStatus.OK 状态和指定的 Data
|
||||
*
|
||||
* @param <T>
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
public static <T> R<T> ok(T data) {
|
||||
return status(HttpStatus.OK, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 HttpStatus.OK
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static R<?> ok() {
|
||||
return new R<>().setResultCode(HttpStatus.OK.value());
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 HttpStatus.UNAUTHORIZED 状态和指定的 Data
|
||||
*
|
||||
* @param <T>
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
public static <T> R<T> unauthorized(T data) {
|
||||
return status(HttpStatus.UNAUTHORIZED, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 HttpStatus.UNAUTHORIZED
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static R<?> unauthorized() {
|
||||
return status(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 HttpStatus.BAD_REQUEST 状态和指定的 Data
|
||||
*
|
||||
* @param <T>
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
public static <T> R<T> badRequest(T data) {
|
||||
return status(HttpStatus.BAD_REQUEST, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 HttpStatus.BAD_REQUEST
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static R<?> badRequest() {
|
||||
return status(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 HttpStatus.INTERNAL_SERVER_ERROR 状态和指定的 Data
|
||||
*
|
||||
* @param <T>
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
public static <T> R<T> internalServerError(T data) {
|
||||
return status(HttpStatus.INTERNAL_SERVER_ERROR, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 HttpStatus.INTERNAL_SERVER_ERROR
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static R<?> internalServerError() {
|
||||
return status(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据布尔值判断返回<br>
|
||||
* true 返回 R.ok()<br>
|
||||
* false throw RException.badRequest()
|
||||
* @param condition
|
||||
* @return
|
||||
*/
|
||||
public static R<?> judge(boolean condition) {
|
||||
if (condition) return R.ok();
|
||||
throw RException.badRequest();
|
||||
}
|
||||
|
||||
|
||||
public static R<?> judgeNonNull(Object obj) {
|
||||
if (obj != null) return R.ok(obj);
|
||||
throw RException.badRequest();
|
||||
}
|
||||
|
||||
|
||||
public static R<?> judge(ThrowingSupplier<?> supplier) {
|
||||
try {
|
||||
return R.ok(supplier.get());
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw RException.badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static R<?> judgeThrow(ThrowingSupplier<?> supplier) throws Exception {
|
||||
return R.ok(supplier.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据布尔值判断返回<br>
|
||||
* true 返回 R.ok(T okData)<br>
|
||||
* false throw RException.badRequest(failedMessage)
|
||||
* @param condition
|
||||
* @return
|
||||
*/
|
||||
public static <T> R<?> judge(boolean condition, T okData, String failedMessage) {
|
||||
if (condition) return R.ok(okData);
|
||||
throw RException.badRequest(failedMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据布尔值判断返回<br>
|
||||
* true 返回 R.ok(null)<br>
|
||||
* false throw RException.badRequest(failedMessage)
|
||||
* @param condition
|
||||
* @return
|
||||
*/
|
||||
public static R<?> judge(boolean condition, String failedMessage) {
|
||||
return R.judge(condition, null, failedMessage);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public static interface ThrowingSupplier<T> {
|
||||
T get() throws Exception;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package quant.rich.emoney.pojo.dto.valid;
|
||||
|
||||
import org.quartz.CronExpression;
|
||||
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
|
||||
public class CronExpressionValidator implements ConstraintValidator<ValidCron, String> {
|
||||
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return CronExpression.isValidExpression(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package quant.rich.emoney.pojo.dto.valid;
|
||||
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
import quant.rich.emoney.entity.sqlite.Plan;
|
||||
|
||||
public class PlanValidator implements ConstraintValidator<ValidPlan, Plan> {
|
||||
|
||||
@Override
|
||||
public boolean isValid(Plan value, ConstraintValidatorContext context) {
|
||||
// TODO Auto-generated method stub
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package quant.rich.emoney.pojo.dto.valid;
|
||||
|
||||
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 jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
@Documented
|
||||
@Constraint(validatedBy = CronExpressionValidator.class)
|
||||
@Target({ ElementType.FIELD, ElementType.PARAMETER })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ValidCron {
|
||||
|
||||
String message() default "非法的 Cron 表达式";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package quant.rich.emoney.pojo.dto.valid;
|
||||
|
||||
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 jakarta.validation.Constraint;
|
||||
|
||||
@Documented
|
||||
@Constraint(validatedBy = PlanValidator.class)
|
||||
@Target({ ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ValidPlan {
|
||||
|
||||
}
|
||||
180
src/main/java/quant/rich/emoney/protocol/ProtocolID.java
Normal file
180
src/main/java/quant/rich/emoney/protocol/ProtocolID.java
Normal file
@@ -0,0 +1,180 @@
|
||||
package quant.rich.emoney.protocol;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
/**
|
||||
* 支持的 ProtocolIDs<br/>
|
||||
* 见:sources\cn\emoney\acg\data\protocol\ProtocolIDs.java
|
||||
*
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
public enum ProtocolID {
|
||||
|
||||
DAILY_STRATEGY(9200), MARKET_CHANGES(9901),
|
||||
// MARKET_SITUATION(9000),
|
||||
OPERATION_POOL(9500), PRESELECTION_POOL(9600), SECTOR_LABEL(9100), STRATEGY_GROUP_POOL(9700, "StrategyStock"),
|
||||
STRATEGY_HEROES(9300), STRATEGY_MARK(9400), STRATEGY_SELECTION(9900),
|
||||
|
||||
BLOCK_INDUSTRY_CHAIN(20800), BLOCK_STOCK_DESC(20700), BLOCK_STRONG_THEME(20900), THEME_BLOCK(2103),
|
||||
TIANJI_STOCK_HOLD(20600),
|
||||
|
||||
GET_RECORD(4011), SET_RECORD(4311),
|
||||
|
||||
BIG_ORDER(8200), BLOCK_ATTACK(8002), BLOCK_INDEX(9000), BLOCK_QZDJ(8003), CAPITAL_RADAR(8300),
|
||||
DAILY_OPPORTUNITY(8800), GROUP_DETAIL(9100), INDEX_INFLOW(8401), INDEX_TREND(8400), LIMIT_COMPARISON(8700),
|
||||
LONG_TERM_OPPORTUNITY(8801), SECTOR_ATTACK(8000), STOCK_PICKING(8100), TRADE_KALENDAR(8501), UPDOWN_STATISTIC(8600),
|
||||
UPDOWN_STATISTIC_DATE(8602), UPDOWN_STATISTIC_PACK(8601, "UpsAndDownsStatistics"),
|
||||
|
||||
FUNDS_DX(2101),
|
||||
|
||||
HAOGU_CARD_SUMMARY(5001), HAOGU_TYPE_CHARGE(5100), HAOGU_TYPE_NORMAL(5000), HAOGU_TYPE_REASON(5300),
|
||||
HAOGU_TYPE_SIMPLE(5200), HAOGU_TYPE_SIMPLE_2(5400), HAOGU_UPDATE_SUMMARY(5500),
|
||||
|
||||
MIXHOME_BK_AND_ZJ(10000), MIXHOME_BK_AND_ZJ_EX(10001), MIXHOME_INDEX_CHART(10100), NET_FLOW_PREVIEW(10200),
|
||||
|
||||
MS_BALANCE_AND_INDEX_TREND(20200), MS_RANK_DETAIL(20300), MS_RANK_LIST(20400), MS_VALID_DATE(20000),
|
||||
|
||||
TREND_LINE_KCB(2303),
|
||||
|
||||
BARGAIN_DATA(3100), BUYING_SELLING(3000), BUYING_SELLING_HIS(3002), BUYING_SELLING_LEV2(3001), CANDLE_STICK(2420),
|
||||
CANDLE_STICK_V2(2421), CANDLE_STICK_V3(2422, "CandleStickWithIndex"), CODE_LIST_UPDATE(2001, "CodeListUpdate"),
|
||||
GLOBAL_LIST(2700),
|
||||
// GoodsTable(2000),
|
||||
HGT_AMOUNT(2800), HGT_AMOUNT_HISTORY_TREND(2802), HGT_AMOUNT_TREND(2801), HGT_HISTORY_TURNOVER(2804),
|
||||
HGT_INSTITUTIONAL_SHARE_HOLDING(5607), HGT_INSTITUTIONAL_TRENDS(5606), HGT_TOP_BUY(5601), HGT_TOP_CONCEPT(5605),
|
||||
HGT_TOP_INDUSTRY(5604), HGT_TOP_SELL(5602), HGT_TOP_SHARE(5603), HGT_TOP_TEN(5600), HGT_TREND_TURNOVER(2805),
|
||||
INDEX_CALC(2920), INDEX_CALC_V2(2921, "IndexCalc"), MATRIXDATA(2500), NET_INFLOW(3200), NET_INFLOW_V2(3201),
|
||||
QUOTE_TICK_DETAIL(3700), RANK_LIST(2600),
|
||||
|
||||
SORT_LIST(2100, "SortedList", (ObjectNode jo) -> {
|
||||
if (jo.has("requestTypeCase")) {
|
||||
int requestTypeCase = jo.get("requestTypeCase").asInt();
|
||||
/*
|
||||
* 1 - system 2 - custom 3 - goods 4 - group
|
||||
*/
|
||||
if (requestTypeCase == 1) {
|
||||
// jo.remove("system");
|
||||
jo.remove("custom");
|
||||
jo.remove("goods");
|
||||
jo.remove("group");
|
||||
} else if (requestTypeCase == 2) {
|
||||
jo.remove("system");
|
||||
// jo.remove("custom");
|
||||
jo.remove("goods");
|
||||
jo.remove("group");
|
||||
} else if (requestTypeCase == 3) {
|
||||
jo.remove("system");
|
||||
jo.remove("custom");
|
||||
// jo.remove("goods");
|
||||
jo.remove("group");
|
||||
} else if (requestTypeCase == 4) {
|
||||
jo.remove("system");
|
||||
jo.remove("custom");
|
||||
jo.remove("goods");
|
||||
// jo.remove("group");
|
||||
}
|
||||
}
|
||||
return jo;
|
||||
}),
|
||||
|
||||
THEME_FUND(2102), TIME_STATUS(3300), TREND_BASIS(3600), TREND_INDEX(3400), TREND_LINE(2300),
|
||||
TREND_LINE_EXTEND(2302), TREND_LINE_FS_HISTORY(2305), TREND_LINE_FS_OUTER(2306), TREND_LINE_FS_SIMPLE(2304),
|
||||
TREND_LINE_WITH_TRANSATION(2301), VALUE_DATA(2200),
|
||||
|
||||
OPTIONAL_GET_CLOUD(4001, "GetGroupGoods"), OPTIONAL_GET_GROUP(4010), OPTIONAL_GROUP_GET(4110, "GetGoodsGroup"),
|
||||
OPTIONAL_GROUP_SET(4210), OPTIONAL_SET(4310),
|
||||
|
||||
GET_MERGE_CODE_LIST(3900), GUESS(4400), SET_MERGE_CODE_LIST(3800),
|
||||
|
||||
GET_ALARM(4601), SET_ALARM(4501),
|
||||
|
||||
GET_ALL_GOODS_RECORD(4901), GET_FIRST_GOODS_RECORD(4902), GET_GOODS_RECORD(4701), SET_GOODS_RECORD(4801),
|
||||
|
||||
SHORT_ALARMS(10301),
|
||||
|
||||
CANCEL_ENTRUST(6400), QRY_BUSINESS(6700, "Business"), QRY_ENTRUST(6600), QUERY_STOCK_OPERABLE_LIMITS(6300),
|
||||
SIMULATE_ACCOUNT(5900, "VTradeAccount"), SIMULATE_ACCOUNT_DETAIL(6000), SIMULATE_BANK_DETAIL(6800),
|
||||
SIMULATE_HIS_YIELD(6100), SIMULATE_ORDER_ENTRUST(6200), SIMULATE_POSITIONS(6500), SIMULATE_RESET_ACC(5901),
|
||||
SIMULATE_RESET_RECODE(5902), SIMULATE_TRADE_STAT(6701), SIMULATE_TRANSFER(6900), SIMULATE_TRANSFER_FLOW(5800),
|
||||
|
||||
MOTIF_GROUP_CONSTITUENT_STOCK(7300), MOTIF_GROUP_CREATOR_DETAIL(7500), MOTIF_GROUP_DETAIL(7100),
|
||||
MOTIF_GROUP_EXCHANGE_RECORD(7400), MOTIF_GROUP_FOLLOW(7600), MOTIF_GROUP_INCOME_TREND(7200), MOTIF_GROUP_LIST(7000),
|
||||
MOTIF_GROUP_PRAISE(7700);
|
||||
|
||||
private Integer protocolId;
|
||||
private String requestClassPrefix = null;
|
||||
private Function<ObjectNode, ObjectNode> beforeDeserialize = null;
|
||||
|
||||
public Integer getProtocolId() {
|
||||
return protocolId;
|
||||
}
|
||||
|
||||
private ProtocolID(int protocolId) {
|
||||
this.protocolId = protocolId;
|
||||
String[] nameParts = this.name().toLowerCase().split("_");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String namePart : nameParts) {
|
||||
sb.append(firstToUpper(namePart));
|
||||
}
|
||||
requestClassPrefix = sb.toString();
|
||||
}
|
||||
|
||||
private ProtocolID(int protocolId, String requestClassPrefix) {
|
||||
this.protocolId = protocolId;
|
||||
this.requestClassPrefix = requestClassPrefix;
|
||||
}
|
||||
|
||||
private ProtocolID(int protocolId, String requestClassPrefix, Function<ObjectNode, ObjectNode> beforeDeserialize) {
|
||||
this(protocolId, requestClassPrefix);
|
||||
this.beforeDeserialize = beforeDeserialize;
|
||||
}
|
||||
|
||||
public static ProtocolID fromValue(int protocolId) {
|
||||
for (ProtocolID p : ProtocolID.values()) {
|
||||
if (p.getProtocolId() == protocolId) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String toRequestClassName() {
|
||||
|
||||
String requestOrResponse = "Request";
|
||||
StringBuilder finalSb = new StringBuilder("nano.");
|
||||
finalSb.append(requestClassPrefix).append(requestOrResponse).append("$").append(requestClassPrefix).append("_")
|
||||
.append(requestOrResponse);
|
||||
;
|
||||
|
||||
return finalSb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化之前清洗
|
||||
*
|
||||
* @param jo
|
||||
* @return
|
||||
*/
|
||||
public JsonNode beforeDeserialization(ObjectNode jo) {
|
||||
if (beforeDeserialize == null)
|
||||
return jo;
|
||||
return beforeDeserialize.apply(jo);
|
||||
}
|
||||
|
||||
private static String firstToUpper(String s) {
|
||||
if (StringUtils.isNotEmpty(s)) {
|
||||
char[] cs = s.toCharArray();
|
||||
if (cs[0] >= 'a' && cs[0] <= 'z') {
|
||||
cs[0] -= 32;
|
||||
}
|
||||
return String.valueOf(cs);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
60
src/main/java/quant/rich/emoney/service/AuthService.java
Normal file
60
src/main/java/quant/rich/emoney/service/AuthService.java
Normal file
@@ -0,0 +1,60 @@
|
||||
package quant.rich.emoney.service;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import quant.rich.emoney.entity.config.PlatformConfig;
|
||||
import quant.rich.emoney.util.SpringContextHolder;
|
||||
|
||||
@Service
|
||||
public class AuthService implements UserDetailsService {
|
||||
|
||||
public static final String LOGIN_USER = "loginUser";
|
||||
|
||||
public static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
|
||||
|
||||
public static final String CAPTCHA = "captcha";
|
||||
|
||||
@Autowired
|
||||
PlatformConfig platformConfig;
|
||||
|
||||
@Autowired
|
||||
HttpSession session;
|
||||
|
||||
public Boolean isLogin() {
|
||||
return session != null && session.getAttribute(SPRING_SECURITY_CONTEXT) != null;
|
||||
}
|
||||
|
||||
public void setLogin(String username, String password) {
|
||||
AuthenticationManager authenticationManager = SpringContextHolder.getBean("authenticationManager");
|
||||
Authentication authentication = authenticationManager.authenticate(
|
||||
new UsernamePasswordAuthenticationToken(username, password)
|
||||
);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
session.setAttribute(SPRING_SECURITY_CONTEXT, SecurityContextHolder.getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
if (StringUtils.isNotBlank(username) && username.equals(platformConfig.getUsername())) {
|
||||
return
|
||||
User.withUsername(username)
|
||||
.password(platformConfig.getPassword())
|
||||
.roles("admin")
|
||||
.build();
|
||||
}
|
||||
throw new UsernameNotFoundException("用户不存在");
|
||||
}
|
||||
|
||||
}
|
||||
370
src/main/java/quant/rich/emoney/service/ConfigService.java
Normal file
370
src/main/java/quant/rich/emoney/service/ConfigService.java
Normal file
@@ -0,0 +1,370 @@
|
||||
package quant.rich.emoney.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.entity.config.SmartViewWriter;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
import quant.rich.emoney.util.SpringContextHolder;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.collect.BiMap;
|
||||
import com.google.common.collect.HashBiMap;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.reflections.Reflections;
|
||||
import org.reflections.scanners.Scanners;
|
||||
import org.reflections.util.ConfigurationBuilder;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jmx.export.annotation.ManagedAttribute;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 配置类服务
|
||||
*
|
||||
* @author Doghole
|
||||
* @see quant.rich.emoney.config.ConfigAutoRegistrar
|
||||
* @see quant.rich.emoney.config.ConfigServiceFactoryBean
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ConfigService implements InitializingBean {
|
||||
|
||||
@Autowired
|
||||
Reflections reflections;
|
||||
|
||||
static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
static {
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从
|
||||
* <b><code> field </code></b>
|
||||
* 到<b><code> ConfigClass </code></b>的<b>一对一</b>映射关系<br>
|
||||
* 例: <code>"deviceInfo" <--> DeviceInfoConfig.class</code><br>
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
BiMap<String, Class<? extends IConfig>> fieldClassCache = HashBiMap.create();
|
||||
|
||||
/**
|
||||
* 从
|
||||
* <b><code>ConfigClass</code></b>
|
||||
* 到对应实例化对象的映射关系。可以理解为对应类的运行时缓存。
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
private Map<Class<? extends IConfig>, Object> classObjectCache = new HashMap<>();
|
||||
|
||||
static final Pattern FIELD_PATTERN = Pattern.compile("^[a-zA-Z_$][a-zA-Z0-9_$]+$");
|
||||
|
||||
/**
|
||||
* 初始化各项配置缓存,只有添加了 @ConfigInfo 的配置类才会被识别并缓存
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@PostConstruct
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
|
||||
if (reflections == null) {
|
||||
reflections = new Reflections(new ConfigurationBuilder()
|
||||
.addScanners(Scanners.MethodsAnnotated, Scanners.SubTypes, Scanners.TypesAnnotated)
|
||||
.forPackages("quant.rich.emoney"));
|
||||
}
|
||||
|
||||
Set<Class<?>> configClasses = reflections.getTypesAnnotatedWith(ConfigInfo.class);
|
||||
|
||||
for (Class<?> clazz : configClasses) {
|
||||
ConfigInfo info = clazz.getAnnotation(ConfigInfo.class);
|
||||
String field = info.field();
|
||||
if (StringUtils.isEmpty(field)) {
|
||||
// 如果 ConfigInfo 注解上 field() 为空,则默认从类名初始化该 field
|
||||
// 规则是该配置类名必须以 Config 结尾,否则不合法
|
||||
field = clazz.getSimpleName();
|
||||
int lastIndexOfConfig = field.lastIndexOf("Config");
|
||||
if (lastIndexOfConfig == -1) {
|
||||
log.warn(
|
||||
"Annotation @ConfigInfo doesn't set a field, "
|
||||
+ "so try to init it from className \"{}\", but "
|
||||
+ "it doesn't end with \"Config\", which is invalid, so ignore.", field);
|
||||
continue;
|
||||
}
|
||||
field = field.substring(0, lastIndexOfConfig);
|
||||
field = firstToLower(field);
|
||||
}
|
||||
else if (!FIELD_PATTERN.matcher(field).matches()) {
|
||||
// field 必须是合法的 Java 变量名
|
||||
log.warn("Invalid field name \"{}\", it cannot be a legal java variable name, so ignore.", field);
|
||||
continue;
|
||||
}
|
||||
if (fieldClassCache.containsKey(field)) {
|
||||
Class<? extends IConfig<?>> existedClass =
|
||||
(Class<? extends IConfig<?>>) fieldClassCache.get(field);
|
||||
log.warn("Found class {} with expected name {} which is already assigned to class {}, ignored.",
|
||||
clazz.getName(), field, existedClass.getName());
|
||||
continue;
|
||||
}
|
||||
Class<? extends IConfig<?>> configClass = (Class<? extends IConfig<?>>) clazz;
|
||||
fieldClassCache.put(field, configClass);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 Config 的类名在缓存字典中取其值,这里偷懒了用了运行时缓存,您也可以根据需要换成 Redis 缓存
|
||||
*
|
||||
* @param <Config> 实现了接口 ConfigInterface 的网站配置类泛型
|
||||
* @param clazz 实现了接口 ConfigInterface 的网站配置类
|
||||
* @return <T>
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private <Config extends IConfig<Config>> Config getCache(Class<Config> clazz) {
|
||||
return (Config) classObjectCache.get(clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将给定的 Config 的实体类缓存到缓存字典中, 这里偷懒了用了运行时缓存, 您也可以根据需要换成 Redis 缓存
|
||||
*
|
||||
* @param <Config> 实现了接口 ConfigInterface 的网站配置类泛型
|
||||
* @param config 实现了接口 ConfigInterface 的网站配置类的实例化对象
|
||||
* @return
|
||||
*/
|
||||
private <Config extends IConfig<Config>> void setCache(Config config) {
|
||||
classObjectCache.put(config.getClass(), config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注释在继承了 ConfigInterface 上的接口
|
||||
*
|
||||
* @param <Config>
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <Config extends IConfig<Config>> ConfigInfo getConfigInfo(Config config) {
|
||||
return getConfigInfo(config.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注释在继承了 ConfigInterface 上的接口
|
||||
*
|
||||
* @param <Config>
|
||||
* @param clazz
|
||||
* @return
|
||||
*/
|
||||
public static <Config extends IConfig<Config>> ConfigInfo getConfigInfo(Class<Config> clazz) {
|
||||
ConfigInfo info = clazz.getAnnotation(ConfigInfo.class);
|
||||
if (info == null) {
|
||||
throw new RuntimeException("Cannot get config info of " + clazz.toString()
|
||||
+ ", please check if @ConfigInfo annotation is set correctly");
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过注解 @ConfigInfo 的 field 获取其本身
|
||||
*
|
||||
* @param field
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public ConfigInfo getConfigInfoByField(String field) {
|
||||
return getConfigInfo(fieldClassCache.get(field));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过注解 @ConfigInfo 的 field 从缓存中获取 Config 的类型
|
||||
*
|
||||
* @param field
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends IConfig<T>> Class<T> getConfigClassByField(String field) {
|
||||
return (Class<T>) fieldClassCache.get(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Config
|
||||
* <p>
|
||||
* 当缓存中有时从缓存取, 缓存没有时从数据库取并更新到缓存, 数据库也没有时, 如果指定的 Config 的 @ConfigInfo
|
||||
* 注解开启了 initDefault = true, 则尝试返回一个初始 Config,否则返回 null
|
||||
*
|
||||
* @param <Config>
|
||||
* @param clazz
|
||||
* @return
|
||||
*/
|
||||
public <Config extends IConfig<Config>> Config getConfig(Class<Config> clazz) {
|
||||
if (classObjectCache.containsKey(clazz)) {
|
||||
try {
|
||||
return getCache(clazz);
|
||||
} catch (Exception e) {
|
||||
log.warn("Cannot get config info of " + clazz.toString() + " from cache, try to read from database", e);
|
||||
}
|
||||
}
|
||||
String field = fieldClassCache.inverse().get(clazz);
|
||||
// 和 doghouse 不同,配置不是从数据库拿的,而是从本地文件系统
|
||||
Config config = getOrCreateConfig(field);
|
||||
if (config == null) {
|
||||
throw new RuntimeException("Cannot get or create config field named " + field);
|
||||
}
|
||||
setCache(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化 Config,同时设置缓存、更新注入
|
||||
*
|
||||
* @param <Config>
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
@ManagedAttribute
|
||||
public <Config extends IConfig<Config>> boolean saveOrUpdate(Config config) {
|
||||
config.beforeSaving();
|
||||
String field = fieldClassCache.inverse().get(config.getClass());
|
||||
ConfigInfo info = getConfigInfoByField(field);
|
||||
|
||||
String configJoString;
|
||||
SmartViewWriter writer = new SmartViewWriter();
|
||||
configJoString = writer.writeWithSmartView(config, IConfig.Views.Persistence.class);
|
||||
|
||||
if (info.save()) {
|
||||
try {
|
||||
String filePath = getConfigFilePath(field, false);
|
||||
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;
|
||||
}
|
||||
}
|
||||
config.afterSaving();
|
||||
setCache(config);
|
||||
SpringContextHolder.updateBean(field + "Config", config);
|
||||
return true;
|
||||
}
|
||||
|
||||
private <Config extends IConfig<Config>> Config getFromFile(String path, Class<Config> configClass) {
|
||||
String configString;
|
||||
Config config = null;
|
||||
try {
|
||||
configString = Files.readString(Path.of(path), Charset.defaultCharset());
|
||||
} catch (IOException e) {
|
||||
String field = fieldClassCache.inverse().get(configClass);
|
||||
log.warn("Cannot read config {}.json: {}", field, e.getMessage());
|
||||
return config;
|
||||
}
|
||||
try {
|
||||
config = mapper.readValue(configString, configClass);
|
||||
} catch (Exception e) {
|
||||
String field = fieldClassCache.inverse().get(configClass);
|
||||
log.warn("Cannot parse configString of {} to Config", field);
|
||||
e.printStackTrace();
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 config field 获取 Config,将读取本地存储的配置,若失败则试图初始化并存储
|
||||
*
|
||||
* @param configField
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <Config extends IConfig<Config>> Config getOrCreateConfig(String field) {
|
||||
|
||||
Class<Config> configClass = (Class<Config>) fieldClassCache.get(field);
|
||||
if (configClass == null) {
|
||||
log.warn("Cannot get class info from fieldClassCache, field name: {}", field);
|
||||
return null;
|
||||
}
|
||||
ConfigInfo info = getConfigInfo(configClass);
|
||||
|
||||
Config config = null;
|
||||
|
||||
// 先从持久化路径拿
|
||||
// fallback 流程:如果持久化文件不存在或者从中反序列化失败,则走 fallback 流程
|
||||
// 如果 fallback 也失败,走初始化流程。但是初始化出的内容不参与 fallback 的持久化
|
||||
// 也就是无论如何,fallback 都不应由程序来写入
|
||||
String filePath = getConfigFilePath(field, false);
|
||||
config = getFromFile(filePath, configClass);
|
||||
if (config == null) {
|
||||
log.info("Cannot init config from local file of {}Config, try fallback", field);
|
||||
// 走 fallback 流程
|
||||
config = getFromFile(getConfigFilePath(field, true), configClass);
|
||||
}
|
||||
|
||||
if (config == null) {
|
||||
log.info("Cannot init config from fallback file of {}Config", field);
|
||||
if (info.initDefault()) {
|
||||
// 还为 null 且允许初始化默认值,则初始化一份
|
||||
log.info("Init {} by default non-args constructor", field);
|
||||
try {
|
||||
config =
|
||||
configClass.getDeclaredConstructor()
|
||||
.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
|
||||
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
|
||||
// 一般是初始化方法内出现未被捕获的错误
|
||||
log.warn("Specific class " + configClass.toString()
|
||||
+ " has @ConfigInfo annotation and enables initDefault, but init failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config == null) {
|
||||
log.warn("Cannot read or init from default/fallback for config {}Config, it will be null", field);
|
||||
}
|
||||
else {
|
||||
saveOrUpdate(config);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 config field 生成配置文件路径
|
||||
*
|
||||
* @param configField
|
||||
* @return
|
||||
*/
|
||||
private static String getConfigFilePath(String field, boolean isFallback) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("./conf/system/");
|
||||
sb.append(field);
|
||||
sb.append(isFallback ? ".fallback.json" : ".json");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 首字母小写
|
||||
*
|
||||
* @param s
|
||||
* @return
|
||||
*/
|
||||
private static String firstToLower(String s) {
|
||||
if (StringUtils.isNotEmpty(s)) {
|
||||
char[] cs = s.toCharArray();
|
||||
if (cs[0] >= 'A' && cs[0] <= 'Z') {
|
||||
cs[0] += 32;
|
||||
}
|
||||
return String.valueOf(cs);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package quant.rich.emoney.service.sqlite;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import quant.rich.emoney.entity.sqlite.Plan;
|
||||
import quant.rich.emoney.mapper.sqlite.PlanMapper;
|
||||
|
||||
@DS("sqlite")
|
||||
@Service
|
||||
public class PlanService extends SqliteServiceImpl<PlanMapper, Plan> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package quant.rich.emoney.service.sqlite;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import quant.rich.emoney.entity.sqlite.ProtocolMatch;
|
||||
import quant.rich.emoney.mapper.sqlite.ProtocolMatchMapper;
|
||||
|
||||
@DS("sqlite")
|
||||
@Service
|
||||
public class ProtocolMatchService extends SqliteServiceImpl<ProtocolMatchMapper, ProtocolMatch> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package quant.rich.emoney.service.sqlite;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import quant.rich.emoney.config.SqliteMybatisConfig;
|
||||
|
||||
/**
|
||||
* 使用 Sqlite 存储数据的 ServiceImpl,重写了部分使用 Transactional 的方法
|
||||
*/
|
||||
public abstract class SqliteServiceImpl<M extends BaseMapper<T>, T> extends ServiceImpl<M, T> {
|
||||
|
||||
@Transactional(transactionManager = SqliteMybatisConfig.SQLITE_TRANSACTION_MANAGER, rollbackFor = Exception.class)
|
||||
@Override
|
||||
public boolean saveBatch(Collection<T> entityList) {
|
||||
return super.saveBatch(entityList);
|
||||
}
|
||||
|
||||
@Transactional(transactionManager = SqliteMybatisConfig.SQLITE_TRANSACTION_MANAGER, rollbackFor = Exception.class)
|
||||
@Override
|
||||
public boolean saveBatch(Collection<T> entityList, int batchSize) {
|
||||
return super.saveBatch(entityList, batchSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(transactionManager = SqliteMybatisConfig.SQLITE_TRANSACTION_MANAGER, rollbackFor = Exception.class)
|
||||
public boolean saveOrUpdateBatch(Collection<T> entityList) {
|
||||
return super.saveOrUpdateBatch(entityList);
|
||||
}
|
||||
|
||||
@Transactional(transactionManager = SqliteMybatisConfig.SQLITE_TRANSACTION_MANAGER, rollbackFor = Exception.class)
|
||||
@Override
|
||||
public boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize) {
|
||||
return super.saveOrUpdateBatch(entityList, batchSize);
|
||||
}
|
||||
|
||||
@Transactional(transactionManager = SqliteMybatisConfig.SQLITE_TRANSACTION_MANAGER,rollbackFor = Exception.class)
|
||||
@Override
|
||||
public boolean updateBatchById(Collection<T> entityList, int batchSize) {
|
||||
return super.updateBatchById(entityList, batchSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(transactionManager = SqliteMybatisConfig.SQLITE_TRANSACTION_MANAGER,rollbackFor = Exception.class)
|
||||
public boolean removeByIds(Collection<?> list) {
|
||||
return super.removeByIds(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(transactionManager = SqliteMybatisConfig.SQLITE_TRANSACTION_MANAGER,rollbackFor = Exception.class)
|
||||
public boolean removeByIds(Collection<?> list, boolean useFill) {
|
||||
return super.removeByIds(list, useFill);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(transactionManager = SqliteMybatisConfig.SQLITE_TRANSACTION_MANAGER,rollbackFor = Exception.class)
|
||||
public boolean removeBatchByIds(Collection<?> list) {
|
||||
return super.removeBatchByIds(list, DEFAULT_BATCH_SIZE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(transactionManager = SqliteMybatisConfig.SQLITE_TRANSACTION_MANAGER,rollbackFor = Exception.class)
|
||||
public boolean removeBatchByIds(Collection<?> list, int batchSize) {
|
||||
return super.removeBatchByIds(list, batchSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(transactionManager = SqliteMybatisConfig.SQLITE_TRANSACTION_MANAGER,rollbackFor = Exception.class)
|
||||
public boolean removeBatchByIds(Collection<?> list, int batchSize, boolean useFill) {
|
||||
return super.removeBatchByIds(list, batchSize, useFill);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(transactionManager = SqliteMybatisConfig.SQLITE_TRANSACTION_MANAGER,rollbackFor = Exception.class)
|
||||
public boolean removeBatchByIds(Collection<?> list, boolean useFill) {
|
||||
return super.removeBatchByIds(list, useFill);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(transactionManager = SqliteMybatisConfig.SQLITE_TRANSACTION_MANAGER,rollbackFor = Exception.class)
|
||||
public boolean updateBatchById(Collection<T> entityList) {
|
||||
return super.updateBatchById(entityList);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package quant.rich.emoney.util;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/* r2.b */
|
||||
public class EmoneyBytesResortUtils {
|
||||
|
||||
/* renamed from: a reason: collision with root package name */
|
||||
private static char[] f42522a = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
|
||||
|
||||
public static int a(int i9) {
|
||||
int i10 = 0;
|
||||
while (i9 != 0) {
|
||||
i10++;
|
||||
i9 &= i9 - 1;
|
||||
}
|
||||
return i10;
|
||||
}
|
||||
|
||||
private static int b() {
|
||||
return 26895494;
|
||||
}
|
||||
|
||||
private static int c() {
|
||||
return 35063394;
|
||||
}
|
||||
|
||||
public static void d(byte[] bArr) {
|
||||
int length = bArr.length;
|
||||
byte[] bytes = Integer.valueOf(b() + c()).toString().getBytes(Charset.forName("UTF-8"));
|
||||
int length2 = bytes.length;
|
||||
int i9 = 0;
|
||||
do {
|
||||
for (int i10 = 0; i10 < length2; i10++) {
|
||||
int i11 = i9 + i10;
|
||||
if (i11 >= length) {
|
||||
break;
|
||||
}
|
||||
bArr[i11] = (byte) (bArr[i11] ^ bytes[i10]);
|
||||
}
|
||||
i9 += length2;
|
||||
} while (i9 < length);
|
||||
}
|
||||
|
||||
public static String[] e(String str, int i9) {
|
||||
int i10;
|
||||
String str2;
|
||||
int length = str.length() / i9;
|
||||
int length2 = str.length() % i9;
|
||||
if (length2 != 0) {
|
||||
i10 = 1;
|
||||
} else {
|
||||
i10 = 0;
|
||||
}
|
||||
int i11 = length + i10;
|
||||
String[] strArr = new String[i11];
|
||||
for (int i12 = 0; i12 < i11; i12++) {
|
||||
if (i12 != i11 - 1 || length2 == 0) {
|
||||
int i13 = i12 * i9;
|
||||
str2 = str.substring(i13, i13 + i9);
|
||||
} else {
|
||||
int i14 = i12 * i9;
|
||||
str2 = str.substring(i14, i14 + length2);
|
||||
}
|
||||
strArr[i12] = str2;
|
||||
}
|
||||
return strArr;
|
||||
}
|
||||
|
||||
}
|
||||
309
src/main/java/quant/rich/emoney/util/EncryptUtils.java
Normal file
309
src/main/java/quant/rich/emoney/util/EncryptUtils.java
Normal file
@@ -0,0 +1,309 @@
|
||||
package quant.rich.emoney.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.bouncycastle.crypto.digests.SHA3Digest;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
|
||||
@Slf4j
|
||||
public class EncryptUtils {
|
||||
|
||||
private static final byte[] IV = "aesiv_emapp@2018".getBytes(StandardCharsets.UTF_8);
|
||||
private static final String PASSWORD_KEY_STRING_BASE64 = "5pxlF/NrBtxlBve0jQwGEw==";
|
||||
private static final String PASSWORD_CIPHER_INSTANCE = "AES/CBC/PKCS5Padding";
|
||||
private static final String AES_DECODE_KEY_STRING_BASE64 = "39KLC1etSfUXuiGxRm5ilDDtqapKXm0COuHxUrWSWlc=";
|
||||
private static final String EM_SIGN_MESS_1 = "23082404151001";
|
||||
private static final String EM_SIGN_MESS_2 = "994fec3c512f2f7756fd5e4403147f01";
|
||||
private static final String SLASH = "/";
|
||||
private static final String COLON = ":";
|
||||
|
||||
/**
|
||||
* 加密用于 Emoney 登录的密码
|
||||
* @param pwd 明文密码
|
||||
* @return 加密过的密码(Base64 字符串)
|
||||
*/
|
||||
public static String encryptAesForEmoneyPassword(String pwd) {
|
||||
try {
|
||||
byte[] keyBytes = Base64.getDecoder().decode(PASSWORD_KEY_STRING_BASE64);
|
||||
|
||||
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(IV);
|
||||
|
||||
Cipher cipher = Cipher.getInstance(PASSWORD_CIPHER_INSTANCE);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
|
||||
|
||||
// 执行加密
|
||||
byte[] encryptedBytes = cipher.doFinal(pwd.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// 将加密结果转换为Base64字符串
|
||||
String encryptedString = Base64.getEncoder().encodeToString(encryptedBytes);
|
||||
return encryptedString;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Encrypt aes for emoney-password failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密由 Emoney 进行加密过的密码
|
||||
* @param pwd 加密过的密码(Base64 字符串)
|
||||
* @return 明文密码
|
||||
*/
|
||||
public static String decryptAesForEmoneyPassword(String pwd) {
|
||||
try {
|
||||
byte[] keyBytes = Base64.getDecoder().decode(PASSWORD_KEY_STRING_BASE64);
|
||||
byte[] passBytes = Base64.getDecoder().decode(pwd);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(IV);
|
||||
|
||||
Cipher cipher = Cipher.getInstance(PASSWORD_CIPHER_INSTANCE);
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
|
||||
|
||||
// 执行解密
|
||||
byte[] decryptedBytes = cipher.doFinal(passBytes);
|
||||
|
||||
return new String(decryptedBytes, "UTF-8");
|
||||
|
||||
} catch (Exception e) {
|
||||
//log.error("Decrypt aes for emoney-password failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用益盟自带 AES key 对指定字节数组进行解密,一般而言该字节数组通过解码 Base64 字符串得到
|
||||
* @param bArr
|
||||
* @return
|
||||
*/
|
||||
private static String decryptAesForEmSignMess(byte[] bArr) {
|
||||
|
||||
try {
|
||||
byte[] keyBytes = Base64.getDecoder().decode(AES_DECODE_KEY_STRING_BASE64);
|
||||
EmoneyBytesResortUtils.d(keyBytes);
|
||||
|
||||
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES");
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec);
|
||||
|
||||
return new String(cipher.doFinal(bArr), "UTF-8");
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException
|
||||
| BadPaddingException | UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串转 32 位小写 md5 字符串
|
||||
*
|
||||
* @param str
|
||||
* @return
|
||||
*/
|
||||
public static String toMD5String(String str) {
|
||||
if (StringUtils.isEmpty(str)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return toMD5String(str.getBytes("UTF-8"));
|
||||
} catch (UnsupportedEncodingException e10) {
|
||||
e10.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字节数组转 32 位小写 md5 字符串
|
||||
* @param bytes
|
||||
* @return
|
||||
*/
|
||||
public static String toMD5String(byte[] bytes) {
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
MessageDigest instance = MessageDigest.getInstance("MD5");
|
||||
instance.update(bytes);
|
||||
return bytesToHex(instance.digest());
|
||||
} catch (NoSuchAlgorithmException e10) {
|
||||
e10.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
// 将字节转换为无符号整数表示的16进制,并且保证每个字节输出两个字符
|
||||
sb.append(String.format("%02x", b & 0xFF));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 content、method 和 xProtocolId 生成 EM-Sign,随机字符串和时间戳自动生成
|
||||
* @param content 请求的字节数组
|
||||
* @param method 请求方法
|
||||
* @param xProtocolId 请求 X-Protocol-Id
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public static String getEMSign(
|
||||
byte[] content,
|
||||
String method,
|
||||
String xProtocolId) throws IOException {
|
||||
String randomString = TextUtils.randomString(10);
|
||||
long timestampFixed = Instant.now().toEpochMilli();
|
||||
return getEMSign(content, method, xProtocolId, randomString, timestampFixed);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 content、method、xProtocolId、指定的随机 10 位字符串、指定的时间戳生成 EM-Sign
|
||||
* @param content 请求的字节数组
|
||||
* @param method 请求方法
|
||||
* @param xProtocolId 请求 X-Protocol-Id
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public static String getEMSign(
|
||||
byte[] content,
|
||||
String method,
|
||||
String xProtocolId,
|
||||
String randomString, long timestampFixed) throws IOException {
|
||||
|
||||
// TODO: 空校验、时间戳校验、随机字符串长度校验等防呆操作
|
||||
|
||||
StringBuffer plainSignSb = new StringBuffer();
|
||||
plainSignSb.append(EM_SIGN_MESS_1);
|
||||
plainSignSb.append(method.toUpperCase());
|
||||
plainSignSb.append(EM_SIGN_MESS_2);
|
||||
plainSignSb.append(SLASH);
|
||||
plainSignSb.append(URLDecoder.decode(xProtocolId, "UTF-8"));
|
||||
plainSignSb.append(timestampFixed);
|
||||
plainSignSb.append(randomString);
|
||||
|
||||
if (content != null && content.length != 0) {
|
||||
plainSignSb.append(toMD5String(content));
|
||||
}
|
||||
|
||||
String md5Sign = EncryptUtils.toMD5String(plainSignSb.toString());
|
||||
|
||||
StringBuilder emSignSb = new StringBuilder();
|
||||
emSignSb.append(EM_SIGN_MESS_1);
|
||||
emSignSb.append(COLON);
|
||||
emSignSb.append(md5Sign);
|
||||
emSignSb.append(COLON);
|
||||
emSignSb.append(randomString);
|
||||
emSignSb.append(COLON);
|
||||
emSignSb.append(timestampFixed);
|
||||
|
||||
String emSign = emSignSb.toString();
|
||||
return emSign;
|
||||
}
|
||||
|
||||
public static byte[] sha3(byte[] bytes, int digit) {
|
||||
SHA3Digest digest = new SHA3Digest(digit);
|
||||
digest.update(bytes, 0, bytes.length);
|
||||
byte[] rsData = new byte[digest.getDigestSize()];
|
||||
digest.doFinal(rsData, 0);
|
||||
return rsData;
|
||||
}
|
||||
|
||||
public static String sha256(String input) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(input.getBytes());
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
} catch (Exception e) {
|
||||
log.error("SHA-256 Hash failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String sha3(String data, int digit) {
|
||||
return Hex.toHexString(sha3(data.getBytes(), digit));
|
||||
}
|
||||
|
||||
public static String toHexString(byte[] data) {
|
||||
return Hex.toHexString(data);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
// EM-Sign:
|
||||
// 23082404151001:356b7041c37848b4f73a8ad5c9416f48:nBZTuInqo1:1721439297485
|
||||
long timestampFixed = 1721439297485L;
|
||||
String randomString = "nBZTuInqo1";
|
||||
byte[] mess = Base64.getDecoder().decode("0JryhAshy7JXgP+5sLmWwaurGM3YHWaFbGaKH/mi7oKN+5mlTnEp5LTeFJVtJTTR");
|
||||
String f13443a = EncryptUtils.decryptAesForEmSignMess(mess);
|
||||
|
||||
String path = "C:\\Users\\Administrator\\Desktop\\Emoney\\login.request.body";
|
||||
|
||||
FileInputStream in = new FileInputStream(new File(path));
|
||||
byte[] requestBody = new byte[in.available()];
|
||||
in.read(requestBody);
|
||||
in.close();
|
||||
|
||||
String requestBodyStr = "{\"date\":0}";// new String(requestBody, "UTF-8") + '\0';
|
||||
requestBody = requestBodyStr.getBytes("UTF-8");
|
||||
|
||||
RequestBody body = RequestBody.create(requestBodyStr, MediaType.parse("application/json"));
|
||||
okio.Buffer buffer = new okio.Buffer();
|
||||
body.writeTo(buffer);
|
||||
requestBody = buffer.readByteArray();
|
||||
|
||||
String requestBodyMd5;
|
||||
if (requestBody == null || requestBody.length == 0) {
|
||||
requestBodyMd5 = "";
|
||||
} else {
|
||||
requestBodyMd5 = toMD5String(requestBody);
|
||||
}
|
||||
|
||||
String str2 = "23082404151001" + "POST" + f13443a + "/"
|
||||
+ URLDecoder.decode("SmartInvestment%2FIndex%2FTianYan", "UTF-8") + timestampFixed + randomString
|
||||
+ requestBodyMd5;
|
||||
|
||||
String md5 = EncryptUtils.toMD5String(str2);
|
||||
log.info("Handmade emsign md5: {}", md5);
|
||||
String emSign = getEMSign(requestBody, "POST", "SmartInvestment%2FIndex%2FTianYan", randomString, timestampFixed);
|
||||
log.info("Method emsign: {}", emSign);
|
||||
|
||||
// 测试密码加解密
|
||||
String encryptPass = encryptAesForEmoneyPassword("123456");
|
||||
log.info("Encrypt pass for 123456: {}", encryptPass);
|
||||
String decryptPass = decryptAesForEmoneyPassword(encryptPass);
|
||||
log.info("Decrypt pass for {}: {}", encryptPass, decryptPass);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package quant.rich.emoney.util;
|
||||
import net.bytebuddy.ByteBuddy;
|
||||
import net.bytebuddy.agent.ByteBuddyAgent;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
|
||||
import static net.bytebuddy.matcher.ElementMatchers.*;
|
||||
|
||||
public class PatchOkHttpUAStatic {
|
||||
|
||||
// 入口方法:只需调用一次
|
||||
public static void replace(String uaString) {
|
||||
try {
|
||||
ByteBuddyAgent.install();
|
||||
|
||||
// 传递目标 UA 值到 Advice
|
||||
UserAgentAdvice.overrideValue = uaString;
|
||||
|
||||
new ByteBuddy()
|
||||
.redefine(Class.forName("okhttp3.Request$Builder"))
|
||||
.visit(Advice.to(UserAgentAdvice.class)
|
||||
.on(named("header").and(takesArguments(String.class, String.class))))
|
||||
.make()
|
||||
.load(Class.forName("okhttp3.Request$Builder").getClassLoader(),
|
||||
ClassReloadingStrategy.fromInstalledAgent());
|
||||
|
||||
System.out.println("[PatchOkHttpUA] Successfully installed custom User-Agent hook.");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
System.err.println("[PatchOkHttpUA] Failed to patch OkHttp.");
|
||||
}
|
||||
}
|
||||
|
||||
// 内部类:修改 header(name, value) 方法逻辑
|
||||
public static class UserAgentAdvice {
|
||||
public static String overrideValue = "MySpoofed-UA";
|
||||
|
||||
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||
static boolean intercept(
|
||||
@Advice.This Object builder,
|
||||
@Advice.Argument(0) String name,
|
||||
@Advice.Argument(value = 1, readOnly = false) String value) {
|
||||
if ("User-Agent".equalsIgnoreCase(name)) {
|
||||
value = overrideValue;
|
||||
System.out.println("[PatchOkHttpUA] Replaced User-Agent with: " + value);
|
||||
}
|
||||
return false; // 不跳过原方法,继续执行但已修改参数
|
||||
}
|
||||
}
|
||||
}
|
||||
215
src/main/java/quant/rich/emoney/util/SpringContextHolder.java
Normal file
215
src/main/java/quant/rich/emoney/util/SpringContextHolder.java
Normal file
@@ -0,0 +1,215 @@
|
||||
package quant.rich.emoney.util;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.support.DefaultSingletonBeanRegistry;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.data.util.ProxyUtils;
|
||||
import org.springframework.jmx.export.annotation.ManagedAttribute;
|
||||
import org.springframework.jmx.export.annotation.ManagedOperation;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.config.ConstructionGuard;
|
||||
|
||||
/**
|
||||
* 以静态变量保存Spring ApplicationContext, 可在任何代码任何地方任何时候取出ApplicaitonContext.
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@Lazy(false)
|
||||
public class SpringContextHolder implements ApplicationContextAware, DisposableBean {
|
||||
|
||||
private static ApplicationContext applicationContext = null;
|
||||
|
||||
/**
|
||||
* 取得存储在静态变量中的ApplicationContext.
|
||||
*/
|
||||
public static ApplicationContext getApplicationContext() {
|
||||
assertContextInjected();
|
||||
return applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T getBean(String name) {
|
||||
assertContextInjected();
|
||||
return (T) applicationContext.getBean(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
|
||||
*/
|
||||
public static <T> T getBean(Class<T> requiredType) {
|
||||
assertContextInjected();
|
||||
return applicationContext.getBean(requiredType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除SpringContextHolder中的ApplicationContext为Null.
|
||||
*/
|
||||
public static void clearHolder() {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext);
|
||||
}
|
||||
applicationContext = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现ApplicationContextAware接口, 注入Context到静态变量中.
|
||||
*/
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
SpringContextHolder.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现DisposableBean接口, 在Context关闭时清理静态变量.
|
||||
*/
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
SpringContextHolder.clearHolder();
|
||||
}
|
||||
|
||||
public static boolean updateBean(String name, Object newBean) {
|
||||
log.debug("Enter updateBean for {}'s update", name);
|
||||
if (SpringContextHolder.applicationContext != null) {
|
||||
if (!applicationContext.containsBean(name)) {
|
||||
log.debug("updateBean is only for initialized bean's updating, but {} doesn't exist or hasn't been initialized yet");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Class<?> beanType = SpringContextHolder.applicationContext.getType(name);
|
||||
if (ConstructionGuard.isConstructing(beanType)) {
|
||||
log.debug("updateBean is called but {} is initializing", name);
|
||||
return false;
|
||||
}
|
||||
Object bean = SpringContextHolder.applicationContext.getBean(name);
|
||||
Class<?> newBeanType = newBean.getClass();
|
||||
if (beanType != newBeanType) {
|
||||
log.warn("Cannot set new bean type {} to {}", newBeanType.getName(), name);
|
||||
return false;
|
||||
}
|
||||
if (bean != null) {
|
||||
if (AopUtils.isAopProxy(bean)) {
|
||||
bean = ProxyUtils.getUserClass(bean);
|
||||
}
|
||||
if (getNeverRefreshable().contains(bean.getClass().getName())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Field[] declaredFields = beanType.getDeclaredFields();
|
||||
for (Field field : declaredFields) {
|
||||
int modifiers = field.getModifiers();
|
||||
if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers)) {
|
||||
continue;
|
||||
}
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
setFieldData(field, bean, field.get(newBean));
|
||||
} catch (Exception e) {
|
||||
log.error("Change bean failed", e);
|
||||
}
|
||||
field.setAccessible(false);
|
||||
}
|
||||
rebindSingleton(name, bean);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("Cannot rebind to " + name, e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void setFieldData(Field field, Object bean, Object obj) throws Exception {
|
||||
Class<?> type = field.getType();
|
||||
if (obj == null) {
|
||||
field.set(bean, null);
|
||||
} else if (type.equals(String.class)) {
|
||||
field.set(bean, String.valueOf(obj));
|
||||
} else if (type.equals(Integer.class)) {
|
||||
field.set(bean, Integer.valueOf(String.valueOf(obj)));
|
||||
} else if (type.equals(Long.class)) {
|
||||
field.set(bean, Long.valueOf(String.valueOf(obj)));
|
||||
} else if (type.equals(Double.class)) {
|
||||
field.set(bean, Double.valueOf(String.valueOf(obj)));
|
||||
} else if (type.equals(Short.class)) {
|
||||
field.set(bean, Short.valueOf(String.valueOf(obj)));
|
||||
} else if (type.equals(Byte.class)) {
|
||||
field.set(bean, Byte.valueOf(String.valueOf(obj)));
|
||||
} else if (type.equals(Boolean.class)) {
|
||||
field.set(bean, Boolean.valueOf(String.valueOf(obj)));
|
||||
} else if (type.equals(Date.class)) {
|
||||
field.set(bean, new Date(Long.valueOf(String.valueOf(obj))));
|
||||
} else if (obj instanceof List) {
|
||||
field.set(bean, new ArrayList<>((List<?>) obj));
|
||||
} else {
|
||||
field.set(bean, obj);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重绑定单例模式 Bean
|
||||
*
|
||||
* @param name
|
||||
* @return
|
||||
* @see <a href=
|
||||
* "http://blog.gxitsky.com/2023/01/31/SpringBoot-63-Auto-Refresh-Environment-Config-Data/">
|
||||
* Spring Boot 2系列(六十三):动态刷新环境配置和Bean属性值</a>
|
||||
*/
|
||||
@ManagedOperation
|
||||
public static boolean rebindSingleton(String name, Object newBean) {
|
||||
if (SpringContextHolder.applicationContext != null) {
|
||||
try {
|
||||
Object bean = SpringContextHolder.applicationContext.getBean(name);
|
||||
|
||||
DefaultSingletonBeanRegistry registry = (DefaultSingletonBeanRegistry) SpringContextHolder.applicationContext
|
||||
.getAutowireCapableBeanFactory();
|
||||
if (bean != null) {
|
||||
if (AopUtils.isAopProxy(bean)) {
|
||||
bean = ProxyUtils.getUserClass(bean);
|
||||
}
|
||||
// TODO: determine a more general approach to fix this.
|
||||
// see https://github.com/spring-cloud/spring-cloud-commons/issues/571
|
||||
if (getNeverRefreshable().contains(bean.getClass().getName())) {
|
||||
return false; // ignore
|
||||
}
|
||||
registry.destroySingleton(name);
|
||||
}
|
||||
registry.registerSingleton(name, newBean);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("Cannot rebind to " + name, e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@ManagedAttribute
|
||||
public static Set<String> getNeverRefreshable() {
|
||||
String neverRefresh = SpringContextHolder.applicationContext.getEnvironment()
|
||||
.getProperty("spring.cloud.refresh.never-refreshable", "com.zaxxer.hikari.HikariDataSource");
|
||||
return StringUtils.commaDelimitedListToSet(neverRefresh);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查ApplicationContext不为空.
|
||||
*/
|
||||
private static void assertContextInjected() {
|
||||
Validate.validState(applicationContext != null, "applicaitonContext 属性未注入,请检查程序是否未正常启动");
|
||||
}
|
||||
}
|
||||
27
src/main/java/quant/rich/emoney/util/TextUtils.java
Normal file
27
src/main/java/quant/rich/emoney/util/TextUtils.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package quant.rich.emoney.util;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
public class TextUtils {
|
||||
|
||||
private static final Random random = new Random();
|
||||
|
||||
/**
|
||||
* 提供 n 位的 A-Za-z0-9 的随机字串
|
||||
*
|
||||
* @param n
|
||||
* @return
|
||||
*/
|
||||
public static String randomString(int n) {
|
||||
return randomString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", n);
|
||||
}
|
||||
|
||||
public static String randomString(String from, int n) {
|
||||
StringBuilder sb = new StringBuilder(n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
sb.append(from.charAt(random.nextInt(from.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
61
src/main/java/quant/rich/emoney/util/VersionComparator.java
Normal file
61
src/main/java/quant/rich/emoney/util/VersionComparator.java
Normal file
@@ -0,0 +1,61 @@
|
||||
package quant.rich.emoney.util;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class VersionComparator implements Comparator<String> {
|
||||
|
||||
public static VersionComparator INSTANCE = new VersionComparator();
|
||||
|
||||
@Override
|
||||
public int compare(String version1, String version2) {
|
||||
// 将版本号拆分为数字部分(以 '.' 为分隔符)
|
||||
String[] version1Parts = version1.split("\\.");
|
||||
String[] version2Parts = version2.split("\\.");
|
||||
|
||||
// 找到较长的版本号长度
|
||||
int length = Math.max(version1Parts.length, version2Parts.length);
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
// 如果 version1Parts 或 version2Parts 长度不足,补齐为 0
|
||||
try {
|
||||
int v1 = (i < version1Parts.length) ? Integer.parseInt(version1Parts[i]) : 0;
|
||||
int v2 = (i < version2Parts.length) ? Integer.parseInt(version2Parts[i]) : 0;
|
||||
|
||||
// 比较版本号的每一部分
|
||||
if (v1 < v2) {
|
||||
return -1; // version1 小于 version2
|
||||
} else if (v1 > v2) {
|
||||
return 1; // version1 大于 version2
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
// 转换数字失败的直接调用 String 的 compare
|
||||
String v1 = (i < version1Parts.length) ? version1 : "";
|
||||
String v2 = (i < version2Parts.length) ? version2 : "";
|
||||
return v1.compareTo(v2);
|
||||
}
|
||||
}
|
||||
// 如果各部分都相等
|
||||
return 0; // version1 等于 version2
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 使用 Comparator 比较版本号
|
||||
VersionComparator versionComparator = new VersionComparator();
|
||||
|
||||
// 示例版本号
|
||||
String version1 = "1.2.3";
|
||||
String version2 = "1.10.5";
|
||||
|
||||
// 比较两个版本号
|
||||
int result = versionComparator.compare(version1, version2);
|
||||
|
||||
if (result == 0) {
|
||||
System.out.println(version1 + " is equal to " + version2);
|
||||
} else if (result < 0) {
|
||||
System.out.println(version1 + " is less than " + version2);
|
||||
} else {
|
||||
System.out.println(version1 + " is greater than " + version2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package quant.rich.emoney.validator;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
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.interfaces.ValidEmoneyRequestConfig;
|
||||
|
||||
public class EmoneyRequestConfigValidator implements ConstraintValidator<ValidEmoneyRequestConfig, IConfig<EmoneyRequestConfig>> {
|
||||
|
||||
static final Pattern androidIdPattern = Pattern.compile("^[0-9a-f]{16}$");
|
||||
|
||||
@Override
|
||||
public boolean isValid(IConfig<EmoneyRequestConfig> value, ConstraintValidatorContext context) {
|
||||
|
||||
if (value == null) return true;
|
||||
if (!(value instanceof EmoneyRequestConfig config)) return true;
|
||||
|
||||
if (!config.getIsAnonymous()) {
|
||||
// 非匿名须判断用户名密码是否为空
|
||||
if (StringUtils.isAnyBlank(config.getUsername(), config.getPassword())) {
|
||||
context.disableDefaultConstraintViolation();
|
||||
context.buildConstraintViolationWithTemplate("配置非匿名时用户名和密码不能为空")
|
||||
.addConstraintViolation();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!androidIdPattern.matcher(config.getAndroidId()).matches()) {
|
||||
// 判断 androidId 合法性
|
||||
context.disableDefaultConstraintViolation();
|
||||
context.buildConstraintViolationWithTemplate("非法的 Android Id,必须是 16 位十六进制小写字符串")
|
||||
.addConstraintViolation();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!DeviceInfoConfig.DeviceInfo.PATTERN.matcher(config.getFingerprint()).matches()) {
|
||||
context.disableDefaultConstraintViolation();
|
||||
context.buildConstraintViolationWithTemplate("非法的 Fingerprint")
|
||||
.addConstraintViolation();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user