更新
This commit is contained in:
2
pom.xml
2
pom.xml
@@ -9,7 +9,7 @@
|
||||
<version>3.3.0</version>
|
||||
<relativePath /> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.littlesweetdog.quant</groupId>
|
||||
<groupId>top.at17.quant</groupId>
|
||||
<artifactId>emo-grab</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>EmoGrab</name>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package quant.rich.emoney.component;
|
||||
package quant.rich.emoney.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
import quant.rich.emoney.component.CallerLockAspect;
|
||||
|
||||
/**
|
||||
* 在方法上添加此注解,可针对调用方加锁,即:<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>
|
||||
* X-Protocol-Id > X-Request-Id > EM-Sign > Authorization >
|
||||
* X-Android-Agent > Emapp-ViewMode > Content-Type > Content-Length >
|
||||
* Host > Connection: "Keep-Alive" > Accept-Encoding: "gzip" > User-Agent</p>
|
||||
* <p>从 X-Protocol-Id 到 Emapp-ViewMode,由本例添加,剩余为 okhttp 默认添加,
|
||||
* <ul>
|
||||
* <li>X-Protocol-Id</li>
|
||||
* <li>X-Request-Id</li>
|
||||
* <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>
|
||||
* @see quant.rich.emoney.patch.okhttp.PatchOkHttp
|
||||
*/
|
||||
@@ -43,19 +55,44 @@ import okhttp3.OkHttpClient;
|
||||
public class EmoneyClient implements Cloneable {
|
||||
|
||||
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 RELOGIN_URL = "https://emapp.emoney.cn/user/auth/ReLogin";
|
||||
private static final String LOGIN_X_PROTOCOL_ID = "user%2Fauth%2Flogin";
|
||||
private static final String RELOGIN_X_PROTOCOL_ID = "user%2Fauth%2FReLogin";
|
||||
|
||||
private static volatile EmoneyRequestConfig emoneyRequestConfig;
|
||||
|
||||
/**
|
||||
* 根据 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() {
|
||||
if (emoneyRequestConfig == null) {
|
||||
synchronized (EmoneyClient.class) {
|
||||
if (emoneyRequestConfig == null) {
|
||||
emoneyRequestConfig = SpringContextHolder.getBean(EmoneyRequestConfig.class);
|
||||
}
|
||||
emoneyRequestConfig = SpringContextHolder.getBean(EmoneyRequestConfig.class);
|
||||
}
|
||||
}
|
||||
return emoneyRequestConfig;
|
||||
@@ -64,7 +101,7 @@ public class EmoneyClient implements Cloneable {
|
||||
private EmoneyClient() {}
|
||||
|
||||
/**
|
||||
* 根据系统配置自动选择登录方式
|
||||
* 根据系统配置自动选择登录方式,即匿名或不匿名
|
||||
* @return
|
||||
* @see EmoneyRequestConfig
|
||||
*/
|
||||
@@ -149,7 +186,7 @@ public class EmoneyClient implements Cloneable {
|
||||
|
||||
if (response.code() != 200) {
|
||||
// 不是 200,重新登录
|
||||
log.debug("ReLogin 重登录验证返回状态码 {}, 触发登录", response.code());
|
||||
log.debug("ReLogin 重登录验证返回状态码 {}, 需要重新登录", response.code());
|
||||
return loginWithManaged();
|
||||
}
|
||||
|
||||
@@ -266,6 +303,11 @@ public class EmoneyClient implements Cloneable {
|
||||
new IllegalArgumentException());
|
||||
}
|
||||
|
||||
String url = getUrlByProtocolId(xProtocolId);
|
||||
if (StringUtils.isBlank(url)) {
|
||||
throw new EmoneyRequestException("无法根据 xProtocolId " + xProtocolId + "获取请求 URL");
|
||||
}
|
||||
|
||||
try {
|
||||
OkHttpClient okHttpClient = OkHttpClientProvider.getInstance();
|
||||
|
||||
@@ -275,7 +317,7 @@ public class EmoneyClient implements Cloneable {
|
||||
MediaType.parse("application/x-protobuf-v3"));
|
||||
|
||||
Request.Builder requestBuilder = new Request.Builder()
|
||||
.url(MBS_URL)
|
||||
.url(url)
|
||||
.post(body)
|
||||
// 这玩意可能也有顺序
|
||||
// 按照 Fiddler HexView 顺序如下:
|
||||
@@ -293,6 +335,10 @@ public class EmoneyClient implements Cloneable {
|
||||
final Call call = okHttpClient.newCall(request);
|
||||
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());
|
||||
return baseResponse;
|
||||
} 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.stereotype.Component;
|
||||
|
||||
import quant.rich.emoney.annotation.LockByCaller;
|
||||
import quant.rich.emoney.util.CallerLockUtil;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
@@ -19,7 +20,7 @@ public class CallerLockAspect {
|
||||
|
||||
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 {
|
||||
MethodSignature signature = (MethodSignature) pjp.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
|
||||
@@ -175,7 +175,8 @@ public class EmoneyAutoPlatformExceptionHandler {
|
||||
if (ex instanceof NoResourceFoundException nrfe) {
|
||||
if (StringUtils.isNotEmpty(nrfe.getMessage())
|
||||
&& 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
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.Set;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.RestController;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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 nano.BaseResponse.Base_Response;
|
||||
import quant.rich.emoney.annotation.ResponseDecodeExtension;
|
||||
import quant.rich.emoney.entity.sqlite.ProtocolMatch;
|
||||
import quant.rich.emoney.exception.RException;
|
||||
import quant.rich.emoney.pojo.dto.EmoneyConvertResult;
|
||||
import quant.rich.emoney.pojo.dto.EmoneyProtobufBody;
|
||||
import quant.rich.emoney.service.sqlite.ProtocolMatchService;
|
||||
import quant.rich.emoney.service.sqlite.StrategyAndPoolService;
|
||||
import quant.rich.emoney.util.SpringBeanDetector;
|
||||
import quant.rich.emoney.util.SpringContextHolder;
|
||||
|
||||
/**
|
||||
* 益盟 ProtocolBuf 报文解析 API 控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/proto")
|
||||
@Slf4j
|
||||
public class ProtoDecodeControllerV1 {
|
||||
|
||||
@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")
|
||||
@PostMapping("/request/decode")
|
||||
public <U extends MessageNano> EmoneyConvertResult requestDecode(
|
||||
@@ -39,13 +131,14 @@ public class ProtoDecodeControllerV1 {
|
||||
|
||||
Integer protocolId = body.getProtocolId();
|
||||
if (Objects.isNull(protocolId)) {
|
||||
throw RException.badRequest("protocolId cannot be null");
|
||||
throw RException.badRequest("protocolId 不能为 null");
|
||||
}
|
||||
|
||||
ProtocolMatch match = protocolMatchService.getById(protocolId);
|
||||
|
||||
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()
|
||||
@@ -111,7 +204,7 @@ public class ProtoDecodeControllerV1 {
|
||||
Integer protocolId = body.getProtocolId();
|
||||
ProtocolMatch match = null;
|
||||
if (Objects.isNull(protocolId)) {
|
||||
log.warn("protocolId is null, cannot update protocolMatch");
|
||||
log.warn("protocolId 为空 null, 无法更新 protocolMatch");
|
||||
}
|
||||
else {
|
||||
match = protocolMatchService.getById(protocolId);
|
||||
@@ -178,8 +271,30 @@ public class ProtoDecodeControllerV1 {
|
||||
U nano = (U)MessageNano.mergeFrom(
|
||||
(MessageNano)clazz.getDeclaredConstructor().newInstance(),
|
||||
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
|
||||
.ok(new ObjectMapper().valueToTree(nano))
|
||||
.ok((Serializable)jo)
|
||||
.setProtocolId(protocolId)
|
||||
.setSupposedClassName(className);
|
||||
}
|
||||
|
||||
@@ -1,43 +1,84 @@
|
||||
package quant.rich.emoney.controller.common;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.exception.RException;
|
||||
import quant.rich.emoney.pojo.dto.R;
|
||||
|
||||
@Slf4j
|
||||
public abstract class UpdateBoolController<T, M extends BaseMapper<T>, S extends ServiceImpl<M, T>> extends BaseController {
|
||||
public abstract class UpdateBoolController<T> extends BaseController {
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext ctx;
|
||||
|
||||
private Map<Class<?>, IService<?>> serviceMap;
|
||||
|
||||
@PostConstruct
|
||||
void init() {
|
||||
serviceMap = new HashMap<>();
|
||||
@SuppressWarnings("rawtypes")
|
||||
Map<String, IService> beans = ctx.getBeansOfType(IService.class);
|
||||
for (IService<?> service : beans.values()) {
|
||||
ResolvableType type = ResolvableType.forClass(service.getClass()).as(IService.class);
|
||||
Class<?> entityType = type.getGeneric(0).resolve();
|
||||
if (entityType != null) {
|
||||
serviceMap.put(entityType, service);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public IService<T> getService(Class<T> entityClass) {
|
||||
return (IService<T>) serviceMap.get(entityClass);
|
||||
}
|
||||
|
||||
@PostMapping("/updateBool")
|
||||
@ResponseBody
|
||||
protected
|
||||
R<?> updateBool(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);
|
||||
Object converted = mapper.convertValue(idValue, tableInfo.getKeyType());
|
||||
Object converted = mapper.convertValue(id, tableInfo.getKeyType());
|
||||
|
||||
// 获取 Service
|
||||
IService<T> s = getService((Class<T>) clazz);
|
||||
|
||||
try {
|
||||
String idField = tableInfo.getKeyColumn();
|
||||
Field declaredField = clazz.getDeclaredField(field);
|
||||
Optional<TableFieldInfo> fieldInfo = tableInfo.getFieldList().stream()
|
||||
.filter(f -> f.getProperty().equals(field))
|
||||
.findFirst();
|
||||
if (declaredField.getType().equals(Boolean.class)) {
|
||||
return R.judge(service.update(
|
||||
return R.judge(s.update(
|
||||
new UpdateWrapper<T>()
|
||||
.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);
|
||||
}
|
||||
|
||||
@PostMapping("/updateBool")
|
||||
@ResponseBody
|
||||
abstract protected R<?> updateBool(String id, String field, Boolean value);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package quant.rich.emoney.controller.manage;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
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.exception.RException;
|
||||
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.LayPageResp;
|
||||
import quant.rich.emoney.pojo.dto.R;
|
||||
@@ -31,7 +29,7 @@ import quant.rich.emoney.service.sqlite.PlanService;
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/admin/v1/manage/plan")
|
||||
public class PlanControllerV1 extends UpdateBoolController<Plan, PlanMapper, PlanService> {
|
||||
public class PlanControllerV1 extends UpdateBoolController<Plan> {
|
||||
|
||||
@Autowired
|
||||
PlanService planService;
|
||||
@@ -51,32 +49,10 @@ public class PlanControllerV1 extends UpdateBoolController<Plan, PlanMapper, Pla
|
||||
@GetMapping("/getOne")
|
||||
@ResponseBody
|
||||
public R<?> getOne(String planId) {
|
||||
|
||||
// 如果 planId 是空,说明可能希望新建一个 Plan,需要返回默认实例化对象
|
||||
if (planId == null) {
|
||||
return R.ok(new Plan());
|
||||
}
|
||||
|
||||
// 否则从数据库取
|
||||
Plan plan = planService.getById(planId);
|
||||
return R.judge(plan != null, plan, "无法找到对应 ID 的 Plan");
|
||||
}
|
||||
|
||||
@PostMapping("/updateEnabledStatus")
|
||||
@ResponseBody
|
||||
public R<?> updateEnabledStatus(String planId, Boolean enabled) {
|
||||
if (planService.update(new LambdaUpdateWrapper<Plan>()
|
||||
.eq(Plan::getPlanId, planId)
|
||||
.set(Plan::getEnabled, enabled))) {
|
||||
return R.ok();
|
||||
}
|
||||
throw RException.badRequest();
|
||||
}
|
||||
|
||||
@PostMapping("/updateBool")
|
||||
@ResponseBody
|
||||
public R<?> updateBool(String id, String field, Boolean value) {
|
||||
return updateBool(planService, Plan.class, Plan::getPlanId, id, field, value);
|
||||
// 如果 planId 是空,说明可能希望新建一个 Plan,需要返回默认实例化对象,否则从数据库取
|
||||
return
|
||||
planId == null ? R.ok(new Plan()) :
|
||||
R.judgeNonNull(planService.getById(planId), "无法找到对应 ID 的 Plan");
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
|
||||
@@ -13,7 +13,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.controller.common.BaseController;
|
||||
import quant.rich.emoney.entity.sqlite.Plan;
|
||||
import quant.rich.emoney.entity.sqlite.ProtocolMatch;
|
||||
import quant.rich.emoney.exception.RException;
|
||||
import quant.rich.emoney.pojo.dto.LayPageReq;
|
||||
@@ -64,11 +63,11 @@ public class ProtocolMatchControllerV1 extends BaseController {
|
||||
}
|
||||
throw RException.badRequest("protocolId 不允许为空");
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@ResponseBody
|
||||
public R<?> delete(Integer protocolMatchId) {
|
||||
return R.judge(protocolMatchService.removeById(protocolMatchId), "删除失败,是否已删除?");
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@ResponseBody
|
||||
public R<?> delete(Integer 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.TableInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
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.lang.NonNull;
|
||||
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.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 lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.controller.common.BaseController;
|
||||
import quant.rich.emoney.controller.common.UpdateBoolController;
|
||||
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.LayPageResp;
|
||||
import quant.rich.emoney.pojo.dto.R;
|
||||
@@ -34,7 +23,7 @@ import quant.rich.emoney.service.sqlite.RequestInfoService;
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/admin/v1/manage/requestInfo")
|
||||
public class RequestInfoControllerV1 extends UpdateBoolController<RequestInfo, RequestInfoMapper, RequestInfoService> {
|
||||
public class RequestInfoControllerV1 extends UpdateBoolController<RequestInfo> {
|
||||
|
||||
@Autowired
|
||||
RequestInfoService requestInfoService;
|
||||
@@ -55,22 +44,15 @@ public class RequestInfoControllerV1 extends UpdateBoolController<RequestInfo, R
|
||||
@ResponseBody
|
||||
public R<?> getOne(Integer id) {
|
||||
|
||||
// 如果 planId 是空,说明可能希望新建一个 Plan,需要返回默认实例化对象
|
||||
// 如果 id 是空,说明可能希望新建并返回默认实例化对象
|
||||
if (id == null) {
|
||||
return R.ok(new RequestInfo());
|
||||
}
|
||||
|
||||
// 否则从数据库取
|
||||
RequestInfo plan = requestInfoService.getById(id);
|
||||
return R.judge(plan != null, plan, "无法找到对应 ID 的 Plan");
|
||||
RequestInfo requestInfo = requestInfoService.getById(id);
|
||||
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")
|
||||
@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
|
||||
*/
|
||||
@JsonIgnore
|
||||
private Serializable userAgentPatchRuleId;
|
||||
private Integer userAgentPatchRuleId;
|
||||
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
@Autowired
|
||||
@@ -207,7 +207,7 @@ public class EmoneyRequestConfig implements IConfig<EmoneyRequestConfig> {
|
||||
chromeVersionsConfig = Objects.requireNonNullElseGet(chromeVersionsConfig, () -> SpringContextHolder.getBean(ChromeVersionsConfig.class));
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
log.debug("SpringContext not ready");
|
||||
log.debug("试图从 SpringContextHolder 初始化 androidSdkLevelConfig, deviceInfoConfig 和 chromeVersionConfig, 但 SpringContextHolder 未准备好");
|
||||
}
|
||||
|
||||
if (ObjectUtils.anyNull(fingerprint, buildId, deviceName, androidVersion, androidSdkLevel, softwareType)) {
|
||||
@@ -219,7 +219,7 @@ public class EmoneyRequestConfig implements IConfig<EmoneyRequestConfig> {
|
||||
// model 和 softwareType 本应交由 deviceInfoConfig 检查以
|
||||
// 应对可能的通过修改本地 json 来进行攻击的方式,可是本身
|
||||
// deviceInfoConfig 对 model 和 softwareType 的信息也来源
|
||||
// 于本地,万一本地的 deviceInfo.(fallback.)json 也不值得信任?
|
||||
// 于本地,万一本地的 deviceInfo(.fallback).json 也不值得信任?
|
||||
// 所以只检查 fingerprint
|
||||
|
||||
DeviceInfo deviceInfo;
|
||||
@@ -228,12 +228,12 @@ public class EmoneyRequestConfig implements IConfig<EmoneyRequestConfig> {
|
||||
deviceInfo = DeviceInfo.from(null, fingerprint);
|
||||
Validate.validState(androidVersion.equals(
|
||||
deviceInfo.getVersionRelease()),
|
||||
"androidVersion(versionRelease) doesn't match");
|
||||
"androidVersion(versionRelease) 与预设 fingerprint 不匹配");
|
||||
Validate.validState(androidSdkLevel.equals(
|
||||
String.valueOf(androidSdkLevelConfig.getSdkLevel(deviceInfo.getVersionRelease()))),
|
||||
"androidSdkLevel doesn't match");
|
||||
"androidSdkLevel 与预设 fingerprint 不匹配");
|
||||
Validate.validState(buildId.equals(deviceInfo.getBuildId()),
|
||||
"buildId doesn't match");
|
||||
"buildId 与预设 fingerprint 不匹配");
|
||||
}
|
||||
catch (Exception e) {
|
||||
valid = false;
|
||||
@@ -468,7 +468,7 @@ public class EmoneyRequestConfig implements IConfig<EmoneyRequestConfig> {
|
||||
@JsonIgnore
|
||||
public ObjectNode getReloginObject() {
|
||||
|
||||
if (ObjectUtils.anyNull(getAuthorization(), getUid())) {
|
||||
if (getUid() == null || StringUtils.isBlank(getAuthorization())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,19 +9,15 @@ import org.springframework.context.annotation.Lazy;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jodd.io.FileUtil;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.Accessors;
|
||||
import okhttp3.ConnectionPool;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import quant.rich.emoney.annotation.LockByCaller;
|
||||
import quant.rich.emoney.client.OkHttpClientProvider;
|
||||
import quant.rich.emoney.component.LockByCaller;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
|
||||
|
||||
@@ -25,25 +25,55 @@ import quant.rich.emoney.mybatis.typehandler.JsonStringTypeHandler;
|
||||
@TableName(value = "plan", autoResultMap = true)
|
||||
public class Plan {
|
||||
|
||||
/**
|
||||
* 键 ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private String planId;
|
||||
|
||||
private String cronExpression;
|
||||
/**
|
||||
* 计划任务表达式
|
||||
*/
|
||||
private String cronExpr;
|
||||
|
||||
/**
|
||||
* 计划名称
|
||||
*/
|
||||
private String planName;
|
||||
|
||||
/**
|
||||
* 指标代码
|
||||
*/
|
||||
private String indexCode;
|
||||
|
||||
/**
|
||||
* 需要抓取的指标周期
|
||||
*/
|
||||
@TableField(typeHandler = CommaListTypeHandler.class)
|
||||
private List<String> periods;
|
||||
|
||||
/**
|
||||
* 参数
|
||||
*/
|
||||
@TableField(typeHandler = JsonStringTypeHandler.class)
|
||||
private JsonNode params;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* 交易日检查,若为 true,则任务执行时判断当日是否为交易日,若是则不执行任务,
|
||||
* 反之无论是否为交易日都执行
|
||||
*/
|
||||
private Boolean openDayCheck;
|
||||
|
||||
/**
|
||||
* 设置抓取周期并去重
|
||||
* @param periods
|
||||
* @return
|
||||
*/
|
||||
public Plan setPeriods(List<String> periods) {
|
||||
if (CollectionUtils.isEmpty(periods)) {
|
||||
this.periods = Collections.emptyList();
|
||||
@@ -59,6 +89,10 @@ public class Plan {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取抓取周期。已去重
|
||||
* @return
|
||||
*/
|
||||
public List<String> getPeriods() {
|
||||
setPeriods(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) {
|
||||
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() {
|
||||
return new RException(HttpStatus.UNAUTHORIZED);
|
||||
@@ -53,6 +64,17 @@ public class RException extends RuntimeException {
|
||||
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() {
|
||||
return new RException(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
@@ -61,4 +83,14 @@ public class RException extends RuntimeException {
|
||||
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
|
||||
* @return 如果 rule 未设置 id,则生成随机 id 并返回,否则返回 rule.getId()
|
||||
*/
|
||||
public static Serializable apply(PatchOkHttpRule rule) {
|
||||
public static Integer apply(PatchOkHttpRule rule) {
|
||||
|
||||
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);
|
||||
log.debug("PatchOkHttp.apply() running in classloader {}", PatchOkHttp.class.getClassLoader());
|
||||
log.debug("PatchOkHttp.apply() 在 ClassLoader {} 中运行", PatchOkHttp.class.getClassLoader());
|
||||
if (!isHooked) hook();
|
||||
return rule.getId();
|
||||
}
|
||||
@@ -56,7 +61,7 @@ public class PatchOkHttp {
|
||||
|
||||
public static void match(RequestContext ctx, String currentHeader, Consumer<String> consumer) {
|
||||
if (!logOnce) {
|
||||
log.debug("PatchOkHttp.match() running in classloader {}", PatchOkHttp.class.getClassLoader());
|
||||
log.debug("PatchOkHttp.match() 在 ClassLoader {} 中运行", PatchOkHttp.class.getClassLoader());
|
||||
logOnce = true;
|
||||
}
|
||||
for (PatchOkHttpRule rule : PatchOkHttp.rules) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package quant.rich.emoney.patch.okhttp;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
import java.util.function.*;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -19,7 +18,7 @@ public class PatchOkHttpRule {
|
||||
@Getter
|
||||
@Setter
|
||||
@Accessors(chain=true)
|
||||
private Serializable id;
|
||||
private Integer id;
|
||||
private final Predicate<RequestContext> condition;
|
||||
private final List<HeaderAction> actions;
|
||||
|
||||
@@ -171,7 +170,6 @@ public class PatchOkHttpRule {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public boolean equals(PatchOkHttpRule other) {
|
||||
if (other == 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)
|
||||
private int resultCode;
|
||||
private String message;
|
||||
private boolean ok;
|
||||
private T data;
|
||||
|
||||
public R() {
|
||||
@@ -172,13 +171,32 @@ public class R<T> implements Serializable {
|
||||
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) {
|
||||
if (obj != null) return R.ok(obj);
|
||||
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) {
|
||||
try {
|
||||
return R.ok(supplier.get());
|
||||
|
||||
@@ -17,13 +17,8 @@ import io.micrometer.core.instrument.util.IOUtils;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -35,7 +30,6 @@ import org.reflections.scanners.Scanners;
|
||||
import org.reflections.util.ConfigurationBuilder;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.jmx.export.annotation.ManagedAttribute;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -280,7 +274,7 @@ public class ConfigService implements InitializingBean {
|
||||
try {
|
||||
// 此处只是读取文件,并不关心该文件是否可写
|
||||
configString = IOUtils.toString(SmartResourceResolver.loadResource(path), Charset.defaultCharset());
|
||||
} catch (UncheckedIOException e) {
|
||||
} catch (IOException e) {
|
||||
String field = fieldClassCache.inverse().get(configClass);
|
||||
log.warn("Cannot read config {}.json: {}", field, e.getMessage());
|
||||
return config;
|
||||
|
||||
@@ -6,7 +6,6 @@ import java.io.Serializable;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.text.MessageFormat;
|
||||
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
|
||||
@Service
|
||||
@@ -359,7 +365,13 @@ public class IndexDetailService {
|
||||
// 视频和图片等,有的要清除并记录,以免有泄露网址、客户端 IP 风险
|
||||
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) {
|
||||
// 不存在则保存
|
||||
saveIndexDetail(detail);
|
||||
@@ -388,8 +400,11 @@ public class IndexDetailService {
|
||||
data.setItems(items);
|
||||
targetDetail.getData().add(data);
|
||||
String path = getIndexDetailPath(targetDetail);
|
||||
|
||||
if (SmartResourceResolver.loadResource(path) == null) {
|
||||
|
||||
try {
|
||||
SmartResourceResolver.loadResource(path);
|
||||
}
|
||||
catch (IOException e) {
|
||||
// 不存在则保存
|
||||
saveIndexDetail(targetDetail);
|
||||
}
|
||||
@@ -505,7 +520,14 @@ public class IndexDetailService {
|
||||
.append(!hasParams ? "nonParams/": "params/")
|
||||
.append(indexCode)
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import quant.rich.emoney.entity.sqlite.ProtocolMatch;
|
||||
import quant.rich.emoney.mapper.sqlite.ProtocolMatchMapper;
|
||||
|
||||
/**
|
||||
* 协议匹配服务,将一些匹配到的需要解析的协议放在数据库内方便查询。因为请求本身只带 ProtocolID 不带类名,不指定的话可能会找不到解析类
|
||||
*/
|
||||
@DS("sqlite")
|
||||
@Service
|
||||
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;
|
||||
|
||||
/**
|
||||
* 资源解析器,用于在不同运行状况(JAR 包、WAR 包及 IDE 调试状态)下读写本地资源
|
||||
*/
|
||||
@Slf4j
|
||||
public class SmartResourceResolver {
|
||||
|
||||
@@ -39,27 +42,24 @@ public class SmartResourceResolver {
|
||||
* @param relativePath 相对路径
|
||||
* @param writable 是否一定可写
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public static InputStream loadResource(String relativePath) {
|
||||
try {
|
||||
Path externalPath = resolveExternalPath(relativePath);
|
||||
public static InputStream loadResource(String relativePath) throws IOException {
|
||||
Path externalPath = resolveExternalPath(relativePath);
|
||||
|
||||
if (externalPath != null && Files.exists(externalPath)) {
|
||||
log.debug("从外部文件系统加载资源: {}", 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);
|
||||
if (externalPath != null && Files.exists(externalPath)) {
|
||||
log.debug("从外部文件系统加载资源: {}", 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);
|
||||
}
|
||||
|
||||
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:
|
||||
username: admin
|
||||
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
|
||||
password: Em0nY_4u70~!
|
||||
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,
|
||||
"username" : "emy1730978",
|
||||
"password" : "ubVa0vNmD+JJC4171eLYUw==",
|
||||
"authorization" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkpXVCJ9.eyJ1dWQiOjEwMTQ2MzA5OTgsInVpZCI6Mjg2MTMyNDksImRpZCI6IjU2ZTFhNThiYmYxMjJiOTMyMjBhYzBkOThhMmQzZmU3IiwidHlwIjo0LCJhY2MiOiI1NmUxYTU4YmJmMTIyYjkzMjIwYWMwZDk4YTJkM2ZlNyIsInN3dCI6MSwibGd0IjoxNzQ3NDQyMzQzNTEzLCJuYmYiOjE3NDc0NDIzNDMsImV4cCI6MTc0OTE3MDM0MywiaWF0IjoxNzQ3NDQyMzQzfQ.QRcKBbTiE6EuIY-5wl_CVXUYVnzrQIy5LYVAkpfZLWM",
|
||||
"uid" : 28613249,
|
||||
"androidId" : "132b692fe032add3",
|
||||
"androidVersion" : "9",
|
||||
"androidSdkLevel" : "28",
|
||||
"username" : "",
|
||||
"password" : "",
|
||||
"authorization" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkpXVCJ9.eyJ1dWQiOjEwMTUyNzk3MzEsInVpZCI6MjkyNTE0NDEsImRpZCI6IjM5N2RmZjEwOWEwOWFmOGY2NGJhNWMzYmYxNmE0ODA2IiwidHlwIjo0LCJhY2MiOiIzOTdkZmYxMDlhMDlhZjhmNjRiYTVjM2JmMTZhNDgwNiIsInN3dCI6MSwibGd0IjoxNzU4MjY0NjYwNzU0LCJuYmYiOjE3NTgyNjQ2NjAsImV4cCI6MTc1OTk5MjY2MCwiaWF0IjoxNzU4MjY0NjYwfQ.Y1aU7PlyuhGauY9aJCgkdqYC5gqcS4SioiHlPX2sSNc",
|
||||
"uid" : 29251441,
|
||||
"androidId" : "2aa9eb6eea32a4c3",
|
||||
"androidVersion" : "13",
|
||||
"androidSdkLevel" : "33",
|
||||
"softwareType" : "Mobile",
|
||||
"okHttpUserAgent" : "okhttp/3.12.2",
|
||||
"deviceName" : "SM-J730F",
|
||||
"fingerprint" : "samsung/j7y17ltexx/j7y17lte:9/PPR1.180610.011/J730FXWU8CUG1:user/release-keys",
|
||||
"buildId" : "PPR1.180610.011",
|
||||
"chromeVersion" : "117.0.5938.141",
|
||||
"deviceName" : "2112123AG",
|
||||
"fingerprint" : "Xiaomi/psyche_global/psyche:13/RKQ1.211001.001/V14.0.6.0.TLDTWXM:user/release-keys",
|
||||
"buildId" : "RKQ1.211001.001",
|
||||
"chromeVersion" : "87.0.4280.141",
|
||||
"emoneyVersion" : "5.8.1",
|
||||
"emappViewMode" : "1"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"proxyType" : "HTTP",
|
||||
"proxyHost" : "127.0.0.1",
|
||||
"proxyPort" : 8888,
|
||||
"proxyPort" : 7897,
|
||||
"ignoreHttpsVerification" : true
|
||||
}
|
||||
Binary file not shown.
@@ -75,7 +75,12 @@
|
||||
</dd>
|
||||
<dd>
|
||||
<a th:href="@{/admin/v1/manage/protocolMatch}"> <i
|
||||
class="fa-fw fa-regular fa-handshake "></i> Protocol 配置
|
||||
class="fa-fw fa-regular fa-handshake"></i> Protocol 配置
|
||||
</a>
|
||||
</dd>
|
||||
<dd>
|
||||
<a th:href="@{/admin/v1/manage/strategyAndPool}"> <i
|
||||
class="fa-fw fa-solid fa-chess-board"></i> 策略信息
|
||||
</a>
|
||||
</dd>
|
||||
</dl></li>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<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="cronExpression" placeholder="" autocomplete="off" class="layui-input"/>
|
||||
<input type="text" lay-verify="required" name="cronExpr" placeholder="" autocomplete="off" class="layui-input"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
@@ -141,7 +141,7 @@
|
||||
content: $('#addPlan').html(),
|
||||
success: async function (layero, layerIndex) {
|
||||
var el = $(layero);
|
||||
['planId', 'planName', 'cronExpression', 'indexCode'].forEach(x => {
|
||||
['planId', 'planName', 'cronExpr', 'indexCode'].forEach(x => {
|
||||
const fieldEl = el[0].querySelector(`[name="${x}"]`);
|
||||
if (!fieldEl) return;
|
||||
fieldEl.value = r.data[x];
|
||||
@@ -149,12 +149,12 @@
|
||||
['enabled', 'openDayCheck'].forEach(x => {
|
||||
const fieldEl = el[0].querySelector(`[name="${x}"]`);
|
||||
if (!fieldEl) return;
|
||||
fieldEl.checked = r.data[x];
|
||||
fieldEl.value = r.data[x];
|
||||
fieldEl.checked = !!r.data[x];
|
||||
fieldEl.value = !!r.data[x];
|
||||
layui.form.on('switch(' + x + ')', obj => obj.elem.value = obj.elem.checked)
|
||||
});
|
||||
layui.cron.render({
|
||||
elem: '[name="cronExpression"]',
|
||||
elem: '[name="cronExpr"]',
|
||||
btns: ['confirm'],
|
||||
cssPath: '/admin/v1/static/css/cron.css'
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
{field:'openDayCheck', title: '交易日校验', width: 95, switchTemplet: true},
|
||||
{field:'planId', hide: true, width: 60, title: 'ID'},
|
||||
{field:'planName', title: '计划名称'},
|
||||
{field:'cronExpression', title: '计划表达式'},
|
||||
{field:'cronExpr', title: '计划表达式'},
|
||||
{field:'indexCode', title: '指标代码'},
|
||||
{field:'params', title: '请求参数', templet: function(d) {
|
||||
if (typeof d.params === 'object' && d.params !== null) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div th:fragment="protocolMatchExtra">
|
||||
<script id="addProtocolMatch" 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" style="margin:10px 15px" id="editProtocolMatchForm" lay-filter="editProtocolMatchForm">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">Protocol ID<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
@@ -58,7 +58,7 @@ function openEditForm(r) {
|
||||
if (r && r.ok) {
|
||||
window.editLayer = layui.layer.open({
|
||||
type: 1,
|
||||
title: `${r.data.planId ? '编辑' : '新增'}Protocol`,
|
||||
title: `${r.data.protocolId ? '编辑' : '新增'}Protocol`,
|
||||
btn: ['提交', '关闭'],
|
||||
yes: function(index, layero) {
|
||||
layero.find('[lay-filter="submitProtocolMatch"]').click()
|
||||
@@ -101,7 +101,7 @@ layui.table.on('tool(protocolMatches)', function(obj) {
|
||||
$.ajax({
|
||||
url: '/admin/v1/manage/protocolMatch/delete',
|
||||
method: 'POST',
|
||||
data: {planId: obj.data.protocolId},
|
||||
data: {protocolId: obj.data.protocolId},
|
||||
success: function (data) {
|
||||
layui.table.reload('protocolMatches',{
|
||||
page: {
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<h1 class="manage-title">
|
||||
<b>Protocol ID 映射表</b>
|
||||
<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>
|
||||
</h1>
|
||||
|
||||
@@ -56,7 +55,7 @@
|
||||
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}"
|
||||
data-field="${fieldName}" data-id="${d.protocolId}"
|
||||
${d[fieldName] ? 'checked' : ''} lay-filter="switchFilter">`;
|
||||
}
|
||||
|
||||
@@ -81,18 +80,18 @@
|
||||
{title: '开启交易日校验', op: 'enableOpenDayCheck'},
|
||||
{title: '关闭交易日校验', op: 'disableOpenDayCheck'}],
|
||||
click: function (data, othis){
|
||||
var checked = layui.table.checkStatus('protocolMatches'), planIds = [];
|
||||
var checked = layui.table.checkStatus('protocolMatches'), protocolIds = [];
|
||||
if (!checked.data.length) {
|
||||
layui.layer.msg('未选中任何项', {time: 1000});
|
||||
return;
|
||||
}
|
||||
$.each(checked.data, function (i, plan){
|
||||
planIds.push(plan.planId);
|
||||
$.each(checked.data, function (i, protocolMatch){
|
||||
protocolIds.push(protocolMatch.protocolId);
|
||||
});
|
||||
data = $.extend(data, {ids: planIds});
|
||||
data = $.extend(data, {ids: protocolIds});
|
||||
var op = function() {
|
||||
$.ajax({
|
||||
url: '/admin/v1/manage/plan/batchOp',
|
||||
url: '/admin/v1/manage/protocolMatch/batchOp',
|
||||
method: 'POST',
|
||||
data: data,
|
||||
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;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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 java.io.IOException;
|
||||
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.Pattern;
|
||||
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
package quant.rich;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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 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.runner.RunWith;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
Reference in New Issue
Block a user