更新
This commit is contained in:
2
pom.xml
2
pom.xml
@@ -9,7 +9,7 @@
|
|||||||
<version>3.3.0</version>
|
<version>3.3.0</version>
|
||||||
<relativePath /> <!-- lookup parent from repository -->
|
<relativePath /> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>com.littlesweetdog.quant</groupId>
|
<groupId>top.at17.quant</groupId>
|
||||||
<artifactId>emo-grab</artifactId>
|
<artifactId>emo-grab</artifactId>
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
<name>EmoGrab</name>
|
<name>EmoGrab</name>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package quant.rich.emoney.component;
|
package quant.rich.emoney.annotation;
|
||||||
|
|
||||||
import java.lang.annotation.*;
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
import quant.rich.emoney.component.CallerLockAspect;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在方法上添加此注解,可针对调用方加锁,即:<br>
|
* 在方法上添加此注解,可针对调用方加锁,即:<br>
|
||||||
* 调用方法为 A 的,多次从 A 调用则加锁,从 B 调用时不受影响<br>
|
* 调用方法为 A 的,多次从 A 调用则加锁,从 B 调用时不受影响<br>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package quant.rich.emoney.annotation;
|
||||||
|
|
||||||
|
import static java.lang.annotation.ElementType.METHOD;
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注解在方法上以获取对 EmoneyProtocol 的额外操作
|
||||||
|
*/
|
||||||
|
@Documented
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
@Target(METHOD)
|
||||||
|
public @interface ResponseDecodeExtension {
|
||||||
|
String protocolId();
|
||||||
|
int order() default -1;
|
||||||
|
}
|
||||||
@@ -30,10 +30,22 @@ import okhttp3.OkHttpClient;
|
|||||||
* 益盟操盘手基本请求客户端,提供基本功能
|
* 益盟操盘手基本请求客户端,提供基本功能
|
||||||
* <p><b>请求头顺序</b></p>
|
* <p><b>请求头顺序</b></p>
|
||||||
* <p>
|
* <p>
|
||||||
* X-Protocol-Id > X-Request-Id > EM-Sign > Authorization >
|
* <ul>
|
||||||
* X-Android-Agent > Emapp-ViewMode > Content-Type > Content-Length >
|
* <li>X-Protocol-Id</li>
|
||||||
* Host > Connection: "Keep-Alive" > Accept-Encoding: "gzip" > User-Agent</p>
|
* <li>X-Request-Id</li>
|
||||||
* <p>从 X-Protocol-Id 到 Emapp-ViewMode,由本例添加,剩余为 okhttp 默认添加,
|
* <li>EM-Sign</li>
|
||||||
|
* <li>Authorization</li>
|
||||||
|
* <li>X-Android-Agent</li>
|
||||||
|
* <li>Emapp-ViewMode</li>
|
||||||
|
* <li>Content-Type</li>
|
||||||
|
* <li>Content-Length</li>
|
||||||
|
* <li>Host</li>
|
||||||
|
* <li>Connection: "Keep-Alive"</li>
|
||||||
|
* <li>Accept-Encoding: "gzip"</li>
|
||||||
|
* <li>User-Agent</li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
* <p>从 X-Protocol-Id 到 Emapp-ViewMode 由本例添加,剩余为 okhttp 默认添加,
|
||||||
* User-Agent 由 ByteBuddy 重写 header 方法控制添加</p>
|
* User-Agent 由 ByteBuddy 重写 header 方法控制添加</p>
|
||||||
* @see quant.rich.emoney.patch.okhttp.PatchOkHttp
|
* @see quant.rich.emoney.patch.okhttp.PatchOkHttp
|
||||||
*/
|
*/
|
||||||
@@ -43,19 +55,44 @@ import okhttp3.OkHttpClient;
|
|||||||
public class EmoneyClient implements Cloneable {
|
public class EmoneyClient implements Cloneable {
|
||||||
|
|
||||||
private static final String MBS_URL = "https://mbs.emoney.cn/";
|
private static final String MBS_URL = "https://mbs.emoney.cn/";
|
||||||
|
private static final String STRATEGY_URL = "https://mbs.emoney.cn/strategy/";
|
||||||
private static final String LOGIN_URL = "https://emapp.emoney.cn/user/auth/login";
|
private static final String LOGIN_URL = "https://emapp.emoney.cn/user/auth/login";
|
||||||
private static final String RELOGIN_URL = "https://emapp.emoney.cn/user/auth/ReLogin";
|
private static final String RELOGIN_URL = "https://emapp.emoney.cn/user/auth/ReLogin";
|
||||||
private static final String LOGIN_X_PROTOCOL_ID = "user%2Fauth%2Flogin";
|
private static final String LOGIN_X_PROTOCOL_ID = "user%2Fauth%2Flogin";
|
||||||
private static final String RELOGIN_X_PROTOCOL_ID = "user%2Fauth%2FReLogin";
|
private static final String RELOGIN_X_PROTOCOL_ID = "user%2Fauth%2FReLogin";
|
||||||
|
|
||||||
private static volatile EmoneyRequestConfig emoneyRequestConfig;
|
private static volatile EmoneyRequestConfig emoneyRequestConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 protocolId 返回 URL
|
||||||
|
* @param protocolId
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private static String getUrlByProtocolId(Serializable protocolId) {
|
||||||
|
if (protocolId instanceof Integer intProtocolId) {
|
||||||
|
switch (intProtocolId) {
|
||||||
|
case 9400: return STRATEGY_URL;
|
||||||
|
default: return MBS_URL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (protocolId instanceof String strProtocolId) {
|
||||||
|
switch (strProtocolId) {
|
||||||
|
case LOGIN_X_PROTOCOL_ID: return LOGIN_URL;
|
||||||
|
case RELOGIN_X_PROTOCOL_ID: return RELOGIN_URL;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Spring 上下文中获取载入的请求配置
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
private static EmoneyRequestConfig getEmoneyRequestConfig() {
|
private static EmoneyRequestConfig getEmoneyRequestConfig() {
|
||||||
if (emoneyRequestConfig == null) {
|
if (emoneyRequestConfig == null) {
|
||||||
synchronized (EmoneyClient.class) {
|
synchronized (EmoneyClient.class) {
|
||||||
if (emoneyRequestConfig == null) {
|
emoneyRequestConfig = SpringContextHolder.getBean(EmoneyRequestConfig.class);
|
||||||
emoneyRequestConfig = SpringContextHolder.getBean(EmoneyRequestConfig.class);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return emoneyRequestConfig;
|
return emoneyRequestConfig;
|
||||||
@@ -64,7 +101,7 @@ public class EmoneyClient implements Cloneable {
|
|||||||
private EmoneyClient() {}
|
private EmoneyClient() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据系统配置自动选择登录方式
|
* 根据系统配置自动选择登录方式,即匿名或不匿名
|
||||||
* @return
|
* @return
|
||||||
* @see EmoneyRequestConfig
|
* @see EmoneyRequestConfig
|
||||||
*/
|
*/
|
||||||
@@ -149,7 +186,7 @@ public class EmoneyClient implements Cloneable {
|
|||||||
|
|
||||||
if (response.code() != 200) {
|
if (response.code() != 200) {
|
||||||
// 不是 200,重新登录
|
// 不是 200,重新登录
|
||||||
log.debug("ReLogin 重登录验证返回状态码 {}, 触发登录", response.code());
|
log.debug("ReLogin 重登录验证返回状态码 {}, 需要重新登录", response.code());
|
||||||
return loginWithManaged();
|
return loginWithManaged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,6 +303,11 @@ public class EmoneyClient implements Cloneable {
|
|||||||
new IllegalArgumentException());
|
new IllegalArgumentException());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String url = getUrlByProtocolId(xProtocolId);
|
||||||
|
if (StringUtils.isBlank(url)) {
|
||||||
|
throw new EmoneyRequestException("无法根据 xProtocolId " + xProtocolId + "获取请求 URL");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
OkHttpClient okHttpClient = OkHttpClientProvider.getInstance();
|
OkHttpClient okHttpClient = OkHttpClientProvider.getInstance();
|
||||||
|
|
||||||
@@ -275,7 +317,7 @@ public class EmoneyClient implements Cloneable {
|
|||||||
MediaType.parse("application/x-protobuf-v3"));
|
MediaType.parse("application/x-protobuf-v3"));
|
||||||
|
|
||||||
Request.Builder requestBuilder = new Request.Builder()
|
Request.Builder requestBuilder = new Request.Builder()
|
||||||
.url(MBS_URL)
|
.url(url)
|
||||||
.post(body)
|
.post(body)
|
||||||
// 这玩意可能也有顺序
|
// 这玩意可能也有顺序
|
||||||
// 按照 Fiddler HexView 顺序如下:
|
// 按照 Fiddler HexView 顺序如下:
|
||||||
@@ -293,6 +335,10 @@ public class EmoneyClient implements Cloneable {
|
|||||||
final Call call = okHttpClient.newCall(request);
|
final Call call = okHttpClient.newCall(request);
|
||||||
Response response = call.execute();
|
Response response = call.execute();
|
||||||
|
|
||||||
|
// 错误的时候这里是 404,比如 strategy 是独立网页的时候,Content-Type 是 text/plain
|
||||||
|
if (response.code() != 200) {
|
||||||
|
throw new EmoneyRequestException("请求返回错误,状态码 " + response.code());
|
||||||
|
}
|
||||||
BaseResponse.Base_Response baseResponse = BaseResponse.Base_Response.parseFrom(response.body().bytes());
|
BaseResponse.Base_Response baseResponse = BaseResponse.Base_Response.parseFrom(response.body().bytes());
|
||||||
return baseResponse;
|
return baseResponse;
|
||||||
} catch (InvalidProtocolBufferNanoException e) {
|
} catch (InvalidProtocolBufferNanoException e) {
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
package quant.rich.emoney.client;
|
|
||||||
|
|
||||||
import java.net.Proxy;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
import com.microsoft.playwright.Browser;
|
|
||||||
import com.microsoft.playwright.BrowserContext;
|
|
||||||
import com.microsoft.playwright.BrowserType;
|
|
||||||
import com.microsoft.playwright.Page;
|
|
||||||
import com.microsoft.playwright.Playwright;
|
|
||||||
import com.microsoft.playwright.Request;
|
|
||||||
import com.microsoft.playwright.BrowserType.LaunchOptions;
|
|
||||||
import com.microsoft.playwright.Route.ResumeOptions;
|
|
||||||
import com.microsoft.playwright.options.HttpHeader;
|
|
||||||
import com.microsoft.playwright.options.WaitUntilState;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.Accessors;
|
|
||||||
import quant.rich.emoney.component.LockByCaller;
|
|
||||||
import quant.rich.emoney.entity.config.EmoneyRequestConfig;
|
|
||||||
import quant.rich.emoney.entity.config.ProxyConfig;
|
|
||||||
import quant.rich.emoney.util.SpringContextHolder;
|
|
||||||
|
|
||||||
public class WebviewClient {
|
|
||||||
|
|
||||||
private static Playwright playwright;
|
|
||||||
private static boolean isReady = false;
|
|
||||||
private static volatile ProxyConfig proxyConfig;
|
|
||||||
private static volatile EmoneyRequestConfig emoneyRequestConfig;
|
|
||||||
|
|
||||||
static {
|
|
||||||
try {
|
|
||||||
playwright = Playwright.create();
|
|
||||||
isReady = true;
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isReady() {
|
|
||||||
return isReady;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ProxyConfig getProxyConfig() {
|
|
||||||
if (proxyConfig == null) {
|
|
||||||
synchronized (WebviewClient.class) {
|
|
||||||
if (proxyConfig == null) {
|
|
||||||
proxyConfig = SpringContextHolder.getBean(ProxyConfig.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return proxyConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static EmoneyRequestConfig getEmoneyRequestConfig() {
|
|
||||||
if (emoneyRequestConfig == null) {
|
|
||||||
synchronized (WebviewClient.class) {
|
|
||||||
if (emoneyRequestConfig == null) {
|
|
||||||
emoneyRequestConfig = SpringContextHolder.getBean(EmoneyRequestConfig.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return emoneyRequestConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
@LockByCaller
|
|
||||||
public static WebviewResponseWrapper getIndexDetailWrapper(String indexCode) {
|
|
||||||
|
|
||||||
String proxyUrl = getProxyConfig().getProxyUrl();
|
|
||||||
LaunchOptions launchOptions = new BrowserType.LaunchOptions()
|
|
||||||
.setHeadless(true);
|
|
||||||
|
|
||||||
// 设置代理
|
|
||||||
if (StringUtils.isNotBlank(proxyUrl)) {
|
|
||||||
launchOptions.setProxy(proxyUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
Browser browser = playwright.chromium().launch(launchOptions);
|
|
||||||
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
|
|
||||||
// 设置 Webview User-Agent
|
|
||||||
.setUserAgent(getEmoneyRequestConfig().getWebviewUserAgent())
|
|
||||||
// 设置是否忽略 HTTPS 证书
|
|
||||||
.setIgnoreHTTPSErrors(getProxyConfig().getIgnoreHttpsVerification())
|
|
||||||
);
|
|
||||||
|
|
||||||
// 设置全局请求头
|
|
||||||
// 根据抓包获得,当前目标版本 5.8.1
|
|
||||||
Map<String, String> headers = new HashMap<>();
|
|
||||||
headers.put("Upgrade-Insecure-Requests", "1");
|
|
||||||
headers.put("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9");
|
|
||||||
headers.put("X-Requested-With", "cn.emoney.emstock");
|
|
||||||
headers.put("Sec-Fetch-Site", "none");
|
|
||||||
headers.put("Sec-Fetch-Mode", "navigate");
|
|
||||||
headers.put("Sec-Fetch-User", "?1");
|
|
||||||
headers.put("Sec-Fetch-Dest", "document");
|
|
||||||
headers.put("Accept-Encoding", "gzip, deflate");
|
|
||||||
headers.put("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7");
|
|
||||||
|
|
||||||
context.setExtraHTTPHeaders(headers);
|
|
||||||
context.route("**/*", route -> {
|
|
||||||
// 清除 Playwright 添加的额外请求头
|
|
||||||
Request request = route.request();
|
|
||||||
List<HttpHeader> requestHeaderList = request.headersArray();
|
|
||||||
Map<String, String> requestHeaders = new HashMap<>();
|
|
||||||
for (HttpHeader header : requestHeaderList) {
|
|
||||||
requestHeaders.put(header.name, header.value);
|
|
||||||
}
|
|
||||||
requestHeaders.remove("sec-ch-ua");
|
|
||||||
requestHeaders.remove("sec-ch-ua-mobile");
|
|
||||||
requestHeaders.remove("sec-ch-ua-platform");
|
|
||||||
requestHeaders.remove("Cache-Control");
|
|
||||||
requestHeaders.remove("Pragma");
|
|
||||||
requestHeaders.remove("cache-control");
|
|
||||||
requestHeaders.remove("pragma");
|
|
||||||
|
|
||||||
// 判断页面附属请求并进行个性化,以尽可能模仿原生 APP
|
|
||||||
System.out.format("url: %s, requestType: %s\r\n", request.url(), request.resourceType());
|
|
||||||
|
|
||||||
String resourceType = request.resourceType();
|
|
||||||
|
|
||||||
if (!"document".equals(resourceType)) {
|
|
||||||
// 非 document(html) 请求的
|
|
||||||
requestHeaders.put("Sec-Fetch-Mode", "no-cors");
|
|
||||||
requestHeaders.remove("Upgrade-Insecure-Requests");
|
|
||||||
}
|
|
||||||
if ("image".equals(request.resourceType())) {
|
|
||||||
// 图片请求
|
|
||||||
requestHeaders.put("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8");
|
|
||||||
}
|
|
||||||
else if ("script".equals(request.resourceType())) {
|
|
||||||
// 图片请求
|
|
||||||
requestHeaders.put("Accept", "*/*");
|
|
||||||
}
|
|
||||||
route.resume(new ResumeOptions().setHeaders(requestHeaders));
|
|
||||||
});
|
|
||||||
|
|
||||||
Page page = context.newPage();
|
|
||||||
WebviewResponseWrapper wrapper = new WebviewResponseWrapper();
|
|
||||||
|
|
||||||
page.onResponse(handler -> {
|
|
||||||
String requestUrl = handler.request().url();
|
|
||||||
if (requestUrl.endsWith(".js")) {
|
|
||||||
String jsContent = handler.request().response().text();
|
|
||||||
wrapper.jsMap.put(requestUrl, jsContent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
StringBuilder urlBuilder = new StringBuilder();
|
|
||||||
urlBuilder.append("https://appstatic.emoney.cn/html/emapp/stock/note/?name=");
|
|
||||||
urlBuilder.append(indexCode);
|
|
||||||
urlBuilder.append("&emoneyScaleType=0&emoneyLandMode=0&token=");
|
|
||||||
urlBuilder.append(getEmoneyRequestConfig().getAuthorization());
|
|
||||||
String url = urlBuilder.toString();
|
|
||||||
|
|
||||||
page.navigate(url, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
|
|
||||||
|
|
||||||
wrapper.setRenderedHtml(page.content());
|
|
||||||
wrapper.setPage(page);
|
|
||||||
browser.close();
|
|
||||||
|
|
||||||
return wrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Accessors(chain=true)
|
|
||||||
public static class WebviewResponseWrapper {
|
|
||||||
private Map<String, String> jsMap = new HashMap<>();
|
|
||||||
private String renderedHtml;
|
|
||||||
private Page page;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
|
|||||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import quant.rich.emoney.annotation.LockByCaller;
|
||||||
import quant.rich.emoney.util.CallerLockUtil;
|
import quant.rich.emoney.util.CallerLockUtil;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
@@ -19,7 +20,7 @@ public class CallerLockAspect {
|
|||||||
|
|
||||||
private final SpelExpressionParser parser = new SpelExpressionParser();
|
private final SpelExpressionParser parser = new SpelExpressionParser();
|
||||||
|
|
||||||
@Around("@annotation(com.example.lock.LockByCaller)")
|
@Around("@annotation(me.qwq.emoney.annotation.LockByCaller)")
|
||||||
public Object around(ProceedingJoinPoint pjp) throws Throwable {
|
public Object around(ProceedingJoinPoint pjp) throws Throwable {
|
||||||
MethodSignature signature = (MethodSignature) pjp.getSignature();
|
MethodSignature signature = (MethodSignature) pjp.getSignature();
|
||||||
Method method = signature.getMethod();
|
Method method = signature.getMethod();
|
||||||
|
|||||||
@@ -175,7 +175,8 @@ public class EmoneyAutoPlatformExceptionHandler {
|
|||||||
if (ex instanceof NoResourceFoundException nrfe) {
|
if (ex instanceof NoResourceFoundException nrfe) {
|
||||||
if (StringUtils.isNotEmpty(nrfe.getMessage())
|
if (StringUtils.isNotEmpty(nrfe.getMessage())
|
||||||
&& nrfe.getMessage().endsWith(" .well-known/appspecific/com.chrome.devtools.json.")) {
|
&& nrfe.getMessage().endsWith(" .well-known/appspecific/com.chrome.devtools.json.")) {
|
||||||
// 傻逼 Chrome 开发工具默认调用该地址
|
// 傻逼 Chrome 开发工具在本地调试时默认调用该地址
|
||||||
|
// see: https://blog.ni18.in/well-known-appspecific-com-chrome-devtools-json-request/
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
package quant.rich.emoney.controller.api;
|
package quant.rich.emoney.controller.api;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
@@ -9,27 +18,110 @@ import org.springframework.web.bind.annotation.RequestBody;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.google.protobuf.nano.MessageNano;
|
import com.google.protobuf.nano.MessageNano;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.reflections.Reflections;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import nano.BaseResponse.Base_Response;
|
import nano.BaseResponse.Base_Response;
|
||||||
|
import quant.rich.emoney.annotation.ResponseDecodeExtension;
|
||||||
import quant.rich.emoney.entity.sqlite.ProtocolMatch;
|
import quant.rich.emoney.entity.sqlite.ProtocolMatch;
|
||||||
import quant.rich.emoney.exception.RException;
|
import quant.rich.emoney.exception.RException;
|
||||||
import quant.rich.emoney.pojo.dto.EmoneyConvertResult;
|
import quant.rich.emoney.pojo.dto.EmoneyConvertResult;
|
||||||
import quant.rich.emoney.pojo.dto.EmoneyProtobufBody;
|
import quant.rich.emoney.pojo.dto.EmoneyProtobufBody;
|
||||||
import quant.rich.emoney.service.sqlite.ProtocolMatchService;
|
import quant.rich.emoney.service.sqlite.ProtocolMatchService;
|
||||||
|
import quant.rich.emoney.service.sqlite.StrategyAndPoolService;
|
||||||
|
import quant.rich.emoney.util.SpringBeanDetector;
|
||||||
|
import quant.rich.emoney.util.SpringContextHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 益盟 ProtocolBuf 报文解析 API 控制器
|
||||||
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/proto")
|
@RequestMapping("/api/v1/proto")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ProtoDecodeControllerV1 {
|
public class ProtoDecodeControllerV1 {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ProtocolMatchService protocolMatchService;
|
ProtocolMatchService protocolMatchService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
StrategyAndPoolService strategyAndPoolService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
Reflections reflections;
|
||||||
|
|
||||||
|
Map<String, List<MethodInfo>> responseDecodeExtensions = new HashMap<String, List<MethodInfo>>();
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
private static class MethodInfo {
|
||||||
|
Method method;
|
||||||
|
Class<?> declaringClass;
|
||||||
|
Integer order;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
void postConstruct() {
|
||||||
|
// Reflections 扫描所有注解并根据 protocolId 和 order 排序
|
||||||
|
Set<Method> methods = reflections.getMethodsAnnotatedWith(ResponseDecodeExtension.class);
|
||||||
|
for (Method m : methods) {
|
||||||
|
MethodInfo info;
|
||||||
|
ResponseDecodeExtension ex = m.getAnnotation(ResponseDecodeExtension.class);
|
||||||
|
String protocolId = ex.protocolId();
|
||||||
|
Integer order = ex.order();
|
||||||
|
|
||||||
|
// 判断 method 是否为单参数接受 JsonNode 的方法
|
||||||
|
Class<?>[] parameterTypes = m.getParameterTypes();
|
||||||
|
Class<?> declaringClass = m.getDeclaringClass();
|
||||||
|
if (parameterTypes.length != 1 || parameterTypes[0] != JsonNode.class) {
|
||||||
|
log.warn("方法 {}#{} 不为类型为 JsonNode 的单参数方法,暂不支持作为解码额外选项",
|
||||||
|
declaringClass.getSimpleName(), m.getName(), declaringClass.getSimpleName());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断 method 是否是静态
|
||||||
|
if (Modifier.isStatic(m.getModifiers())) {
|
||||||
|
info = new MethodInfo(m, null, order);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!SpringBeanDetector.isSpringManagedClass(declaringClass)) {
|
||||||
|
log.warn("方法 {} 所属类 {} 不归属于 Spring 管理,目前暂不支持作为解码额外选项",
|
||||||
|
m.getName(), declaringClass.getSimpleName());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
info = new MethodInfo(m, declaringClass, order);
|
||||||
|
}
|
||||||
|
List<MethodInfo> list = responseDecodeExtensions.get(protocolId);
|
||||||
|
if (list == null) {
|
||||||
|
list = new ArrayList<>();
|
||||||
|
list.add(info);
|
||||||
|
responseDecodeExtensions.put(protocolId, list);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
list.add(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (List<MethodInfo> list : responseDecodeExtensions.values()) {
|
||||||
|
list.sort(Comparator.comparingInt(info -> info.getOrder()));
|
||||||
|
}
|
||||||
|
log.debug("共载入 {} 个 ProtocolID 的 {} 个方法",
|
||||||
|
responseDecodeExtensions.keySet().size(), responseDecodeExtensions.values().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 emoney protobuf 的请求
|
||||||
|
* @param <U>
|
||||||
|
* @param body
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@PostMapping("/request/decode")
|
@PostMapping("/request/decode")
|
||||||
public <U extends MessageNano> EmoneyConvertResult requestDecode(
|
public <U extends MessageNano> EmoneyConvertResult requestDecode(
|
||||||
@@ -39,13 +131,14 @@ public class ProtoDecodeControllerV1 {
|
|||||||
|
|
||||||
Integer protocolId = body.getProtocolId();
|
Integer protocolId = body.getProtocolId();
|
||||||
if (Objects.isNull(protocolId)) {
|
if (Objects.isNull(protocolId)) {
|
||||||
throw RException.badRequest("protocolId cannot be null");
|
throw RException.badRequest("protocolId 不能为 null");
|
||||||
}
|
}
|
||||||
|
|
||||||
ProtocolMatch match = protocolMatchService.getById(protocolId);
|
ProtocolMatch match = protocolMatchService.getById(protocolId);
|
||||||
|
|
||||||
if (Objects.isNull(match) || StringUtils.isBlank(match.getClassName())) {
|
if (Objects.isNull(match) || StringUtils.isBlank(match.getClassName())) {
|
||||||
throw RException.badRequest("暂无对应 protocolId = " + protocolId + " 的记录,可等 response decoder 搜集到后重试");
|
throw RException
|
||||||
|
.badRequest("暂无对应 protocolId = ", protocolId, " 的记录,可等待 response decoder 收集到后再重试");
|
||||||
}
|
}
|
||||||
|
|
||||||
String className = new StringBuilder()
|
String className = new StringBuilder()
|
||||||
@@ -111,7 +204,7 @@ public class ProtoDecodeControllerV1 {
|
|||||||
Integer protocolId = body.getProtocolId();
|
Integer protocolId = body.getProtocolId();
|
||||||
ProtocolMatch match = null;
|
ProtocolMatch match = null;
|
||||||
if (Objects.isNull(protocolId)) {
|
if (Objects.isNull(protocolId)) {
|
||||||
log.warn("protocolId is null, cannot update protocolMatch");
|
log.warn("protocolId 为空 null, 无法更新 protocolMatch");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
match = protocolMatchService.getById(protocolId);
|
match = protocolMatchService.getById(protocolId);
|
||||||
@@ -178,8 +271,30 @@ public class ProtoDecodeControllerV1 {
|
|||||||
U nano = (U)MessageNano.mergeFrom(
|
U nano = (U)MessageNano.mergeFrom(
|
||||||
(MessageNano)clazz.getDeclaredConstructor().newInstance(),
|
(MessageNano)clazz.getDeclaredConstructor().newInstance(),
|
||||||
baseResponse.detail.getValue());
|
baseResponse.detail.getValue());
|
||||||
|
|
||||||
|
JsonNode jo = new ObjectMapper().valueToTree(nano);
|
||||||
|
|
||||||
|
// 协议 9400 则更新到 StrategyAndPool 里面去
|
||||||
|
if (protocolId == 9400) {
|
||||||
|
strategyAndPoolService.updateByQueryResponse(jo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找 ResponseDecodeExtension
|
||||||
|
List<MethodInfo> methodInfos = responseDecodeExtensions.get(protocolId.toString());
|
||||||
|
if (methodInfos != null) {
|
||||||
|
for (MethodInfo methodInfo : methodInfos) {
|
||||||
|
Object instance = null;
|
||||||
|
if (methodInfo.getDeclaringClass() != null) {
|
||||||
|
// 获取 spring 管理的实例类
|
||||||
|
instance = SpringContextHolder.getBean(methodInfo.getDeclaringClass());
|
||||||
|
}
|
||||||
|
// invoke
|
||||||
|
methodInfo.getMethod().invoke(instance, jo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return EmoneyConvertResult
|
return EmoneyConvertResult
|
||||||
.ok(new ObjectMapper().valueToTree(nano))
|
.ok((Serializable)jo)
|
||||||
.setProtocolId(protocolId)
|
.setProtocolId(protocolId)
|
||||||
.setSupposedClassName(className);
|
.setSupposedClassName(className);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,84 @@
|
|||||||
package quant.rich.emoney.controller.common;
|
package quant.rich.emoney.controller.common;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
|
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
|
||||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import quant.rich.emoney.exception.RException;
|
import quant.rich.emoney.exception.RException;
|
||||||
import quant.rich.emoney.pojo.dto.R;
|
import quant.rich.emoney.pojo.dto.R;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public abstract class UpdateBoolController<T, M extends BaseMapper<T>, S extends ServiceImpl<M, T>> extends BaseController {
|
public abstract class UpdateBoolController<T> extends BaseController {
|
||||||
|
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApplicationContext ctx;
|
||||||
|
|
||||||
|
private Map<Class<?>, IService<?>> serviceMap;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
void init() {
|
||||||
|
serviceMap = new HashMap<>();
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
Map<String, IService> beans = ctx.getBeansOfType(IService.class);
|
||||||
|
for (IService<?> service : beans.values()) {
|
||||||
|
ResolvableType type = ResolvableType.forClass(service.getClass()).as(IService.class);
|
||||||
|
Class<?> entityType = type.getGeneric(0).resolve();
|
||||||
|
if (entityType != null) {
|
||||||
|
serviceMap.put(entityType, service);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public IService<T> getService(Class<T> entityClass) {
|
||||||
|
return (IService<T>) serviceMap.get(entityClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/updateBool")
|
||||||
|
@ResponseBody
|
||||||
protected
|
protected
|
||||||
R<?> updateBool(S service, Class<T> clazz, SFunction<T, ?> idName, Object idValue, String field, Boolean value) {
|
R<?> updateBool(String id, String field, Boolean value) {
|
||||||
|
|
||||||
|
|
||||||
|
ResolvableType type = ResolvableType.forClass(this.getClass()).as(UpdateBoolController.class);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Class<T> clazz = (Class<T>) type.getGeneric(0).resolve();
|
||||||
|
|
||||||
TableInfo tableInfo = TableInfoHelper.getTableInfo(clazz);
|
TableInfo tableInfo = TableInfoHelper.getTableInfo(clazz);
|
||||||
Object converted = mapper.convertValue(idValue, tableInfo.getKeyType());
|
Object converted = mapper.convertValue(id, tableInfo.getKeyType());
|
||||||
|
|
||||||
|
// 获取 Service
|
||||||
|
IService<T> s = getService((Class<T>) clazz);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
String idField = tableInfo.getKeyColumn();
|
||||||
Field declaredField = clazz.getDeclaredField(field);
|
Field declaredField = clazz.getDeclaredField(field);
|
||||||
Optional<TableFieldInfo> fieldInfo = tableInfo.getFieldList().stream()
|
Optional<TableFieldInfo> fieldInfo = tableInfo.getFieldList().stream()
|
||||||
.filter(f -> f.getProperty().equals(field))
|
.filter(f -> f.getProperty().equals(field))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
if (declaredField.getType().equals(Boolean.class)) {
|
if (declaredField.getType().equals(Boolean.class)) {
|
||||||
return R.judge(service.update(
|
return R.judge(s.update(
|
||||||
new UpdateWrapper<T>()
|
new UpdateWrapper<T>()
|
||||||
.set(fieldInfo.get().getColumn(), value)
|
.set(fieldInfo.get().getColumn(), value)
|
||||||
.lambda().eq(idName, converted)
|
.eq(idField, converted)
|
||||||
), "更新失败,请查看日志");
|
), "更新失败,请查看日志");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,8 +88,4 @@ public abstract class UpdateBoolController<T, M extends BaseMapper<T>, S extends
|
|||||||
throw RException.badRequest().setLogRequest(true);
|
throw RException.badRequest().setLogRequest(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/updateBool")
|
|
||||||
@ResponseBody
|
|
||||||
abstract protected R<?> updateBool(String id, String field, Boolean value);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package quant.rich.emoney.controller.manage;
|
package quant.rich.emoney.controller.manage;
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -22,7 +21,6 @@ import quant.rich.emoney.controller.common.UpdateBoolController;
|
|||||||
import quant.rich.emoney.entity.sqlite.Plan;
|
import quant.rich.emoney.entity.sqlite.Plan;
|
||||||
import quant.rich.emoney.exception.RException;
|
import quant.rich.emoney.exception.RException;
|
||||||
import quant.rich.emoney.interfaces.IQueryableEnum;
|
import quant.rich.emoney.interfaces.IQueryableEnum;
|
||||||
import quant.rich.emoney.mapper.sqlite.PlanMapper;
|
|
||||||
import quant.rich.emoney.pojo.dto.LayPageReq;
|
import quant.rich.emoney.pojo.dto.LayPageReq;
|
||||||
import quant.rich.emoney.pojo.dto.LayPageResp;
|
import quant.rich.emoney.pojo.dto.LayPageResp;
|
||||||
import quant.rich.emoney.pojo.dto.R;
|
import quant.rich.emoney.pojo.dto.R;
|
||||||
@@ -31,7 +29,7 @@ import quant.rich.emoney.service.sqlite.PlanService;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/admin/v1/manage/plan")
|
@RequestMapping("/admin/v1/manage/plan")
|
||||||
public class PlanControllerV1 extends UpdateBoolController<Plan, PlanMapper, PlanService> {
|
public class PlanControllerV1 extends UpdateBoolController<Plan> {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
PlanService planService;
|
PlanService planService;
|
||||||
@@ -51,32 +49,10 @@ public class PlanControllerV1 extends UpdateBoolController<Plan, PlanMapper, Pla
|
|||||||
@GetMapping("/getOne")
|
@GetMapping("/getOne")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public R<?> getOne(String planId) {
|
public R<?> getOne(String planId) {
|
||||||
|
// 如果 planId 是空,说明可能希望新建一个 Plan,需要返回默认实例化对象,否则从数据库取
|
||||||
// 如果 planId 是空,说明可能希望新建一个 Plan,需要返回默认实例化对象
|
return
|
||||||
if (planId == null) {
|
planId == null ? R.ok(new Plan()) :
|
||||||
return R.ok(new Plan());
|
R.judgeNonNull(planService.getById(planId), "无法找到对应 ID 的 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 id, String field, Boolean value) {
|
|
||||||
return updateBool(planService, Plan.class, Plan::getPlanId, id, field, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/save")
|
@PostMapping("/save")
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import quant.rich.emoney.controller.common.BaseController;
|
import quant.rich.emoney.controller.common.BaseController;
|
||||||
import quant.rich.emoney.entity.sqlite.Plan;
|
|
||||||
import quant.rich.emoney.entity.sqlite.ProtocolMatch;
|
import quant.rich.emoney.entity.sqlite.ProtocolMatch;
|
||||||
import quant.rich.emoney.exception.RException;
|
import quant.rich.emoney.exception.RException;
|
||||||
import quant.rich.emoney.pojo.dto.LayPageReq;
|
import quant.rich.emoney.pojo.dto.LayPageReq;
|
||||||
@@ -64,11 +63,11 @@ public class ProtocolMatchControllerV1 extends BaseController {
|
|||||||
}
|
}
|
||||||
throw RException.badRequest("protocolId 不允许为空");
|
throw RException.badRequest("protocolId 不允许为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/delete")
|
@PostMapping("/delete")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public R<?> delete(Integer protocolMatchId) {
|
public R<?> delete(Integer protocolMatchId) {
|
||||||
return R.judge(protocolMatchService.removeById(protocolMatchId), "删除失败,是否已删除?");
|
return R.judge(protocolMatchService.removeById(protocolMatchId), "删除失败,是否已删除?");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
|||||||
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
|
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
|
||||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
package quant.rich.emoney.controller.manage;
|
package quant.rich.emoney.controller.manage;
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
@@ -14,18 +10,11 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import quant.rich.emoney.controller.common.BaseController;
|
|
||||||
import quant.rich.emoney.controller.common.UpdateBoolController;
|
import quant.rich.emoney.controller.common.UpdateBoolController;
|
||||||
import quant.rich.emoney.entity.sqlite.RequestInfo;
|
import quant.rich.emoney.entity.sqlite.RequestInfo;
|
||||||
import quant.rich.emoney.exception.RException;
|
|
||||||
import quant.rich.emoney.mapper.sqlite.RequestInfoMapper;
|
|
||||||
import quant.rich.emoney.pojo.dto.LayPageReq;
|
import quant.rich.emoney.pojo.dto.LayPageReq;
|
||||||
import quant.rich.emoney.pojo.dto.LayPageResp;
|
import quant.rich.emoney.pojo.dto.LayPageResp;
|
||||||
import quant.rich.emoney.pojo.dto.R;
|
import quant.rich.emoney.pojo.dto.R;
|
||||||
@@ -34,7 +23,7 @@ import quant.rich.emoney.service.sqlite.RequestInfoService;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/admin/v1/manage/requestInfo")
|
@RequestMapping("/admin/v1/manage/requestInfo")
|
||||||
public class RequestInfoControllerV1 extends UpdateBoolController<RequestInfo, RequestInfoMapper, RequestInfoService> {
|
public class RequestInfoControllerV1 extends UpdateBoolController<RequestInfo> {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
RequestInfoService requestInfoService;
|
RequestInfoService requestInfoService;
|
||||||
@@ -55,22 +44,15 @@ public class RequestInfoControllerV1 extends UpdateBoolController<RequestInfo, R
|
|||||||
@ResponseBody
|
@ResponseBody
|
||||||
public R<?> getOne(Integer id) {
|
public R<?> getOne(Integer id) {
|
||||||
|
|
||||||
// 如果 planId 是空,说明可能希望新建一个 Plan,需要返回默认实例化对象
|
// 如果 id 是空,说明可能希望新建并返回默认实例化对象
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
return R.ok(new RequestInfo());
|
return R.ok(new RequestInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 否则从数据库取
|
// 否则从数据库取
|
||||||
RequestInfo plan = requestInfoService.getById(id);
|
RequestInfo requestInfo = requestInfoService.getById(id);
|
||||||
return R.judge(plan != null, plan, "无法找到对应 ID 的 Plan");
|
return R.judgeNonNull(requestInfo, "无法找到对应 ID 的请求信息");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/updateBool")
|
|
||||||
@ResponseBody
|
|
||||||
@Override
|
|
||||||
protected R<?> updateBool(String id, String field, Boolean value) {
|
|
||||||
return updateBool(requestInfoService, RequestInfo.class, RequestInfo::getId, id, field, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/save")
|
@PostMapping("/save")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package quant.rich.emoney.controller.manage;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
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.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import quant.rich.emoney.controller.common.BaseController;
|
||||||
|
import quant.rich.emoney.entity.sqlite.StrategyAndPool;
|
||||||
|
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.StrategyAndPoolService;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/admin/v1/manage/strategyAndPool")
|
||||||
|
public class StrategyAndPoolControllerV1 extends BaseController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
StrategyAndPoolService strategyAndPoolService;
|
||||||
|
|
||||||
|
@GetMapping({"", "/", "/index"})
|
||||||
|
public String index() {
|
||||||
|
System.out.println(strategyAndPoolService.exportPreparedStrategies().toPrettyString());
|
||||||
|
return "/admin/v1/manage/strategyAndPool/index";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
@ResponseBody
|
||||||
|
public LayPageResp<?> list(LayPageReq<StrategyAndPool> pageReq) {
|
||||||
|
Page<StrategyAndPool> planPage = strategyAndPoolService.page(pageReq,
|
||||||
|
new LambdaQueryWrapper<StrategyAndPool>()
|
||||||
|
.orderByAsc(StrategyAndPool::getStrategyId, StrategyAndPool::getPoolId));
|
||||||
|
return new LayPageResp<>(planPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/getOne")
|
||||||
|
@ResponseBody
|
||||||
|
public R<?> getOne(Integer poolId) {
|
||||||
|
|
||||||
|
// 如果 planId 是空,说明可能希望新建一个 Plan,需要返回默认实例化对象
|
||||||
|
if (poolId == null) {
|
||||||
|
return R.ok(new StrategyAndPool());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则从数据库取
|
||||||
|
StrategyAndPool strategyAndPool = strategyAndPoolService.getById(poolId);
|
||||||
|
return R.judge(strategyAndPool != null, strategyAndPool, "无法找到对应 ID 的 StrategyAndPool");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/save")
|
||||||
|
@ResponseBody
|
||||||
|
public R<?> save(@RequestBody StrategyAndPool strategyAndPool) {
|
||||||
|
if (Objects.nonNull(strategyAndPool.getPoolId())) {
|
||||||
|
return R.judge(
|
||||||
|
strategyAndPoolService.saveOrUpdate(strategyAndPool), "保存失败");
|
||||||
|
}
|
||||||
|
throw RException.badRequest("poolId 不允许为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/delete")
|
||||||
|
@ResponseBody
|
||||||
|
public R<?> delete(Integer poolId) {
|
||||||
|
return R.judge(strategyAndPoolService.removeById(poolId), "删除失败,是否已删除?");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出 prepared_strategies.json
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
@GetMapping("/exportPreparedStrategies")
|
||||||
|
@ResponseBody
|
||||||
|
public void exportPreparedStrategies() throws IOException {
|
||||||
|
|
||||||
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
|
response.setHeader("Content-Disposition", "attachment; filename=\"prepared_strategies.json\"");
|
||||||
|
|
||||||
|
JsonNode json = strategyAndPoolService.exportPreparedStrategies();
|
||||||
|
response.getWriter().write(json.toPrettyString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -185,7 +185,7 @@ public class EmoneyRequestConfig implements IConfig<EmoneyRequestConfig> {
|
|||||||
* OkHttp 用于注入 User-Agent 规则的 id
|
* OkHttp 用于注入 User-Agent 规则的 id
|
||||||
*/
|
*/
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private Serializable userAgentPatchRuleId;
|
private Integer userAgentPatchRuleId;
|
||||||
|
|
||||||
@Getter(AccessLevel.PRIVATE)
|
@Getter(AccessLevel.PRIVATE)
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -207,7 +207,7 @@ public class EmoneyRequestConfig implements IConfig<EmoneyRequestConfig> {
|
|||||||
chromeVersionsConfig = Objects.requireNonNullElseGet(chromeVersionsConfig, () -> SpringContextHolder.getBean(ChromeVersionsConfig.class));
|
chromeVersionsConfig = Objects.requireNonNullElseGet(chromeVersionsConfig, () -> SpringContextHolder.getBean(ChromeVersionsConfig.class));
|
||||||
}
|
}
|
||||||
catch (IllegalStateException e) {
|
catch (IllegalStateException e) {
|
||||||
log.debug("SpringContext not ready");
|
log.debug("试图从 SpringContextHolder 初始化 androidSdkLevelConfig, deviceInfoConfig 和 chromeVersionConfig, 但 SpringContextHolder 未准备好");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ObjectUtils.anyNull(fingerprint, buildId, deviceName, androidVersion, androidSdkLevel, softwareType)) {
|
if (ObjectUtils.anyNull(fingerprint, buildId, deviceName, androidVersion, androidSdkLevel, softwareType)) {
|
||||||
@@ -219,7 +219,7 @@ public class EmoneyRequestConfig implements IConfig<EmoneyRequestConfig> {
|
|||||||
// model 和 softwareType 本应交由 deviceInfoConfig 检查以
|
// model 和 softwareType 本应交由 deviceInfoConfig 检查以
|
||||||
// 应对可能的通过修改本地 json 来进行攻击的方式,可是本身
|
// 应对可能的通过修改本地 json 来进行攻击的方式,可是本身
|
||||||
// deviceInfoConfig 对 model 和 softwareType 的信息也来源
|
// deviceInfoConfig 对 model 和 softwareType 的信息也来源
|
||||||
// 于本地,万一本地的 deviceInfo.(fallback.)json 也不值得信任?
|
// 于本地,万一本地的 deviceInfo(.fallback).json 也不值得信任?
|
||||||
// 所以只检查 fingerprint
|
// 所以只检查 fingerprint
|
||||||
|
|
||||||
DeviceInfo deviceInfo;
|
DeviceInfo deviceInfo;
|
||||||
@@ -228,12 +228,12 @@ public class EmoneyRequestConfig implements IConfig<EmoneyRequestConfig> {
|
|||||||
deviceInfo = DeviceInfo.from(null, fingerprint);
|
deviceInfo = DeviceInfo.from(null, fingerprint);
|
||||||
Validate.validState(androidVersion.equals(
|
Validate.validState(androidVersion.equals(
|
||||||
deviceInfo.getVersionRelease()),
|
deviceInfo.getVersionRelease()),
|
||||||
"androidVersion(versionRelease) doesn't match");
|
"androidVersion(versionRelease) 与预设 fingerprint 不匹配");
|
||||||
Validate.validState(androidSdkLevel.equals(
|
Validate.validState(androidSdkLevel.equals(
|
||||||
String.valueOf(androidSdkLevelConfig.getSdkLevel(deviceInfo.getVersionRelease()))),
|
String.valueOf(androidSdkLevelConfig.getSdkLevel(deviceInfo.getVersionRelease()))),
|
||||||
"androidSdkLevel doesn't match");
|
"androidSdkLevel 与预设 fingerprint 不匹配");
|
||||||
Validate.validState(buildId.equals(deviceInfo.getBuildId()),
|
Validate.validState(buildId.equals(deviceInfo.getBuildId()),
|
||||||
"buildId doesn't match");
|
"buildId 与预设 fingerprint 不匹配");
|
||||||
}
|
}
|
||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
valid = false;
|
valid = false;
|
||||||
@@ -468,7 +468,7 @@ public class EmoneyRequestConfig implements IConfig<EmoneyRequestConfig> {
|
|||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public ObjectNode getReloginObject() {
|
public ObjectNode getReloginObject() {
|
||||||
|
|
||||||
if (ObjectUtils.anyNull(getAuthorization(), getUid())) {
|
if (getUid() == null || StringUtils.isBlank(getAuthorization())) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,19 +9,15 @@ import org.springframework.context.annotation.Lazy;
|
|||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonView;
|
import com.fasterxml.jackson.annotation.JsonView;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import jodd.io.FileUtil;
|
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
import okhttp3.ConnectionPool;
|
import okhttp3.ConnectionPool;
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
|
import quant.rich.emoney.annotation.LockByCaller;
|
||||||
import quant.rich.emoney.client.OkHttpClientProvider;
|
import quant.rich.emoney.client.OkHttpClientProvider;
|
||||||
import quant.rich.emoney.component.LockByCaller;
|
|
||||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||||
import quant.rich.emoney.interfaces.IConfig;
|
import quant.rich.emoney.interfaces.IConfig;
|
||||||
|
|
||||||
|
|||||||
@@ -25,25 +25,55 @@ import quant.rich.emoney.mybatis.typehandler.JsonStringTypeHandler;
|
|||||||
@TableName(value = "plan", autoResultMap = true)
|
@TableName(value = "plan", autoResultMap = true)
|
||||||
public class Plan {
|
public class Plan {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 键 ID
|
||||||
|
*/
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private String planId;
|
private String planId;
|
||||||
|
|
||||||
private String cronExpression;
|
/**
|
||||||
|
* 计划任务表达式
|
||||||
|
*/
|
||||||
|
private String cronExpr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计划名称
|
||||||
|
*/
|
||||||
private String planName;
|
private String planName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指标代码
|
||||||
|
*/
|
||||||
private String indexCode;
|
private String indexCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需要抓取的指标周期
|
||||||
|
*/
|
||||||
@TableField(typeHandler = CommaListTypeHandler.class)
|
@TableField(typeHandler = CommaListTypeHandler.class)
|
||||||
private List<String> periods;
|
private List<String> periods;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数
|
||||||
|
*/
|
||||||
@TableField(typeHandler = JsonStringTypeHandler.class)
|
@TableField(typeHandler = JsonStringTypeHandler.class)
|
||||||
private JsonNode params;
|
private JsonNode params;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
private Boolean enabled;
|
private Boolean enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交易日检查,若为 true,则任务执行时判断当日是否为交易日,若是则不执行任务,
|
||||||
|
* 反之无论是否为交易日都执行
|
||||||
|
*/
|
||||||
private Boolean openDayCheck;
|
private Boolean openDayCheck;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置抓取周期并去重
|
||||||
|
* @param periods
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
public Plan setPeriods(List<String> periods) {
|
public Plan setPeriods(List<String> periods) {
|
||||||
if (CollectionUtils.isEmpty(periods)) {
|
if (CollectionUtils.isEmpty(periods)) {
|
||||||
this.periods = Collections.emptyList();
|
this.periods = Collections.emptyList();
|
||||||
@@ -59,6 +89,10 @@ public class Plan {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取抓取周期。已去重
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
public List<String> getPeriods() {
|
public List<String> getPeriods() {
|
||||||
setPeriods(periods);
|
setPeriods(periods);
|
||||||
return periods;
|
return periods;
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package quant.rich.emoney.entity.sqlite;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.ToString;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 益盟策略和策略池
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Accessors(chain=true)
|
||||||
|
@TableName(value="strategy_and_pool")
|
||||||
|
@ToString
|
||||||
|
public class StrategyAndPool implements Comparable<StrategyAndPool> {
|
||||||
|
|
||||||
|
private String strategyName;
|
||||||
|
private Integer strategyId;
|
||||||
|
private String poolName;
|
||||||
|
@TableId
|
||||||
|
private Integer poolId;
|
||||||
|
|
||||||
|
public StrategyAndPool() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public StrategyAndPool(String strategyName, Integer strategyId, String poolName, Integer poolId) {
|
||||||
|
this.strategyName = strategyName;
|
||||||
|
this.strategyId = strategyId;
|
||||||
|
this.poolName = poolName;
|
||||||
|
this.poolId = poolId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof StrategyAndPool)) return false;
|
||||||
|
StrategyAndPool strategyAndPool = (StrategyAndPool) o;
|
||||||
|
return
|
||||||
|
strategyName == strategyAndPool.strategyName &&
|
||||||
|
strategyId == strategyAndPool.strategyId &&
|
||||||
|
poolName == strategyAndPool.poolName &&
|
||||||
|
poolId == strategyAndPool.poolId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(strategyName, strategyId, poolName, poolId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序,先按 strategyId 升序,再按 poolId 升序
|
||||||
|
* @param other
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int compareTo(StrategyAndPool other) {
|
||||||
|
int cmp = Integer.compare(this.strategyId, other.strategyId);
|
||||||
|
if (cmp == 0) {
|
||||||
|
cmp = Integer.compare(this.poolId, other.poolId);
|
||||||
|
}
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,17 @@ public class RException extends RuntimeException {
|
|||||||
public static RException badRequest(String message) {
|
public static RException badRequest(String message) {
|
||||||
return new RException(HttpStatus.BAD_REQUEST, message);
|
return new RException(HttpStatus.BAD_REQUEST, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static RException badRequest(Object... message) {
|
||||||
|
if (message == null || message.length == 0) {
|
||||||
|
return RException.badRequest();
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < message.length; i++) {
|
||||||
|
sb.append(message[i]);
|
||||||
|
}
|
||||||
|
return new RException(HttpStatus.BAD_REQUEST, sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
public static RException unauthorized() {
|
public static RException unauthorized() {
|
||||||
return new RException(HttpStatus.UNAUTHORIZED);
|
return new RException(HttpStatus.UNAUTHORIZED);
|
||||||
@@ -53,6 +64,17 @@ public class RException extends RuntimeException {
|
|||||||
return new RException(HttpStatus.UNAUTHORIZED, message);
|
return new RException(HttpStatus.UNAUTHORIZED, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static RException unauthorized(Object... message) {
|
||||||
|
if (message == null || message.length == 0) {
|
||||||
|
return RException.unauthorized();
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < message.length; i++) {
|
||||||
|
sb.append(message[i]);
|
||||||
|
}
|
||||||
|
return new RException(HttpStatus.UNAUTHORIZED, sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
public static RException internalServerError() {
|
public static RException internalServerError() {
|
||||||
return new RException(HttpStatus.INTERNAL_SERVER_ERROR);
|
return new RException(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
@@ -61,4 +83,14 @@ public class RException extends RuntimeException {
|
|||||||
return new RException(HttpStatus.INTERNAL_SERVER_ERROR, message);
|
return new RException(HttpStatus.INTERNAL_SERVER_ERROR, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static RException internalServerError(Object... message) {
|
||||||
|
if (message == null || message.length == 0) {
|
||||||
|
return RException.internalServerError();
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < message.length; i++) {
|
||||||
|
sb.append(message[i]);
|
||||||
|
}
|
||||||
|
return new RException(HttpStatus.INTERNAL_SERVER_ERROR, sb.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.StrategyAndPool;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Mapper
|
||||||
|
@DS("sqlite")
|
||||||
|
public interface StrategyAndPoolMapper extends BaseMapper<StrategyAndPool> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -32,14 +32,19 @@ public class PatchOkHttp {
|
|||||||
* @param rule
|
* @param rule
|
||||||
* @return 如果 rule 未设置 id,则生成随机 id 并返回,否则返回 rule.getId()
|
* @return 如果 rule 未设置 id,则生成随机 id 并返回,否则返回 rule.getId()
|
||||||
*/
|
*/
|
||||||
public static Serializable apply(PatchOkHttpRule rule) {
|
public static Integer apply(PatchOkHttpRule rule) {
|
||||||
|
|
||||||
if (rule.getId() == null) {
|
if (rule.getId() == null) {
|
||||||
rule.setId(random.nextInt());
|
Integer[] randomIds = new Integer[] { random.nextInt() };
|
||||||
|
while (rules.parallelStream().anyMatch(r -> Integer.compare(randomIds[0], r.getId()) == 0)) {
|
||||||
|
// 如果 rules 有已有 ID 的话要重新设置 ID,不能重复
|
||||||
|
randomIds[0] = random.nextInt();
|
||||||
|
}
|
||||||
|
rule.setId(randomIds[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
rules.add(rule);
|
rules.add(rule);
|
||||||
log.debug("PatchOkHttp.apply() running in classloader {}", PatchOkHttp.class.getClassLoader());
|
log.debug("PatchOkHttp.apply() 在 ClassLoader {} 中运行", PatchOkHttp.class.getClassLoader());
|
||||||
if (!isHooked) hook();
|
if (!isHooked) hook();
|
||||||
return rule.getId();
|
return rule.getId();
|
||||||
}
|
}
|
||||||
@@ -56,7 +61,7 @@ public class PatchOkHttp {
|
|||||||
|
|
||||||
public static void match(RequestContext ctx, String currentHeader, Consumer<String> consumer) {
|
public static void match(RequestContext ctx, String currentHeader, Consumer<String> consumer) {
|
||||||
if (!logOnce) {
|
if (!logOnce) {
|
||||||
log.debug("PatchOkHttp.match() running in classloader {}", PatchOkHttp.class.getClassLoader());
|
log.debug("PatchOkHttp.match() 在 ClassLoader {} 中运行", PatchOkHttp.class.getClassLoader());
|
||||||
logOnce = true;
|
logOnce = true;
|
||||||
}
|
}
|
||||||
for (PatchOkHttpRule rule : PatchOkHttp.rules) {
|
for (PatchOkHttpRule rule : PatchOkHttp.rules) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package quant.rich.emoney.patch.okhttp;
|
package quant.rich.emoney.patch.okhttp;
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.*;
|
import java.util.function.*;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -19,7 +18,7 @@ public class PatchOkHttpRule {
|
|||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Accessors(chain=true)
|
@Accessors(chain=true)
|
||||||
private Serializable id;
|
private Integer id;
|
||||||
private final Predicate<RequestContext> condition;
|
private final Predicate<RequestContext> condition;
|
||||||
private final List<HeaderAction> actions;
|
private final List<HeaderAction> actions;
|
||||||
|
|
||||||
@@ -171,7 +170,6 @@ public class PatchOkHttpRule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public boolean equals(PatchOkHttpRule other) {
|
public boolean equals(PatchOkHttpRule other) {
|
||||||
if (other == null) return false;
|
if (other == null) return false;
|
||||||
if (this.id == null || other.id == null) return false;
|
if (this.id == null || other.id == null) return false;
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ public class R<T> implements Serializable {
|
|||||||
@Getter(AccessLevel.PRIVATE)
|
@Getter(AccessLevel.PRIVATE)
|
||||||
private int resultCode;
|
private int resultCode;
|
||||||
private String message;
|
private String message;
|
||||||
private boolean ok;
|
|
||||||
private T data;
|
private T data;
|
||||||
|
|
||||||
public R() {
|
public R() {
|
||||||
@@ -172,13 +171,32 @@ public class R<T> implements Serializable {
|
|||||||
throw RException.badRequest();
|
throw RException.badRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 obj 是否为空返回对应内容<p>
|
||||||
|
* obj 不为空时,等效于 <code>R.<i>ok</i>(obj)</code><br>
|
||||||
|
* obj 为空时抛出 <code>RException.<i>badRequest</i>()</code>
|
||||||
|
* @param obj
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
public static R<?> judgeNonNull(Object obj) {
|
public static R<?> judgeNonNull(Object obj) {
|
||||||
if (obj != null) return R.ok(obj);
|
if (obj != null) return R.ok(obj);
|
||||||
throw RException.badRequest();
|
throw RException.badRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 obj 是否为空返回对应内容<p>
|
||||||
|
* obj 不为空时,等效于 <code>R.<i>ok</i>(obj)</code><br>
|
||||||
|
* obj 为空时抛出 <code>RException.<i>badRequest</i>(failedMessage)</code>
|
||||||
|
* @param obj
|
||||||
|
* @param failedMessage
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static R<?> judgeNonNull(Object obj, String failedMessage) {
|
||||||
|
if (obj != null) return R.ok(obj);
|
||||||
|
throw RException.badRequest(failedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
public static R<?> judge(ThrowingSupplier<?> supplier) {
|
public static R<?> judge(ThrowingSupplier<?> supplier) {
|
||||||
try {
|
try {
|
||||||
return R.ok(supplier.get());
|
return R.ok(supplier.get());
|
||||||
|
|||||||
@@ -17,13 +17,8 @@ import io.micrometer.core.instrument.util.IOUtils;
|
|||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.nio.charset.Charset;
|
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.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -35,7 +30,6 @@ import org.reflections.scanners.Scanners;
|
|||||||
import org.reflections.util.ConfigurationBuilder;
|
import org.reflections.util.ConfigurationBuilder;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
|
||||||
import org.springframework.jmx.export.annotation.ManagedAttribute;
|
import org.springframework.jmx.export.annotation.ManagedAttribute;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -280,7 +274,7 @@ public class ConfigService implements InitializingBean {
|
|||||||
try {
|
try {
|
||||||
// 此处只是读取文件,并不关心该文件是否可写
|
// 此处只是读取文件,并不关心该文件是否可写
|
||||||
configString = IOUtils.toString(SmartResourceResolver.loadResource(path), Charset.defaultCharset());
|
configString = IOUtils.toString(SmartResourceResolver.loadResource(path), Charset.defaultCharset());
|
||||||
} catch (UncheckedIOException e) {
|
} catch (IOException e) {
|
||||||
String field = fieldClassCache.inverse().get(configClass);
|
String field = fieldClassCache.inverse().get(configClass);
|
||||||
log.warn("Cannot read config {}.json: {}", field, e.getMessage());
|
log.warn("Cannot read config {}.json: {}", field, e.getMessage());
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import java.io.Serializable;
|
|||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -52,6 +51,13 @@ import quant.rich.emoney.pojo.dto.ParamsIndexDetail;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指标详情的服务
|
* 获取指标详情的服务
|
||||||
|
* <p>
|
||||||
|
* <ul>
|
||||||
|
* <li>
|
||||||
|
* 无参指标通过模拟 Webview 获取,其介绍都在欲加载网页的 <script> 脚本中,以 js object
|
||||||
|
* 的形式存在,需要循环载入网页的所有 script 并进行正则匹配
|
||||||
|
* <li>
|
||||||
|
* 有参指标通过接口获取
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@@ -359,7 +365,13 @@ public class IndexDetailService {
|
|||||||
// 视频和图片等,有的要清除并记录,以免有泄露网址、客户端 IP 风险
|
// 视频和图片等,有的要清除并记录,以免有泄露网址、客户端 IP 风险
|
||||||
detail.sanitize();
|
detail.sanitize();
|
||||||
|
|
||||||
InputStream inputStream = SmartResourceResolver.loadResource(path);
|
InputStream inputStream = null;
|
||||||
|
try {
|
||||||
|
inputStream = SmartResourceResolver.loadResource(path);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
log.warn("读取资源文件时发生错误: {}, {}", path, e);
|
||||||
|
}
|
||||||
if (inputStream == null) {
|
if (inputStream == null) {
|
||||||
// 不存在则保存
|
// 不存在则保存
|
||||||
saveIndexDetail(detail);
|
saveIndexDetail(detail);
|
||||||
@@ -388,8 +400,11 @@ public class IndexDetailService {
|
|||||||
data.setItems(items);
|
data.setItems(items);
|
||||||
targetDetail.getData().add(data);
|
targetDetail.getData().add(data);
|
||||||
String path = getIndexDetailPath(targetDetail);
|
String path = getIndexDetailPath(targetDetail);
|
||||||
|
|
||||||
if (SmartResourceResolver.loadResource(path) == null) {
|
try {
|
||||||
|
SmartResourceResolver.loadResource(path);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
// 不存在则保存
|
// 不存在则保存
|
||||||
saveIndexDetail(targetDetail);
|
saveIndexDetail(targetDetail);
|
||||||
}
|
}
|
||||||
@@ -505,7 +520,14 @@ public class IndexDetailService {
|
|||||||
.append(!hasParams ? "nonParams/": "params/")
|
.append(!hasParams ? "nonParams/": "params/")
|
||||||
.append(indexCode)
|
.append(indexCode)
|
||||||
.append(".json").toString();
|
.append(".json").toString();
|
||||||
InputStream inputStream = SmartResourceResolver.loadResource(path);
|
InputStream inputStream = null;
|
||||||
|
try {
|
||||||
|
inputStream = SmartResourceResolver.loadResource(path);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
log.warn("读取资源文件时发生错误: {}, {}", path, e);
|
||||||
|
}
|
||||||
|
|
||||||
return inputStream;
|
return inputStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import com.baomidou.dynamic.datasource.annotation.DS;
|
|||||||
import quant.rich.emoney.entity.sqlite.ProtocolMatch;
|
import quant.rich.emoney.entity.sqlite.ProtocolMatch;
|
||||||
import quant.rich.emoney.mapper.sqlite.ProtocolMatchMapper;
|
import quant.rich.emoney.mapper.sqlite.ProtocolMatchMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 协议匹配服务,将一些匹配到的需要解析的协议放在数据库内方便查询。因为请求本身只带 ProtocolID 不带类名,不指定的话可能会找不到解析类
|
||||||
|
*/
|
||||||
@DS("sqlite")
|
@DS("sqlite")
|
||||||
@Service
|
@Service
|
||||||
public class ProtocolMatchService extends SqliteServiceImpl<ProtocolMatchMapper, ProtocolMatch> {
|
public class ProtocolMatchService extends SqliteServiceImpl<ProtocolMatchMapper, ProtocolMatch> {
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package quant.rich.emoney.service.sqlite;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.JsonNodeType;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import quant.rich.emoney.annotation.ResponseDecodeExtension;
|
||||||
|
import quant.rich.emoney.entity.sqlite.StrategyAndPool;
|
||||||
|
import quant.rich.emoney.mapper.sqlite.StrategyAndPoolMapper;
|
||||||
|
|
||||||
|
@DS("sqlite")
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class StrategyAndPoolService extends SqliteServiceImpl<StrategyAndPoolMapper, StrategyAndPool> {
|
||||||
|
|
||||||
|
static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据库中导入所有已记录的策略和池,形成可供益盟 Fiddler 插件使用的 prepared_strategies.json
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public JsonNode exportPreparedStrategies() {
|
||||||
|
List<StrategyAndPool> all = this.list();
|
||||||
|
Collections.sort(all);
|
||||||
|
|
||||||
|
Integer currentStrategyId = null;
|
||||||
|
ArrayNode preparedStrategies = OBJECT_MAPPER.createArrayNode();
|
||||||
|
ObjectNode current = null;
|
||||||
|
|
||||||
|
for (StrategyAndPool strategyAndPool : all) {
|
||||||
|
if (currentStrategyId == null || Integer.compare(currentStrategyId, strategyAndPool.getStrategyId()) != 0) {
|
||||||
|
|
||||||
|
if (current != null) {
|
||||||
|
preparedStrategies.add(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStrategyId = strategyAndPool.getStrategyId();
|
||||||
|
// 意味着要新增一个 current 来容纳同 strategyId 的所有 pool
|
||||||
|
current = OBJECT_MAPPER.createObjectNode();
|
||||||
|
current.put("code", "strategy_" + strategyAndPool.getStrategyId());
|
||||||
|
current.put("name", strategyAndPool.getStrategyName() + "策略");
|
||||||
|
current.put("type", 0);
|
||||||
|
current.put("blockGroupLogic", 0);
|
||||||
|
current.put("isLocked", false);
|
||||||
|
current.put("matchType", 0);
|
||||||
|
current.put("startDate", 0);
|
||||||
|
current.put("endDate", 0);
|
||||||
|
current.putArray("fields");
|
||||||
|
}
|
||||||
|
ObjectNode pool = OBJECT_MAPPER.createObjectNode();
|
||||||
|
pool.put("code", "pool_" + strategyAndPool.getPoolId());
|
||||||
|
pool.put("name", strategyAndPool.getPoolName());
|
||||||
|
pool.put("isLocked", false);
|
||||||
|
pool.put("isDisabled", false);
|
||||||
|
pool.put("valueType", "Switch");
|
||||||
|
((ArrayNode)current.get("fields")).add(pool);
|
||||||
|
}
|
||||||
|
if (current != null) {
|
||||||
|
preparedStrategies.add(current);
|
||||||
|
}
|
||||||
|
return preparedStrategies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从解析后的 Nano 转换的 JsonNode 转换成 StrategyAndPool 的集合,并存储到数据库中
|
||||||
|
* @param jsonNode
|
||||||
|
*/
|
||||||
|
@ResponseDecodeExtension(protocolId="9400")
|
||||||
|
public void updateByQueryResponse(JsonNode jsonNode) {
|
||||||
|
// jsonNode.output[].band[]/.tech[]
|
||||||
|
|
||||||
|
if (jsonNode == null) {
|
||||||
|
log.warn("试图更新 StrategyAndPool, 但提供的响应 json 为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode output = jsonNode.get("output");
|
||||||
|
if (output == null || output.getNodeType() != JsonNodeType.ARRAY) {
|
||||||
|
log.warn("试图更新 StrategyAndPool, 但提供的响应 json.data.output 为 null 或不为 ARRAY");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<StrategyAndPool> set = new HashSet<>();
|
||||||
|
String[] types = new String[] {"band", "tech", "val"}; // 波段/技术/基本面
|
||||||
|
for (JsonNode node : output) {
|
||||||
|
for (String type : types) {
|
||||||
|
JsonNode strategies = node.get(type);
|
||||||
|
if (strategies != null && strategies.getNodeType() == JsonNodeType.ARRAY) {
|
||||||
|
for (JsonNode simpleStrategy : strategies) {
|
||||||
|
StrategyAndPool strategyAndPool = new StrategyAndPool(
|
||||||
|
simpleStrategy.get("strategyName").asText(),
|
||||||
|
simpleStrategy.get("strategyId").asInt(),
|
||||||
|
simpleStrategy.get("poolName").asText(),
|
||||||
|
simpleStrategy.get("poolId").asInt()
|
||||||
|
);
|
||||||
|
set.add(strategyAndPool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.saveOrUpdateBatch(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,6 +6,9 @@ import java.nio.file.*;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源解析器,用于在不同运行状况(JAR 包、WAR 包及 IDE 调试状态)下读写本地资源
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class SmartResourceResolver {
|
public class SmartResourceResolver {
|
||||||
|
|
||||||
@@ -39,27 +42,24 @@ public class SmartResourceResolver {
|
|||||||
* @param relativePath 相对路径
|
* @param relativePath 相对路径
|
||||||
* @param writable 是否一定可写
|
* @param writable 是否一定可写
|
||||||
* @return
|
* @return
|
||||||
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
public static InputStream loadResource(String relativePath) {
|
public static InputStream loadResource(String relativePath) throws IOException {
|
||||||
try {
|
Path externalPath = resolveExternalPath(relativePath);
|
||||||
Path externalPath = resolveExternalPath(relativePath);
|
|
||||||
|
|
||||||
if (externalPath != null && Files.exists(externalPath)) {
|
if (externalPath != null && Files.exists(externalPath)) {
|
||||||
log.debug("从外部文件系统加载资源: {}", externalPath);
|
log.debug("从外部文件系统加载资源: {}", externalPath);
|
||||||
return Files.newInputStream(externalPath);
|
return Files.newInputStream(externalPath);
|
||||||
}
|
|
||||||
|
|
||||||
// 否则回退到 classpath(JAR、WAR、IDE)
|
|
||||||
InputStream in = SmartResourceResolver.class.getClassLoader().getResourceAsStream(relativePath);
|
|
||||||
if (in != null) {
|
|
||||||
log.debug("从 classpath 内部加载资源: {}", relativePath);
|
|
||||||
return in;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new FileNotFoundException("无法找到资源: " + relativePath);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException("读取资源失败: " + relativePath, e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 否则回退到 classpath(JAR、WAR、IDE)
|
||||||
|
InputStream in = SmartResourceResolver.class.getClassLoader().getResourceAsStream(relativePath);
|
||||||
|
if (in != null) {
|
||||||
|
log.debug("从 classpath 内部加载资源: {}", relativePath);
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new FileNotFoundException("无法找到资源: " + relativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void saveText(String relativePath, String content) throws IOException {
|
public static void saveText(String relativePath, String content) throws IOException {
|
||||||
|
|||||||
44
src/main/java/quant/rich/emoney/util/SpringBeanDetector.java
Normal file
44
src/main/java/quant/rich/emoney/util/SpringBeanDetector.java
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package quant.rich.emoney.util;
|
||||||
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
|
import org.springframework.stereotype.*;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SpringBeanDetector {
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
private static final List<Class> SPRING_COMPONENT_ANNOTATIONS = List.of(
|
||||||
|
Component.class,
|
||||||
|
Service.class,
|
||||||
|
Repository.class,
|
||||||
|
Controller.class,
|
||||||
|
RestController.class,
|
||||||
|
Configuration.class,
|
||||||
|
ControllerAdvice.class,
|
||||||
|
RestControllerAdvice.class,
|
||||||
|
Aspect.class
|
||||||
|
);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static boolean isSpringManagedClass(Class<?> clazz) {
|
||||||
|
for (@SuppressWarnings("rawtypes") Class ann : SPRING_COMPONENT_ANNOTATIONS) {
|
||||||
|
if (AnnotationUtils.findAnnotation(clazz, ann) != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static String findMatchedAnnotation(Class<?> clazz) {
|
||||||
|
for (@SuppressWarnings("rawtypes") Class ann : SPRING_COMPONENT_ANNOTATIONS) {
|
||||||
|
if (AnnotationUtils.findAnnotation(clazz, ann) != null) {
|
||||||
|
return ann.getSimpleName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,60 +57,4 @@ kaptcha:
|
|||||||
# 当数据库不存在配置时默认加载文件内配置
|
# 当数据库不存在配置时默认加载文件内配置
|
||||||
emoney-auto-config:
|
emoney-auto-config:
|
||||||
username: admin
|
username: admin
|
||||||
password: Em0nY_4u70~!
|
password: Em0nY_4u70~!
|
||||||
|
|
||||||
# emoney settings
|
|
||||||
emoney-client:
|
|
||||||
emoney-login-form:
|
|
||||||
acc-id: emy1730978
|
|
||||||
acc-type: 1
|
|
||||||
app-version: 5.0.0
|
|
||||||
channel-id: 1711
|
|
||||||
device-name: LIO-AN00
|
|
||||||
real-ex-identify:
|
|
||||||
imei:
|
|
||||||
android-id: 6e530d685bac6e00
|
|
||||||
mac:
|
|
||||||
os-finger-print: asus/android_x86/x86:5.1.1/LMY47I/V9.5.3.0.LACCNFA:user/release-keys
|
|
||||||
guid: 64de523312b56f1ab10c0423d537e2d6
|
|
||||||
hardware: 970e18b41745962e3d2dc359d81e4602
|
|
||||||
osVersion: 22
|
|
||||||
platform: android
|
|
||||||
product-id: 4
|
|
||||||
pwd: 777988
|
|
||||||
softwareType: Mobile
|
|
||||||
ssid: 0
|
|
||||||
|
|
||||||
|
|
||||||
emoney-anonymous-login-form:
|
|
||||||
acc-id: d31b7d82cb9c328e7351610bbefcece7
|
|
||||||
acc-type: 4
|
|
||||||
app-version: 5.0.0
|
|
||||||
channel-id: 1711
|
|
||||||
device-name: DT1901A
|
|
||||||
real-ex-identify:
|
|
||||||
imei: 864213083058479
|
|
||||||
android-id: a892551da95d26fa
|
|
||||||
mac:
|
|
||||||
os-finger-print: samsung/star2qltezh/star2qltechn:9/PQ3B.190801.002/G9650ZHU2ARC6:user/release-keys
|
|
||||||
guid: d31b7d82cb9c328e7351610bbefcece7
|
|
||||||
hardware: c19d981ba874e7c0f8911bcf575d6a2b
|
|
||||||
osVersion: 22
|
|
||||||
platform: android
|
|
||||||
product-id: 4
|
|
||||||
pwd:
|
|
||||||
softwareType: Mobile
|
|
||||||
ssid: 0
|
|
||||||
|
|
||||||
emoney-login-header:
|
|
||||||
emapp-view-mode: 1
|
|
||||||
x-android-agent: EMAPP/5.0.0(Android;22)
|
|
||||||
x-protocol-id: user%2Fauth%2Flogin
|
|
||||||
authorization:
|
|
||||||
user-agent: okhttp/3.12.2
|
|
||||||
|
|
||||||
emoney-request-header:
|
|
||||||
emapp-view-mode: 1
|
|
||||||
x-android-agent: EMAPP/5.0.0(Android;22)
|
|
||||||
authorization:
|
|
||||||
user-agent: okhttp/3.12.2
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +1,11 @@
|
|||||||
{"id":null,"name":"估值分位","nameCode":"10013000","data":[{"title":"估值分位指标说明:","items":["估值分位是基于个股PETTM的分位数得到的价格区间,反映的是当前股票价格所处的估值位置;机会值和危险值分别表示近七年PETTM的30%分位值和70%分位值;","当前股价进入高估区域, 说明股票当前处于相对高估的位置;","当前股价进入低估区域,说明股票当前处于相对低估的位置;","当前股价处于低估与高估之间,说明股票当前处于相对合理的位置;"],"image":null}],"original":"{\"name\":\"估值分位\",\"nameCode\":\"10013000\",\"data\":[{\"title\":\"估值分位指标说明:\",\"items\":[\"估值分位是基于个股PETTM的分位数得到的价格区间,反映的是当前股票价格所处的估值位置;机会值和危险值分别表示近七年PETTM的30%分位值和70%分位值;\",\"当前股价进入高估区域, 说明股票当前处于相对高估的位置;\",\"当前股价进入低估区域,说明股票当前处于相对低估的位置;\",\"当前股价处于低估与高估之间,说明股票当前处于相对合理的位置;\"]}]}","indexCode":"10013000","indexName":"估值分位","details":[{"content":"估值分位指标说明:","type":"TITLE"},{"content":"估值分位是基于个股PETTM的分位数得到的价格区间,反映的是当前股票价格所处的估值位置;机会值和危险值分别表示近七年PETTM的30%分位值和70%分位值;","type":"TEXT"},{"content":"当前股价进入高估区域, 说明股票当前处于相对高估的位置;","type":"TEXT"},{"content":"当前股价进入低估区域,说明股票当前处于相对低估的位置;","type":"TEXT"},{"content":"当前股价处于低估与高估之间,说明股票当前处于相对合理的位置;","type":"TEXT"}]}
|
{
|
||||||
|
"id" : null,
|
||||||
|
"name" : "估值分位",
|
||||||
|
"nameCode" : "10013000",
|
||||||
|
"data" : [ {
|
||||||
|
"title" : "估值分位指标说明:",
|
||||||
|
"items" : [ "估值分位是基于个股PETTM的分位数得到的价格区间,反映的是当前股票价格所处的估值位置;机会值和危险值分别表示近七年PETTM的30%分位值和70%分位值;", "当前股价进入高估区域, 说明股票当前处于相对高估的位置;", "当前股价进入低估区域,说明股票当前处于相对低估的位置;", "当前股价处于低估与高估之间,说明股票当前处于相对合理的位置;" ],
|
||||||
|
"image" : null
|
||||||
|
} ],
|
||||||
|
"original" : "{\"name\":\"估值分位\",\"nameCode\":\"10013000\",\"data\":[{\"title\":\"估值分位指标说明:\",\"items\":[\"估值分位是基于个股PETTM的分位数得到的价格区间,反映的是当前股票价格所处的估值位置;机会值和危险值分别表示近七年PETTM的30%分位值和70%分位值;\",\"当前股价进入高估区域, 说明股票当前处于相对高估的位置;\",\"当前股价进入低估区域,说明股票当前处于相对低估的位置;\",\"当前股价处于低估与高估之间,说明股票当前处于相对合理的位置;\"]}]}"
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"id" : null,
|
||||||
|
"name" : null,
|
||||||
|
"code" : "10007100",
|
||||||
|
"descriptions" : [ "该指标说明接口返回为空" ],
|
||||||
|
"original" : null
|
||||||
|
}
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"isAnonymous" : true,
|
"isAnonymous" : true,
|
||||||
"username" : "emy1730978",
|
"username" : "",
|
||||||
"password" : "ubVa0vNmD+JJC4171eLYUw==",
|
"password" : "",
|
||||||
"authorization" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkpXVCJ9.eyJ1dWQiOjEwMTQ2MzA5OTgsInVpZCI6Mjg2MTMyNDksImRpZCI6IjU2ZTFhNThiYmYxMjJiOTMyMjBhYzBkOThhMmQzZmU3IiwidHlwIjo0LCJhY2MiOiI1NmUxYTU4YmJmMTIyYjkzMjIwYWMwZDk4YTJkM2ZlNyIsInN3dCI6MSwibGd0IjoxNzQ3NDQyMzQzNTEzLCJuYmYiOjE3NDc0NDIzNDMsImV4cCI6MTc0OTE3MDM0MywiaWF0IjoxNzQ3NDQyMzQzfQ.QRcKBbTiE6EuIY-5wl_CVXUYVnzrQIy5LYVAkpfZLWM",
|
"authorization" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkpXVCJ9.eyJ1dWQiOjEwMTUyNzk3MzEsInVpZCI6MjkyNTE0NDEsImRpZCI6IjM5N2RmZjEwOWEwOWFmOGY2NGJhNWMzYmYxNmE0ODA2IiwidHlwIjo0LCJhY2MiOiIzOTdkZmYxMDlhMDlhZjhmNjRiYTVjM2JmMTZhNDgwNiIsInN3dCI6MSwibGd0IjoxNzU4MjY0NjYwNzU0LCJuYmYiOjE3NTgyNjQ2NjAsImV4cCI6MTc1OTk5MjY2MCwiaWF0IjoxNzU4MjY0NjYwfQ.Y1aU7PlyuhGauY9aJCgkdqYC5gqcS4SioiHlPX2sSNc",
|
||||||
"uid" : 28613249,
|
"uid" : 29251441,
|
||||||
"androidId" : "132b692fe032add3",
|
"androidId" : "2aa9eb6eea32a4c3",
|
||||||
"androidVersion" : "9",
|
"androidVersion" : "13",
|
||||||
"androidSdkLevel" : "28",
|
"androidSdkLevel" : "33",
|
||||||
"softwareType" : "Mobile",
|
"softwareType" : "Mobile",
|
||||||
"okHttpUserAgent" : "okhttp/3.12.2",
|
"okHttpUserAgent" : "okhttp/3.12.2",
|
||||||
"deviceName" : "SM-J730F",
|
"deviceName" : "2112123AG",
|
||||||
"fingerprint" : "samsung/j7y17ltexx/j7y17lte:9/PPR1.180610.011/J730FXWU8CUG1:user/release-keys",
|
"fingerprint" : "Xiaomi/psyche_global/psyche:13/RKQ1.211001.001/V14.0.6.0.TLDTWXM:user/release-keys",
|
||||||
"buildId" : "PPR1.180610.011",
|
"buildId" : "RKQ1.211001.001",
|
||||||
"chromeVersion" : "117.0.5938.141",
|
"chromeVersion" : "87.0.4280.141",
|
||||||
"emoneyVersion" : "5.8.1",
|
"emoneyVersion" : "5.8.1",
|
||||||
"emappViewMode" : "1"
|
"emappViewMode" : "1"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"proxyType" : "HTTP",
|
"proxyType" : "HTTP",
|
||||||
"proxyHost" : "127.0.0.1",
|
"proxyHost" : "127.0.0.1",
|
||||||
"proxyPort" : 8888,
|
"proxyPort" : 7897,
|
||||||
"ignoreHttpsVerification" : true
|
"ignoreHttpsVerification" : true
|
||||||
}
|
}
|
||||||
Binary file not shown.
@@ -75,7 +75,12 @@
|
|||||||
</dd>
|
</dd>
|
||||||
<dd>
|
<dd>
|
||||||
<a th:href="@{/admin/v1/manage/protocolMatch}"> <i
|
<a th:href="@{/admin/v1/manage/protocolMatch}"> <i
|
||||||
class="fa-fw fa-regular fa-handshake "></i> Protocol 配置
|
class="fa-fw fa-regular fa-handshake"></i> Protocol 配置
|
||||||
|
</a>
|
||||||
|
</dd>
|
||||||
|
<dd>
|
||||||
|
<a th:href="@{/admin/v1/manage/strategyAndPool}"> <i
|
||||||
|
class="fa-fw fa-solid fa-chess-board"></i> 策略信息
|
||||||
</a>
|
</a>
|
||||||
</dd>
|
</dd>
|
||||||
</dl></li>
|
</dl></li>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="layui-form-item">
|
<div class="layui-form-item">
|
||||||
<label class="layui-form-label">计划表达式<span>*</span></label>
|
<label class="layui-form-label">计划表达式<span>*</span></label>
|
||||||
<div class="layui-input-block">
|
<div class="layui-input-block">
|
||||||
<input type="text" lay-verify="required" name="cronExpression" placeholder="" autocomplete="off" class="layui-input"/>
|
<input type="text" lay-verify="required" name="cronExpr" placeholder="" autocomplete="off" class="layui-input"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layui-form-item">
|
<div class="layui-form-item">
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
content: $('#addPlan').html(),
|
content: $('#addPlan').html(),
|
||||||
success: async function (layero, layerIndex) {
|
success: async function (layero, layerIndex) {
|
||||||
var el = $(layero);
|
var el = $(layero);
|
||||||
['planId', 'planName', 'cronExpression', 'indexCode'].forEach(x => {
|
['planId', 'planName', 'cronExpr', 'indexCode'].forEach(x => {
|
||||||
const fieldEl = el[0].querySelector(`[name="${x}"]`);
|
const fieldEl = el[0].querySelector(`[name="${x}"]`);
|
||||||
if (!fieldEl) return;
|
if (!fieldEl) return;
|
||||||
fieldEl.value = r.data[x];
|
fieldEl.value = r.data[x];
|
||||||
@@ -149,12 +149,12 @@
|
|||||||
['enabled', 'openDayCheck'].forEach(x => {
|
['enabled', 'openDayCheck'].forEach(x => {
|
||||||
const fieldEl = el[0].querySelector(`[name="${x}"]`);
|
const fieldEl = el[0].querySelector(`[name="${x}"]`);
|
||||||
if (!fieldEl) return;
|
if (!fieldEl) return;
|
||||||
fieldEl.checked = r.data[x];
|
fieldEl.checked = !!r.data[x];
|
||||||
fieldEl.value = r.data[x];
|
fieldEl.value = !!r.data[x];
|
||||||
layui.form.on('switch(' + x + ')', obj => obj.elem.value = obj.elem.checked)
|
layui.form.on('switch(' + x + ')', obj => obj.elem.value = obj.elem.checked)
|
||||||
});
|
});
|
||||||
layui.cron.render({
|
layui.cron.render({
|
||||||
elem: '[name="cronExpression"]',
|
elem: '[name="cronExpr"]',
|
||||||
btns: ['confirm'],
|
btns: ['confirm'],
|
||||||
cssPath: '/admin/v1/static/css/cron.css'
|
cssPath: '/admin/v1/static/css/cron.css'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
{field:'openDayCheck', title: '交易日校验', width: 95, switchTemplet: true},
|
{field:'openDayCheck', title: '交易日校验', width: 95, switchTemplet: true},
|
||||||
{field:'planId', hide: true, width: 60, title: 'ID'},
|
{field:'planId', hide: true, width: 60, title: 'ID'},
|
||||||
{field:'planName', title: '计划名称'},
|
{field:'planName', title: '计划名称'},
|
||||||
{field:'cronExpression', title: '计划表达式'},
|
{field:'cronExpr', title: '计划表达式'},
|
||||||
{field:'indexCode', title: '指标代码'},
|
{field:'indexCode', title: '指标代码'},
|
||||||
{field:'params', title: '请求参数', templet: function(d) {
|
{field:'params', title: '请求参数', templet: function(d) {
|
||||||
if (typeof d.params === 'object' && d.params !== null) {
|
if (typeof d.params === 'object' && d.params !== null) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div th:fragment="protocolMatchExtra">
|
<div th:fragment="protocolMatchExtra">
|
||||||
<script id="addProtocolMatch" type="text/html">
|
<script id="addProtocolMatch" type="text/html">
|
||||||
<style>.layui-form-select dl{max-height: 160px}</style>
|
<style>.layui-form-select dl{max-height: 160px}</style>
|
||||||
<div class="layui-form" style="margin:10px 15px" id="editPlanForm" lay-filter="editPlanForm">
|
<div class="layui-form" style="margin:10px 15px" id="editProtocolMatchForm" lay-filter="editProtocolMatchForm">
|
||||||
<div class="layui-form-item">
|
<div class="layui-form-item">
|
||||||
<label class="layui-form-label">Protocol ID<span>*</span></label>
|
<label class="layui-form-label">Protocol ID<span>*</span></label>
|
||||||
<div class="layui-input-block">
|
<div class="layui-input-block">
|
||||||
@@ -58,7 +58,7 @@ function openEditForm(r) {
|
|||||||
if (r && r.ok) {
|
if (r && r.ok) {
|
||||||
window.editLayer = layui.layer.open({
|
window.editLayer = layui.layer.open({
|
||||||
type: 1,
|
type: 1,
|
||||||
title: `${r.data.planId ? '编辑' : '新增'}Protocol`,
|
title: `${r.data.protocolId ? '编辑' : '新增'}Protocol`,
|
||||||
btn: ['提交', '关闭'],
|
btn: ['提交', '关闭'],
|
||||||
yes: function(index, layero) {
|
yes: function(index, layero) {
|
||||||
layero.find('[lay-filter="submitProtocolMatch"]').click()
|
layero.find('[lay-filter="submitProtocolMatch"]').click()
|
||||||
@@ -101,7 +101,7 @@ layui.table.on('tool(protocolMatches)', function(obj) {
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/admin/v1/manage/protocolMatch/delete',
|
url: '/admin/v1/manage/protocolMatch/delete',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {planId: obj.data.protocolId},
|
data: {protocolId: obj.data.protocolId},
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
layui.table.reload('protocolMatches',{
|
layui.table.reload('protocolMatches',{
|
||||||
page: {
|
page: {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
<h1 class="manage-title">
|
<h1 class="manage-title">
|
||||||
<b>Protocol ID 映射表</b>
|
<b>Protocol ID 映射表</b>
|
||||||
<a href="#" class="help"><i class="layui-icon layui-icon-help"></i></a>
|
<a href="#" class="help"><i class="layui-icon layui-icon-help"></i></a>
|
||||||
<a href="javascript:openUploadForm()" class="operate">上传</a>
|
|
||||||
<a href="javascript:openNewForm()" class="operate">新增</a>
|
<a href="javascript:openNewForm()" class="operate">新增</a>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -56,7 +55,7 @@
|
|||||||
function switchTemplet(d) {
|
function switchTemplet(d) {
|
||||||
var fieldName = d.LAY_COL.field;
|
var fieldName = d.LAY_COL.field;
|
||||||
return `<input type="checkbox" lay-skin="switch" lay-text="|"
|
return `<input type="checkbox" lay-skin="switch" lay-text="|"
|
||||||
data-field="${fieldName}" data-id="${d.planId}"
|
data-field="${fieldName}" data-id="${d.protocolId}"
|
||||||
${d[fieldName] ? 'checked' : ''} lay-filter="switchFilter">`;
|
${d[fieldName] ? 'checked' : ''} lay-filter="switchFilter">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,18 +80,18 @@
|
|||||||
{title: '开启交易日校验', op: 'enableOpenDayCheck'},
|
{title: '开启交易日校验', op: 'enableOpenDayCheck'},
|
||||||
{title: '关闭交易日校验', op: 'disableOpenDayCheck'}],
|
{title: '关闭交易日校验', op: 'disableOpenDayCheck'}],
|
||||||
click: function (data, othis){
|
click: function (data, othis){
|
||||||
var checked = layui.table.checkStatus('protocolMatches'), planIds = [];
|
var checked = layui.table.checkStatus('protocolMatches'), protocolIds = [];
|
||||||
if (!checked.data.length) {
|
if (!checked.data.length) {
|
||||||
layui.layer.msg('未选中任何项', {time: 1000});
|
layui.layer.msg('未选中任何项', {time: 1000});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$.each(checked.data, function (i, plan){
|
$.each(checked.data, function (i, protocolMatch){
|
||||||
planIds.push(plan.planId);
|
protocolIds.push(protocolMatch.protocolId);
|
||||||
});
|
});
|
||||||
data = $.extend(data, {ids: planIds});
|
data = $.extend(data, {ids: protocolIds});
|
||||||
var op = function() {
|
var op = function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/admin/v1/manage/plan/batchOp',
|
url: '/admin/v1/manage/protocolMatch/batchOp',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: data,
|
data: data,
|
||||||
success: function () {
|
success: function () {
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<div th:fragment="strategyAndPoolExtra">
|
||||||
|
<script id="addStrategyAndPool" type="text/html">
|
||||||
|
<style>.layui-form-select dl{max-height: 160px}</style>
|
||||||
|
<div class="layui-form" style="margin:10px 15px" id="editPlanForm" lay-filter="editPlanForm">
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">池ID<span>*</span></label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" lay-verify="required|number" name="poolId" placeholder="池ID" autocomplete="off" class="layui-input"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">池名称<span>*</span></label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" lay-verify="required" name="poolName" placeholder="池名称" autocomplete="off" class="layui-input"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">策略集ID<span>*</span></label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" lay-verify="required|number" name="strategyId" placeholder="策略集ID" autocomplete="off" class="layui-input"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">策略集名称<span>*</span></label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" lay-verify="required" name="strategyName" placeholder="策略集名称" autocomplete="off" class="layui-input"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:none" class="layui-form-item">
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<button class="layui-btn" lay-submit="*" lay-filter="submitStrategyAndPool">提交</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
layui.form.on('submit(submitStrategyAndPool)', function(obj){
|
||||||
|
var field = obj.field, data = {params: {}}
|
||||||
|
Object.keys(field).forEach(key => {
|
||||||
|
data[key] = field[key]
|
||||||
|
});
|
||||||
|
$.ajax({
|
||||||
|
url: '/admin/v1/manage/strategyAndPool/save',
|
||||||
|
method: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
success: function (r) {
|
||||||
|
layer.msg('操作成功', {
|
||||||
|
offset: '15px',
|
||||||
|
icon: 1,
|
||||||
|
time: 1000
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
if (window.editLayer) {
|
||||||
|
layui.layer.close(window.editLayer);
|
||||||
|
}
|
||||||
|
layui.table.reload('strategyAndPools',{
|
||||||
|
page: {
|
||||||
|
curr: $(".layui-laypage-em").next().html() //当前页码值
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
function openEditForm(r) {
|
||||||
|
if (r && r.ok) {
|
||||||
|
window.editLayer = layui.layer.open({
|
||||||
|
type: 1,
|
||||||
|
title: `${r.data.poolId ? '编辑' : '新增'}策略`,
|
||||||
|
btn: ['提交', '关闭'],
|
||||||
|
yes: function(index, layero) {
|
||||||
|
layero.find('[lay-filter="submitStrategyAndPool"]').click()
|
||||||
|
},
|
||||||
|
skin: "layui-anim layui-anim-rl layui-layer-adminRight",
|
||||||
|
area: '500px',
|
||||||
|
anim: -1,
|
||||||
|
shadeClose: !0,
|
||||||
|
closeBtn: !1,
|
||||||
|
move: !1,
|
||||||
|
offset: 'r',
|
||||||
|
content: $('#addStrategyAndPool').html(),
|
||||||
|
success: function(layero, layerIndex) {
|
||||||
|
Helper.fillEditForm(r, layero, layerIndex);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else layer.msg(r && r.data || '服务器错误', {offset: '15px', icon: 2, time: 1000})
|
||||||
|
}
|
||||||
|
function openNewForm(poolId) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/admin/v1/manage/strategyAndPool/getOne',
|
||||||
|
data: {poolId: poolId},
|
||||||
|
success: function(r) {
|
||||||
|
openEditForm(r)
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
var r = xhr.responseJSON;
|
||||||
|
layer.msg(r && r.data || '服务器错误', {offset: '15px', icon: 2, time: 100})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
layui.table.on('tool(strategyAndPools)', function(obj) {
|
||||||
|
if (obj.event == 'edit') {
|
||||||
|
openNewForm(obj.data.poolId)
|
||||||
|
}
|
||||||
|
else if (obj.event == 'del') {
|
||||||
|
layui.layer.confirm('确定删除该映射吗?', function (index) {
|
||||||
|
layui.layer.close(index);
|
||||||
|
$.ajax({
|
||||||
|
url: '/admin/v1/manage/strategyAndPool/delete',
|
||||||
|
method: 'POST',
|
||||||
|
data: {poolId: obj.data.poolId},
|
||||||
|
success: function (data) {
|
||||||
|
layui.table.reload('strategyAndPools',{
|
||||||
|
page: {
|
||||||
|
curr: $(".layui-laypage-em").next().html() //当前页码值
|
||||||
|
}
|
||||||
|
});
|
||||||
|
layer.msg('删除成功', {offset: '15px', icon: 1, time: 1000})
|
||||||
|
},
|
||||||
|
error: function (res) {
|
||||||
|
var r = res.responseJSON;
|
||||||
|
layer.msg(r&&r.data||'服务器错误',
|
||||||
|
{offset: '15px', icon: 2, time: 2000});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head th:insert="~{admin/v1/include::head}"
|
||||||
|
th:with="title=${'策略信息'}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<ul th:replace="~{admin/v1/include::nav}"></ul>
|
||||||
|
|
||||||
|
<div class="manage-body">
|
||||||
|
<div>
|
||||||
|
<h1 class="manage-title">
|
||||||
|
<b>策略表</b>
|
||||||
|
<a href="#" class="help"><i class="layui-icon layui-icon-help"></i></a>
|
||||||
|
<a href="javascript:openNewForm()" class="operate">新增</a>
|
||||||
|
<a href="/admin/v1/manage/strategyAndPool/exportPreparedStrategies" target="_blank" class="operate">导出</a>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<button class="layui-btn layui-btn-sm operdown">
|
||||||
|
<span>选中项<i
|
||||||
|
class="layui-icon layui-icon-sm layui-icon-triangle-d"></i></span>
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<table class="layui-table" id="strategyAndPools" lay-filter="strategyAndPools">
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div th:replace="~{admin/v1/include::feet}"></div>
|
||||||
|
<script type="text/html" id="operationTpl">
|
||||||
|
<div class="layui-btn-group">
|
||||||
|
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
|
||||||
|
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
<th:block th:replace="~{admin/v1/include::head-script}"></th:block>
|
||||||
|
<script th:inline="javascript">
|
||||||
|
layui
|
||||||
|
.extend({
|
||||||
|
xmSelect: '/admin/v1/static/layuiadmin/lib/xm-select',
|
||||||
|
cron: '/admin/v1/static/layuiadmin/lib/cron'
|
||||||
|
})
|
||||||
|
.use(['table', 'form', 'dropdown', 'layer', 'xmSelect', 'cron'], function(){
|
||||||
|
var dropdown = layui.dropdown, table = layui.table, form = layui.form;
|
||||||
|
|
||||||
|
$('.help').on('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
layer.open({
|
||||||
|
title: '帮助',
|
||||||
|
content:
|
||||||
|
'益盟操盘手的策略集和策略池关系<br>'+
|
||||||
|
'可以在破解软件权限后,通过载入日K图获取。<br>' +
|
||||||
|
'获取后解析 /strategy 链接提供的 EmoneyProtobuf 信息,会自动抓取并存储'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function switchTemplet(d) {
|
||||||
|
var fieldName = d.LAY_COL.field;
|
||||||
|
return `<input type="checkbox" lay-skin="switch" lay-text="|"
|
||||||
|
data-field="${fieldName}" data-id="${d.planId}"
|
||||||
|
${d[fieldName] ? 'checked' : ''} lay-filter="switchFilter">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.render({
|
||||||
|
elem: '#strategyAndPools',
|
||||||
|
url:'/admin/v1/manage/strategyAndPool/list',
|
||||||
|
page:true, skin:'line',
|
||||||
|
cols: [ [
|
||||||
|
{type:'checkbox'},
|
||||||
|
{field:'poolId', width: 100, title: '池ID'},
|
||||||
|
{field:'poolName', title: '池名'},
|
||||||
|
{field:'strategyId', title: '策略ID'},
|
||||||
|
{field:'strategyName', title: '策略名'},
|
||||||
|
{field:'operation', title: '操作', width: 150, toolbar: '#operationTpl'}
|
||||||
|
]]
|
||||||
|
});
|
||||||
|
|
||||||
|
dropdown.render({
|
||||||
|
elem: '.operdown',
|
||||||
|
data: [
|
||||||
|
{title: '删除'}
|
||||||
|
],
|
||||||
|
click: function (data, othis){
|
||||||
|
var checked = layui.table.checkStatus('strategyAndPools'), planIds = [];
|
||||||
|
if (!checked.data.length) {
|
||||||
|
layui.layer.msg('未选中任何项', {time: 1000});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.each(checked.data, function (i, plan){
|
||||||
|
planIds.push(plan.planId);
|
||||||
|
});
|
||||||
|
data = $.extend(data, {ids: planIds});
|
||||||
|
var op = function() {
|
||||||
|
$.ajax({
|
||||||
|
url: '/admin/v1/manage/strategyAndPool/batchOp',
|
||||||
|
method: 'POST',
|
||||||
|
data: data,
|
||||||
|
success: function () {
|
||||||
|
layer.msg('批量操作成功', {
|
||||||
|
offset: '15px',
|
||||||
|
icon: 1,
|
||||||
|
time: 1000
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
layui.table.reload('strategyAndPools', {
|
||||||
|
page: {
|
||||||
|
curr: $(".layui-laypage-em").next().html() //当前页码值
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
error: function (res) {
|
||||||
|
var r = res.responseJSON;
|
||||||
|
layer.msg(r&&r.data||'服务器错误', {
|
||||||
|
offset: '15px',
|
||||||
|
icon: 2,
|
||||||
|
time: 1000
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
data.op ? op() : layer.confirm('确认批量删除吗?该操作不可恢复', function(){
|
||||||
|
op();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<th:block th:replace="~{admin/v1/manage/strategyAndPool/include::strategyAndPoolExtra}"></th:block>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
47
src/test/java/quant/rich/EmoneyStrategyMarkTests.java
Normal file
47
src/test/java/quant/rich/EmoneyStrategyMarkTests.java
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package quant.rich;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import nano.CandleStickNewWithIndexExResponse.CandleStickNewWithIndexEx_Response;
|
||||||
|
import nano.CandleStickRequest.CandleStick_Request;
|
||||||
|
import nano.CandleStickWithIndexRequest.CandleStickWithIndex_Request;
|
||||||
|
import nano.CandleStickWithIndexRequest.CandleStickWithIndex_Request.IndexInfo;
|
||||||
|
import nano.StrategyMarkRequest.StrategyMark_Request;
|
||||||
|
import nano.StrategyMarkResponse.StrategyMark_Response;
|
||||||
|
import quant.rich.emoney.EmoneyAutoApplication;
|
||||||
|
import quant.rich.emoney.client.EmoneyClient;
|
||||||
|
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@ContextConfiguration(classes = EmoneyAutoApplication.class)
|
||||||
|
@RunWith(SpringJUnit4ClassRunner.class)
|
||||||
|
@Slf4j
|
||||||
|
class EmoneyStrategyMarkTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void contextLoads() {
|
||||||
|
EmoneyClient.relogin();
|
||||||
|
|
||||||
|
StrategyMark_Request request = new StrategyMark_Request();
|
||||||
|
|
||||||
|
request.setGoodsId(600325);
|
||||||
|
|
||||||
|
StrategyMark_Response response = EmoneyClient.post(
|
||||||
|
request, StrategyMark_Response.class,
|
||||||
|
9400, null);
|
||||||
|
//JSONObject jo = JSONObject.from(request);
|
||||||
|
ObjectNode jo = new ObjectMapper().valueToTree(response);
|
||||||
|
log.info(jo.toPrettyString());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,16 +1,9 @@
|
|||||||
package quant.rich;
|
package quant.rich;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import quant.rich.emoney.EmoneyAutoApplication;
|
import quant.rich.emoney.EmoneyAutoApplication;
|
||||||
import quant.rich.emoney.client.EmoneyClient;
|
|
||||||
import quant.rich.emoney.client.WebviewClient;
|
|
||||||
import quant.rich.emoney.client.WebviewClient.WebviewResponseWrapper;
|
|
||||||
import quant.rich.emoney.util.SmartResourceResolver;
|
import quant.rich.emoney.util.SmartResourceResolver;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
package quant.rich;
|
package quant.rich;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import quant.rich.emoney.EmoneyAutoApplication;
|
import quant.rich.emoney.EmoneyAutoApplication;
|
||||||
import quant.rich.emoney.client.EmoneyClient;
|
|
||||||
import quant.rich.emoney.client.WebviewClient;
|
|
||||||
import quant.rich.emoney.client.WebviewClient.WebviewResponseWrapper;
|
|
||||||
import quant.rich.emoney.util.SmartResourceResolver;
|
import quant.rich.emoney.util.SmartResourceResolver;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import javax.script.ScriptEngine;
|
|
||||||
import javax.script.ScriptEngineManager;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|||||||
Reference in New Issue
Block a user