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

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

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;
@@ -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), "删除失败,是否已删除?");
}
}

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

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

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

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

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,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);
}
// 否则回退到 classpathJAR、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);
}
// 否则回退到 classpathJAR、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 {

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