This commit is contained in:
2025-11-03 10:33:30 +08:00
parent 148583cdaa
commit e924e8c0e6
46 changed files with 1151 additions and 428 deletions

View File

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

View File

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

View File

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

View File

@@ -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,6 +55,7 @@ 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";
@@ -50,21 +63,45 @@ public class EmoneyClient implements Cloneable {
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);
}
}
}
return emoneyRequestConfig;
}
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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,21 +44,14 @@ 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");
}
@PostMapping("/updateBool")
@ResponseBody
@Override
protected R<?> updateBool(String id, String field, Boolean value) {
return updateBool(requestInfoService, RequestInfo.class, RequestInfo::getId, id, field, value);
RequestInfo requestInfo = requestInfoService.getById(id);
return R.judgeNonNull(requestInfo, "无法找到对应 ID 的请求信息");
}
@PostMapping("/save")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,6 +45,17 @@ public class RException extends RuntimeException {
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());
}
}

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

@@ -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 获取,其介绍都在欲加载网页的 &lt;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);
@@ -389,7 +401,10 @@ public class IndexDetailService {
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;
}

View File

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

View File

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

View File

@@ -6,6 +6,9 @@ import java.nio.file.*;
import lombok.extern.slf4j.Slf4j;
/**
* 资源解析器用于在不同运行状况JAR 包、WAR 包及 IDE 调试状态)下读写本地资源
*/
@Slf4j
public class SmartResourceResolver {
@@ -39,9 +42,9 @@ public class SmartResourceResolver {
* @param relativePath 相对路径
* @param writable 是否一定可写
* @return
* @throws IOException
*/
public static InputStream loadResource(String relativePath) {
try {
public static InputStream loadResource(String relativePath) throws IOException {
Path externalPath = resolveExternalPath(relativePath);
if (externalPath != null && Files.exists(externalPath)) {
@@ -57,9 +60,6 @@ public class SmartResourceResolver {
}
throw new FileNotFoundException("无法找到资源: " + relativePath);
} catch (Exception e) {
throw new RuntimeException("读取资源失败: " + relativePath, e);
}
}
public static void saveText(String relativePath, String content) throws IOException {

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

View File

@@ -58,59 +58,3 @@ 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

File diff suppressed because one or more lines are too long

View File

@@ -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%分位值;\",\"当前股价进入高估区域, 说明股票当前处于相对高估的位置;\",\"当前股价进入低估区域,说明股票当前处于相对低估的位置;\",\"当前股价处于低估与高估之间,说明股票当前处于相对合理的位置;\"]}]}"
}

View File

@@ -0,0 +1,7 @@
{
"id" : null,
"name" : null,
"code" : "10007100",
"descriptions" : [ "该指标说明接口返回为空" ],
"original" : null
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"proxyType" : "HTTP",
"proxyHost" : "127.0.0.1",
"proxyPort" : 8888,
"proxyPort" : 7897,
"ignoreHttpsVerification" : true
}

Binary file not shown.

View File

@@ -78,6 +78,11 @@
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>
<li class="layui-nav-item"><a href="javascript:;"> <i
class="fa-fw fa-solid fa-gears"></i> 设置

View File

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

View File

@@ -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) {

View File

@@ -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: {

View File

@@ -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 () {

View File

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

View File

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

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

View File

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

View File

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