计划任务编辑详情前后端适配
This commit is contained in:
@@ -29,7 +29,8 @@ import quant.rich.emoney.util.SpringContextHolder;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
/**
|
||||
* 益盟操盘手基本请求客户端,提供基本功能
|
||||
* 益盟操盘手基本请求客户端,提供基本功能,如登录和请求。
|
||||
* <p>本类一般只提供静态方法,故不能也不该实例化本类。具体请求内容需要自己封装。整个系统共用一套鉴权,所以不要复制本例
|
||||
* <p><b>请求头顺序</b></p>
|
||||
* <p>
|
||||
* <ul>
|
||||
@@ -54,12 +55,13 @@ import okhttp3.OkHttpClient;
|
||||
@Data
|
||||
@Slf4j
|
||||
@Accessors(chain = true)
|
||||
public class EmoneyClient implements Cloneable {
|
||||
public class EmoneyClient {
|
||||
|
||||
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 STRATEGY_X_PROTOCOL_ID = "9400";
|
||||
private static final String LOGIN_X_PROTOCOL_ID = "user%2Fauth%2Flogin";
|
||||
private static final String RELOGIN_X_PROTOCOL_ID = "user%2Fauth%2FReLogin";
|
||||
|
||||
@@ -69,25 +71,28 @@ public class EmoneyClient implements Cloneable {
|
||||
/**
|
||||
* 根据 protocolId 返回 URL
|
||||
* <p>益盟操盘手对于不同的 protocolId 有不同的 URL 负责。在进行某类请求之前,请先通过调试 APP 进行确认,否则可能无法获取到相应内容
|
||||
*
|
||||
* @param protocolId
|
||||
* @return
|
||||
* @return 对应的 URL,当所给 protocolId 为 null 或非 String/Integer 类型时,返回为 null,使用前需要检查
|
||||
*/
|
||||
private static String getUrlByProtocolId(Serializable protocolId) {
|
||||
String strProtocolId;
|
||||
if (protocolId instanceof Integer intProtocolId) {
|
||||
switch (intProtocolId) {
|
||||
case 9400: return STRATEGY_URL;
|
||||
default: return MBS_URL;
|
||||
strProtocolId = String.valueOf(intProtocolId);
|
||||
}
|
||||
else if (protocolId instanceof String s) {
|
||||
strProtocolId = s;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
else if (protocolId instanceof String strProtocolId) {
|
||||
switch (strProtocolId) {
|
||||
case STRATEGY_X_PROTOCOL_ID: return STRATEGY_URL;
|
||||
case LOGIN_X_PROTOCOL_ID: return LOGIN_URL;
|
||||
case RELOGIN_X_PROTOCOL_ID: return RELOGIN_URL;
|
||||
default: return null;
|
||||
default: return MBS_URL;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Spring 上下文中获取由 RequestInfoService 管理的默认请求配置
|
||||
@@ -126,6 +131,9 @@ public class EmoneyClient implements Cloneable {
|
||||
return requestResponseInspectService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 不允许外部实例化对象
|
||||
*/
|
||||
private EmoneyClient() {}
|
||||
|
||||
/**
|
||||
@@ -142,7 +150,6 @@ public class EmoneyClient implements Cloneable {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 使用系统管理的用户名密码登录
|
||||
* <p>建议仅在调试时使用,其他情况请用 {@code loginWithManaged()}</p>
|
||||
@@ -182,7 +189,7 @@ public class EmoneyClient implements Cloneable {
|
||||
* 触发重登陆验证
|
||||
* @return
|
||||
*/
|
||||
public static Boolean relogin() {
|
||||
public static Boolean reloginCheck() {
|
||||
RequestInfo requestInfo = getDefaultRequestInfo();
|
||||
ObjectNode reloginObject = requestInfo.getReloginObject();
|
||||
if (reloginObject == null) {
|
||||
@@ -250,11 +257,9 @@ public class EmoneyClient implements Cloneable {
|
||||
*/
|
||||
private static Boolean login(ObjectNode formObject) {
|
||||
try {
|
||||
//OkHttpClient okHttpClient = new OkHttpClient();
|
||||
RequestInfo requestInfo = getDefaultRequestInfo();
|
||||
OkHttpClient okHttpClient = OkHttpClientProvider.getInstance();
|
||||
MediaType type = MediaType.parse("application/json");
|
||||
//type.charset(StandardCharsets.UTF_8);
|
||||
byte[] content = formObject.toString().getBytes("utf-8");
|
||||
RequestBody body = RequestBody.create(
|
||||
content, type);
|
||||
@@ -273,7 +278,7 @@ public class EmoneyClient implements Cloneable {
|
||||
.header("Authorization", "")
|
||||
.header("X-Android-Agent", requestInfo.getXAndroidAgent())
|
||||
.header("Emapp-ViewMode", requestInfo.getEmappViewMode());
|
||||
//.header("User-Agent", requestInfo.getOkHttpUserAgent())
|
||||
//此处 User-Agent 由 ByteBuddy 拦截修改
|
||||
|
||||
Request request = requestBuilder.build();
|
||||
|
||||
@@ -312,8 +317,10 @@ public class EmoneyClient implements Cloneable {
|
||||
/**
|
||||
* 获取基本返回 Base_Response
|
||||
*
|
||||
* @param <T>
|
||||
* @param nanoRequest
|
||||
* @param <T> 请求类型泛型
|
||||
* @param nanoRequest 请求
|
||||
* @param xProtocolId 请求对应的 Protocol ID
|
||||
* @param xRequestId 视具体请求而定。可能是 null,也可能是 String.valueOf(System.currentTimeMillis())
|
||||
* @return
|
||||
*/
|
||||
protected static <T extends MessageNano> BaseResponse.Base_Response post(
|
||||
@@ -355,8 +362,8 @@ public class EmoneyClient implements Cloneable {
|
||||
.header("EM-Sign", EncryptUtils.getEMSign(content, "POST", xProtocolId.toString()))
|
||||
.header("Authorization", requestInfo.getAuthorization())
|
||||
.header("X-Android-Agent", requestInfo.getXAndroidAgent())
|
||||
.header("Emapp-ViewMode", requestInfo.getEmappViewMode())
|
||||
;
|
||||
.header("Emapp-ViewMode", requestInfo.getEmappViewMode());
|
||||
//此处 User-Agent 由 ByteBuddy 拦截修改
|
||||
|
||||
Request request = requestBuilder.build();
|
||||
|
||||
@@ -377,15 +384,20 @@ public class EmoneyClient implements Cloneable {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定 clazz 获取返回
|
||||
*
|
||||
* @param <T>
|
||||
* @param <U>
|
||||
* @param nanoRequest
|
||||
* @param clazz
|
||||
* 请求并返回
|
||||
* @param <T> 请求类型泛型
|
||||
* @param <U> 返回类型泛型
|
||||
* @param nanoRequest 请求
|
||||
* @param clazz 返回类型
|
||||
* @param xProtocolId 请求对应的 Protocol ID
|
||||
* @param xRequestId 视具体请求而定。可能是 null,也可能是 String.valueOf(System.currentTimeMillis())
|
||||
* @return
|
||||
*/
|
||||
public static <T extends MessageNano, U extends MessageNano> U post(T nanoRequest, Class<U> clazz, Serializable xProtocolId, Serializable xRequestId) {
|
||||
public static <T extends MessageNano, U extends MessageNano> U post(
|
||||
T nanoRequest,
|
||||
Class<U> clazz,
|
||||
Serializable xProtocolId,
|
||||
Serializable xRequestId) {
|
||||
|
||||
BaseResponse.Base_Response baseResponse;
|
||||
try {
|
||||
|
||||
@@ -29,6 +29,8 @@ import quant.rich.emoney.util.SpringContextHolder;
|
||||
|
||||
/**
|
||||
* OkHttpClient 提供器
|
||||
* <p>
|
||||
* 此处提供的 OkHttpClient 方便使用平台配置的代理,方便是否启用 HTTPS 证书认证等
|
||||
* @see quant.rich.emoney.entity.config.ProxyConfig
|
||||
* @see okhttp3.internal.http.BridgeInterceptor
|
||||
*/
|
||||
|
||||
@@ -57,7 +57,7 @@ public class RequireAuthAndProxyAspect {
|
||||
throw new RuntimeException("鉴权登录失败");
|
||||
}
|
||||
}
|
||||
else if (!EmoneyClient.relogin()) {
|
||||
else if (!EmoneyClient.reloginCheck()) {
|
||||
throw new RuntimeException("检查重鉴权失败");
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,10 @@ public class SecurityConfig {
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.headers(headers -> headers.cacheControl(cache -> cache.disable()))
|
||||
.headers(headers -> headers
|
||||
.cacheControl(cache -> cache.disable())
|
||||
.frameOptions(f -> f.sameOrigin())
|
||||
)
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/favicon.ico").permitAll()
|
||||
|
||||
@@ -29,21 +29,8 @@ public class ErrorPageController implements ErrorController {
|
||||
@GetMapping(value = ERROR_PATH)
|
||||
@PostMapping(value = ERROR_PATH)
|
||||
public String errorHtml(HttpServletRequest request) {
|
||||
HttpStatus status = getStatus(request);
|
||||
String prefix = "error/";
|
||||
|
||||
return prefix + "error_400";
|
||||
|
||||
// switch (status) {
|
||||
// case BAD_REQUEST:
|
||||
// return prefix + "error_400";
|
||||
// case NOT_FOUND:
|
||||
// return prefix + "error_404";
|
||||
// case METHOD_NOT_ALLOWED:
|
||||
// return prefix + "error_405";
|
||||
// default:
|
||||
// return prefix + "error_5xx";
|
||||
// }
|
||||
}
|
||||
|
||||
@GetMapping(value = ERROR_PATH, produces = "application/json")
|
||||
|
||||
@@ -13,6 +13,8 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import com.google.code.kaptcha.impl.DefaultKaptcha;
|
||||
|
||||
import quant.rich.emoney.service.AuthService;
|
||||
import quant.rich.emoney.util.ArithmeticCaptchaGen;
|
||||
import quant.rich.emoney.util.ArithmeticCaptchaGen.Captcha;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/captcha")
|
||||
@@ -20,13 +22,13 @@ public class KaptchaController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
DefaultKaptcha kaptcha;
|
||||
|
||||
@GetMapping(value = "/get", produces = MediaType.IMAGE_JPEG_VALUE)
|
||||
public byte[] getCaptcha() throws Exception {
|
||||
String createText = kaptcha.createText();
|
||||
|
||||
Captcha captcha = ArithmeticCaptchaGen.generate();
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
session.setAttribute(AuthService.CAPTCHA, createText);
|
||||
ImageIO.write(kaptcha.createImage(createText), "jpg", os);
|
||||
session.setAttribute(AuthService.CAPTCHA, captcha.answer());
|
||||
ImageIO.write(kaptcha.createImage(captcha.expr()), "jpg", os);
|
||||
byte[] result = os.toByteArray();
|
||||
os.close();
|
||||
return result;
|
||||
|
||||
@@ -2,20 +2,29 @@ package quant.rich.emoney.controller.manage;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.controller.common.UpdateBoolServiceController;
|
||||
import quant.rich.emoney.entity.sqlite.Plan;
|
||||
import quant.rich.emoney.enums.PlanType;
|
||||
import quant.rich.emoney.exception.PageNotFoundException;
|
||||
import quant.rich.emoney.exception.RException;
|
||||
import quant.rich.emoney.interfaces.IQueryableEnum;
|
||||
import quant.rich.emoney.pojo.dto.LayPageReq;
|
||||
@@ -31,6 +40,9 @@ public class PlanControllerV1 extends UpdateBoolServiceController<Plan> {
|
||||
@Autowired
|
||||
PlanService planService;
|
||||
|
||||
@Autowired
|
||||
ObjectMapper objectMapper;
|
||||
|
||||
@GetMapping({"", "/", "/index"})
|
||||
public String index() {
|
||||
return "/admin/v1/manage/plan/index";
|
||||
@@ -48,6 +60,60 @@ public class PlanControllerV1 extends UpdateBoolServiceController<Plan> {
|
||||
return super.getOne(planId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用编辑接口,跳转对应模式的编辑页
|
||||
* @param planId
|
||||
* @param ra
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/edit")
|
||||
public String edit(Integer planId, RedirectAttributes ra) {
|
||||
Plan plan = null;
|
||||
if (planId != null) {
|
||||
plan = planService.getById(planId);
|
||||
}
|
||||
if (plan == null) {
|
||||
plan = new Plan();
|
||||
}
|
||||
if (plan.getPlanId() != null) {
|
||||
ra.addAttribute("planId", plan.getPlanId());
|
||||
}
|
||||
if (plan.getPlanType() == null || plan.getPlanType() == PlanType.SINGLE_INDEX) {
|
||||
return "redirect:editSingleIndex";
|
||||
}
|
||||
else if (plan.getPlanType() == PlanType.MULTI_INDEX) {
|
||||
return "redirect:editMultiIndex";
|
||||
}
|
||||
else if (plan.getPlanType() == PlanType.STOCK_STRATEGY) {
|
||||
return "redirect:editStockStrategy";
|
||||
}
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
|
||||
@GetMapping("/edit{planTypeString}")
|
||||
public String editPlanType(@PathVariable String planTypeString, Integer planId) {
|
||||
Optional<PlanType> optional = tryParseEnum(PlanType.class, planTypeString);
|
||||
if (optional.isEmpty()) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
Plan plan = null;
|
||||
if (planId != null) {
|
||||
plan = planService.getById(planId);
|
||||
}
|
||||
if (plan == null) {
|
||||
plan = new Plan();
|
||||
}
|
||||
String planJsonString = "{}";
|
||||
try {
|
||||
planJsonString = objectMapper.writeValueAsString(plan);
|
||||
}
|
||||
catch (Exception e) {}
|
||||
request.setAttribute("plan", plan);
|
||||
request.setAttribute("planType", optional.get().toString());
|
||||
request.setAttribute("planJsonString", planJsonString);
|
||||
return "/admin/v1/manage/plan/edit" + planTypeString;
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
@ResponseBody
|
||||
public R<?> save(@RequestBody Plan plan) {
|
||||
@@ -120,4 +186,62 @@ public class PlanControllerV1 extends UpdateBoolServiceController<Plan> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将大驼峰转换为大写+下划线分隔形式
|
||||
* @param s
|
||||
* @return
|
||||
*/
|
||||
public static String toUpperSnake(String s) {
|
||||
if (s == null) return null;
|
||||
s = s.trim();
|
||||
if (s.isEmpty()) return s;
|
||||
|
||||
// 先把空格/连字符这类统一成下划线(可按需扩展)
|
||||
s = s.replace('-', '_').replace(' ', '_');
|
||||
|
||||
// 1) fooBar / foo1Bar -> foo_Bar / foo1_Bar
|
||||
s = s.replaceAll("([a-z0-9])([A-Z])", "$1_$2");
|
||||
// 2) HTTPServer -> HTTP_Server(在 "P" 和 "S" 之间插)
|
||||
s = s.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2");
|
||||
|
||||
// 合并多余下划线
|
||||
s = s.replaceAll("_+", "_");
|
||||
// 去掉首尾下划线
|
||||
s = s.replaceAll("^_+|_+$", "");
|
||||
|
||||
return s.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public static <E extends Enum<E>> Optional<E> tryParseEnum(Class<E> enumClass, String input) {
|
||||
if (enumClass == null || input == null) return Optional.empty();
|
||||
|
||||
String raw = input.trim();
|
||||
if (raw.isEmpty()) return Optional.empty();
|
||||
|
||||
// 1) 直接按原样尝试(允许用户本来就传了正确的常量名)
|
||||
E e = tryValueOf(enumClass, raw);
|
||||
if (e != null) return Optional.of(e);
|
||||
|
||||
// 2) 尝试 UPPER_SNAKE_CASE
|
||||
String upperSnake = toUpperSnake(raw);
|
||||
e = tryValueOf(enumClass, upperSnake);
|
||||
if (e != null) return Optional.of(e);
|
||||
|
||||
// 3) 兜底:忽略大小写匹配(成本 O(n),但很稳)
|
||||
for (E constant : enumClass.getEnumConstants()) {
|
||||
if (constant.name().equalsIgnoreCase(raw) || constant.name().equalsIgnoreCase(upperSnake)) {
|
||||
return Optional.of(constant);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static <E extends Enum<E>> E tryValueOf(Class<E> enumClass, String name) {
|
||||
try {
|
||||
return Enum.valueOf(enumClass, name);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package quant.rich.emoney.crawler;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
|
||||
/**
|
||||
* 益盟爬虫爬取成功与否及对应的爬虫配置,方便重新爬取
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class EmoneyCrawlerResult<T> {
|
||||
|
||||
private T callable;
|
||||
private boolean success;
|
||||
private Exception exception;
|
||||
|
||||
}
|
||||
204
src/main/java/quant/rich/emoney/crawler/EmoneyIndexCallable.java
Normal file
204
src/main/java/quant/rich/emoney/crawler/EmoneyIndexCallable.java
Normal file
@@ -0,0 +1,204 @@
|
||||
package quant.rich.emoney.crawler;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import nano.CandleStickNewResponse.CandleStickNew_Response.CandleStick;
|
||||
import nano.CandleStickNewWithIndexExResponse.CandleStickNewWithIndexEx_Response;
|
||||
import nano.CandleStickNewWithIndexExResponse.CandleStickNewWithIndexEx_Response.IndLine;
|
||||
import nano.CandleStickRequest.CandleStick_Request;
|
||||
import nano.CandleStickWithIndexRequest.CandleStickWithIndex_Request;
|
||||
import nano.CandleStickWithIndexRequest.CandleStickWithIndex_Request.IndexInfo;
|
||||
import nano.IndexCalcNewExResponse.IndexCalcNewEx_Response.outputline;
|
||||
import quant.rich.emoney.client.EmoneyClient;
|
||||
import quant.rich.emoney.entity.postgre.EmoneyIndex;
|
||||
import quant.rich.emoney.enums.StockSpan;
|
||||
import quant.rich.emoney.exception.EmoneyRequestException;
|
||||
import quant.rich.emoney.util.DateUtils;
|
||||
import quant.rich.emoney.util.StockCodeUtils;
|
||||
|
||||
/**
|
||||
* 执行爬虫时最终被调用的方法。该方法使用 2422(多指标、带 K 线)更新指标数据
|
||||
*/
|
||||
@Data
|
||||
@Slf4j
|
||||
public class EmoneyIndexCallable implements Callable<EmoneyCrawlerResult<EmoneyIndexCallable>> {
|
||||
|
||||
private static final int LIMIT_SIZE = 250;
|
||||
|
||||
private EmoneyCrawlerResult<EmoneyIndexCallable> result;
|
||||
|
||||
private String tsCode;
|
||||
|
||||
private StockSpan stockSpan;
|
||||
|
||||
private Long beginPosition;
|
||||
|
||||
private Long totalNeeds;
|
||||
|
||||
private Function<List<EmoneyIndex>, Boolean> func;
|
||||
|
||||
private IndexInfo[] indexInfos;
|
||||
|
||||
@Setter(AccessLevel.PRIVATE)
|
||||
private String indexNames;
|
||||
|
||||
public EmoneyIndexCallable setIndexInfos(IndexInfo[] indexInfos) {
|
||||
this.indexInfos = indexInfos;
|
||||
indexNames = Arrays.stream(indexInfos)
|
||||
.map(IndexInfo::getIndexName)
|
||||
.collect(Collectors.joining(", "));
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* 初始化一个 Callable
|
||||
* @param tsCode
|
||||
* @param beginPosition 起始位置,一般为当日或当时,如 20230921L
|
||||
* @param stockSpan 数据粒度
|
||||
* @param totalNeeds 经过计算需要的总数据数
|
||||
* @param func 消费 {@code List<EmoneyIndex>} 的方法,返回的布尔值辅助判断本次调用是否成功
|
||||
* @param indexInfos 指标类型数组
|
||||
*/
|
||||
public EmoneyIndexCallable(
|
||||
String tsCode,
|
||||
Long beginPosition,
|
||||
StockSpan stockSpan,
|
||||
Long totalNeeds,
|
||||
Function<List<EmoneyIndex>, Boolean> func,
|
||||
IndexInfo...indexInfos) {
|
||||
|
||||
this.tsCode = tsCode;
|
||||
this.beginPosition = beginPosition;
|
||||
this.stockSpan = stockSpan;
|
||||
setIndexInfos(indexInfos);
|
||||
this.totalNeeds = totalNeeds;
|
||||
this.func = func;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 EmoneyIndexCallable 恢复 Callable
|
||||
* @param request
|
||||
*/
|
||||
public EmoneyIndexCallable(EmoneyIndexCallable other) {
|
||||
this.tsCode = other.tsCode;
|
||||
this.beginPosition = other.beginPosition;
|
||||
this.stockSpan = other.stockSpan;
|
||||
this.indexInfos = other.indexInfos;
|
||||
this.indexNames = other.indexNames;
|
||||
this.totalNeeds = other.totalNeeds;
|
||||
this.func = other.func;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmoneyCrawlerResult<EmoneyIndexCallable> call() throws Exception {
|
||||
|
||||
List<EmoneyIndex> emoneyIndexList = new ArrayList<>();
|
||||
while (totalNeeds > 0) {
|
||||
|
||||
// 组装请求
|
||||
/*
|
||||
* 2422 CANDLE_STICK_V3
|
||||
* 该种方式除返回指标和对应时间外,还会返回对应时间粒度的 K 线(蜡烛图),数据量会比 2921 大。
|
||||
* 使用 beginPosition, 如 20231230L 和 limitSize, 如 400 向前追溯
|
||||
*/
|
||||
CandleStickWithIndex_Request request = new CandleStickWithIndex_Request();
|
||||
CandleStick_Request candleRequest = new CandleStick_Request();
|
||||
candleRequest.setBeginPosition(beginPosition)
|
||||
.setDataPeriod(stockSpan.getEmoneyCode())
|
||||
.setExFlag(0)
|
||||
.setGoodsId(StockCodeUtils.tsCodeToGoodsId(tsCode))
|
||||
.setLastVolume(0)
|
||||
.setLimitSize(Math.min(totalNeeds, LIMIT_SIZE))
|
||||
.setReqFhsp(false)
|
||||
.setReqHisShares(false);
|
||||
request.candleRequest = candleRequest;
|
||||
|
||||
/*IndexInfo[] indexInfos = new IndexInfo[indexNames.length];
|
||||
|
||||
for (int i = 0; i < indexNames.length; i++) {
|
||||
IndexInfo indexInfo = new IndexInfo().setIndexName(indexNames[i]);
|
||||
indexInfos[i] = indexInfo;
|
||||
}*/
|
||||
request.indexRequest = indexInfos;
|
||||
|
||||
result = new EmoneyCrawlerResult<EmoneyIndexCallable>().setCallable(this).setSuccess(false);
|
||||
|
||||
|
||||
CandleStickNewWithIndexEx_Response response;
|
||||
try {
|
||||
// 时间戳作为 xRequestId
|
||||
response = EmoneyClient.post(
|
||||
request,
|
||||
CandleStickNewWithIndexEx_Response.class,
|
||||
2422,
|
||||
System.currentTimeMillis());
|
||||
}
|
||||
catch (EmoneyRequestException e) {
|
||||
return result.setSuccess(false).setException(e);
|
||||
}
|
||||
|
||||
log.info("Emoney index [{}] for {} called, totalNeeds left: {}, current received length/limitSize: {}/{}",
|
||||
indexNames, tsCode, totalNeeds, response.kLines.length, LIMIT_SIZE);
|
||||
|
||||
for (int i = 0; i < response.kLines.length; i++) {
|
||||
|
||||
CandleStick candleStick = response.kLines[i];
|
||||
|
||||
long dateTime = candleStick.getDatetime();
|
||||
LocalDateTime date = DateUtils.longDatetimeToLocalDateTime(dateTime);
|
||||
|
||||
for (IndexInfo indexInfo : indexInfos) {
|
||||
IndLine indLine = response.indexDatas.get(indexInfo.getIndexName());
|
||||
for (outputline lineValue : indLine.lineValue) {
|
||||
|
||||
EmoneyIndex emoneyIndex = new EmoneyIndex()
|
||||
.setIndexName(indexInfo.getIndexName())
|
||||
.setDate(date).setLineName(lineValue.getLineName())
|
||||
.setLineShape(lineValue.getLineShape())
|
||||
.setStockSpan(stockSpan)
|
||||
.setTsCode(tsCode)
|
||||
.setValue(lineValue.lineData[i]);
|
||||
|
||||
emoneyIndexList.add(emoneyIndex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Thread.sleep(2500);
|
||||
|
||||
// 判断数据是否结束了
|
||||
if (response.kLines.length < LIMIT_SIZE) {
|
||||
break;
|
||||
}
|
||||
|
||||
totalNeeds -= LIMIT_SIZE;
|
||||
|
||||
// 重设起始点为返回数据的最旧时间
|
||||
beginPosition = response.kLines[0].getDatetime();
|
||||
}
|
||||
Boolean b = true;
|
||||
if (!emoneyIndexList.isEmpty()) {
|
||||
try {
|
||||
b = func.apply(emoneyIndexList);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return result.setSuccess(false).setException(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result.setSuccess(b);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package quant.rich.emoney.entity.postgre;
|
||||
|
||||
import java.util.Date;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -36,7 +36,7 @@ public class EmoneyIndex {
|
||||
* 时间
|
||||
*/
|
||||
@TableField("trade_date")
|
||||
private Date date;
|
||||
private LocalDateTime date;
|
||||
/**
|
||||
* 指标值
|
||||
*/
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
package quant.rich.emoney.entity.sqlite;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
@@ -17,7 +8,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import quant.rich.emoney.mybatis.typehandler.CommaListTypeHandler;
|
||||
import quant.rich.emoney.enums.PlanType;
|
||||
import quant.rich.emoney.mybatis.typehandler.JsonStringTypeHandler;
|
||||
|
||||
/**
|
||||
@@ -27,6 +18,7 @@ import quant.rich.emoney.mybatis.typehandler.JsonStringTypeHandler;
|
||||
* <li>个股策略(/strategy, id=9400)的抓取
|
||||
* <li>选股策略的抓取
|
||||
* </ul>
|
||||
* 选股策略的抓取的筛选器自带股票过滤。其他抓取应该可以选股策略抓取的结果、tushare 的直接结果作为抓取的范围。
|
||||
* 未来可能存在更多的计划任务, 所以需要考虑如何设计才能更好地兼容后续添加的任务类型
|
||||
*/
|
||||
@Data
|
||||
@@ -38,7 +30,7 @@ public class Plan {
|
||||
* 键 ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private String planId;
|
||||
private Integer planId;
|
||||
|
||||
/**
|
||||
* 计划任务表达式
|
||||
@@ -51,21 +43,15 @@ public class Plan {
|
||||
private String planName;
|
||||
|
||||
/**
|
||||
* 指标代码
|
||||
* 计划类型
|
||||
*/
|
||||
private String indexCode;
|
||||
|
||||
/**
|
||||
* 需要抓取的指标周期
|
||||
*/
|
||||
@TableField(typeHandler = CommaListTypeHandler.class)
|
||||
private List<String> periods;
|
||||
private PlanType planType;
|
||||
|
||||
/**
|
||||
* 参数
|
||||
*/
|
||||
@TableField(typeHandler = JsonStringTypeHandler.class)
|
||||
private JsonNode params;
|
||||
private JsonNode detail;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
@@ -78,33 +64,4 @@ public class Plan {
|
||||
*/
|
||||
private Boolean openDayCheck;
|
||||
|
||||
/**
|
||||
* 设置抓取周期并去重
|
||||
* @param periods
|
||||
* @return
|
||||
*/
|
||||
public Plan setPeriods(List<String> periods) {
|
||||
if (CollectionUtils.isEmpty(periods)) {
|
||||
this.periods = Collections.emptyList();
|
||||
return this;
|
||||
}
|
||||
Set<String> hashSet = new HashSet<>();
|
||||
periods.forEach(s -> {
|
||||
if (StringUtils.isNotBlank(s)) {
|
||||
hashSet.add(s.trim());
|
||||
}
|
||||
});
|
||||
this.periods = new ArrayList<>(hashSet);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取抓取周期。已去重
|
||||
* @return
|
||||
*/
|
||||
public List<String> getPeriods() {
|
||||
setPeriods(periods);
|
||||
return periods;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
20
src/main/java/quant/rich/emoney/enums/PlanType.java
Normal file
20
src/main/java/quant/rich/emoney/enums/PlanType.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package quant.rich.emoney.enums;
|
||||
|
||||
/**
|
||||
* 计划任务类型枚举
|
||||
*/
|
||||
public enum PlanType {
|
||||
|
||||
/**
|
||||
* 单指标
|
||||
*/
|
||||
SINGLE_INDEX,
|
||||
/**
|
||||
* 多指标
|
||||
*/
|
||||
MULTI_INDEX,
|
||||
|
||||
STOCK_STRATEGY,
|
||||
STOCK_PICKING
|
||||
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package quant.rich.emoney.mapper.postgre;
|
||||
|
||||
import java.util.Collection;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
@@ -13,4 +15,7 @@ import quant.rich.emoney.entity.postgre.EmoneyIndex;
|
||||
@DS("postgre")
|
||||
public interface EmoneyIndexMapper extends BaseMapper<EmoneyIndex> {
|
||||
|
||||
int insertOrUpdateBatch(
|
||||
@Param("list") Collection<EmoneyIndex> list,
|
||||
@Param("batchSize") int batchSize);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package quant.rich.emoney.pojo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class MultiIndexPlanDetail {
|
||||
|
||||
/**
|
||||
* 指标
|
||||
*/
|
||||
List<MultiIndexPlanPart> indexes;
|
||||
|
||||
/**
|
||||
* 抓取的 K 线粒度
|
||||
*/
|
||||
List<Integer> periods;
|
||||
|
||||
/**
|
||||
* 设置抓取周期并去重
|
||||
* @param periods
|
||||
* @return
|
||||
*/
|
||||
public MultiIndexPlanDetail setPeriods(List<Integer> periods) {
|
||||
if (CollectionUtils.isEmpty(periods)) {
|
||||
this.periods = Collections.emptyList();
|
||||
return this;
|
||||
}
|
||||
Set<Integer> hashSet = new HashSet<>();
|
||||
periods.forEach(period -> {
|
||||
if (period != null) {
|
||||
hashSet.add(period);
|
||||
}
|
||||
});
|
||||
this.periods = new ArrayList<>(hashSet);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取抓取周期。已去重
|
||||
* @return
|
||||
*/
|
||||
public List<Integer> getPeriods() {
|
||||
setPeriods(periods);
|
||||
return periods;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public static class MultiIndexPlanPart {
|
||||
|
||||
/**
|
||||
* 单指标代码
|
||||
*/
|
||||
String indexCode;
|
||||
|
||||
/**
|
||||
* 单指标参数
|
||||
*/
|
||||
Map<String, String> params;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package quant.rich.emoney.pojo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class SingleIndexPlanDetail {
|
||||
|
||||
/**
|
||||
* 单指标代码
|
||||
*/
|
||||
String indexCode;
|
||||
|
||||
/**
|
||||
* 单指标参数
|
||||
*/
|
||||
Map<String, String> params;
|
||||
|
||||
/**
|
||||
* 抓取的 K 线粒度
|
||||
*/
|
||||
List<Integer> periods;
|
||||
|
||||
/**
|
||||
* 设置抓取周期并去重
|
||||
* @param periods
|
||||
* @return
|
||||
*/
|
||||
public SingleIndexPlanDetail setPeriods(List<Integer> periods) {
|
||||
if (CollectionUtils.isEmpty(periods)) {
|
||||
this.periods = Collections.emptyList();
|
||||
return this;
|
||||
}
|
||||
Set<Integer> hashSet = new HashSet<>();
|
||||
periods.forEach(period -> {
|
||||
if (period != null) {
|
||||
hashSet.add(period);
|
||||
}
|
||||
});
|
||||
this.periods = new ArrayList<>(hashSet);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取抓取周期。已去重
|
||||
* @return
|
||||
*/
|
||||
public List<Integer> getPeriods() {
|
||||
setPeriods(periods);
|
||||
return periods;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package quant.rich.emoney.service.postgre;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
|
||||
|
||||
import quant.rich.emoney.config.PostgreMybatisConfig;
|
||||
import quant.rich.emoney.entity.postgre.EmoneyIndex;
|
||||
import quant.rich.emoney.mapper.postgre.EmoneyIndexMapper;
|
||||
|
||||
public class EmoneyIndexService extends PostgreServiceImpl<EmoneyIndexMapper, EmoneyIndex> {
|
||||
|
||||
@Override
|
||||
@Transactional(transactionManager = PostgreMybatisConfig.POSTGRE_TRANSACTION_MANAGER, rollbackFor = Exception.class)
|
||||
public boolean saveOrUpdateBatch(Collection<EmoneyIndex> entityList, int batchSize) {
|
||||
return SqlHelper.retBool(baseMapper.insertOrUpdateBatch(entityList, batchSize));
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,16 @@ import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import feign.Headers;
|
||||
|
||||
/**
|
||||
* 对接 TushareDataService 的 Feign 客户端
|
||||
* <p>
|
||||
* 服务器上配置了 fail2ban 对默认 User-Agent
|
||||
* 拦截(Java/*),所以这里自定义 User-Agent
|
||||
*/
|
||||
@FeignClient(name="tushare-data-service-client", url="http://localhost:9999")
|
||||
@Headers("User-Agent: At17DataService/1.0")
|
||||
@FeignClient(name="tushare-data-service-client", url="https://tushare.database.at17.link")
|
||||
public interface TushareDataServiceClient {
|
||||
|
||||
@GetMapping("/api/v1/common/stockInfo/list")
|
||||
|
||||
164
src/main/java/quant/rich/emoney/util/ArithmeticCaptchaGen.java
Normal file
164
src/main/java/quant/rich/emoney/util/ArithmeticCaptchaGen.java
Normal file
@@ -0,0 +1,164 @@
|
||||
package quant.rich.emoney.util;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class ArithmeticCaptchaGen {
|
||||
|
||||
private static final SecureRandom RND = new SecureRandom();
|
||||
private static final char[] OPS = new char[]{'+', '-', '×', '÷'};
|
||||
|
||||
public static class Captcha {
|
||||
public final int a;
|
||||
public final int b;
|
||||
public final char op;
|
||||
|
||||
public Captcha(int a, int b, char op) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
this.op = op;
|
||||
}
|
||||
|
||||
public String expr() {
|
||||
return a + " " + op + " " + b;
|
||||
}
|
||||
|
||||
public int answer() {
|
||||
return switch (op) {
|
||||
case '+' -> a + b;
|
||||
case '-' -> a - b;
|
||||
case '×' -> a * b;
|
||||
case '÷' -> a / b; // 保证整除
|
||||
default -> throw new IllegalStateException("Unexpected op: " + op);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return expr() + " = ?";
|
||||
}
|
||||
}
|
||||
|
||||
/** 对外:随机生成一题 */
|
||||
public static Captcha generate() {
|
||||
char op = OPS[RND.nextInt(OPS.length)];
|
||||
return generate(op);
|
||||
}
|
||||
|
||||
/** 对外:指定运算符生成 */
|
||||
public static Captcha generate(char op) {
|
||||
return switch (op) {
|
||||
case '+' -> genAddCarryAllowedButIfCarryUnitsZero();
|
||||
case '-' -> genSubNoBorrowNonNegative();
|
||||
case '×' -> genMulNoCarryOneDigit();
|
||||
case '÷' -> genDivExact();
|
||||
default -> throw new IllegalArgumentException("Unsupported op: " + op);
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------- +:允许进位;若发生进位,则结果个位必须为 0;且和 <= 100 ----------------
|
||||
private static Captcha genAddCarryAllowedButIfCarryUnitsZero() {
|
||||
// 仍保留 100 的特殊题(不是必须,但能保证覆盖 100)
|
||||
if (RND.nextInt(20) == 0) { // 5% 概率出 100
|
||||
if (RND.nextBoolean()) return new Captcha(100, 0, '+');
|
||||
return new Captcha(0, 100, '+');
|
||||
}
|
||||
|
||||
while (true) {
|
||||
int a = RND.nextInt(101); // 0..100
|
||||
int b = RND.nextInt(101); // 0..100
|
||||
int sum = a + b;
|
||||
if (sum > 100) continue;
|
||||
|
||||
if (carryHappened(a, b)) {
|
||||
// 发生进位:个位必须为 0
|
||||
if (sum % 10 == 0) return new Captcha(a, b, '+');
|
||||
} else {
|
||||
// 未发生进位:不限制个位
|
||||
return new Captcha(a, b, '+');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 判断十进制逐位相加是否发生过进位(任意一位) */
|
||||
private static boolean carryHappened(int a, int b) {
|
||||
int carry = 0;
|
||||
while (a > 0 || b > 0) {
|
||||
int da = a % 10;
|
||||
int db = b % 10;
|
||||
int s = da + db + carry;
|
||||
if (s >= 10) return true;
|
||||
carry = 0; // 因为 s<10 时 carry 必为 0
|
||||
a /= 10;
|
||||
b /= 10;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------- -:无退位,差 >= 0 ----------------
|
||||
private static Captcha genSubNoBorrowNonNegative() {
|
||||
while (true) {
|
||||
int a = RND.nextInt(100); // 0..99
|
||||
int b = RND.nextInt(100); // 0..99
|
||||
|
||||
int aT = a / 10, aU = a % 10;
|
||||
int bT = b / 10, bU = b % 10;
|
||||
|
||||
// 无退位:每一位都要 a>=b;且整体差>=0
|
||||
if (aU >= bU && aT >= bT) {
|
||||
return new Captcha(a, b, '-'); // 此时 a>=b 自然成立
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- *:无进位;一元为 0..9;另一元每位*d < 10 ----------------
|
||||
private static Captcha genMulNoCarryOneDigit() {
|
||||
while (true) {
|
||||
int d = RND.nextInt(10); // 0..9 其中一个因子
|
||||
int x = RND.nextInt(100); // 另一元 0..99(你可自行放大范围)
|
||||
|
||||
// 如果 d=0,任何 x 都满足
|
||||
if (d == 0) return orderMulOperands(d, x);
|
||||
|
||||
// 对 x 的每一位要求:digit*d < 10 才不会在该位产生进位
|
||||
if (allDigitsMulLessThan10(x, d)) {
|
||||
return orderMulOperands(d, x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Captcha orderMulOperands(int d, int x) {
|
||||
// 随机决定把 0..9 放左边还是右边
|
||||
if (RND.nextBoolean()) return new Captcha(d, x, '×');
|
||||
return new Captcha(x, d, '×');
|
||||
}
|
||||
|
||||
private static boolean allDigitsMulLessThan10(int x, int d) {
|
||||
// x 是非负
|
||||
if (x == 0) return true;
|
||||
int t = x;
|
||||
while (t > 0) {
|
||||
int digit = t % 10;
|
||||
if (digit * d >= 10) return false;
|
||||
t /= 10;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------- /:必须整除 ----------------
|
||||
private static Captcha genDivExact() {
|
||||
// 控制题目大小: dividend <= 100
|
||||
while (true) {
|
||||
int b = 1 + RND.nextInt(10); // 除数 1..10
|
||||
int q = RND.nextInt(21); // 商 0..20
|
||||
int a = b * q; // 被除数
|
||||
if (a <= 100) return new Captcha(a, b, '÷');
|
||||
}
|
||||
}
|
||||
|
||||
// demo
|
||||
public static void main(String[] args) {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
Captcha c = generate();
|
||||
System.out.println(c.expr() + " = " + c.answer());
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/main/java/quant/rich/emoney/util/ChunkRandomIter.java
Normal file
28
src/main/java/quant/rich/emoney/util/ChunkRandomIter.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package quant.rich.emoney.util;
|
||||
import java.util.*;
|
||||
|
||||
public class ChunkRandomIter {
|
||||
|
||||
final static Random RANDOM = new Random();
|
||||
|
||||
public static <T> List<T> splitShuffleAndFlatten(List<T> list, int n) {
|
||||
int size = list.size();
|
||||
if (n <= 0) throw new IllegalArgumentException("n must be > 0");
|
||||
n = Math.min(n, size == 0 ? 1 : size);
|
||||
|
||||
List<T> parts = new ArrayList<>(n);
|
||||
|
||||
int base = size / n; // 每份至少 base 个
|
||||
int extra = size % n; // 前 extra 份多一个
|
||||
|
||||
int idx = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int partSize = base + (i < extra ? 1 : 0);
|
||||
List<T> part = new ArrayList<>(list.subList(idx, idx + partSize));
|
||||
Collections.shuffle(part, RANDOM);
|
||||
parts.addAll(part);
|
||||
idx += partSize;
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package quant.rich.emoney.util;
|
||||
|
||||
import java.time.DateTimeException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -55,4 +56,40 @@ public final class DateUtils {
|
||||
LocalDate date = LocalDate.parse(value, formatter);
|
||||
return date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数字类型日期转换成相应的 LocalDateTime
|
||||
* <p>
|
||||
* <li> 2309191130 → 2023-09-19 11:30:00
|
||||
* <li> 230919 → 2023-09-19
|
||||
*/
|
||||
public static LocalDateTime longDatetimeToLocalDateTime(long dateTime) {
|
||||
if (dateTime <= 0) {
|
||||
throw new IllegalArgumentException("dateTime must be positive, got: " + dateTime);
|
||||
}
|
||||
|
||||
try {
|
||||
if (dateTime > 99_999_999L) {
|
||||
// yyMMddHHmm 例如:2309191130 -> 2023-09-19 11:30
|
||||
int yy = (int) (dateTime / 100_000_000L); // 23
|
||||
int year = 2000 + yy; // 2023
|
||||
int month = (int) ((dateTime % 100_000_000L) / 1_000_000L); // 09
|
||||
int day = (int) ((dateTime % 1_000_000L) / 10_000L); // 19
|
||||
int hour = (int) ((dateTime % 10_000L) / 100L); // 11
|
||||
int minute = (int) (dateTime % 100L); // 30
|
||||
|
||||
return LocalDateTime.of(year, month, day, hour, minute, 0);
|
||||
} else {
|
||||
// yyyyMMdd 例如:20230919 -> 2023-09-19 00:00
|
||||
int year = (int) (dateTime / 10_000L); // 2023
|
||||
int month = (int) ((dateTime % 10_000L) / 100L); // 09
|
||||
int day = (int) (dateTime % 100L); // 19
|
||||
|
||||
return LocalDateTime.of(year, month, day, 0, 0, 0);
|
||||
}
|
||||
} catch (DateTimeException ex) {
|
||||
// 月/日/时/分越界会进这里(比如 month=13)
|
||||
throw new IllegalArgumentException("Invalid dateTime value: " + dateTime, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,7 @@ spring:
|
||||
devtools:
|
||||
restart:
|
||||
enabled: true
|
||||
additional-exclude:
|
||||
- '**/*.html'
|
||||
- '**/*.js'
|
||||
- '**/*.css'
|
||||
additional-exclude: '**/*.html,**/*.js,**/*.css'
|
||||
additional-paths: lib/
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
|
||||
Binary file not shown.
@@ -3,11 +3,14 @@
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||
<mapper namespace="quant.rich.emoney.mapper.postgre.EmoneyIndexMapper">
|
||||
|
||||
<insert id="insertOrUpdate" parameterType="list">
|
||||
<insert id="insertOrUpdateBatch" parameterType="list">
|
||||
<foreach collection="list" item="item" index="idx" separator="">
|
||||
<if test="idx % batchSize == 0">
|
||||
<!-- batch 开头用 INSERT INTO 语句 -->
|
||||
INSERT INTO emoney_index
|
||||
(ts_code, trade_date, "value", index_param, index_name, line_name, line_shape, data_period)
|
||||
VALUES
|
||||
<foreach collection="list" item="item" index="index" separator=",">
|
||||
</if>
|
||||
(#{item.tsCode},
|
||||
#{item.date},
|
||||
#{item.value},
|
||||
@@ -17,15 +20,25 @@
|
||||
#{item.lineShape},
|
||||
#{item.dataPeriod}
|
||||
)
|
||||
</foreach>
|
||||
ON CONFLICT (ts_code, trade_date, data_period, index_name, index_param, line_name ) DO UPDATE SET
|
||||
<choose>
|
||||
<when test="(idx + 1) % batchSize == 0 or (idx + 1) == list.size()">
|
||||
<!-- 每个子 batch 结尾加 -->
|
||||
ON CONFLICT (ts_code, trade_date, data_period, index_name, index_param, line_name )
|
||||
DO UPDATE SET
|
||||
ts_code = EXCLUDED.ts_code,
|
||||
trade_date = EXCLUDED.trade_date,
|
||||
"value" = EXCLUDED."value",
|
||||
index_param = EXCLUDED.index_param,
|
||||
index_name = EXCLUDED.index_name,
|
||||
line_shape = EXCLUDED.line_shape,
|
||||
data_period = EXCLUDED.data_period
|
||||
data_period = EXCLUDED.data_period;
|
||||
</when>
|
||||
<otherwise>
|
||||
<!-- 其余情况属于 batch 内多 VALUES 分割 -->
|
||||
,
|
||||
</otherwise>
|
||||
</choose>
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<select id="getLatestTradeDate" resultType="java.util.Date">
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
namespace="quant.rich.emoney.mapper.postgre.StockStrategyMapper">
|
||||
|
||||
<insert id="insertOrUpdateBatch">
|
||||
<bind name="total" value="0"/>
|
||||
<foreach collection="list" item="item" index="idx" separator=",">
|
||||
<foreach collection="list" item="item" index="idx" separator="">
|
||||
<if test="idx % batchSize == 0">
|
||||
<!-- batch 开头用 INSERT INTO 语句 -->
|
||||
INSERT INTO stock_strategy
|
||||
(goods_id, date, pool_id, pool_name, strategy_id, strategy_name, type)
|
||||
VALUES
|
||||
@@ -20,15 +20,21 @@
|
||||
#{item.strategyId},
|
||||
#{item.strategyName},
|
||||
#{item.type})
|
||||
|
||||
<if test="(idx + 1) % batchSize == 0 or (idx + 1) == list.size()">
|
||||
<choose>
|
||||
<when test="(idx + 1) % batchSize == 0 or (idx + 1) == list.size()">
|
||||
<!-- 每个子 batch 结尾加 -->
|
||||
ON CONFLICT (goods_id, date, pool_id)
|
||||
DO UPDATE SET
|
||||
pool_name = EXCLUDED.pool_name,
|
||||
strategy_id = EXCLUDED.strategy_id,
|
||||
strategy_name = EXCLUDED.strategy_name,
|
||||
type = EXCLUDED.type
|
||||
</if>
|
||||
type = EXCLUDED.type;
|
||||
</when>
|
||||
<otherwise>
|
||||
<!-- 其余情况属于 batch 内多 VALUES 分割 -->
|
||||
,
|
||||
</otherwise>
|
||||
</choose>
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
if (!window.Helper) { window.Helper = {} }
|
||||
window.Helper = {
|
||||
/**
|
||||
* 将 emoney period 转换为可读文字
|
||||
* @param x emoney period, number 形式, 如 1, 5, 15, ..., 10000, ...
|
||||
*/
|
||||
emoneyPeriodToName: x => x < 10000 && `${x} 分钟线` || [null, '日线', '周线', '月线', '季线', '半年线', '年线'][x / 10000],
|
||||
/**
|
||||
* 所有支持的 emoney periods
|
||||
*/
|
||||
allEmoneyPeriods: [1, 5, 15, 30, 60, 120, 10000, 20000, 30000, 40000, 50000, 60000],
|
||||
/**
|
||||
* 显示指标详情弹层
|
||||
*/
|
||||
showIndexDetailLayer: async function(obj, forceRefresh) {
|
||||
// obj: {indexCode: _, indexName: _}
|
||||
var layer = layui.layer;
|
||||
@@ -36,14 +46,14 @@ window.Helper = {
|
||||
}
|
||||
}
|
||||
console.log(res.data);
|
||||
layer.open({
|
||||
top.layui.layer.open({
|
||||
title: obj.indexName + '指标说明',
|
||||
content: html.join(''),
|
||||
skin: 'layui-layer-indexDetail',
|
||||
area: ['520px', '320px'],
|
||||
btn: ['刷新', '确定'],
|
||||
btn1: function(index, _, _) {
|
||||
layer.close(index);
|
||||
top.layui.layer.close(index);
|
||||
Helper.showIndexDetailLayer(obj, !0);
|
||||
},
|
||||
success: function(layero, _) {
|
||||
@@ -61,6 +71,11 @@ window.Helper = {
|
||||
const pattern = new RegExp(`^[${escaped}]+|[${escaped}]+$`, 'g');
|
||||
return str.replace(pattern, '');
|
||||
},
|
||||
/**
|
||||
* 将指定弹层 (layero) 的指定索引的按钮设置为主按钮
|
||||
* @param layero 弹层对象
|
||||
* @param index 指定索引, 以 0 起始。当为负数时, 以 -1 为倒数第一个按钮
|
||||
*/
|
||||
setLayerMainBtn: function(layero, index) {
|
||||
var btns = layero.find('.layui-layer-btn>*'), j = 1;
|
||||
if (index < 0) index = btns.length + index;
|
||||
@@ -73,6 +88,10 @@ window.Helper = {
|
||||
btn.setAttribute('class', filtered.join(' '));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 在右侧展开弹层
|
||||
* @param option 与 layer 一致的选项
|
||||
*/
|
||||
openR: function(option) {
|
||||
const defaultOption = {
|
||||
type: 1, area: '500px',
|
||||
@@ -353,5 +372,165 @@ window.Helper = {
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 从指定根元素(root)下提取表单值
|
||||
* 覆盖:input(含 hidden)、textarea、select(单/多选)、checkbox、radio
|
||||
*
|
||||
* @param {Element|string} root - DOM 元素或选择器
|
||||
* @param {object} [opt]
|
||||
* @param {boolean} [opt.includeDisabled=false] - 是否包含 disabled 控件
|
||||
* @param {"auto"|"string"|"array"} [opt.checkboxMode="auto"]
|
||||
* checkboxMode:
|
||||
* - auto: - 同名多个 => array;
|
||||
* - 单个 => boolean(无 value 时)或 string(有 value 时)
|
||||
* - string: - 永远输出 string(未选中则不输出该字段)
|
||||
* - array: - 永远输出 array(未选中则 [])
|
||||
* - checked: - 单个 checkbox => boolean
|
||||
* - 同名多个 checkbox => boolean[](按 DOM 顺序)
|
||||
* @param {"last"|"array"} [opt.duplicateMode="array"]
|
||||
* duplicateMode:
|
||||
* - array: 同名非 checkbox/radio 也会聚合成 array
|
||||
* - last: 取最后一个
|
||||
* @param {"byName"|"entries"} [opt.output="byName"]
|
||||
* output:
|
||||
* - byName: {name: value}
|
||||
* - entries: [{name, type, value, el}]
|
||||
*
|
||||
* @returns {object|Array}
|
||||
*/
|
||||
getFormValues(root, opt = {}) {
|
||||
|
||||
function cssEscape(s) {
|
||||
if (window.CSS && typeof window.CSS.escape === "function") return window.CSS.escape(s);
|
||||
return String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
const {
|
||||
includeDisabled = false,
|
||||
checkboxMode = "auto", // "auto" | "string" | "array" | "checked"
|
||||
duplicateMode = "array",
|
||||
output = "byName", // "byName" | "entries"
|
||||
} = opt;
|
||||
|
||||
const $root = typeof root === "string" ? document.querySelector(root) : root;
|
||||
if (!$root) throw new Error("root not found");
|
||||
|
||||
const controls = Array.from($root.querySelectorAll("input, textarea, select"));
|
||||
|
||||
const byName = Object.create(null);
|
||||
const entries = [];
|
||||
|
||||
const pushValue = (name, value, meta) => {
|
||||
if (!name) return;
|
||||
|
||||
if (output === "entries") {
|
||||
entries.push({ name, ...meta, value });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(name in byName)) {
|
||||
byName[name] = value;
|
||||
return;
|
||||
}
|
||||
if (duplicateMode === "last") {
|
||||
byName[name] = value;
|
||||
return;
|
||||
}
|
||||
const cur = byName[name];
|
||||
byName[name] = Array.isArray(cur) ? cur.concat([value]) : [cur, value];
|
||||
};
|
||||
|
||||
// 用于 checkboxMode="checked":确保同名组只处理一次
|
||||
const processedCheckboxNames = new Set();
|
||||
|
||||
for (const el of controls) {
|
||||
if (!includeDisabled && el.disabled) continue;
|
||||
|
||||
const tag = el.tagName.toLowerCase();
|
||||
const name = el.name || el.getAttribute("name") || "";
|
||||
|
||||
if (tag === "select") {
|
||||
const type = el.multiple ? "select-multiple" : "select-one";
|
||||
if (el.multiple) {
|
||||
const vals = Array.from(el.selectedOptions).map(o => o.value);
|
||||
pushValue(name, vals, { type, el });
|
||||
} else {
|
||||
pushValue(name, el.value, { type, el });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tag === "textarea") {
|
||||
pushValue(name, el.value, { type: "textarea", el });
|
||||
continue;
|
||||
}
|
||||
|
||||
const inputType = (el.type || "text").toLowerCase();
|
||||
|
||||
if (inputType === "radio") {
|
||||
if (!el.checked) continue;
|
||||
pushValue(name, el.value, { type: "radio", el });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inputType === "checkbox") {
|
||||
// === 新增模式:checked ===
|
||||
if (checkboxMode === "checked") {
|
||||
if (!name) continue; // 没 name 没法聚合;如你要支持无 name,也可以改成 entries 模式收集
|
||||
if (processedCheckboxNames.has(name)) continue;
|
||||
|
||||
const group = Array.from(
|
||||
$root.querySelectorAll(`input[type="checkbox"][name="${cssEscape(name)}"]`)
|
||||
).filter(x => includeDisabled || !x.disabled);
|
||||
|
||||
if (group.length <= 1) {
|
||||
const one = group[0] || el; // 理论上 group[0] 就是 el
|
||||
pushValue(name, !!one.checked, { type: "checkbox", el: one });
|
||||
} else {
|
||||
const states = group.map(x => !!x.checked);
|
||||
pushValue(name, states, { type: "checkbox", el: group[0] });
|
||||
}
|
||||
|
||||
processedCheckboxNames.add(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// === 原有模式逻辑 ===
|
||||
const hasValueAttr = el.hasAttribute("value");
|
||||
const valueAttr = el.value;
|
||||
const checked = el.checked;
|
||||
|
||||
const sameNameCheckboxes = name
|
||||
? $root.querySelectorAll(`input[type="checkbox"][name="${cssEscape(name)}"]`)
|
||||
: [];
|
||||
const isGroup = name && sameNameCheckboxes.length > 1;
|
||||
|
||||
if (checkboxMode === "array" || (checkboxMode === "auto" && isGroup)) {
|
||||
if (checked) pushValue(name, valueAttr, { type: "checkbox", el });
|
||||
else if (checkboxMode === "array" && !(name in byName) && output !== "entries") {
|
||||
byName[name] = [];
|
||||
}
|
||||
} else if (checkboxMode === "string") {
|
||||
if (checked) pushValue(name, valueAttr, { type: "checkbox", el });
|
||||
} else {
|
||||
if (hasValueAttr) {
|
||||
if (checked) pushValue(name, valueAttr, { type: "checkbox", el });
|
||||
} else {
|
||||
pushValue(name, checked, { type: "checkbox", el });
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inputType === "file") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// hidden/text/number/password/date... 都取 value(包含 hidden)
|
||||
pushValue(name, el.value, { type: inputType, el });
|
||||
}
|
||||
|
||||
return output === "entries" ? entries : byName;
|
||||
}
|
||||
}
|
||||
2
src/main/resources/static/admin/v1/static/sortable/Sortable.min.js
vendored
Normal file
2
src/main/resources/static/admin/v1/static/sortable/Sortable.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -179,6 +179,7 @@
|
||||
</script>
|
||||
</div>
|
||||
<th:block th:fragment="head-script">
|
||||
<th:block th:fragment="head-script-ref-only">
|
||||
<script type="text/javascript" src="/admin/v1/static/js/helper.js"></script>
|
||||
<script th:src="@{/admin/v1/static/layui/layui.js}"></script>
|
||||
<script
|
||||
@@ -188,7 +189,7 @@
|
||||
defer></script>
|
||||
<script th:src="@{/admin/v1/static/giggity/toast.js}"></script>
|
||||
<script th:src="@{/admin/v1/static/js/dog.js}"></script>
|
||||
|
||||
</th:block>
|
||||
<script>
|
||||
|
||||
let refreshTimer = null;
|
||||
|
||||
204
src/main/resources/webpage/admin/v1/manage/plan/editCommon.html
Normal file
204
src/main/resources/webpage/admin/v1/manage/plan/editCommon.html
Normal file
@@ -0,0 +1,204 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<th:block th:fragment="commonFormStart">
|
||||
<input type="hidden" id="planJsonString" th:value="${planJsonString}">
|
||||
<style>
|
||||
.layui-form-select dl{
|
||||
max-height: 160px
|
||||
}
|
||||
body {
|
||||
background: transparent
|
||||
}
|
||||
</style>
|
||||
<div class="layui-form" style="margin:0 15px;padding: 10px 0" id="editPlanForm" lay-filter="editPlanForm">
|
||||
<div class="layui-form-item">
|
||||
<input type="hidden" name="planId" th:value="${plan.planId}"/>
|
||||
<label class="layui-form-label">计划名称<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text"
|
||||
th:value="${plan.planName}"
|
||||
lay-verify="required"
|
||||
name="planName"
|
||||
placeholder=""
|
||||
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"
|
||||
th:value="${plan.cronExpr}"
|
||||
lay-verify="required"
|
||||
name="cronExpr"
|
||||
placeholder=""
|
||||
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="radio" name="planType" value="SINGLE_INDEX" lay-filter="planTypeRadio" title="单指标" th:checked="${planType == 'SINGLE_INDEX'}">
|
||||
<input type="radio" name="planType" value="MULTI_INDEX" lay-filter="planTypeRadio" title="多指标" th:checked="${planType == 'MULTI_INDEX'}">
|
||||
<input type="radio" name="planType" value="STOCK_STRATEGY" lay-filter="planTypeRadio" title="个股策略" th:checked="${planType == 'STOCK_STRATEGY'}">
|
||||
<input type="radio" name="planType" value="STOCK_PICKING" lay-filter="planTypeRadio" title="策略选股" th:checked="${planType == 'STOCK_PICKING'}">
|
||||
</div>
|
||||
</div>
|
||||
<div id="planSettings">
|
||||
</th:block>
|
||||
<th:block th:fragment="commonFormEnd">
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">启用<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="enabled" th:attr="checked=${plan.enabled}" th:value="${plan.enabled}" lay-skin="switch" lay-filter="enabled" lay-text="ON|OFF">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">交易日校验<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="openDayCheck" th:attr="checked=${plan.openDayCheck}" th:value="${plan.openDayCheck}" lay-skin="switch" lay-filter="openDayCheck" lay-text="ON|OFF">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:none" class="layui-form-item submit">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit="*" lay-filter="submitPlan">提交</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/html" id="periodsTemplet">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">更新粒度<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
<div id="xmSelectPeriod" style="width: 100%" class="layui-inline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/html" id="paramTemplet">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">{{d.pName}}<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
<input type="number"
|
||||
min="{{d.pMin}}" max="{{d.pMax}}"
|
||||
name="params.{{d.pName}}"
|
||||
lay-verify="required|min:{{d.pMin}}|max:{{d.pMax}}|number"
|
||||
placeholder="范围:{{d.pMin}} ~ {{d.pMax}}"
|
||||
value="{{d.pValue}}" autocomplete="off" class="layui-input"/>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
</th:block>
|
||||
<th:block th:fragment="commonScript">
|
||||
<script>
|
||||
|
||||
layui
|
||||
.extend({
|
||||
xmSelect: '/admin/v1/static/layuiadmin/lib/xm-select',
|
||||
cron: '/admin/v1/static/layuiadmin/lib/cron'
|
||||
}).use(['table', 'form', 'dropdown', 'layer', 'xmSelect', 'cron'], async function (){
|
||||
|
||||
// 渲染计划任务表达式组件
|
||||
layui.cron.render({
|
||||
elem: '[name="cronExpr"]',
|
||||
btns: ['confirm'],
|
||||
cssPath: '/admin/v1/static/css/cron.css'
|
||||
});
|
||||
|
||||
// 绑定 radio 切换更新类型选择
|
||||
layui.form.on('radio(planTypeRadio)', function (obj) {
|
||||
const planType = obj.value;
|
||||
const planId = document.querySelector('[name="planId"]').value;
|
||||
let url = '/admin/v1/manage/plan/edit';
|
||||
if (planType == 'SINGLE_INDEX') {
|
||||
url += 'SingleIndex';
|
||||
}
|
||||
else if (planType == 'MULTI_INDEX') {
|
||||
url += 'MultiIndex';
|
||||
}
|
||||
else if (planType == 'STOCK_STRATEGY') {
|
||||
url += 'StockStrategy'
|
||||
}
|
||||
else {
|
||||
Dog.error({msg: '未知的计划任务类型'});
|
||||
return;
|
||||
}
|
||||
if (planId) {
|
||||
url += '?planId=' + planId;
|
||||
}
|
||||
location.href = url;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
if (window == top) {
|
||||
// 如果不是处在 layer 层中则显示提交按钮
|
||||
document.querySelector('.submit').style.display = '';
|
||||
}
|
||||
// 绑定开关事件修改值
|
||||
const editPlanForm = document.getElementById('editPlanForm');
|
||||
editPlanForm.querySelectorAll('[lay-skin="switch"][lay-filter]').forEach(el => {
|
||||
layui.form.on('switch(' + el.getAttribute('lay-filter') + ')', obj => {
|
||||
obj.elem.value = obj.elem.checked;
|
||||
})
|
||||
});
|
||||
// 获取 configIndOnline
|
||||
async function getConfigIndOnline() {
|
||||
if (window.configIndOnline) {
|
||||
return window.configIndOnline;
|
||||
}
|
||||
const loadConfigLayer = layui.layer.load(2);
|
||||
try {
|
||||
const json = await (await fetch('/admin/v1/manage/indexInfo/getFields?fields=configIndOnline')).json();
|
||||
if (json.ok) {
|
||||
window.configIndOnline = json.data.configIndOnline;
|
||||
return window.configIndOnline;
|
||||
}
|
||||
Dog.error({msg: json.data || '获取指标配置错误'});
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
Dog.error({msg: `获取指标配置错误 ${e.message}`});
|
||||
}
|
||||
finally {
|
||||
layui.layer.close(loadConfigLayer);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function showIndexDetailLayer(el, event) {
|
||||
event.preventDefault();
|
||||
Helper.showIndexDetailLayer({
|
||||
indexCode: el.dataset.indexCode,
|
||||
indexName: el.dataset.indexName
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 保存 data 到 plan 并关闭弹窗、刷新表格
|
||||
*/
|
||||
function savePlan(data) {
|
||||
$.ajax({
|
||||
url: '/admin/v1/manage/plan/save',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(data),
|
||||
success: function (r) {
|
||||
Dog.success({
|
||||
onClose: function () {
|
||||
if (window != top) {
|
||||
top.Dog.reloadTable('plans');
|
||||
if (top.editLayer) {
|
||||
top.layui.layer.close(top.editLayer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
||||
@@ -0,0 +1,520 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- 多指标抓取配置 -->
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:insert="~{admin/v1/include::head}" th:with="title='多指标抓取配置'">
|
||||
</head>
|
||||
<style>
|
||||
/* 表格内的表单样式 */
|
||||
.params .layui-form-label {
|
||||
padding: 9px 0;
|
||||
width: 60px;
|
||||
text-align: left
|
||||
}
|
||||
.params .layui-input-block {
|
||||
margin-left: 60px
|
||||
}
|
||||
.params > .layui-form-item:last-of-type {
|
||||
margin-bottom: 0
|
||||
}
|
||||
#multiIndexTable td {
|
||||
vertical-align: top;
|
||||
}
|
||||
#multiIndexTable {
|
||||
background-color: #fbfbfb;
|
||||
transition: .25s ease-in-out
|
||||
}
|
||||
#multiIndexTable tbody>tr:hover {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
#multiIndexTable tr:not(:last-of-type) {
|
||||
cursor: move;
|
||||
}
|
||||
.indexCodeWrap {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.indexCode {
|
||||
width: 90%;
|
||||
margin-right: 9px;
|
||||
}
|
||||
a.showIndexDetailLayer {
|
||||
vertical-align: middle;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<th:block th:replace="~{admin/v1/include::head-script-ref-only}"></th:block>
|
||||
<th:block th:replace="~{admin/v1/manage/plan/editCommon::commonFormStart}"></th:block>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">更新指标<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
<table id="multiIndexTable" class="layui-table" lay-skin="line">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="270">指标</th>
|
||||
<th>参数</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="periods"></div>
|
||||
<th:block th:replace="~{admin/v1/manage/plan/editCommon::commonFormEnd}"></th:block>
|
||||
<th:block th:replace="~{admin/v1/manage/plan/editCommon::commonScript}"></th:block>
|
||||
<script th:src="@{/admin/v1/static/sortable/Sortable.min.js}"></script>
|
||||
<script>
|
||||
function isLastRow(tr) {
|
||||
return tr && tr.parentElement && tr === tr.parentElement.lastElementChild;
|
||||
}
|
||||
const sortableInstance = new Sortable(
|
||||
document.querySelector('#multiIndexTable tbody'), {
|
||||
animation: 150,
|
||||
draggable: 'tr',
|
||||
handle: 'td',
|
||||
filter: (evt, target) => {
|
||||
// 配合 preventOnFilter
|
||||
// 返回 true 则被 filter 过滤
|
||||
const tr = target.closest('tr');
|
||||
return isLastRow(tr);
|
||||
},
|
||||
preventOnFilter: true,
|
||||
onMove: (evt) => {
|
||||
const dragged = evt.dragged; // 正在拖的 tr
|
||||
const related = evt.related; // 鼠标附近将要插入位置参考的 tr
|
||||
|
||||
// 最后一行不能被拖
|
||||
if (isLastRow(dragged)) return false;
|
||||
|
||||
// 不允许把任何行插到“最后一行”前面或后面(拖到末尾时 related 通常就是最后一行)
|
||||
if (isLastRow(related)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 取 options period 的交集
|
||||
*/
|
||||
function getPeriods(options, commonPeriods) {
|
||||
|
||||
options.forEach(option => {
|
||||
console.debug(option.name, 'period: ', option.indInfo?.supportPeriod)
|
||||
})
|
||||
|
||||
// 取出所有带 supportPeriod 的数组
|
||||
const periodsList = options
|
||||
.map(o => o?.indInfo?.supportPeriod)
|
||||
.filter(Array.isArray);
|
||||
|
||||
// 没有任何 supportPeriod => 返回 commonPeriods(不改原数组)
|
||||
if (periodsList.length === 0) return commonPeriods.slice();
|
||||
|
||||
// 交集:从第一组开始逐步 intersect
|
||||
let inter = new Set(periodsList[0]);
|
||||
|
||||
for (let i = 1; i < periodsList.length; i++) {
|
||||
const cur = new Set(periodsList[i]);
|
||||
inter = new Set([...inter].filter(x => cur.has(x)));
|
||||
if (inter.size === 0) break; // 提前结束
|
||||
}
|
||||
|
||||
// 如果你想保持 commonPeriods 的顺序(且过滤掉不在 commonPeriods 的值)
|
||||
const interSet = inter;
|
||||
return commonPeriods.filter(x => interSet.has(x));
|
||||
}
|
||||
/**
|
||||
* 两个数组是否有交集
|
||||
*/
|
||||
function hasIntersection(a, b) {
|
||||
if (!a?.length || !b?.length) return false;
|
||||
const setB = new Set(b);
|
||||
return a.some(x => setB.has(x));
|
||||
}
|
||||
/**
|
||||
* period 数组转多选选项
|
||||
*/
|
||||
function periodArrayToMultiSelectOptions(periods) {
|
||||
const periodData = [];
|
||||
for (var i = 0; i < periods.length; i++) {
|
||||
const period = periods[i];
|
||||
periodData.push({
|
||||
name: Helper.emoneyPeriodToName(period),
|
||||
value: period
|
||||
})
|
||||
}
|
||||
return periodData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有 indexCode 下拉框的 disable
|
||||
* 由于 on 方法内选择后有所延迟, 获取的 multiSelect 的 value 还没更改过来,
|
||||
* 所以在此若提供了 currentSelect 和 currentOption 则用来代替找寻到的 select
|
||||
* 及其选择值
|
||||
*/
|
||||
function updateIndexCodeDisabled(currentSelect, currentOption) {
|
||||
// 更新除本例以外的下拉框的指标为禁选
|
||||
const allIndexCodeSelects = layui.xmSelect.getWithSelector('.indexCode');
|
||||
const codes =
|
||||
allIndexCodeSelects.length ?
|
||||
allIndexCodeSelects.map(x => x.getValue()[0]).map(x => x?.value):
|
||||
[];
|
||||
// 用 currentValue 替代
|
||||
if (currentSelect != null) {
|
||||
const currentSelectIndex = allIndexCodeSelects.indexOf(currentSelect);
|
||||
if (currentSelectIndex != -1) {
|
||||
codes[currentSelectIndex] = currentOption?.value;
|
||||
}
|
||||
}
|
||||
// 遍历这些 select, 如果有值选中则配置 showIndexDetailLayer
|
||||
codes.forEach((code, i) => {
|
||||
const showIndexLayerDetailEl = allIndexCodeSelects[i].options.dom.nextSibling;
|
||||
if (code !== undefined) {
|
||||
const s = allIndexCodeSelects[i].options.data.find(d => d.value == code);
|
||||
showIndexLayerDetailEl.dataset.indexCode = code;
|
||||
showIndexLayerDetailEl.dataset.indexName = s.indName;
|
||||
showIndexLayerDetailEl.style.display = '';
|
||||
}
|
||||
else {
|
||||
showIndexLayerDetailEl.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// 遍历这些 select, 禁用处于 codes 内的且非本 select 选择的选项
|
||||
allIndexCodeSelects.forEach((select, i) => {
|
||||
const disableds = codes.filter((_, j) => j !== i).filter(v => v !== undefined);
|
||||
select.options.data.forEach(option => {
|
||||
const exists = disableds.indexOf(option.value) !== -1;
|
||||
option.disabled = exists || option.disabledForPeriodConflict;
|
||||
option.disabledForExists = exists;
|
||||
})
|
||||
});
|
||||
updateIndexCodeSelectTips();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 period 下拉框的 disable
|
||||
* 由于 on 方法内选择后有所延迟, 获取的 multiSelect 的 value 还没更改过来,
|
||||
* 所以在此若提供了 currentSelect 和 currentOption 则用来代替找寻到的 select
|
||||
* 及其选择值.
|
||||
* 返回:可选的 period
|
||||
*/
|
||||
function updateAfterPeriodsDisabled(currentSelect, currentOption) {
|
||||
const allIndexCodeSelects = layui.xmSelect.getWithSelector('.indexCode');
|
||||
const options =
|
||||
allIndexCodeSelects.length ?
|
||||
allIndexCodeSelects.map(x => x.getValue()[0]).filter(x => x !== undefined):
|
||||
[];
|
||||
const xmSelectPeriod = layui.xmSelect.get('#xmSelectPeriod', true);
|
||||
// 用 currentValue 替代
|
||||
if (currentSelect != null) {
|
||||
const currentSelectIndex = allIndexCodeSelects.indexOf(currentSelect);
|
||||
if (currentSelectIndex != -1) {
|
||||
options[currentSelectIndex] = currentOption;
|
||||
}
|
||||
}
|
||||
if (!options.length) {
|
||||
// 没有任何选择, 清空 period
|
||||
xmSelectPeriod.update({
|
||||
data: []
|
||||
})
|
||||
return;
|
||||
}
|
||||
const unconflictPeriods = getPeriods(options, Helper.allEmoneyPeriods);
|
||||
console.log(unconflictPeriods);
|
||||
const newData = periodArrayToMultiSelectOptions(unconflictPeriods);
|
||||
const oldData = xmSelectPeriod.options.data;
|
||||
|
||||
// 把 newData 中在 oldData 内选中的也设置成选中
|
||||
newData.filter(x => oldData.find(y => y.value == x.value && y.selected)).forEach(x => x.selected = true)
|
||||
const initPlanEl = document.getElementById('planJsonString');
|
||||
if (!initPlanEl.inited) {
|
||||
initPlanEl.inited = true;
|
||||
const initPlan = JSON.parse(initPlanEl.value || '{}');
|
||||
const initPeriods = initPlan.detail?.periods || [];
|
||||
newData.filter(x => initPeriods.indexOf(x.value) !== -1).forEach(x => x.selected = true);
|
||||
}
|
||||
|
||||
// 更新 periods 选框数据
|
||||
xmSelectPeriod.update({
|
||||
data: newData
|
||||
});
|
||||
|
||||
// 更新所有 indexCode 选框选项, 把 period 冲突的 disable
|
||||
allIndexCodeSelects.forEach(select => {
|
||||
select.options.data.forEach(option => {
|
||||
const conflict = !option.selected && option.indInfo.supportPeriod && !hasIntersection(unconflictPeriods, option.indInfo.supportPeriod)
|
||||
option.disabled = conflict || option.disabledForExists;
|
||||
option.disabledForPeriodConflict = conflict;
|
||||
});
|
||||
});
|
||||
updateIndexCodeSelectTips();
|
||||
return unconflictPeriods;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 indexCode 下拉框禁用提示
|
||||
*/
|
||||
function updateIndexCodeSelectTips() {
|
||||
const allIndexCodeSelects = layui.xmSelect.getWithSelector('.indexCode');
|
||||
const options =
|
||||
allIndexCodeSelects.length ?
|
||||
allIndexCodeSelects.map(x => x.getValue()[0]).filter(x => x !== undefined):
|
||||
[];
|
||||
|
||||
// 更新所有 indexCode 选框选项, 把 period 冲突的 disable
|
||||
allIndexCodeSelects.forEach(select => {
|
||||
|
||||
for (let i = 0; i < select.options.data.length; i++) {
|
||||
const option = select.options.data[i];
|
||||
const title = [];
|
||||
if (option.disabledForPeriodConflict) {
|
||||
title.push('更新粒度冲突');
|
||||
}
|
||||
if (option.disabledForExists) {
|
||||
title.push('已被其他选中');
|
||||
}
|
||||
if (title.length) {
|
||||
select.options.dom.querySelector(`[value="${option.value}"]`).title = `因${title.join("、")}而被禁用`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
layui.use(['xmSelect'], async function() {
|
||||
|
||||
// 为 xmSelect hack 筛选方法
|
||||
layui.xmSelect.getWithSelector = selector => {
|
||||
return layui.xmSelect.get().filter(x => x.options.dom.matches(selector));
|
||||
};
|
||||
|
||||
const initPlan = JSON.parse(document.getElementById('planJsonString')?.value || '{}');
|
||||
const periodsTemplet = document.getElementById('periodsTemplet');
|
||||
const periodsEl = document.getElementById('periods');
|
||||
const paramTemplet = document.getElementById('paramTemplet');
|
||||
periodsEl.insertAdjacentHTML('beforeend', periodsTemplet.textContent);
|
||||
|
||||
// 先渲染表格
|
||||
const initIndexes = initPlan.detail?.indexes || [];
|
||||
|
||||
// 获取指标信息
|
||||
const configIndOnline = await getConfigIndOnline();
|
||||
if (configIndOnline == null) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const indMap = configIndOnline.indMap;
|
||||
addEmptyLine();
|
||||
// 为“增加”按钮绑定点击事件,事件包含行添加删除、其中下拉框渲染逻辑
|
||||
const addBtn = document.querySelector('#multiIndexTable .add');
|
||||
function addEmptyLine() {
|
||||
var element = $([
|
||||
'<tr>',
|
||||
'<td>',
|
||||
'<div class="indexCodeWrap">',
|
||||
'<div class="indexCode"></div>',
|
||||
'<a style="display: none" href="#" class="showIndexDetailLayer" title="查看指标详情" onclick="javascript:showIndexDetailLayer(this, event)"><i class="fa fa-solid fa-circle-question"></i></a>',
|
||||
'</div>',
|
||||
'</td>',
|
||||
'<td class="params"></td>',
|
||||
'</tr>',
|
||||
].join(''));
|
||||
|
||||
|
||||
// 指标多选框数据填充
|
||||
|
||||
const indexCodeEl = element.find('.indexCode')[0];
|
||||
const selectData = [];
|
||||
|
||||
// 先找寻有无其他指标选择框,如果有,要从中 disable
|
||||
const allIndexCodeSelects = layui.xmSelect.getWithSelector('.indexCode');
|
||||
let expectCodes = [];
|
||||
if (allIndexCodeSelects.length) {
|
||||
expectCodes = allIndexCodeSelects.map(x => x.getValue()[0]).filter(x => x).map(x => x.value);
|
||||
}
|
||||
|
||||
// 初始化的数据, 或 undefined
|
||||
const initIndex = initIndexes.shift();
|
||||
|
||||
Object.keys(indMap).forEach(key => {
|
||||
const indInfo = indMap[key], indName = configIndOnline.indIdNameConfig[key];
|
||||
if (indInfo.isCalc) {
|
||||
// 算数型指标
|
||||
return;
|
||||
}
|
||||
if (indInfo.supportStockMethod && indInfo.supportStockMethod.indexOf('isA') == -1) {
|
||||
// 明确非 A 股指标
|
||||
return;
|
||||
}
|
||||
const data = {};
|
||||
if (expectCodes.indexOf(key) != -1) {
|
||||
console.debug(indName, '已被其他下拉框选中, 将禁用');
|
||||
data.disabled = true;
|
||||
data.tips = ['被其他下拉框选中'];
|
||||
}
|
||||
data.name = `${key} ${indName}`;
|
||||
data.value = key;
|
||||
data.indInfo = indInfo;
|
||||
data.indName = indName;
|
||||
data.selected = key == initIndex?.indexCode;
|
||||
selectData.push(data);
|
||||
});
|
||||
|
||||
const indexCodeSelect = layui.xmSelect.render({
|
||||
el: indexCodeEl,
|
||||
radio: true,
|
||||
clickClose: true,
|
||||
filterable: true,
|
||||
model: { label: { type: 'text' } },
|
||||
data: selectData,
|
||||
on: function (data) {
|
||||
console.log(data);
|
||||
const row = indexCodeEl.closest('tr');
|
||||
const rows = document.querySelectorAll('#multiIndexTable tbody>tr');
|
||||
const rowsCount = rows.length;
|
||||
if (!data.arr.length) {
|
||||
if (rowsCount > 1) {
|
||||
row.remove();
|
||||
updateIndexCodeDisabled();
|
||||
updateAfterPeriodsDisabled();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (Array.from(rows).indexOf(row) == rowsCount - 1){
|
||||
addEmptyLine();
|
||||
}
|
||||
|
||||
const paramsEl = row.querySelector('.params');
|
||||
paramsEl.textContent = ''; // 清除
|
||||
|
||||
const value = data.arr[0];
|
||||
const indInfo = value.indInfo;
|
||||
|
||||
updateIndexCodeDisabled(indexCodeSelect, value);
|
||||
const inconflictPeriods = updateAfterPeriodsDisabled(indexCodeSelect, value);
|
||||
|
||||
if (indInfo.indParam) {
|
||||
// 填充 params
|
||||
const tpl = layui.laytpl(paramTemplet.textContent);
|
||||
|
||||
for (let i = 0; i < indInfo.indParam.pName.length; i++) {
|
||||
const d = {
|
||||
pName: indInfo.indParam.pName[i],
|
||||
pMin: indInfo.indParam.min[i],
|
||||
pMax: indInfo.indParam.max[i],
|
||||
pValue: indInfo.indParam.value[i]
|
||||
}
|
||||
paramsEl.insertAdjacentHTML('beforeend', tpl.render(d))
|
||||
}
|
||||
}
|
||||
},
|
||||
done: function () {
|
||||
if (initIndex) {
|
||||
const indInfo = indMap[initIndex.indexCode], indName = configIndOnline.indIdNameConfig[initIndex.indexCode];
|
||||
const row = indexCodeEl.closest('tr');
|
||||
if (indInfo.indParam) {
|
||||
// 填充 params
|
||||
const paramsEl = row.querySelector('.params');
|
||||
const tpl = layui.laytpl(paramTemplet.textContent);
|
||||
for (let i = 0; i < indInfo.indParam.pName.length; i++) {
|
||||
const pName = indInfo.indParam.pName[i];
|
||||
const d = {
|
||||
pName,
|
||||
pMin: indInfo.indParam.min[i],
|
||||
pMax: indInfo.indParam.max[i],
|
||||
pValue: initIndex.params[pName]
|
||||
}
|
||||
paramsEl.insertAdjacentHTML('beforeend', tpl.render(d))
|
||||
}
|
||||
}
|
||||
addEmptyLine();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$('#multiIndexTable tbody')[initIndex ? 'prepend' : 'append'](element);
|
||||
}
|
||||
|
||||
// 最后渲染 periods
|
||||
const initPeriods = initPlan.detail?.periods || [];
|
||||
const periodData = [];
|
||||
initPeriods.forEach(period => {
|
||||
periodData.push({
|
||||
})
|
||||
});
|
||||
layui.xmSelect.render({
|
||||
name: 'periods',
|
||||
el: '#xmSelectPeriod',
|
||||
language: 'zn',
|
||||
data: [],
|
||||
layVerify: 'required',
|
||||
autoRow: !0
|
||||
});
|
||||
updateIndexCodeDisabled();
|
||||
updateAfterPeriodsDisabled();
|
||||
|
||||
// 提交
|
||||
layui.form.on('submit(submitPlan)', function (obj) {
|
||||
// 包装要提交的数据
|
||||
const field = Helper.getFormValues(
|
||||
document.querySelector('.layui-form'),
|
||||
{checkboxMode: 'checked'});
|
||||
|
||||
const data = {
|
||||
detail: {
|
||||
periods: [],
|
||||
indexes: []
|
||||
}
|
||||
};
|
||||
|
||||
function elementIndex(el) {
|
||||
if (!el || !el.parentElement) return -1;
|
||||
return Array.prototype.indexOf.call(el.parentElement.children, el);
|
||||
}
|
||||
|
||||
// 找 index 配置
|
||||
const selects = layui.xmSelect.getWithSelector('.indexCode').filter(x => x.getValue()[0] !== undefined);
|
||||
if (!selects.length) {
|
||||
layer.msg('请选择至少一项更新指标', {
|
||||
icon : 5,
|
||||
shift : 6,
|
||||
time : 2000
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// 根据 select 在表格中的顺序排序
|
||||
selects.sort((a, b) => {
|
||||
return elementIndex(a.options.dom.closest('tr')) - elementIndex(b.options.dom.closest('tr'))
|
||||
});
|
||||
|
||||
// 装载 detail
|
||||
selects.forEach(select => {
|
||||
const row = select.options.dom.closest('tr');
|
||||
const index = {indexCode: select.getValue('valueStr'), params: {}};
|
||||
const paramInputs = row.querySelectorAll('input[name^="params."]');
|
||||
paramInputs.forEach(paramInput => {
|
||||
const name = paramInput.name;
|
||||
index.params[name.substr(7)] = parseInt(paramInput.value);
|
||||
});
|
||||
data.detail.indexes.push(index);
|
||||
});
|
||||
|
||||
//console.log(data);
|
||||
|
||||
Object.keys(field).forEach(key => {
|
||||
if (key.indexOf('params') == 0) {
|
||||
// 动态添加的参数 name="params.XXX", 取 substr(7) 即为 XXX
|
||||
// data.detail.params[key.substr(7)] = field[key]
|
||||
}
|
||||
else if (key == 'periods') {
|
||||
data.detail.periods = field.periods.split(',').map(x => parseInt(x));
|
||||
}
|
||||
else data[key] = field[key]
|
||||
});
|
||||
console.log(data);
|
||||
savePlan(data);
|
||||
})
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,163 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- 单指标抓取配置 -->
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:insert="~{admin/v1/include::head}" th:with="title='单指标抓取配置'">
|
||||
</head>
|
||||
<body>
|
||||
<th:block th:replace="~{admin/v1/include::head-script-ref-only}"></th:block>
|
||||
<th:block th:replace="~{admin/v1/manage/plan/editCommon::commonFormStart}"></th:block>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">更新指标<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
<div class="layui-inline">
|
||||
<select name="indexCode" lay-filter="indexCodeFilter" lay-search>
|
||||
<option value="">选择更新指标</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<a style="display:none" href="#" id="showIndexDetailLayer" title="查看指标详情" onclick="javascript:showIndexDetailLayer(this, event)"><i class="fa fa-solid fa-circle-question"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="params"></div>
|
||||
<div id="periods"></div>
|
||||
<th:block th:replace="~{admin/v1/manage/plan/editCommon::commonFormEnd}"></th:block>
|
||||
<th:block th:replace="~{admin/v1/manage/plan/editCommon::commonScript}"></th:block>
|
||||
<script>
|
||||
|
||||
layui.use(['table', 'form', 'dropdown', 'layer', 'xmSelect', 'cron'], async function() {
|
||||
const initPlanEl = document.getElementById('planJsonString');
|
||||
const initPlan = JSON.parse(initPlanEl?.value || '{}');
|
||||
|
||||
const configIndOnline = await getConfigIndOnline();
|
||||
if (configIndOnline == null) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
// indexCode 单选控件触发事件,仅当当前更新为单指标更新时有用
|
||||
layui.form.on('select(indexCodeFilter)', async function (obj) {
|
||||
|
||||
console.log(obj);
|
||||
const paramsEl = document.getElementById('params'); // 参数选择控件
|
||||
const periodsEl = document.getElementById('periods'); // 时间粒度选择控件
|
||||
paramsEl.textContent = periodsEl.textContent = ''; // 清除参数和时间粒度选择控件
|
||||
const dataset = obj.elem.querySelector(`[value="${obj.value}"]`).dataset;
|
||||
const detailTriggerEl = document.getElementById('showIndexDetailLayer');
|
||||
if (!dataset || !dataset.indInfo) {
|
||||
// 未存在 dataset/.indInfo, 则去除可能存在的指标详情按钮
|
||||
detailTriggerEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
detailTriggerEl.style.display = '';
|
||||
detailTriggerEl.dataset.indexCode = obj.value;
|
||||
detailTriggerEl.dataset.indexName = dataset.indName;
|
||||
const indInfo = JSON.parse(dataset.indInfo);
|
||||
console.log(indInfo);
|
||||
const paramTemplet = document.getElementById('paramTemplet');
|
||||
const periodsTemplet = document.getElementById('periodsTemplet');
|
||||
if (indInfo.indParam) {
|
||||
var tpl = layui.laytpl(paramTemplet.textContent);
|
||||
for (var i = 0; i < indInfo.indParam.pName.length; i++) {
|
||||
var d = {
|
||||
pName: indInfo.indParam.pName[i],
|
||||
pMin: indInfo.indParam.min[i],
|
||||
pMax: indInfo.indParam.max[i],
|
||||
pValue: indInfo.indParam.value[i]
|
||||
}
|
||||
if (obj.params && obj.params[d.pName]) {
|
||||
d.pValue = obj.params[d.pName]
|
||||
}
|
||||
paramsEl.insertAdjacentHTML('beforeend', tpl.render(d));
|
||||
}
|
||||
}
|
||||
let periods = Helper.allEmoneyPeriods;
|
||||
if (indInfo.supportPeriod) periods = indInfo.supportPeriod;
|
||||
periodsEl.insertAdjacentHTML('beforeend', periodsTemplet.textContent);
|
||||
|
||||
const periodData = [];
|
||||
for (var i = 0; i < periods.length; i++) {
|
||||
const period = periods[i];
|
||||
const planPeriods = initPlan?.detail?.periods || [];
|
||||
periodData.push({
|
||||
name: Helper.emoneyPeriodToName(period),
|
||||
value: period,
|
||||
selected: planPeriods.indexOf(period) != -1
|
||||
})
|
||||
}
|
||||
layui.xmSelect.render({
|
||||
name: 'periods',
|
||||
el: '#xmSelectPeriod',
|
||||
language: 'zn',
|
||||
data: periodData,
|
||||
layVerify: 'required',
|
||||
autoRow: !0
|
||||
});
|
||||
layui.form.render()
|
||||
}) // indexCode 单选控件触发事件结束
|
||||
|
||||
// 渲染指标下拉框内容
|
||||
const indexNameSelectEl = document.querySelector('[name="indexCode"]');
|
||||
const indMap = configIndOnline.indMap;
|
||||
let selectedIndexCode = undefined;
|
||||
const optionsFragment = document.createDocumentFragment();
|
||||
Object.keys(indMap).forEach(key => {
|
||||
const indInfo = indMap[key];
|
||||
const indName = configIndOnline.indIdNameConfig[key];
|
||||
if (indInfo.isCalc) {
|
||||
console.debug(indName, '为算数型指标, 忽略');
|
||||
return;
|
||||
}
|
||||
if (indInfo.supportStockMethod && indInfo.supportStockMethod.indexOf('isA') == -1) {
|
||||
console.debug(indName, '提供 supportMethod', indInfo.supportStockMethod.join(', '), '非 A 股指标, 忽略');
|
||||
return;
|
||||
}
|
||||
const option = document.createElement('option');
|
||||
option.value = key;
|
||||
option.textContent = `${key} ${indName}`;
|
||||
if (initPlan?.detail?.indexCode == key) {
|
||||
selectedIndexCode = key;
|
||||
option.selected = true;
|
||||
}
|
||||
option.dataset.indInfo = JSON.stringify(indInfo);
|
||||
option.dataset.indName = indName;
|
||||
|
||||
optionsFragment.appendChild(option);
|
||||
indexNameSelectEl.appendChild(optionsFragment);
|
||||
})
|
||||
if (selectedIndexCode) {
|
||||
// 如果有选择内容, 手动触发指标选择下拉框,并附带参数
|
||||
layui.event.call(this, 'form', 'select(indexCodeFilter)', {
|
||||
elem: indexNameSelectEl,
|
||||
value: selectedIndexCode,
|
||||
params: initPlan?.detail?.params
|
||||
});
|
||||
}
|
||||
layui.form.render();
|
||||
|
||||
// 提交
|
||||
layui.form.on('submit(submitPlan)', function (obj) {
|
||||
// 包装要提交的数据
|
||||
const field = Helper.getFormValues(
|
||||
document.querySelector('.layui-form'),
|
||||
{checkboxMode: 'checked'});
|
||||
const data = {detail: {params:{}, periods: []}}
|
||||
Object.keys(field).forEach(key => {
|
||||
if (key.indexOf('params') == 0) {
|
||||
// 动态添加的参数 name="params.XXX", 取 substr(7) 即为 XXX
|
||||
data.detail.params[key.substr(7)] = field[key]
|
||||
}
|
||||
else if (key == 'periods') {
|
||||
data.detail.periods = field.periods.split(',').map(x => parseInt(x))
|
||||
}
|
||||
else if (key == 'indexCode') {
|
||||
data.detail.indexCode = field.indexCode
|
||||
}
|
||||
else data[key] = field[key]
|
||||
})
|
||||
savePlan(data);
|
||||
})
|
||||
|
||||
}); // layui.use
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- 单指标抓取配置 -->
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:insert="~{admin/v1/include::head}" th:with="title='个股策略抓取配置'">
|
||||
</head>
|
||||
<body>
|
||||
<th:block th:replace="~{admin/v1/include::head-script-ref-only}"></th:block>
|
||||
<th:block th:replace="~{admin/v1/manage/plan/editCommon::commonFormStart}"></th:block>
|
||||
<th:block th:replace="~{admin/v1/manage/plan/editCommon::commonFormEnd}"></th:block>
|
||||
<th:block th:replace="~{admin/v1/manage/plan/editCommon::commonScript}"></th:block>
|
||||
<script>
|
||||
|
||||
layui.use(['table', 'form', 'dropdown', 'layer', 'xmSelect', 'cron'], async function() {
|
||||
|
||||
layui.form.render();
|
||||
// 提交
|
||||
layui.form.on('submit(submitPlan)', function (obj) {
|
||||
// 包装要提交的数据
|
||||
const data = Helper.getFormValues(
|
||||
document.querySelector('.layui-form'),
|
||||
{checkboxMode: 'checked'});
|
||||
savePlan(data);
|
||||
})
|
||||
|
||||
}); // layui.use
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,300 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<div th:fragment="planExtra">
|
||||
<script id="addPlan" 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">
|
||||
<input type="hidden" name="planId"/>
|
||||
<label class="layui-form-label">计划名称<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" lay-verify="required" name="planName" placeholder="" 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="cronExpr" placeholder="" 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">
|
||||
<div class="layui-inline">
|
||||
<select name="indexCode" lay-filter="indexCodeFilter" lay-search>
|
||||
<option value="">选择更新指标</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<a style="display:none" href="#" id="showIndexDetailLayer" onclick="javascript:showIndexDetailLayer(this, event)"><i class="fa fa-solid fa-circle-question"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="params">
|
||||
</div>
|
||||
<div id="periods">
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">启用<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox"
|
||||
name="enabled" lay-skin="switch" lay-filter="enabled" lay-text="ON|OFF">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">交易日校验<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox"
|
||||
name="openDayCheck" lay-skin="switch" lay-filter="openDayCheck" lay-text="ON|OFF">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:none" class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit="*" lay-filter="submitPlan">提交</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/html" id="periodsTemplet">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">更新粒度<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
<div id="period-xm-select" style="width: 100%" class="layui-inline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/html" id="paramTemplet">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">{{d.pName}}<span>*</span></label>
|
||||
<div class="layui-input-block">
|
||||
<input type="number"
|
||||
min="{{d.pMin}}" max="{{d.pMax}}"
|
||||
name="params.{{d.pName}}"
|
||||
lay-verify="required|min:{{d.pMin}}|max:{{d.pMax}}|number"
|
||||
placeholder="范围:{{d.pMin}} ~ {{d.pMax}}"
|
||||
value="{{d.pValue}}" autocomplete="off" class="layui-input"/>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
layui.form.on('submit(submitPlan)', function (obj) {
|
||||
var field = obj.field, data = {params: {}}
|
||||
Object.keys(field).forEach(key => {
|
||||
if (key.indexOf('params') == 0) {
|
||||
data.params[key.substr(7)] = field[key]
|
||||
}
|
||||
else if (key == 'periods') {
|
||||
data.periods = field.periods.split(',')
|
||||
}
|
||||
else {
|
||||
data[key] = field[key]
|
||||
}
|
||||
})
|
||||
$.ajax({
|
||||
url: '/admin/v1/manage/plan/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('plans', {
|
||||
page: {
|
||||
curr: $(".layui-laypage-em").next().html() //当前页码值
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
function showIndexDetailLayer(el, event) {
|
||||
event.preventDefault();
|
||||
Helper.showIndexDetailLayer({
|
||||
indexCode: el.dataset.indexCode,
|
||||
indexName: el.dataset.indexName
|
||||
});
|
||||
}
|
||||
function openEditForm(r) {
|
||||
if (r && r.ok) {
|
||||
window.editLayer = layui.layer.open({
|
||||
type: 1,
|
||||
title: `${r.data.planId ? '编辑' : '新增'}计划任务`,
|
||||
btn: ['提交', '关闭'],
|
||||
yes: function (index, layero) {
|
||||
layero.find('[lay-filter="submitPlan"]').click()
|
||||
},
|
||||
skin: "layui-anim layui-anim-rl layui-layer-adminRight",
|
||||
area: '500px',
|
||||
anim: -1,
|
||||
shadeClose: !0,
|
||||
closeBtn: !1,
|
||||
move: !1,
|
||||
offset: 'r',
|
||||
content: $('#addPlan').html(),
|
||||
success: async function (layero, layerIndex) {
|
||||
var el = $(layero);
|
||||
['planId', 'planName', 'cronExpr', 'indexCode'].forEach(x => {
|
||||
const fieldEl = el[0].querySelector(`[name="${x}"]`);
|
||||
if (!fieldEl) return;
|
||||
fieldEl.value = r.data[x];
|
||||
});
|
||||
['enabled', 'openDayCheck'].forEach(x => {
|
||||
const fieldEl = el[0].querySelector(`[name="${x}"]`);
|
||||
if (!fieldEl) return;
|
||||
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="cronExpr"]',
|
||||
btns: ['confirm'],
|
||||
cssPath: '/admin/v1/static/css/cron.css'
|
||||
});
|
||||
layui.form.on('select(indexCodeFilter)', async function (obj) {
|
||||
const paramsEl = document.getElementById('params'); // 参数选择控件
|
||||
const periodsEl = document.getElementById('periods'); // 时间粒度选择控件
|
||||
paramsEl.textContent = periodsEl.textContent = ''; // 清除参数和时间粒度选择控件
|
||||
const dataset = obj.elem.querySelector(`[value="${obj.value}"]`).dataset;
|
||||
const detailTriggerEl = document.getElementById('showIndexDetailLayer');
|
||||
if (!dataset || !dataset.indInfo) {
|
||||
// 未存在 dataset/.indInfo, 则去除可能存在的指标详情按钮
|
||||
detailTriggerEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
detailTriggerEl.style.display = '';
|
||||
detailTriggerEl.dataset.indexCode = obj.value;
|
||||
detailTriggerEl.dataset.indexName = dataset.indName;
|
||||
const indInfo =
|
||||
JSON.parse(dataset.indInfo);
|
||||
const paramTemplet = document.getElementById('paramTemplet');
|
||||
const periodsTemplet = document.getElementById('periodsTemplet');
|
||||
if (indInfo.indParam) {
|
||||
var tpl = layui.laytpl(paramTemplet.textContent);
|
||||
for (var i = 0; i < indInfo.indParam.pName.length; i++) {
|
||||
var d = {
|
||||
pName: indInfo.indParam.pName[i],
|
||||
pMin: indInfo.indParam.min[i],
|
||||
pMax: indInfo.indParam.max[i],
|
||||
pValue: indInfo.indParam.value[i]
|
||||
}
|
||||
if (obj.params && obj.params[d.pName]) {
|
||||
d.pValue = obj.params[d.pName]
|
||||
}
|
||||
paramsEl.insertAdjacentHTML('beforeend', tpl.render(d));
|
||||
}
|
||||
}
|
||||
var periods = Helper.allEmoneyPeriods;
|
||||
if (indInfo.supportPeriod) periods = indInfo.supportPeriod;
|
||||
periodsEl.insertAdjacentHTML('beforeend', periodsTemplet.textContent);
|
||||
|
||||
var data = [];
|
||||
for (var i = 0; i < periods.length; i++) {
|
||||
var period = periods[i];
|
||||
data.push({
|
||||
name: Helper.emoneyPeriodToName(period),
|
||||
value: period,
|
||||
selected: r.data.periods.indexOf(period + '') != -1
|
||||
})
|
||||
}
|
||||
layui.xmSelect.render({
|
||||
name: 'periods',
|
||||
el: '#period-xm-select',
|
||||
language: 'zn',
|
||||
data: data,
|
||||
autoRow: !0
|
||||
})
|
||||
layui.form.render()
|
||||
})
|
||||
|
||||
const loadConfigLayer = layui.layer.load(2);
|
||||
const json = await (await fetch('/admin/v1/manage/indexInfo/getFields?fields=configIndOnline')).json();
|
||||
|
||||
if (json.ok) {
|
||||
const indexNameSelectEl = document.querySelector('[name="indexCode"]');
|
||||
const jo = json.data.configIndOnline, indMap = jo.indMap;
|
||||
let selected = undefined;
|
||||
const optionsFragment = document.createDocumentFragment();
|
||||
Object.keys(indMap).forEach(key => {
|
||||
const indInfo = indMap[key];
|
||||
if (indInfo.isCalc) return;
|
||||
if (r.data.indexCode == key) selected = key;
|
||||
const option = document.createElement('option');
|
||||
option.value = key;
|
||||
option.textContent = `${key} ${jo.indIdNameConfig[key]}`;
|
||||
option.selected = r.data.indexCode == key;
|
||||
option.dataset.indInfo = JSON.stringify(indInfo);
|
||||
option.dataset.indName = jo.indIdNameConfig[key];
|
||||
|
||||
optionsFragment.appendChild(option);
|
||||
})
|
||||
indexNameSelectEl.appendChild(optionsFragment);
|
||||
if (selected)
|
||||
// 如果有选择内容, 手动触发指标选择下拉框,并附带参数
|
||||
layui.event.call(this, 'form', 'select(indexCodeFilter)', {
|
||||
elem: indexNameSelectEl,
|
||||
value: selected,
|
||||
params: r.data.params
|
||||
});
|
||||
layui.form.render();
|
||||
}
|
||||
else {
|
||||
Dog.error({
|
||||
msg: json.data || '获取指标配置错误'
|
||||
});
|
||||
layui.layer.close(window.editLayer);
|
||||
}
|
||||
layui.form.render();
|
||||
layui.layer.close(loadConfigLayer);
|
||||
}
|
||||
})
|
||||
}
|
||||
else Dog.error({
|
||||
msg: r && r.data || '服务器错误'
|
||||
});
|
||||
}
|
||||
async function openNewForm(planId) {
|
||||
const json = await (await fetch((() => {
|
||||
const url = '/admin/v1/manage/plan/getOne';
|
||||
if (planId) return url + '?planId=' + planId;
|
||||
return url
|
||||
})())).json();
|
||||
if (json.ok) {
|
||||
openEditForm(json)
|
||||
}
|
||||
else {
|
||||
Dog.error()
|
||||
}
|
||||
}
|
||||
layui.table.on('tool(plans)', async function (obj) {
|
||||
if (obj.event == 'edit') {
|
||||
openNewForm(obj.data.planId)
|
||||
}
|
||||
else if (obj.event == 'del') {
|
||||
layui.layer.confirm('确定删除该计划任务吗?', function (index) {
|
||||
layui.layer.close(index);
|
||||
const load = layui.layer.load(2);
|
||||
$.ajax({
|
||||
url: '/admin/v1/manage/plan/delete',
|
||||
method: 'POST',
|
||||
data: {planId: obj.data.planId},
|
||||
success: () => Dog.success({
|
||||
msg: '删除成功', onClose: () => Dog.reloadTable('plans')
|
||||
}),
|
||||
error: res => Dog.error({msg: res}),
|
||||
complete: () => layui.layer.close(load)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
|
||||
</html>
|
||||
@@ -11,9 +11,8 @@
|
||||
<div>
|
||||
<h1 class="manage-title">
|
||||
<i class="fa-fw fa-regular fa-calendar"></i>
|
||||
<b>计划任务</b><a href="javascript:openNewForm()" class="operate">新增</a>
|
||||
<b>计划任务</b><a href="javascript:openEditForm()" class="operate">新增</a>
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
<button class="layui-btn layui-btn-sm operdown">
|
||||
<span>选中项<i
|
||||
@@ -50,6 +49,15 @@
|
||||
{field:'openDayCheck', title: '交易日校验', width: 95, switchTemplet: true},
|
||||
{field:'planId', hide: true, width: 60, title: 'ID'},
|
||||
{field:'planName', title: '计划名称'},
|
||||
{field:'planType', title: '计划类型', templet: d => {
|
||||
switch(d.planType) {
|
||||
case 'SINGLE_INDEX': return '单指标';
|
||||
case 'MULTI_INDEX': return '多指标';
|
||||
case 'STOCK_STRATEGY': return '个股策略';
|
||||
case 'STOCK_PICKING': return '策略选股';
|
||||
}
|
||||
return '';
|
||||
}},
|
||||
{field:'cronExpr', title: '计划表达式'},
|
||||
{field:'indexCode', title: '指标代码'},
|
||||
{field:'params', title: '请求参数', templet: function(d) {
|
||||
@@ -79,9 +87,43 @@
|
||||
tableFilter: 'plans',
|
||||
idName: 'planId'
|
||||
});
|
||||
layui.table.on('tool(plans)', async function (obj) {
|
||||
if (obj.event == 'edit') {
|
||||
openEditForm(obj.data.planId)
|
||||
}
|
||||
else if (obj.event == 'del') {
|
||||
layui.layer.confirm('确定删除该计划任务吗?', function (index) {
|
||||
layui.layer.close(index);
|
||||
const load = layui.layer.load(2);
|
||||
$.ajax({
|
||||
url: '/admin/v1/manage/plan/delete',
|
||||
method: 'POST',
|
||||
data: {planId: obj.data.planId},
|
||||
success: () => Dog.success({
|
||||
msg: '删除成功', onClose: () => Dog.reloadTable('plans')
|
||||
}),
|
||||
error: res => Dog.error({msg: res}),
|
||||
complete: () => layui.layer.close(load)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}); // layui.use
|
||||
function openEditForm(planId) {
|
||||
window.editLayer = Helper.openR({
|
||||
type: 2,
|
||||
title: '编辑计划任务',
|
||||
btn: ['提交', '关闭'],
|
||||
yes: function (index, layero) {
|
||||
const iframe = document.querySelector('iframe');
|
||||
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
doc.querySelector('[lay-filter="submitPlan"]').click();
|
||||
},
|
||||
area: '40%',
|
||||
content: '/admin/v1/manage/plan/edit' + (planId !== undefined ? '?planId=' + planId : '')
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<th:block th:replace="~{admin/v1/manage/plan/include::planExtra}"></th:block>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -25,7 +25,7 @@ class EmoneyAutoApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
EmoneyClient.relogin();
|
||||
EmoneyClient.reloginCheck();
|
||||
|
||||
CandleStickWithIndex_Request request = new CandleStickWithIndex_Request();
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ public class EmoneyIndexScraper {
|
||||
@Test
|
||||
void test() {
|
||||
|
||||
EmoneyClient.relogin();
|
||||
EmoneyClient.reloginCheck();
|
||||
|
||||
String url = buildUrl(10002800);
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class EmoneyStrategyMarkTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
EmoneyClient.relogin();
|
||||
EmoneyClient.reloginCheck();
|
||||
StrategyMark_Request request = new StrategyMark_Request();
|
||||
|
||||
request.setGoodsId(600325);
|
||||
|
||||
48
src/test/java/quant/rich/emoney/StockStrategyUpdateTest.java
Normal file
48
src/test/java/quant/rich/emoney/StockStrategyUpdateTest.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package quant.rich.emoney;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
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.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.service.postgre.StockStrategyService;
|
||||
|
||||
/**
|
||||
* 测试从本地读取包含 stock strategy 的 json, 转换为列表并更新到数据库中
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ContextConfiguration(classes = EmoneyAutoApplication.class)
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@Slf4j
|
||||
public class StockStrategyUpdateTest {
|
||||
|
||||
@Autowired
|
||||
ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
StockStrategyService stockStrategyService;
|
||||
|
||||
@Test
|
||||
void test() throws IOException, InterruptedException, ExecutionException {
|
||||
|
||||
// 首先从 tushare-data-service feign 客户端取数据
|
||||
|
||||
|
||||
String str = Files.readString(Path.of("C:\\Users\\Administrator\\Desktop\\stock_strategy.json"));
|
||||
JsonNode node = objectMapper.readTree(str);
|
||||
Future<Boolean> future = stockStrategyService.updateByQueryResponseAsync(node);
|
||||
future.get();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user