Commit
This commit is contained in:
6
pom.xml
6
pom.xml
@@ -90,6 +90,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-devtools</artifactId>
|
<artifactId>spring-boot-devtools</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -255,6 +256,11 @@
|
|||||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.yulichang</groupId>
|
||||||
|
<artifactId>mybatis-plus-join-boot-starter</artifactId>
|
||||||
|
<version>1.4.11</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
package link.at17.mid.tushare;
|
package link.at17.mid.tushare;
|
||||||
|
|
||||||
import org.mybatis.spring.annotation.MapperScan;
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.cache.annotation.EnableCaching;
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
import org.springframework.scheduling.annotation.EnableAsync;
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
import link.at17.mid.tushare.task.scheduler.DailyUpdateDataScheduler;
|
|
||||||
|
|
||||||
@EnableAsync
|
@EnableAsync
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@@ -17,9 +14,6 @@ import link.at17.mid.tushare.task.scheduler.DailyUpdateDataScheduler;
|
|||||||
@EnableCaching(proxyTargetClass=true)
|
@EnableCaching(proxyTargetClass=true)
|
||||||
public class TushareDataServiceApplication {
|
public class TushareDataServiceApplication {
|
||||||
|
|
||||||
@Autowired
|
|
||||||
DailyUpdateDataScheduler dailyUpdateDataScheduler;
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(TushareDataServiceApplication.class, args);
|
SpringApplication.run(TushareDataServiceApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package link.at17.mid.tushare.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 升级参数
|
||||||
|
*/
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface UpdateMethod {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 升级方法名称
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
String name();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载到内存中的排序
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
int order() default -1;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import link.at17.mid.tushare.dao.StockCalendarDao;
|
import link.at17.mid.tushare.data.service.StockCalendarService;
|
||||||
import link.at17.mid.tushare.enums.StockMarket;
|
import link.at17.mid.tushare.enums.StockMarket;
|
||||||
import link.at17.mid.tushare.system.util.LocalDateTimeUtils;
|
import link.at17.mid.tushare.system.util.LocalDateTimeUtils;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -18,18 +18,18 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
public class StockCalendarController {
|
public class StockCalendarController {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
StockCalendarDao stockCalendarDao;
|
StockCalendarService stockCalendarService;
|
||||||
|
|
||||||
@GetMapping("todayIsOpen")
|
@GetMapping("todayIsOpen")
|
||||||
private String todayIsOpen(@Param("stockMarket") StockMarket stockMarket) {
|
private String todayIsOpen(@Param("stockMarket") StockMarket stockMarket) {
|
||||||
return String.valueOf(stockCalendarDao.isOpen(LocalDateTime.now(), stockMarket));
|
return String.valueOf(stockCalendarService.isOpen(LocalDateTime.now(), stockMarket));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("isOpen")
|
@GetMapping("isOpen")
|
||||||
private String isOpen(@Param("stockMarket") StockMarket stockMarket, @Param("date") String date) {
|
private String isOpen(@Param("stockMarket") StockMarket stockMarket, @Param("date") String date) {
|
||||||
LocalDateTime dateTime = LocalDateTimeUtils.parseDate(date);
|
LocalDateTime dateTime = LocalDateTimeUtils.parseDate(date);
|
||||||
|
|
||||||
return String.valueOf(stockCalendarDao.isOpen(dateTime, stockMarket));
|
return String.valueOf(stockCalendarService.isOpen(dateTime, stockMarket));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package link.at17.mid.tushare.component;
|
||||||
|
|
||||||
|
import java.net.Proxy;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.system.config.SystemConfig;
|
||||||
|
import link.at17.mid.tushare.system.util.SpringContextHolder;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OkHttpClient 提供器
|
||||||
|
* @see link.at17.mid.tushare.system.config.SystemConfig
|
||||||
|
* @see okhttp3.internal.http.BridgeInterceptor
|
||||||
|
*/
|
||||||
|
public class OkHttpClientProvider {
|
||||||
|
|
||||||
|
private static volatile SystemConfig systemConfig;
|
||||||
|
|
||||||
|
private static SystemConfig getSystemConfig() {
|
||||||
|
if (systemConfig == null) {
|
||||||
|
synchronized (OkHttpClientProvider.class) {
|
||||||
|
if (systemConfig == null) {
|
||||||
|
systemConfig = SpringContextHolder.getBean(SystemConfig.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return systemConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 SystemConfig 获取一个 OkHttpClient 实例
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static OkHttpClient getInstance() {
|
||||||
|
return getInstance(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 SystemConfig 获取一个 OkHttpClient 实例
|
||||||
|
* @param builderConsumer 可根据该 consumer 自定义 builder 其他参数,注意 proxy、https 校验等最终仍会根据 systemConfig 情况覆盖
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static OkHttpClient getInstance(Consumer<OkHttpClient.Builder> builderConsumer) {
|
||||||
|
SystemConfig systemConfig = getSystemConfig();
|
||||||
|
return getInstance(
|
||||||
|
systemConfig.getProxy(),
|
||||||
|
systemConfig.getIgnoreHttpsVerification(),
|
||||||
|
builderConsumer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据指定代理和是否忽略 https 证书获取一个 OkHttpClient 实例
|
||||||
|
* @param proxy 指定代理
|
||||||
|
* @param ignoreHttpsVerification 是否忽略 https 证书
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static OkHttpClient getInstance(Proxy proxy, boolean ignoreHttpsVerification) {
|
||||||
|
return getInstance(proxy, ignoreHttpsVerification, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据指定代理、是否忽略 https 证书和额外 builder 设置获取一个 OkHttpClient 实例
|
||||||
|
* @param proxy 指定代理
|
||||||
|
* @param ignoreHttpsVerification 是否忽略 https 证书
|
||||||
|
* @param builderConsumer 可根据该 consumer 自定义 builder 其他参数,注意 proxy、https 校验等最终仍会根据其他参数覆盖
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static OkHttpClient getInstance(
|
||||||
|
Proxy proxy,
|
||||||
|
boolean ignoreHttpsVerification,
|
||||||
|
Consumer<OkHttpClient.Builder> builderConsumer) {
|
||||||
|
OkHttpClient.Builder builder = new OkHttpClient.Builder();
|
||||||
|
if (builderConsumer != null) {
|
||||||
|
builderConsumer.accept(builder);
|
||||||
|
}
|
||||||
|
builder.proxy(proxy);
|
||||||
|
if (ignoreHttpsVerification) {
|
||||||
|
builder
|
||||||
|
.sslSocketFactory(getSSLSocketFactory(), getX509TrustManager())
|
||||||
|
.hostnameVerifier(getHostnameVerifier());
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HostnameVerifier getHostnameVerifier() {
|
||||||
|
HostnameVerifier hostnameVerifier = new HostnameVerifier() {
|
||||||
|
@Override
|
||||||
|
public boolean verify(String s, SSLSession sslSession) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return hostnameVerifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SSLSocketFactory getSSLSocketFactory() {
|
||||||
|
try {
|
||||||
|
SSLContext sslContext = SSLContext.getInstance("SSL");
|
||||||
|
sslContext.init(null, getTrustManager(), new SecureRandom());
|
||||||
|
return sslContext.getSocketFactory();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static X509TrustManager getX509TrustManager() {
|
||||||
|
X509TrustManager trustManager = null;
|
||||||
|
try {
|
||||||
|
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||||
|
trustManagerFactory.init((KeyStore) null);
|
||||||
|
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
|
||||||
|
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
|
||||||
|
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
|
||||||
|
}
|
||||||
|
trustManager = (X509TrustManager) trustManagers[0];
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return trustManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TrustManager[] getTrustManager() {
|
||||||
|
TrustManager[] trustAllCerts = new TrustManager[]{
|
||||||
|
new X509TrustManager() {
|
||||||
|
@Override
|
||||||
|
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X509Certificate[] getAcceptedIssuers() {
|
||||||
|
return new X509Certificate[]{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return trustAllCerts;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package link.at17.mid.tushare.component;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.reflections.Reflections;
|
import org.reflections.Reflections;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -13,15 +12,6 @@ import org.springframework.web.method.HandlerMethod;
|
|||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import link.at17.mid.tushare.annotation.StaticAttribute;
|
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,17 +24,6 @@ public class PlatformInterceptor implements HandlerInterceptor {
|
|||||||
@Autowired
|
@Autowired
|
||||||
Reflections reflections;
|
Reflections reflections;
|
||||||
|
|
||||||
/**
|
|
||||||
* 静态注入的类缓存
|
|
||||||
*/
|
|
||||||
Map<String, Class<?>> staticAttributeClassCache;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 静态注入的枚举字段缓存
|
|
||||||
*/
|
|
||||||
Map<String, Map<String, Enum<?>>> options = null;
|
|
||||||
Map<String, String> optionNameMap = new HashMap<>();
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||||
|
|
||||||
@@ -75,85 +54,6 @@ public class PlatformInterceptor implements HandlerInterceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
request.setAttribute("request", request); // 把 request 本身放到 attribute 里去,供前端部分位置用
|
request.setAttribute("request", request); // 把 request 本身放到 attribute 里去,供前端部分位置用
|
||||||
|
|
||||||
// 查找添加了 @StaticAttribute 注解的类,将其中的枚举注入前端供使用
|
|
||||||
if (staticAttributeClassCache == null) {
|
|
||||||
log.debug("init static attribute classes.");
|
|
||||||
staticAttributeClassCache = new HashMap<>();
|
|
||||||
|
|
||||||
|
|
||||||
Set<Class<?>> staticClasses = reflections.getTypesAnnotatedWith(StaticAttribute.class);
|
|
||||||
for (Class<?> staticClass : staticClasses) {
|
|
||||||
StaticAttribute sa = staticClass.getAnnotation(StaticAttribute.class);
|
|
||||||
String name = staticClass.getSimpleName();
|
|
||||||
if (StringUtils.isNotBlank(sa.value())) {
|
|
||||||
name = sa.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (staticAttributeClassCache.containsKey(name)) {
|
|
||||||
// 缓存中已经存在了这个名字,直接忽略
|
|
||||||
log.warn("StaticAttribute annotation name {} for class {} has been taken, ignore.",
|
|
||||||
name, staticClass.getName());
|
|
||||||
}
|
|
||||||
staticAttributeClassCache.put(name, staticClass);
|
|
||||||
log.debug("{} injected as name {}", staticClass, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (staticAttributeClassCache.isEmpty()) {
|
|
||||||
// 跑完一次后,缓存仍然为空,可能不正常,提示一下
|
|
||||||
log.warn("StaticAttributes' staticAttributeClassCache is empty, it's unusual, if you didn't exclude it manually, please check.");
|
|
||||||
}
|
|
||||||
Iterator<Map.Entry<String, Class<?>>> iterator = staticAttributeClassCache.entrySet().iterator();
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
Map.Entry<String, Class<?>> entry = iterator.next();
|
|
||||||
request.setAttribute(entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options == null) {
|
|
||||||
options = new HashMap<>();
|
|
||||||
Set<Field> enumOptionFields = reflections.getFieldsAnnotatedWith(StaticAttribute.class);
|
|
||||||
for (Field f : enumOptionFields) {
|
|
||||||
if (!f.isAnnotationPresent(StaticAttribute.class)) continue;
|
|
||||||
if (!Enum.class.isAssignableFrom(f.getType())) continue;
|
|
||||||
|
|
||||||
// 拿到注解和名前缀
|
|
||||||
StaticAttribute anno = f.getAnnotation(StaticAttribute.class);
|
|
||||||
String prefix = anno.value().isBlank() ?
|
|
||||||
f.getName() + "Enum" :
|
|
||||||
anno.value();
|
|
||||||
|
|
||||||
prefix = prefix.toUpperCase().charAt(0) + prefix.substring(1);
|
|
||||||
|
|
||||||
if (optionNameMap.containsKey(prefix)) {
|
|
||||||
log.warn("EnumOption name {}:{} has already been taken by {}, please check",
|
|
||||||
prefix, f.getType().getName(), optionNameMap.get(prefix));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
optionNameMap.put(prefix, f.getType().getName());
|
|
||||||
|
|
||||||
// enum 值列表
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Class<? extends Enum<?>> enumType = (Class<? extends Enum<?>>) f.getType();
|
|
||||||
Enum<?>[] constants = enumType.getEnumConstants();
|
|
||||||
|
|
||||||
// 构造 Map<name, constant>
|
|
||||||
Map<String, Enum<?>> optionsMap = new LinkedHashMap<>();
|
|
||||||
for (Enum<?> c : constants) {
|
|
||||||
optionsMap.put(c.name(), c);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 放到 Model 里
|
|
||||||
options.put(prefix, optionsMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
for (Entry<String, Map<String, Enum<?>>> entry : options.entrySet()) {
|
|
||||||
request.setAttribute(entry.getKey(), entry.getValue());
|
|
||||||
log.debug("Inject enums {}: {} to request {}",
|
|
||||||
entry.getKey(), optionNameMap.get(entry.getKey()),
|
|
||||||
request.getRequestURI());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package link.at17.mid.tushare.component;
|
||||||
|
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.reflections.Reflections;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.method.HandlerMethod;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import link.at17.mid.tushare.annotation.StaticAttribute;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注入拦截器
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class StaticAttributeInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
Reflections reflections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 静态注入的类缓存
|
||||||
|
*/
|
||||||
|
Map<String, Class<?>> staticAttributeClassCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 静态注入的枚举字段缓存
|
||||||
|
*/
|
||||||
|
Map<String, Map<String, Enum<?>>> options = null;
|
||||||
|
Map<String, String> optionNameMap = new HashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||||
|
injectNecessary(request, handler);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为前端模板注入变量
|
||||||
|
* @param request
|
||||||
|
*/
|
||||||
|
public void injectNecessary(HttpServletRequest request, Object handler) {
|
||||||
|
|
||||||
|
// 只在 Controller(HandlerMethod)里才做注入
|
||||||
|
if (!(handler instanceof HandlerMethod)) {
|
||||||
|
// 静态资源、图片、css、js 都会被 ResourceHttpRequestHandler 处理,
|
||||||
|
// 这里一律跳过
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排除 @ResponseBody/json 接口
|
||||||
|
HandlerMethod hm = (HandlerMethod) handler;
|
||||||
|
if (hm.hasMethodAnnotation(ResponseBody.class)
|
||||||
|
|| hm.getBeanType().isAnnotationPresent(RestController.class)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找添加了 @StaticAttribute 注解的类,将其中的枚举注入前端供使用
|
||||||
|
if (staticAttributeClassCache == null) {
|
||||||
|
log.debug("init static attribute classes.");
|
||||||
|
staticAttributeClassCache = new HashMap<>();
|
||||||
|
|
||||||
|
|
||||||
|
Set<Class<?>> staticClasses = reflections.getTypesAnnotatedWith(StaticAttribute.class);
|
||||||
|
for (Class<?> staticClass : staticClasses) {
|
||||||
|
StaticAttribute sa = staticClass.getAnnotation(StaticAttribute.class);
|
||||||
|
String name = staticClass.getSimpleName();
|
||||||
|
if (StringUtils.isNotBlank(sa.value())) {
|
||||||
|
name = sa.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (staticAttributeClassCache.containsKey(name)) {
|
||||||
|
// 缓存中已经存在了这个名字,直接忽略
|
||||||
|
log.warn("StaticAttribute 注解的类 {} 指定的名称 {} 已经存在, 旧值会被覆盖",
|
||||||
|
staticClass.getName(),
|
||||||
|
name);
|
||||||
|
}
|
||||||
|
staticAttributeClassCache.put(name, staticClass);
|
||||||
|
log.debug("{} 类以 {} 为名称注入", staticClass, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (staticAttributeClassCache.isEmpty()) {
|
||||||
|
// 跑完一次后,缓存仍然为空,可能不正常,提示一下
|
||||||
|
log.warn("初始化后,StaticAttribute 缓存为空,这仅在无任何类被 @StaticAttribute 注解的情况下出现。如果这与您的期望不符,请检查");
|
||||||
|
}
|
||||||
|
Iterator<Map.Entry<String, Class<?>>> iterator = staticAttributeClassCache.entrySet().iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Map.Entry<String, Class<?>> entry = iterator.next();
|
||||||
|
request.setAttribute(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options == null) {
|
||||||
|
options = new HashMap<>();
|
||||||
|
Set<Field> enumOptionFields = reflections.getFieldsAnnotatedWith(StaticAttribute.class);
|
||||||
|
for (Field f : enumOptionFields) {
|
||||||
|
if (!f.isAnnotationPresent(StaticAttribute.class)) continue;
|
||||||
|
if (!Enum.class.isAssignableFrom(f.getType())) continue;
|
||||||
|
|
||||||
|
// 拿到注解和名前缀
|
||||||
|
StaticAttribute anno = f.getAnnotation(StaticAttribute.class);
|
||||||
|
String prefix = anno.value().isBlank() ?
|
||||||
|
f.getName() + "Enum" :
|
||||||
|
anno.value();
|
||||||
|
|
||||||
|
prefix = prefix.toUpperCase().charAt(0) + prefix.substring(1);
|
||||||
|
|
||||||
|
if (optionNameMap.containsKey(prefix)) {
|
||||||
|
log.warn("EnumOption name {}:{} has already been taken by {}, please check",
|
||||||
|
prefix, f.getType().getName(), optionNameMap.get(prefix));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
optionNameMap.put(prefix, f.getType().getName());
|
||||||
|
|
||||||
|
// enum 值列表
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Class<? extends Enum<?>> enumType = (Class<? extends Enum<?>>) f.getType();
|
||||||
|
Enum<?>[] constants = enumType.getEnumConstants();
|
||||||
|
|
||||||
|
// 构造 Map<name, constant>
|
||||||
|
Map<String, Enum<?>> optionsMap = new LinkedHashMap<>();
|
||||||
|
for (Enum<?> c : constants) {
|
||||||
|
optionsMap.put(c.name(), c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 放到 Model 里
|
||||||
|
options.put(prefix, optionsMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
for (Entry<String, Map<String, Enum<?>>> entry : options.entrySet()) {
|
||||||
|
request.setAttribute(entry.getKey(), entry.getValue());
|
||||||
|
log.debug("Inject enums {}: {} to request {}",
|
||||||
|
entry.getKey(), optionNameMap.get(entry.getKey()),
|
||||||
|
request.getRequestURI());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package link.at17.mid.tushare.component;
|
||||||
|
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.reflections.Reflections;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.method.HandlerMethod;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import link.at17.mid.tushare.annotation.StaticAttribute;
|
||||||
|
import link.at17.mid.tushare.data.models.UpdateMethodInfo;
|
||||||
|
import link.at17.mid.tushare.service.UpdateMethodService;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新注入拦截器
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class UpdateMethodInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
Reflections reflections;
|
||||||
|
|
||||||
|
Map<String, Class<?>> optionArgCache;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
UpdateMethodService updateDataService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||||
|
injectNecessary(request, handler);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为前端模板注入变量
|
||||||
|
* @param request
|
||||||
|
*/
|
||||||
|
public void injectNecessary(HttpServletRequest request, Object handler) {
|
||||||
|
|
||||||
|
// 只在 Controller(HandlerMethod)里才做注入
|
||||||
|
if (!(handler instanceof HandlerMethod)) {
|
||||||
|
// 静态资源、图片、css、js 都会被 ResourceHttpRequestHandler 处理,
|
||||||
|
// 这里一律跳过
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排除 @ResponseBody/json 接口
|
||||||
|
HandlerMethod hm = (HandlerMethod) handler;
|
||||||
|
if (hm.hasMethodAnnotation(ResponseBody.class)
|
||||||
|
|| hm.getBeanType().isAnnotationPresent(RestController.class)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (optionArgCache == null) {
|
||||||
|
|
||||||
|
optionArgCache = new HashMap<>();
|
||||||
|
|
||||||
|
List<UpdateMethodInfo> potentialUpdateMethods = updateDataService.getPotentialUpdateMethodInfos();
|
||||||
|
for (UpdateMethodInfo info : potentialUpdateMethods) {
|
||||||
|
for (UpdateMethodInfo.UpdateParamInfo paramInfo : info.getParams()) {
|
||||||
|
Class<?> typeClass = paramInfo.getTypeClass();
|
||||||
|
if (typeClass == null) continue;
|
||||||
|
if (typeClass.isEnum()) {
|
||||||
|
StaticAttribute sa = typeClass.getAnnotation(StaticAttribute.class);
|
||||||
|
String simpleNmae = typeClass.getSimpleName();
|
||||||
|
if (sa != null) {
|
||||||
|
log.info("UpdateMethod {} 的参数 {}, 类型 {} 已被 @StaticAttribute 注解,不再重复注入",
|
||||||
|
info.getMethodName(), paramInfo.getName(), simpleNmae);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (optionArgCache.containsKey(simpleNmae)) {
|
||||||
|
log.warn("UpdateMethod {} 参数 {}, 类型 {} 指定的名称 {} 已经存在, 旧值会被覆盖",
|
||||||
|
typeClass,
|
||||||
|
simpleNmae);
|
||||||
|
}
|
||||||
|
optionArgCache.put(simpleNmae, typeClass);
|
||||||
|
log.debug("{} 类以 {} 为名称注入", typeClass, simpleNmae);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (optionArgCache.isEmpty()) {
|
||||||
|
// 跑完一次后,缓存仍然为空,可能不正常,提示一下
|
||||||
|
log.warn("初始化后,Option(s)Arg(s)' 缓存为空,这仅在无任何方法被 UpdateMethod 注解,或 UpdateMethod 不存在枚举类参数的情况下出现。如果这与您的期望不符,请检查");
|
||||||
|
}
|
||||||
|
Iterator<Map.Entry<String, Class<?>>> iterator = optionArgCache.entrySet().iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Map.Entry<String, Class<?>> entry = iterator.next();
|
||||||
|
request.setAttribute(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package link.at17.mid.tushare.component;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.aspectj.lang.JoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.AfterReturning;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.annotation.Pointcut;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.data.models.UpdatePlan;
|
||||||
|
import link.at17.mid.tushare.service.UpdatePlanService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新计划同步切片器<p>
|
||||||
|
* 主要用于拦截通过 updatePlanService 下的所有 save/update/remove 方法
|
||||||
|
* <p>注意:
|
||||||
|
* <ol>
|
||||||
|
* <li>仅限第一个参数是 {@code UpdatePlan}, {@code Collection<UpdatePlan>} 或 {@code Wrapper<UpdatePlan>} 的方法
|
||||||
|
* <li>由于切片是对 UpdatePlanService 切片, 但切片器内部又调用了 UpdatePlanService,
|
||||||
|
* 如果调用的方法恰好是被切片的方法, 则会进入调用递归导致栈溢出, 需要额外注意.
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
@Validated
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UpdatePlanSyncAspect {
|
||||||
|
|
||||||
|
private final UpdatePlanService updatePlanService;
|
||||||
|
|
||||||
|
// 拦截所有 UpdatePlanService 的 save/update/remove 方法
|
||||||
|
@Pointcut("(" +
|
||||||
|
"execution(* com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.save*(..)) || " +
|
||||||
|
"execution(* com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.update*(..)) || " +
|
||||||
|
"execution(* com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.remove*(..)) || " +
|
||||||
|
"execution(* com.baomidou.mybatisplus.extension.service.IService.save*(..)) || " +
|
||||||
|
"execution(* com.baomidou.mybatisplus.extension.service.IService.update*(..)) || " +
|
||||||
|
"execution(* com.baomidou.mybatisplus.extension.service.IService.remove*(..))" +
|
||||||
|
") && " +
|
||||||
|
"target(link.at17.mid.tushare.service.UpdatePlanService)")
|
||||||
|
public void taskOps() {}
|
||||||
|
|
||||||
|
@AfterReturning(pointcut = "taskOps()", returning = "result")
|
||||||
|
public void afterTaskOp(JoinPoint jp, Object result) {
|
||||||
|
|
||||||
|
if (!(result instanceof Boolean bool) || !bool) {
|
||||||
|
// 未成功执行,不进行切片
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object arg = jp.getArgs()[0];
|
||||||
|
if (arg instanceof UpdatePlan task) {
|
||||||
|
handle(task, jp.getSignature().getName());
|
||||||
|
}
|
||||||
|
else if (arg instanceof Collection<?> coll) {
|
||||||
|
coll.stream()
|
||||||
|
.filter(UpdatePlan.class::isInstance)
|
||||||
|
.map(UpdatePlan.class::cast)
|
||||||
|
.forEach(t -> handle(t, jp.getSignature().getName()));
|
||||||
|
}
|
||||||
|
else if (arg instanceof Wrapper<?> wrapper) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<UpdatePlan> plans = updatePlanService.list((Wrapper<UpdatePlan>)wrapper);
|
||||||
|
plans.stream()
|
||||||
|
.filter(UpdatePlan.class::isInstance)
|
||||||
|
.map(UpdatePlan.class::cast)
|
||||||
|
.forEach(t -> handle(t, jp.getSignature().getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handle(UpdatePlan task, String methodName) {
|
||||||
|
|
||||||
|
// 但凡存在的都应该 reschedule
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (methodName.startsWith("remove")) {
|
||||||
|
updatePlanService.deleteTask(task.getId());
|
||||||
|
log.info("已同步删除 Quartz 任务: [{}]{}", task.getId(), task.getName());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
updatePlanService.rescheduleTask(task);
|
||||||
|
log.info("已同步更新 Quartz 任务: [{}]{}", task.getId(), task.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
log.error("同步 Quartz 调度失败: [{}]{}", task.getId(), task.getName(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,17 +8,17 @@ import org.springframework.beans.factory.annotation.Qualifier;
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
|
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
|
||||||
|
|
||||||
import link.at17.mid.tushare.task.TaskSchedulerFactory;
|
import link.at17.mid.tushare.task.AutowireCapableJobFactory;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class QuartzConfig {
|
public class QuartzConfig {
|
||||||
@Autowired
|
@Autowired
|
||||||
private TaskSchedulerFactory taskSchedulerFactory;
|
private AutowireCapableJobFactory autowireCapableJobFactory;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SchedulerFactoryBean schedulerFactoryBean() {
|
public SchedulerFactoryBean schedulerFactoryBean() {
|
||||||
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
|
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
|
||||||
schedulerFactoryBean.setJobFactory(taskSchedulerFactory);
|
schedulerFactoryBean.setJobFactory(autowireCapableJobFactory);
|
||||||
return schedulerFactoryBean;
|
return schedulerFactoryBean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,18 +7,28 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
|||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
import link.at17.mid.tushare.component.ConfigAutoRegistrar;
|
import link.at17.mid.tushare.component.ConfigAutoRegistrar;
|
||||||
import link.at17.mid.tushare.component.PlatformInterceptor;
|
import link.at17.mid.tushare.component.StaticAttributeInterceptor;
|
||||||
|
import link.at17.mid.tushare.component.UpdateMethodInterceptor;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@Import(ConfigAutoRegistrar.class)
|
@Import(ConfigAutoRegistrar.class)
|
||||||
public class VerichConfig implements WebMvcConfigurer {
|
public class VerichConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
PlatformInterceptor platformInterceptor;
|
StaticAttributeInterceptor platformInterceptor;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
UpdateMethodInterceptor updateMethodInterceptor;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
registry.addInterceptor(platformInterceptor)
|
registry
|
||||||
|
.addInterceptor(platformInterceptor)
|
||||||
.addPathPatterns("/**");
|
.addPathPatterns("/**");
|
||||||
|
|
||||||
|
registry
|
||||||
|
.addInterceptor(updateMethodInterceptor)
|
||||||
|
.addPathPatterns("/**");
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ import java.util.List;
|
|||||||
@Mapper
|
@Mapper
|
||||||
public interface StockAdjustDao extends ITsTradeDate {
|
public interface StockAdjustDao extends ITsTradeDate {
|
||||||
@BatchInsert
|
@BatchInsert
|
||||||
int insertOrUpdateListTushare(@BatchList List<JSONObject> list);
|
int insertOrUpdateList(@BatchList List<JSONObject> list);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package link.at17.mid.tushare.dao;
|
package link.at17.mid.tushare.dao;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.github.yulichang.base.MPJBaseMapper;
|
||||||
|
|
||||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||||
import link.at17.mid.tushare.annotation.BatchList;
|
import link.at17.mid.tushare.annotation.BatchList;
|
||||||
@@ -22,13 +22,13 @@ import java.util.List;
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface StockCalendarDao extends BaseMapper<StockCalendar> {
|
public interface StockCalendarDao extends MPJBaseMapper<StockCalendar> {
|
||||||
/**
|
/**
|
||||||
* 更新股票日历
|
* 更新股票日历
|
||||||
* @param list
|
* @param list
|
||||||
*/
|
*/
|
||||||
@BatchInsert
|
@BatchInsert
|
||||||
void insertOrUpdateList(@BatchList List<JSONObject> list);
|
int insertOrUpdateList(@BatchList List<JSONObject> list);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取数据库内指定证交所的最近一个交易日历
|
* 获取数据库内指定证交所的最近一个交易日历
|
||||||
@@ -37,36 +37,6 @@ public interface StockCalendarDao extends BaseMapper<StockCalendar> {
|
|||||||
*/
|
*/
|
||||||
StockCalendar getLatest(@Nullable StockMarket stockMarket);
|
StockCalendar getLatest(@Nullable StockMarket stockMarket);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取数据库内指定证交所的最早一个交易日历的日期
|
|
||||||
* <p>仅为日期,并未指定是否是开市日
|
|
||||||
* @see #getGreatest
|
|
||||||
* @param stockMarket 指定证交所,若为 {@code null} 则取最近一条(市场不确定)
|
|
||||||
* @return {@code LocalDate} 或 {@code null}
|
|
||||||
*/
|
|
||||||
default LocalDate getLatestLocalDate(@Nullable StockMarket stockMarket) {
|
|
||||||
StockCalendar stockCalendar = getLatest(stockMarket);
|
|
||||||
if (stockCalendar != null) {
|
|
||||||
return stockCalendar.getDate().atStartOfDay().toLocalDate();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取数据库内指定证交所的最早一个交易日历的日期时间
|
|
||||||
* <p>仅为日期时间,并未指定是否是开市日
|
|
||||||
* @see #getGreatest
|
|
||||||
* @param stockMarket 指定证交所,若为 {@code null} 则取最近一条(市场不确定)
|
|
||||||
* @return {@code LocalDateTime} 或 {@code null}
|
|
||||||
*/
|
|
||||||
default LocalDateTime getLatestLocalDateTime(@Nullable StockMarket stockMarket) {
|
|
||||||
StockCalendar stockCalendar = getLatest(stockMarket);
|
|
||||||
if (stockCalendar != null) {
|
|
||||||
return stockCalendar.getDate().atStartOfDay();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取数据库内指定证交所的最早一个交易日历
|
* 获取数据库内指定证交所的最早一个交易日历
|
||||||
* <p>仅为日期,并未指定是否是开市日
|
* <p>仅为日期,并未指定是否是开市日
|
||||||
@@ -75,52 +45,24 @@ public interface StockCalendarDao extends BaseMapper<StockCalendar> {
|
|||||||
*/
|
*/
|
||||||
StockCalendar getGreatest(@Nullable StockMarket stockMarket);
|
StockCalendar getGreatest(@Nullable StockMarket stockMarket);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取数据库内指定证交所的最早一个交易日历的日期
|
|
||||||
* <p>仅为日期,并未指定是否是开市日
|
|
||||||
* @see #getGreatest
|
|
||||||
* @param stockMarket 指定证交所,若为 {@code null} 则取最早一条(市场不确定)
|
|
||||||
* @return {@code LocalDate} 或 {@code null}
|
|
||||||
*/
|
|
||||||
default LocalDate getGreatestLocalDate(@Nullable StockMarket stockMarket) {
|
|
||||||
StockCalendar stockCalendar = getGreatest(stockMarket);
|
|
||||||
if (stockCalendar != null) {
|
|
||||||
return stockCalendar.getDate().atStartOfDay().toLocalDate();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取数据库内指定证交所的最早一个交易日历的日期时间
|
* 获取指定证交所两个日期(含)之间的交易日个数<br>
|
||||||
* <p>仅为日期,并未指定是否是开市日
|
* <p>
|
||||||
* @see #getGreatest
|
|
||||||
* @param stockMarket 指定证交所,若为 {@code null} 则取最早一条(市场不确定)
|
|
||||||
* @return {@code LocalDateTime} 或 {@code null}
|
|
||||||
*/
|
|
||||||
default LocalDateTime getGreatestLocalDateTime(@Nullable StockMarket stockMarket) {
|
|
||||||
StockCalendar stockCalendar = getGreatest(stockMarket);
|
|
||||||
if (stockCalendar != null) {
|
|
||||||
return stockCalendar.getDate().atStartOfDay();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定证交所两个日期(含)之间的交易日个数<br>>
|
|
||||||
* 存储在数据库内的类型为无时分秒的 timestamp 类型,可视作 {@code java.time.LocalDate} <br>
|
* 存储在数据库内的类型为无时分秒的 timestamp 类型,可视作 {@code java.time.LocalDate} <br>
|
||||||
* 若传入的是 java.time.LocalDateTime 类型,会忽略时分秒
|
* 若传入的是 java.time.LocalDateTime 类型,会忽略时分秒
|
||||||
* <p>例:
|
* <p>例:
|
||||||
* <ul><li>
|
* <ul><li>
|
||||||
* <code>startDate : LocalDateTime 2021-08-16 01:20:30</code><br>
|
* <code>startDate : LocalDateTime 2021-08-16 01:20:30</code><br>
|
||||||
* <code>endDate : LocalDateTime 2021-08-20 19:20:30</code><br>
|
* <code>endDate : LocalDateTime 2021-08-20 19:20:30</code><br>
|
||||||
* 返回 5
|
* 返回 5
|
||||||
* <li><code>countOpenDaysBetween(StockMarket.<b><i>SH</i></b>, startDate, endDate)</code> 其中<br>
|
* <li><code>countOpenDaysBetween(StockMarket.<b><i>SH</i></b>, startDate, endDate)</code> 其中<br>
|
||||||
* <code>startDate : LocalDateTime 2021-08-16 01:20:30</code><br>
|
* <code>startDate : LocalDateTime 2021-08-16 01:20:30</code><br>
|
||||||
* <code>endDate : LocalDateTime 2021-08-16 12:34:56</code><br>
|
* <code>endDate : LocalDateTime 2021-08-16 12:34:56</code><br>
|
||||||
* 返回 1
|
* 返回 1
|
||||||
* <li><code>countOpenDaysBetween(StockMarket.<b><i>SH</i></b>, startDate, endDate)</code> 其中<br>
|
* <li><code>countOpenDaysBetween(StockMarket.<b><i>SH</i></b>, startDate, endDate)</code> 其中<br>
|
||||||
* <code>startDate : LocalDateTime 2021-08-15 01:20:30</code><br>
|
* <code>startDate : LocalDateTime 2021-08-15 01:20:30</code><br>
|
||||||
* <code>endDate : LocalDateTime 2021-08-16 12:34:56</code><br>
|
* <code>endDate : LocalDateTime 2021-08-16 12:34:56</code><br>
|
||||||
* 返回 1
|
* 返回 1
|
||||||
* </ul>
|
* </ul>
|
||||||
* </p>
|
* </p>
|
||||||
@@ -133,15 +75,24 @@ public interface StockCalendarDao extends BaseMapper<StockCalendar> {
|
|||||||
long countOpenDaysBetween(@Param("exchange") StockMarket exchange, @Param("startDate") Temporal startDate, @Param("endDate") Temporal endDate);
|
long countOpenDaysBetween(@Param("exchange") StockMarket exchange, @Param("startDate") Temporal startDate, @Param("endDate") Temporal endDate);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定证交所两个日期(含)之间的所有交易日<br>
|
* 获取指定证交所两个日期(含)之间的所有交易日<br>
|
||||||
* 存储在数据库内的类型为无时分秒的 timestamp 类型,可视作 {@code java.time.LocalDate} <br>
|
* 存储在数据库内的类型为无时分秒的 timestamp 类型,可视作 {@code java.time.LocalDate} <br>
|
||||||
* 若传入的是 java.time.LocalDateTime 类型,会忽略时分秒
|
* 若传入的是 java.time.LocalDateTime 类型,会忽略时分秒
|
||||||
* <p>例:
|
* <p>例:
|
||||||
* <ul><li><code>countOpenDaysBetween(StockMarket.<b><i>SH</i></b>, startDate, endDate)</code> 其中<br>
|
* <ul><li><code>countOpenDaysBetween(StockMarket.<b><i>SH</i></b>, startDate, endDate)</code> 其中<br>
|
||||||
* <code>startDate : LocalDateTime 2021-08-16 01:20:30</code><br>
|
* <code>startDate : LocalDateTime 2021-08-16 01:20:30</code><br>
|
||||||
* <code>endDate : LocalDateTime 2021-08-20 19:20:30</code><br>
|
* <code>endDate : LocalDateTime 2021-08-20 19:20:30</code><br>
|
||||||
* 返回:2021-08-16 2021-08-17 2021-08-18 2021-08-19 2021-08-20
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* 返回:
|
||||||
|
* <ul>
|
||||||
|
* <li>2021-08-16
|
||||||
|
* <li>2021-08-17
|
||||||
|
* <li>2021-08-18
|
||||||
|
* <li>2021-08-19
|
||||||
|
* <li>2021-08-20
|
||||||
* </ul>
|
* </ul>
|
||||||
* </p>
|
* </p>
|
||||||
* @param exchange 股市类型枚举,若提供的枚举不为 SZ 或 SH,则采用 SZ + SH 的所有交易日并去重获得结果
|
* @param exchange 股市类型枚举,若提供的枚举不为 SZ 或 SH,则采用 SZ + SH 的所有交易日并去重获得结果
|
||||||
@@ -164,10 +115,10 @@ public interface StockCalendarDao extends BaseMapper<StockCalendar> {
|
|||||||
/**
|
/**
|
||||||
* 获取指定证交所指定日期(含)以后的所有交易日
|
* 获取指定证交所指定日期(含)以后的所有交易日
|
||||||
* @param exchange
|
* @param exchange
|
||||||
* @param after 留空则查询所有
|
* @param sence 留空则查询所有
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
List<LocalDateTime> getAllOpenDate(@Param("exchange") @Nullable StockMarket exchange, @Param("after") @Nullable Temporal after);
|
List<LocalDateTime> getAllOpenDateSence(@Param("exchange") @Nullable StockMarket exchange, @Param("after") @Nullable Temporal sence);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断指定日是否为指定交易所的交易日
|
* 判断指定日是否为指定交易所的交易日
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package link.at17.mid.tushare.dao;
|
package link.at17.mid.tushare.dao;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.github.yulichang.base.MPJBaseMapper;
|
||||||
|
|
||||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||||
import link.at17.mid.tushare.annotation.BatchList;
|
import link.at17.mid.tushare.annotation.BatchList;
|
||||||
|
import link.at17.mid.tushare.data.models.StockValueEx;
|
||||||
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
@@ -13,11 +15,11 @@ import java.util.List;
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface StockDailyBasicDao extends ITsTradeDate {
|
public interface StockDailyBasicDao extends MPJBaseMapper<StockValueEx>, ITsTradeDate {
|
||||||
/**
|
/**
|
||||||
* 批量插入/更新日基本数据
|
* 批量插入/更新日基本数据
|
||||||
* @param list
|
* @param list
|
||||||
*/
|
*/
|
||||||
@BatchInsert
|
@BatchInsert
|
||||||
void insertOrUpdateList(@BatchList List<JSONObject> list);
|
int insertOrUpdateList(@BatchList List<JSONObject> list);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package link.at17.mid.tushare.dao;
|
package link.at17.mid.tushare.dao;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.github.yulichang.base.MPJBaseMapper;
|
||||||
|
|
||||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||||
import link.at17.mid.tushare.annotation.BatchList;
|
import link.at17.mid.tushare.annotation.BatchList;
|
||||||
import link.at17.mid.tushare.data.models.StockValue;
|
import link.at17.mid.tushare.data.models.StockValue;
|
||||||
import link.at17.mid.tushare.data.models.StockValueEx;
|
|
||||||
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
@@ -13,65 +13,65 @@ import org.apache.ibatis.annotations.Param;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.temporal.Temporal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface StockDailyDao extends ITsTradeDate {
|
public interface StockDailyDao extends MPJBaseMapper<StockValue>, ITsTradeDate {
|
||||||
/**
|
/**
|
||||||
* 批量插入/更新日线数据
|
* 批量插入/更新日线数据
|
||||||
* @param list
|
* @param list
|
||||||
*/
|
*/
|
||||||
@BatchInsert
|
@BatchInsert
|
||||||
void insertOrUpdateList(@BatchList List<JSONObject> list);
|
int insertOrUpdateList(@BatchList List<JSONObject> list);
|
||||||
/**
|
/**
|
||||||
* 获取除权日线数据 + 基本行情数据
|
* 获取除权日线数据 + 基本行情数据
|
||||||
* @param stockCode 股票代码,不允许为空
|
* @param tsCode Tushare 股票代码,不允许为空
|
||||||
* @param endDate 结束日期(包含),留空则为最新一个交易日
|
* @param endDate 结束日期(包含),留空则为最新一个交易日
|
||||||
* @param before 多少个交易日以前,留空则查询上市以来所有数据
|
* @param before 多少个交易日以前,留空则查询上市至 endDate 以来所有数据
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
List<StockValueEx> getExDailyBeforeTushare(@Param("stockCode") @NotNull String stockCode, @Param("endDate") LocalDateTime endDate, @Param("before") Long before);
|
<T extends StockValue> List<T> getExDailyBefore(@Param("stockCode") @NotNull String tsCode, @Param("endDate") Temporal endDate, @Param("before") Long before);
|
||||||
/**
|
/**
|
||||||
* 获取前复权日线数据
|
* 获取前复权日线数据
|
||||||
* @param stockCode 股票代码,不允许为空
|
* @param tsCode Tushare 股票代码,不允许为空
|
||||||
* @param startDate 开始日期,留空则为股票上市日起
|
* @param startDate 开始日期,留空则为股票上市日起
|
||||||
* @param endDate 结束日期,null 则为最新一个交易日
|
* @param endDate 结束日期,null 则为最新一个交易日
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
List<StockValue> getQfqDailyTushare(@Param("stockCode") @NotNull String stockCode, @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
|
<T extends StockValue> List<T> getQfqDaily(@Param("stockCode") @NotNull String tsCode, @Param("startDate") Temporal startDate, @Param("endDate") Temporal endDate);
|
||||||
/**
|
/**
|
||||||
* 获取前复权日线数据
|
* 获取前复权日线数据
|
||||||
* @param stockCode 股票代码,不允许为空
|
* @param tsCode Tushare 股票代码,不允许为空
|
||||||
* @param endDate 结束日期,留空则为最新一个交易日
|
* @param endDate 结束日期,留空则为最新一个交易日
|
||||||
* @param before 多少个交易日以前,null 则查询上市以来所有数据
|
* @param before 多少个交易日以前,null 则查询上市以来所有数据
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
List<StockValue> getQfqDailyBeforeTushare(@Param("stockCode") @NotNull String stockCode, @Param("endDate") LocalDateTime endDate, @Param("before") Long before);
|
<T extends StockValue> List<T> getQfqDailyBefore(@Param("stockCode") @NotNull String tsCode, @Param("endDate") Temporal endDate, @Param("before") Long before);
|
||||||
/**
|
/**
|
||||||
* 获取前复权日线数据 + 基本行情数据
|
* 获取前复权日线数据 + 基本行情数据
|
||||||
* @param stockCode 股票代码,不允许为空
|
* @param tsCode Tushare 股票代码,不允许为空
|
||||||
* @param startDate 开始日期(包含),留空则为上市第一日
|
* @param startDate 开始日期(包含),留空则为上市第一日
|
||||||
* @param after 多少个交易日以前,null 则查询 startDate 以来所有数据
|
* @param after 多少个交易日以前,null 则查询 startDate 以来所有数据
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
List<StockValueEx> getExQfqDailyAfterTushare(@Param("stockCode") @NotNull String stockCode, @Param("startDate") LocalDateTime startDate, @Param("after") Long after);
|
<T extends StockValue> List<T> getExQfqDailyAfter(@Param("stockCode") @NotNull String tsCode, @Param("startDate") Temporal startDate, @Param("after") Long after);
|
||||||
/**
|
/**
|
||||||
* 获取前复权日线数据 + 基本行情数据
|
* 获取前复权日线数据 + 基本行情数据
|
||||||
* @param stockCode 股票代码,不允许为空
|
* @param tsCode Tushare 股票代码,不允许为空
|
||||||
* @param endDate 结束日期,留空则为最新一个交易日
|
* @param endDate 结束日期,留空则为最新一个交易日
|
||||||
* @param before 多少个交易日以前,null 则查询上市以来所有数据
|
* @param before 多少个交易日以前,null 则查询上市以来所有数据
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
List<StockValueEx> getExQfqDailyBeforeTushare(@Param("stockCode") @NotNull String stockCode, @Param("endDate") LocalDateTime endDate, @Param("before") Long before);
|
<T extends StockValue> List<T> getExQfqDailyBefore(@Param("stockCode") @NotNull String tsCode, @Param("endDate") Temporal endDate, @Param("before") Long before);
|
||||||
/**
|
/**
|
||||||
* 获取前复权日线数据
|
* 获取前复权日线数据
|
||||||
* @param stockCode 股票代码,不允许为空
|
* @param tsCode Tushare 股票代码,不允许为空
|
||||||
* @param startDate 开始日期(包含),留空则为上市第一日
|
* @param startDate 开始日期(包含),留空则为上市第一日
|
||||||
* @param after 多少个交易日以前,null 则查询 startDate 以来所有数据
|
* @param after 多少个交易日以前,null 则查询 startDate 以来所有数据
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
List<StockValue> getQfqDailyAfterTushare(@Param("stockCode") @NotNull String stockCode, @Param("startDate") LocalDateTime startDate, @Param("after") Long after);
|
<T extends StockValue> List<T> getQfqDailyAfter(@Param("stockCode") @NotNull String tsCode, @Param("startDate") Temporal startDate, @Param("after") Long after);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package link.at17.mid.tushare.dao;
|
package link.at17.mid.tushare.dao;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.github.yulichang.base.MPJBaseMapper;
|
||||||
|
|
||||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||||
import link.at17.mid.tushare.annotation.BatchList;
|
import link.at17.mid.tushare.annotation.BatchList;
|
||||||
@@ -18,9 +19,9 @@ import java.util.List;
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface StockHolderDao extends ITsTradeDate {
|
public interface StockHolderDao extends MPJBaseMapper<StockHolder>, ITsTradeDate {
|
||||||
@BatchInsert
|
@BatchInsert
|
||||||
int insertOrUpdateListTushare(@BatchList List<JSONObject> list, @NonNull StockHolderType holderType);
|
int insertOrUpdateList(@BatchList List<JSONObject> list, @NonNull StockHolderType holderType);
|
||||||
|
|
||||||
@EvictAfterUpdate("tushare")
|
@EvictAfterUpdate("tushare")
|
||||||
@Cacheable("stockHolderDao.getAllByStockCode")
|
@Cacheable("stockHolderDao.getAllByStockCode")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package link.at17.mid.tushare.dao;
|
package link.at17.mid.tushare.dao;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.github.yulichang.base.MPJBaseMapper;
|
||||||
|
|
||||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||||
import link.at17.mid.tushare.annotation.BatchList;
|
import link.at17.mid.tushare.annotation.BatchList;
|
||||||
@@ -16,7 +16,7 @@ import java.util.List;
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface StockInfoDao extends BaseMapper<StockInfo> {
|
public interface StockInfoDao extends MPJBaseMapper<StockInfo> {
|
||||||
@BatchInsert
|
@BatchInsert
|
||||||
void insertOrUpdateList(@BatchList List<JSONObject> list);
|
void insertOrUpdateList(@BatchList List<JSONObject> list);
|
||||||
StockInfo getStockInfoByStockCode(@Param("stockCode") String stockCode);
|
StockInfo getStockInfoByStockCode(@Param("stockCode") String stockCode);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package link.at17.mid.tushare.dao;
|
package link.at17.mid.tushare.dao;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.github.yulichang.base.MPJBaseMapper;
|
||||||
|
|
||||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||||
import link.at17.mid.tushare.annotation.BatchList;
|
import link.at17.mid.tushare.annotation.BatchList;
|
||||||
@@ -15,7 +15,7 @@ import java.util.List;
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface StockLimitDao extends BaseMapper<StockLimit>, ITsTradeDate {
|
public interface StockLimitDao extends MPJBaseMapper<StockLimit>, ITsTradeDate {
|
||||||
@BatchInsert
|
@BatchInsert
|
||||||
int insertOrUpdateListTushare(@BatchList List<JSONObject> list);
|
int insertOrUpdateList(@BatchList List<JSONObject> list);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.temporal.Temporal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -24,7 +25,7 @@ public interface StockMinuteDao {
|
|||||||
* @param list
|
* @param list
|
||||||
*/
|
*/
|
||||||
@BatchInsert
|
@BatchInsert
|
||||||
void insertOrUpdateList(@NotNull StockSpan stockSpan, @BatchList List<JSONObject> list);
|
int insertOrUpdateList(@BatchList List<JSONObject> list, @NotNull StockSpan stockSpan);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取最新交易日<br/>
|
* 获取最新交易日<br/>
|
||||||
@@ -57,7 +58,7 @@ public interface StockMinuteDao {
|
|||||||
* @param endDate 结束日期,留空则为最新一个交易日
|
* @param endDate 结束日期,留空则为最新一个交易日
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
List<StockValue> getQfqMinuteTushare(@NotNull String stockCode, @NotNull StockSpan stockSpan, LocalDateTime startDate, LocalDateTime endDate);
|
List<StockValue> getQfqMinute(@NotNull String stockCode, @NotNull StockSpan stockSpan, Temporal startDate, Temporal endDate);
|
||||||
/**
|
/**
|
||||||
* 获取前复权日线数据
|
* 获取前复权日线数据
|
||||||
* @param stockCode 股票代码,不允许为空
|
* @param stockCode 股票代码,不允许为空
|
||||||
@@ -65,7 +66,7 @@ public interface StockMinuteDao {
|
|||||||
* @param before 多少个交易日以前,留空则查询上市以来所有数据
|
* @param before 多少个交易日以前,留空则查询上市以来所有数据
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
List<StockValue> getQfqMinuteBeforeTushare(@NotNull String stockCode, @NotNull StockSpan stockSpan, LocalDateTime endDate, Long before);
|
List<StockValue> getQfqMinuteBefore(@NotNull String stockCode, @NotNull StockSpan stockSpan, Temporal endDate, Long before);
|
||||||
/**
|
/**
|
||||||
* 获取前复权日线 Ex 数据
|
* 获取前复权日线 Ex 数据
|
||||||
* @param stockCode 股票代码,不允许为空
|
* @param stockCode 股票代码,不允许为空
|
||||||
@@ -73,5 +74,5 @@ public interface StockMinuteDao {
|
|||||||
* @param before 多少个交易日以前,留空则查询上市以来所有数据
|
* @param before 多少个交易日以前,留空则查询上市以来所有数据
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
List<StockValueEx> getExQfqMinuteBeforeTushare(@NotNull String stockCode, @NotNull StockSpan stockSpan, LocalDateTime endDate, Long before);
|
List<StockValueEx> getExQfqMinuteBefore(@NotNull String stockCode, @NotNull StockSpan stockSpan, Temporal endDate, Long before);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ import java.util.List;
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface StockThsDailyDao extends ITsTradeDate {
|
public interface ThsDailyDao extends ITsTradeDate {
|
||||||
/**
|
/**
|
||||||
* 批量插入/更新日线数据
|
* 批量插入/更新日线数据
|
||||||
* @param list
|
* @param list
|
||||||
*/
|
*/
|
||||||
@BatchInsert
|
@BatchInsert
|
||||||
void insertOrUpdateList(@BatchList List<JSONObject> list);
|
int insertOrUpdateList(@BatchList List<JSONObject> list);
|
||||||
/**
|
/**
|
||||||
* 获取板块日线数据
|
* 获取板块日线数据
|
||||||
* @param stockCode 板块代码,不允许为空
|
* @param stockCode 板块代码,不允许为空
|
||||||
@@ -2,7 +2,7 @@ package link.at17.mid.tushare.dao;
|
|||||||
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.github.yulichang.base.MPJBaseMapper;
|
||||||
|
|
||||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||||
import link.at17.mid.tushare.annotation.BatchList;
|
import link.at17.mid.tushare.annotation.BatchList;
|
||||||
@@ -16,9 +16,9 @@ import java.util.List;
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface StockThsListDao extends BaseMapper<ThsStockInfo> {
|
public interface ThsListDao extends MPJBaseMapper<ThsStockInfo> {
|
||||||
@BatchInsert
|
@BatchInsert
|
||||||
void insertOrUpdateList(@BatchList List<JSONObject> list);
|
int insertOrUpdateList(@BatchList List<JSONObject> list);
|
||||||
default List<ThsStockInfo> listByExchange(ThsStockMarket exchange){
|
default List<ThsStockInfo> listByExchange(ThsStockMarket exchange){
|
||||||
return selectList(new LambdaQueryWrapper<ThsStockInfo>().eq(exchange != null, ThsStockInfo::getExchange, exchange));
|
return selectList(new LambdaQueryWrapper<ThsStockInfo>().eq(exchange != null, ThsStockInfo::getExchange, exchange));
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
package link.at17.mid.tushare.dao;
|
package link.at17.mid.tushare.dao;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
|
||||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||||
import link.at17.mid.tushare.annotation.BatchList;
|
import link.at17.mid.tushare.annotation.BatchList;
|
||||||
import link.at17.mid.tushare.data.models.StockInfo;
|
import link.at17.mid.tushare.data.models.StockInfo;
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -13,13 +11,13 @@ import java.util.List;
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface StockThsMemberDao {
|
public interface ThsMemberDao {
|
||||||
@BatchInsert
|
@BatchInsert
|
||||||
void insertOrUpdateList(@BatchList List<JSONObject> list);
|
int insertOrUpdateList(@BatchList List<JSONObject> list);
|
||||||
List<StockInfo> getThsMembers(String tsCode);
|
List<StockInfo> getThsMembers(String tsCode);
|
||||||
/**
|
/**
|
||||||
* 获取同花顺所属概念
|
* 获取个股的同花顺所属概念,以逗号分隔
|
||||||
* @param tsCode
|
* @param tsCode 个股的 Tushare 代码
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
String getBelongings(String tsCode);
|
String getBelongings(String tsCode);
|
||||||
12
src/main/java/link/at17/mid/tushare/dao/UpdatePlanDao.java
Normal file
12
src/main/java/link/at17/mid/tushare/dao/UpdatePlanDao.java
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package link.at17.mid.tushare.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import com.github.yulichang.base.MPJBaseMapper;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.data.models.UpdatePlan;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface UpdatePlanDao extends MPJBaseMapper<UpdatePlan> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
package link.at17.mid.tushare.data.crawler;
|
package link.at17.mid.tushare.data.crawler;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.annotation.StaticAttribute;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询方式
|
* 查询方式
|
||||||
* @author Barry
|
* @author Barry
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
@StaticAttribute
|
||||||
public enum QueryWay {
|
public enum QueryWay {
|
||||||
/**
|
/**
|
||||||
* 以日期更新,适用于已有从上市开始的部分数据的更新
|
* 以日期更新,适用于已有从上市开始的部分数据的更新
|
||||||
|
|||||||
@@ -6,11 +6,18 @@ import java.util.ArrayList;
|
|||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSONArray;
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
import com.alibaba.fastjson2.JSONException;
|
import com.alibaba.fastjson2.JSONException;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.component.OkHttpClientProvider;
|
||||||
|
import link.at17.mid.tushare.system.config.SystemConfig;
|
||||||
|
import link.at17.mid.tushare.system.util.SpringContextHolder;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import okhttp3.Call;
|
import okhttp3.Call;
|
||||||
import okhttp3.MediaType;
|
import okhttp3.MediaType;
|
||||||
@@ -20,13 +27,21 @@ import okhttp3.RequestBody;
|
|||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@Component
|
||||||
public class TushareClient {
|
public class TushareClient {
|
||||||
|
|
||||||
private static final String TUSHARE_URL = "http://api.tushare.pro";
|
private static final String TUSHARE_URL = "http://api.tushare.pro";
|
||||||
// private static String token = "83a82fadb0bbb803f008b31ce09479e5107f4aba3f28d5df2174c642";
|
|
||||||
// private static String token = "c473f86ae2f5703f58eecf9864fa9ec91d67edbc01e3294f6a4f9c32";
|
private static String getToken() {
|
||||||
// private static String token = "ec3d7415bf3dfebf0f27b1e5a9805f809b083fb1f8590c8c2bdc633f";
|
SystemConfig systemConfig = SpringContextHolder.getBean(SystemConfig.class);
|
||||||
private static String token = "6f284d9246bad80c3eff946f3ecae8442072b1e60652785f66007509";
|
String token = systemConfig.getTushareToken();
|
||||||
|
if (!StringUtils.isEmpty(token) && Pattern.matches("^[0-9a-f]{56}$", token)) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "6f284d9246bad80c3eff946f3ecae8442072b1e60652785f66007509";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回原始查询结果
|
* 返回原始查询结果
|
||||||
@@ -34,8 +49,8 @@ public class TushareClient {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static JSONObject query(TushareRequestBody tr) throws IOException, JSONException {
|
public static JSONObject query(TushareRequestBody tr) throws IOException, JSONException {
|
||||||
OkHttpClient okHttpClient = new OkHttpClient();
|
OkHttpClient okHttpClient = OkHttpClientProvider.getInstance();
|
||||||
tr.setToken(token);
|
tr.setToken(getToken());
|
||||||
Request request = new Request.Builder()
|
Request request = new Request.Builder()
|
||||||
.url(TUSHARE_URL)
|
.url(TUSHARE_URL)
|
||||||
.post(
|
.post(
|
||||||
|
|||||||
@@ -1,53 +1,32 @@
|
|||||||
package link.at17.mid.tushare.data.crawler.tushare;
|
package link.at17.mid.tushare.data.crawler.tushare;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.redisson.api.RedissonClient;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSONException;
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
|
||||||
import link.at17.mid.tushare.dao.StockAdjustDao;
|
|
||||||
import link.at17.mid.tushare.dao.StockCalendarDao;
|
|
||||||
import link.at17.mid.tushare.dao.StockDailyBasicDao;
|
|
||||||
import link.at17.mid.tushare.dao.StockDailyDao;
|
|
||||||
import link.at17.mid.tushare.dao.StockHolderDao;
|
|
||||||
import link.at17.mid.tushare.dao.StockInfoDao;
|
|
||||||
import link.at17.mid.tushare.dao.StockLimitDao;
|
|
||||||
import link.at17.mid.tushare.dao.StockMinuteDao;
|
|
||||||
import link.at17.mid.tushare.dao.StockThsDailyDao;
|
|
||||||
import link.at17.mid.tushare.dao.StockThsListDao;
|
|
||||||
import link.at17.mid.tushare.dao.StockThsMemberDao;
|
|
||||||
import link.at17.mid.tushare.data.crawler.QueryWay;
|
import link.at17.mid.tushare.data.crawler.QueryWay;
|
||||||
import link.at17.mid.tushare.data.crawler.RetryAndDelay;
|
import link.at17.mid.tushare.data.crawler.RetryAndDelay;
|
||||||
import link.at17.mid.tushare.data.models.StockInfo;
|
import link.at17.mid.tushare.data.models.StockInfo;
|
||||||
import link.at17.mid.tushare.data.models.ThsStockInfo;
|
|
||||||
import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo;
|
import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo;
|
||||||
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
||||||
import link.at17.mid.tushare.enums.StockHolderType;
|
import link.at17.mid.tushare.data.service.StockCalendarService;
|
||||||
|
import link.at17.mid.tushare.data.service.StockInfoService;
|
||||||
import link.at17.mid.tushare.enums.StockMarket;
|
import link.at17.mid.tushare.enums.StockMarket;
|
||||||
import link.at17.mid.tushare.enums.StockSpan;
|
import link.at17.mid.tushare.enums.StockSpan;
|
||||||
import link.at17.mid.tushare.enums.ThsStockMarket;
|
|
||||||
import link.at17.mid.tushare.system.util.LocalDateTimeUtils;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,32 +40,9 @@ public class TushareCrawler {
|
|||||||
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private StockInfoDao stockInfoDao;
|
private StockInfoService stockInfoService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private StockDailyDao stockDailyDao;
|
private StockCalendarService stockCalendarService;
|
||||||
@Autowired
|
|
||||||
private StockMinuteDao stockMinuteDao;
|
|
||||||
@Autowired
|
|
||||||
private StockDailyBasicDao stockDailyBasicDao;
|
|
||||||
@Autowired
|
|
||||||
private StockCalendarDao stockCalendarDao;
|
|
||||||
@Autowired
|
|
||||||
private StockAdjustDao stockAdjustDao;
|
|
||||||
@Autowired
|
|
||||||
private StockThsListDao stockThsListDao;
|
|
||||||
@Autowired
|
|
||||||
private StockThsMemberDao stockThsMemberDao;
|
|
||||||
@Autowired
|
|
||||||
private StockThsDailyDao stockThsDailyDao;
|
|
||||||
@Autowired
|
|
||||||
private StockHolderDao stockHolderDao;
|
|
||||||
@Autowired
|
|
||||||
private StockLimitDao stockLimitDao;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private RedissonClient redis;
|
|
||||||
|
|
||||||
private static final LocalDateTime THS_DAILY_BEGIN_LOCALDATE = LocalDateTime.of(2007, 8, 1, 0, 0, 0);
|
|
||||||
|
|
||||||
private static final int MAX_THREADS = 5;
|
private static final int MAX_THREADS = 5;
|
||||||
/**
|
/**
|
||||||
@@ -103,7 +59,7 @@ public class TushareCrawler {
|
|||||||
* @return
|
* @return
|
||||||
* @see TushareCrawler#rollingQueryByStock
|
* @see TushareCrawler#rollingQueryByStock
|
||||||
*/
|
*/
|
||||||
private List<Future<TushareCrawlerResult>> rollingQueryByDate(TushareRequestBody baseRequest, ITsTradeDate iTradeDate,
|
public List<Future<TushareCrawlerResult>> rollingQueryByDate(TushareRequestBody baseRequest, ITsTradeDate iTradeDate,
|
||||||
Function<List<JSONObject>, Boolean> processDataFunc, QueryWay queryWay, LocalDateTime start){
|
Function<List<JSONObject>, Boolean> processDataFunc, QueryWay queryWay, LocalDateTime start){
|
||||||
ExecutorService es = Executors.newFixedThreadPool(MAX_THREADS);
|
ExecutorService es = Executors.newFixedThreadPool(MAX_THREADS);
|
||||||
RetryAndDelay retryAndDelay = new RetryAndDelay().setThreadNum(MAX_THREADS);
|
RetryAndDelay retryAndDelay = new RetryAndDelay().setThreadNum(MAX_THREADS);
|
||||||
@@ -122,7 +78,7 @@ public class TushareCrawler {
|
|||||||
queryWay = QueryWay.ByDateCrossCheck;
|
queryWay = QueryWay.ByDateCrossCheck;
|
||||||
return rollingQueryByDate(baseRequest, iTradeDate, processDataFunc, queryWay, start);
|
return rollingQueryByDate(baseRequest, iTradeDate, processDataFunc, queryWay, start);
|
||||||
}
|
}
|
||||||
start = stockCalendarDao.getGreatestLocalDateTime(null);
|
start = stockCalendarService.getGreatestLocalDateTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryWay == QueryWay.ByDateUpdate && iTradeDate != null) {
|
if (queryWay == QueryWay.ByDateUpdate && iTradeDate != null) {
|
||||||
@@ -137,7 +93,7 @@ public class TushareCrawler {
|
|||||||
LocalDateTime end = LocalDateTime.now().toLocalDate().atStartOfDay();
|
LocalDateTime end = LocalDateTime.now().toLocalDate().atStartOfDay();
|
||||||
|
|
||||||
while(!start.isAfter(end)) {
|
while(!start.isAfter(end)) {
|
||||||
if (all || stockCalendarDao.isOpen(start, (StockMarket)null)) {
|
if (all || stockCalendarService.isOpen(start, (StockMarket)null)) {
|
||||||
TushareRequestBody rq = baseRequest.clone()
|
TushareRequestBody rq = baseRequest.clone()
|
||||||
.addDateParam("trade_date", start);
|
.addDateParam("trade_date", start);
|
||||||
executeResult.add(es.submit(new TushareResponseCallable(rq, processDataFunc)
|
executeResult.add(es.submit(new TushareResponseCallable(rq, processDataFunc)
|
||||||
@@ -152,10 +108,10 @@ public class TushareCrawler {
|
|||||||
}
|
}
|
||||||
// 交叉检查
|
// 交叉检查
|
||||||
final LocalDateTime currentDate = LocalDateTime.now();
|
final LocalDateTime currentDate = LocalDateTime.now();
|
||||||
final List<LocalDateTime> szStockCalendars = stockCalendarDao.getAllOpenDatesBetween(StockMarket.SZ, null, currentDate);
|
final List<LocalDateTime> szStockCalendars = stockCalendarService.getAllOpenDatesBetween(StockMarket.SZ, null, currentDate);
|
||||||
final List<LocalDateTime> shStockCalendars = stockCalendarDao.getAllOpenDatesBetween(StockMarket.SH, null, currentDate);
|
final List<LocalDateTime> shStockCalendars = stockCalendarService.getAllOpenDatesBetween(StockMarket.SH, null, currentDate);
|
||||||
final List<LocalDateTime> allStockCalendars = stockCalendarDao.getAllOpenDatesBetween((StockMarket)null, null, currentDate);
|
final List<LocalDateTime> allStockCalendars = stockCalendarService.getAllOpenDatesBetween((StockMarket)null, null, currentDate);
|
||||||
final List<StockInfo> stockInfos = stockInfoDao.getStockListByListStatus(null);
|
final List<StockInfo> stockInfos = stockInfoService.list();
|
||||||
TreeSet<LocalDateTime> needUpdates = new TreeSet<>();
|
TreeSet<LocalDateTime> needUpdates = new TreeSet<>();
|
||||||
stockInfos.forEach(stockInfo -> {
|
stockInfos.forEach(stockInfo -> {
|
||||||
List<LocalDateTime> stockTradeDates = iTradeDate.getAllTradeDates(stockInfo);
|
List<LocalDateTime> stockTradeDates = iTradeDate.getAllTradeDates(stockInfo);
|
||||||
@@ -204,7 +160,7 @@ public class TushareCrawler {
|
|||||||
* @return 每条请求的执行结果
|
* @return 每条请求的执行结果
|
||||||
* @see TushareCrawler#rollingQueryByDate
|
* @see TushareCrawler#rollingQueryByDate
|
||||||
*/
|
*/
|
||||||
private List<Future<TushareCrawlerResult>> rollingQueryByStock(
|
public List<Future<TushareCrawlerResult>> rollingQueryByStock(
|
||||||
TushareRequestBody baseRequest,
|
TushareRequestBody baseRequest,
|
||||||
List<? extends ITsStockInfo> stockList,
|
List<? extends ITsStockInfo> stockList,
|
||||||
ITsTradeDate iTradeDate,
|
ITsTradeDate iTradeDate,
|
||||||
@@ -269,7 +225,7 @@ public class TushareCrawler {
|
|||||||
StockMarket exchange = stockCode.endsWith("SZ") ? StockMarket.SZ : StockMarket.SH ; //stockCode.endsWith("SZ") ? "SZSE" : stockCode.endsWith("SH") ? "SSE" : null;
|
StockMarket exchange = stockCode.endsWith("SZ") ? StockMarket.SZ : StockMarket.SH ; //stockCode.endsWith("SZ") ? "SZSE" : stockCode.endsWith("SH") ? "SSE" : null;
|
||||||
|
|
||||||
// 计算 start 和 end 之间一共有多少个交易日
|
// 计算 start 和 end 之间一共有多少个交易日
|
||||||
long dataBetween = stockCalendarDao.countOpenDaysBetween(exchange, start, end);
|
long dataBetween = stockCalendarService.countOpenDaysBetween(exchange, start, end);
|
||||||
long dataPerDay = 1;
|
long dataPerDay = 1;
|
||||||
if (stockSpan.compareTo(StockSpan.Daily) == -1 && stockSpan.compareTo(StockSpan.Minute) >= 0) {
|
if (stockSpan.compareTo(StockSpan.Daily) == -1 && stockSpan.compareTo(StockSpan.Minute) >= 0) {
|
||||||
// 分钟数,重新算下 dataBetween
|
// 分钟数,重新算下 dataBetween
|
||||||
@@ -277,14 +233,14 @@ public class TushareCrawler {
|
|||||||
}
|
}
|
||||||
dataBetween *= dataPerDay;
|
dataBetween *= dataPerDay;
|
||||||
long daySpan = singleMax / dataPerDay;
|
long daySpan = singleMax / dataPerDay;
|
||||||
LocalDateTime tmpEndDate = stockCalendarDao.getOpenDateOffset(exchange, start, daySpan);
|
LocalDateTime tmpEndDate = stockCalendarService.getOpenDateOffset(exchange, start, daySpan);
|
||||||
while (dataBetween > singleMax) {
|
while (dataBetween > singleMax) {
|
||||||
rq.addDateParam("end_date",tmpEndDate);
|
rq.addDateParam("end_date",tmpEndDate);
|
||||||
executeResult.add(es.submit(new TushareResponseCallable(rq.clone(), afterRespFunc)
|
executeResult.add(es.submit(new TushareResponseCallable(rq.clone(), afterRespFunc)
|
||||||
.setRetryAndDelay(retryAndDelay)));
|
.setRetryAndDelay(retryAndDelay)));
|
||||||
|
|
||||||
start = tmpEndDate;
|
start = tmpEndDate;
|
||||||
tmpEndDate = stockCalendarDao.getOpenDateOffset(exchange, start, daySpan);
|
tmpEndDate = stockCalendarService.getOpenDateOffset(exchange, start, daySpan);
|
||||||
rq.addDateParam("start_date", start);
|
rq.addDateParam("start_date", start);
|
||||||
dataBetween -= daySpan * dataPerDay;
|
dataBetween -= daySpan * dataPerDay;
|
||||||
}
|
}
|
||||||
@@ -302,7 +258,7 @@ public class TushareCrawler {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// 交叉检查
|
// 交叉检查
|
||||||
final List<StockInfo> stockInfos = stockInfoDao.getStockListByListStatus(null);
|
final List<StockInfo> stockInfos = stockInfoService.list();
|
||||||
for(StockInfo stockInfo : stockInfos) {
|
for(StockInfo stockInfo : stockInfos) {
|
||||||
StockMarket exchange = stockInfo.getExchange();
|
StockMarket exchange = stockInfo.getExchange();
|
||||||
List<LocalDateTime> missingDates = iTradeDate.getAllMissingDates(stockInfo);
|
List<LocalDateTime> missingDates = iTradeDate.getAllMissingDates(stockInfo);
|
||||||
@@ -320,7 +276,7 @@ public class TushareCrawler {
|
|||||||
// = 2022-12-06 两天,成为范围以后将会变成更新 start 和 end 之间
|
// = 2022-12-06 两天,成为范围以后将会变成更新 start 和 end 之间
|
||||||
// 的所有数据,这样将会大大增加不必要的更新请求。
|
// 的所有数据,这样将会大大增加不必要的更新请求。
|
||||||
|
|
||||||
long dataBetween = stockCalendarDao.countOpenDaysBetween(exchange, start, end);
|
long dataBetween = stockCalendarService.countOpenDaysBetween(exchange, start, end);
|
||||||
long dataPerDay = 1;
|
long dataPerDay = 1;
|
||||||
if (stockSpan.compareTo(StockSpan.Daily) == -1 && stockSpan.compareTo(StockSpan.Minute) >= 0) {
|
if (stockSpan.compareTo(StockSpan.Daily) == -1 && stockSpan.compareTo(StockSpan.Minute) >= 0) {
|
||||||
// 分钟数,重新算下 dataBetween
|
// 分钟数,重新算下 dataBetween
|
||||||
@@ -329,7 +285,7 @@ public class TushareCrawler {
|
|||||||
dataBetween *= dataPerDay;
|
dataBetween *= dataPerDay;
|
||||||
long daySpan = singleMax / dataPerDay;
|
long daySpan = singleMax / dataPerDay;
|
||||||
|
|
||||||
LocalDateTime tmpEndDate = stockCalendarDao.getOpenDateOffset(exchange, start, daySpan);
|
LocalDateTime tmpEndDate = stockCalendarService.getOpenDateOffset(exchange, start, daySpan);
|
||||||
while (dataBetween > singleMax) {
|
while (dataBetween > singleMax) {
|
||||||
rq.addDateParam("end_date", tmpEndDate);
|
rq.addDateParam("end_date", tmpEndDate);
|
||||||
executeResult.add(es.submit(new TushareResponseCallable(rq.clone(), afterRespFunc)
|
executeResult.add(es.submit(new TushareResponseCallable(rq.clone(), afterRespFunc)
|
||||||
@@ -339,7 +295,7 @@ public class TushareCrawler {
|
|||||||
while (i < maxI && start.isBefore(missingDates.get(i + 1))) {
|
while (i < maxI && start.isBefore(missingDates.get(i + 1))) {
|
||||||
start = missingDates.get(++i);
|
start = missingDates.get(++i);
|
||||||
}
|
}
|
||||||
tmpEndDate = stockCalendarDao.getOpenDateOffset(exchange, start, daySpan);
|
tmpEndDate = stockCalendarService.getOpenDateOffset(exchange, start, daySpan);
|
||||||
rq.addDateParam("start_date", start);
|
rq.addDateParam("start_date", start);
|
||||||
dataBetween -= daySpan * dataPerDay;
|
dataBetween -= daySpan * dataPerDay;
|
||||||
}
|
}
|
||||||
@@ -357,471 +313,4 @@ public class TushareCrawler {
|
|||||||
}
|
}
|
||||||
return executeResult;
|
return executeResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <a href="https://tushare.pro/document/2?doc_id=25"><b>股票列表</b></a><br>
|
|
||||||
* <b>更新股票列表</b><br/>
|
|
||||||
* 接口:stock_basic,可以通过数据工具调试和查看数据<br>
|
|
||||||
* 描述:获取基础信息数据,包括股票代码、名称、上市日期、退市日期等<br>
|
|
||||||
* 积分:2000积分起<br>
|
|
||||||
* 包括上市、退市和暂停上市,无权限(积分不足)状态下每小时最多访问该接口 1 次
|
|
||||||
*/
|
|
||||||
public boolean updateStockList() {
|
|
||||||
try {
|
|
||||||
List<JSONObject> stockInfos = new ArrayList<>();
|
|
||||||
// Tushare 经常改请求参数的规则,2024/11/13 更新:默认不提供 list_status 时,默认值是 L
|
|
||||||
TushareRequestBody requestBody = new TushareRequestBody("stock_basic")
|
|
||||||
.addFields("ts_code,name,area,industry,market,list_status,list_date,delist_date");
|
|
||||||
|
|
||||||
requestBody.addParam("list_status", "L");
|
|
||||||
stockInfos.addAll(TushareClient.queryList(requestBody));
|
|
||||||
|
|
||||||
requestBody.addParam("list_status", "D");
|
|
||||||
stockInfos.addAll(TushareClient.queryList(requestBody));
|
|
||||||
|
|
||||||
requestBody.addParam("list_status", "P");
|
|
||||||
stockInfos.addAll(TushareClient.queryList(requestBody));
|
|
||||||
|
|
||||||
stockInfoDao.insertOrUpdateList(stockInfos);
|
|
||||||
|
|
||||||
log.info("更新股票列表完成");
|
|
||||||
return true;
|
|
||||||
} catch (JSONException | IOException e) {
|
|
||||||
log.error("更新股票列表时发生错误", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <a href="https://tushare.pro/document/2?doc_id=259"><b>同花顺概念和行业指数</b></a><br>
|
|
||||||
* 接口:ths_index<br>
|
|
||||||
* 描述:获取同花顺板块指数。注:数据版权归属同花顺,如做商业用途,请主动联系同花顺,如需帮助请联系微信migedata 。<br>
|
|
||||||
* 限量:本接口需获得600积分,单次最大5000,一次可提取全部数据,请勿循环提取。
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public boolean updateThsList() {
|
|
||||||
try {
|
|
||||||
List<JSONObject> thsIndexes = TushareClient.queryList(new TushareRequestBody("ths_index"));
|
|
||||||
stockThsListDao.insertOrUpdateList(thsIndexes);
|
|
||||||
log.info("更新同花顺板块列表完成");
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("更新同花顺板块列表时发生错误", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <a href="https://tushare.pro/document/2?doc_id=261"><b>同花顺概念板块成分</b></a><br>
|
|
||||||
* 接口:ths_member<br>
|
|
||||||
* 描述:获取同花顺概念板块成分列表注:数据版权归属同花顺,如做商业用途,请主动联系同花顺。<br>
|
|
||||||
* 限量:用户积累5000积分可调取,可按概念板块代码循环提取所有成分<br>
|
|
||||||
*/
|
|
||||||
public void updateThsMember() {
|
|
||||||
TushareRequestBody baseRequest = new TushareRequestBody("ths_member").addFields("ts_code,con_code,con_name,weight,in_date,out_date,is_new");
|
|
||||||
Function<List<JSONObject>, Boolean> function = (t) -> {
|
|
||||||
stockThsMemberDao.insertOrUpdateList(t);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
List<ThsStockInfo> stockInfoList = stockThsListDao.listByExchange(null);
|
|
||||||
while (true) {
|
|
||||||
List<Future<TushareCrawlerResult>> executeResult = rollingQueryByStock(baseRequest, stockInfoList, null, function, 5000L, QueryWay.ByStock, null);
|
|
||||||
|
|
||||||
stockInfoList.clear();
|
|
||||||
for (Future<TushareCrawlerResult> f : executeResult) {
|
|
||||||
try {
|
|
||||||
TushareCrawlerResult result = f.get();
|
|
||||||
JSONObject request = result.getRequest();
|
|
||||||
if (result.isFatal()) {
|
|
||||||
log.error("同花顺概念板块成分数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
|
||||||
}
|
|
||||||
else if (!result.isSuccess()) {
|
|
||||||
log.warn("同花顺概念板块成分数据未获取:{}", request.toJSONString());
|
|
||||||
stockInfoList.add(request.getJSONObject("params").to(ThsStockInfo.class));
|
|
||||||
}
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
|
||||||
log.error("同花顺概念板块成分数据执行结果时发生错误", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (stockInfoList.size() == 0) {
|
|
||||||
log.info("同花顺概念板块成分更新完成");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
log.info("重新获取未获取成功的同花顺概念板块成分");
|
|
||||||
}
|
|
||||||
if (baseRequest.hasFingerprint()) {
|
|
||||||
redis.getBucket(baseRequest.getFingerprint()).delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <a href="https://tushare.pro/document/2?doc_id=260"><b>同花顺板块指数行情</b></a><br>
|
|
||||||
* 接口:ths_daily<br>
|
|
||||||
* 描述:获取同花顺板块指数行情。注:数据版权归属同花顺,如做商业用途,请主动联系同花顺,如需帮助请联系微信migedata 。<br>
|
|
||||||
* 限量:单次最大3000行数据,可根据指数代码、日期参数循环提取。<br>
|
|
||||||
* @param queryWay
|
|
||||||
*/
|
|
||||||
public void updateThsDaily(QueryWay queryWay) {
|
|
||||||
TushareRequestBody baseRequest = new TushareRequestBody("ths_daily").addFields("ts_code,trade_date,close,open,high,low,pre_close,avg_price,change,pct_change,vol,turnover_rate,total_mv,float_mv");
|
|
||||||
Function<List<JSONObject>, Boolean> function = (t) -> {
|
|
||||||
stockThsDailyDao.insertOrUpdateList(t);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
List<Future<TushareCrawlerResult>> executeResult;
|
|
||||||
if (queryWay.compareTo(QueryWay.ByStock) >= 0) {
|
|
||||||
executeResult = rollingQueryByStock(baseRequest, stockThsListDao.listByExchange(ThsStockMarket.A), stockThsDailyDao, function, 3000L, queryWay, null);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
executeResult = rollingQueryByDate(baseRequest, stockThsDailyDao, function, queryWay, THS_DAILY_BEGIN_LOCALDATE);
|
|
||||||
}
|
|
||||||
log.info("同花顺板块指数行情更新完成");
|
|
||||||
for (Future<TushareCrawlerResult> f : executeResult) {
|
|
||||||
try {
|
|
||||||
TushareCrawlerResult result = f.get();
|
|
||||||
JSONObject request = result.getRequest();
|
|
||||||
if (result.isFatal()) {
|
|
||||||
log.error("同花顺板块指数行情数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
|
||||||
}
|
|
||||||
else if (!result.isSuccess()) {
|
|
||||||
log.warn("同花顺板块指数行情数据未获取:{}", request.toJSONString());
|
|
||||||
}
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
|
||||||
log.error("同花顺板块指数行情数据执行结果时发生错误", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (baseRequest.hasFingerprint()) {
|
|
||||||
redis.getBucket(baseRequest.getFingerprint()).delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <b><a href="https://waditu.com/document/2?doc_id=27">更新日K数据</a></b><br/>
|
|
||||||
* 基础积分每分钟内最多调取500次,每次5000条数据,相当于23年历史,用户获得超过5000积分正常调取无频次限制。
|
|
||||||
* <br>
|
|
||||||
* <font color="red">请先更新交易日历和股票列表后再调用该方法,否则可能造成股票日 K 数据缺失</font>
|
|
||||||
* @param queryWay 滚动查询方式
|
|
||||||
* @see TushareCrawler#rollingQueryByDate
|
|
||||||
* @see TushareCrawler#rollingQueryByStock
|
|
||||||
*/
|
|
||||||
public void updateStockDaily(QueryWay queryWay) {
|
|
||||||
TushareRequestBody baseRequest = new TushareRequestBody("daily")
|
|
||||||
.addFields("ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount");
|
|
||||||
Function<List<JSONObject>, Boolean> function = (t) -> {
|
|
||||||
stockDailyDao.insertOrUpdateList(t);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
List<Future<TushareCrawlerResult>> executeResult;
|
|
||||||
if (queryWay.compareTo(QueryWay.ByStock) >= 0) {
|
|
||||||
executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), stockDailyDao, function, 5000L, queryWay, null);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
executeResult = rollingQueryByDate(baseRequest, stockDailyDao, function, queryWay, null);
|
|
||||||
}
|
|
||||||
log.info("日 K 数据更新完成");
|
|
||||||
for (Future<TushareCrawlerResult> f : executeResult) {
|
|
||||||
try {
|
|
||||||
TushareCrawlerResult result = f.get();
|
|
||||||
JSONObject request = f.get().getRequest();
|
|
||||||
if (result.isFatal()) {
|
|
||||||
log.error("日 K 数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
|
||||||
}
|
|
||||||
else if (!result.isSuccess()) {
|
|
||||||
log.warn("日 K 数据未获取:{}", request.toJSONString());
|
|
||||||
}
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
|
||||||
log.error("检查日 K 数据执行结果时发生错误", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (baseRequest.hasFingerprint()) {
|
|
||||||
redis.getBucket(baseRequest.getFingerprint()).delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* <b><a href="https://tushare.pro/document/1?doc_id=290">更新分钟K数据</a></b><br/>
|
|
||||||
* <p>有权限时,每分钟500次,每次8000行数据,总量不限制</p>
|
|
||||||
* <font color="red">请先更新交易日历和股票列表后再调用该方法,否则可能造成股票分钟 K 数据缺失</font>
|
|
||||||
* @param queryWay 滚动查询方式,仅支持 StockSpan.ByStock... 系列
|
|
||||||
* @param stockSpan 股票粒度,仅支持 StockSpan.Minute - StockSpan.SixtyMinute
|
|
||||||
* @see TushareCrawler#rollingQueryByDate
|
|
||||||
* @see TushareCrawler#rollingQueryByStock
|
|
||||||
* @see link.at17.mid.tushare.enums.StockSpan
|
|
||||||
*/
|
|
||||||
public void updateStockMinValue(QueryWay queryWay, StockSpan stockSpan) {
|
|
||||||
Assert.isTrue(Objects.nonNull(stockSpan), "stockSpan 不允许为空");
|
|
||||||
Assert.isTrue(Objects.nonNull(stockSpan.getMin()), "不支持的 StockSpan:" + stockSpan + ", 仅支持分钟数据类型");
|
|
||||||
Assert.isTrue(queryWay.compareTo(QueryWay.ByStock) >= 0, "不支持的 QueryWay!");
|
|
||||||
String freq = stockSpan.getMin() + "min";
|
|
||||||
TushareRequestBody baseRequest = new TushareRequestBody("stk_mins")
|
|
||||||
.addFields("ts_code,trade_time,open,close,high,low,vol,amount")
|
|
||||||
.addParam("freq", freq);
|
|
||||||
Function<List<JSONObject>, Boolean> function = (t) -> {
|
|
||||||
stockMinuteDao.insertOrUpdateList(stockSpan, t);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
// 特殊重写:获取最新日期和所有交易日,该重写仅对更新数据生效
|
|
||||||
ITsTradeDate minTradeDate = new ITsTradeDate() {
|
|
||||||
@Override
|
|
||||||
public LocalDateTime getLatestTradeDate(ITsStockInfo stockInfo) {
|
|
||||||
return stockMinuteDao.getLatestTradeDate(stockSpan, stockInfo);
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public List<LocalDateTime> getAllTradeDates(ITsStockInfo stockInfo) {
|
|
||||||
return stockMinuteDao.getAllTradeDates(stockSpan, stockInfo);
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public List<LocalDateTime> getAllMissingDates(ITsStockInfo stockInfo) {
|
|
||||||
return stockMinuteDao.getAllMissingDates(stockSpan, stockInfo);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
List<Future<TushareCrawlerResult>> executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), minTradeDate, function, 8000L, queryWay, stockSpan);
|
|
||||||
log.info("{} 分钟 K 数据更新完成", stockSpan.getMin());
|
|
||||||
for (Future<TushareCrawlerResult> f : executeResult) {
|
|
||||||
try {
|
|
||||||
TushareCrawlerResult result = f.get();
|
|
||||||
JSONObject request = f.get().getRequest();
|
|
||||||
if (result.isFatal()) {
|
|
||||||
log.error("{} 分钟 K 数据未获取:{},发生致命错误,将不会重试:{}", stockSpan.getMin(), request.toJSONString());
|
|
||||||
}
|
|
||||||
else if (!result.isSuccess()) {
|
|
||||||
log.warn("{} 分钟 K 数据未获取:{}", stockSpan.getMin(), request.toJSONString());
|
|
||||||
}
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
|
||||||
log.error("检查 {} 分钟 K 数据执行结果时发生错误\r\n{}", stockSpan.getMin(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (baseRequest.hasFingerprint()) {
|
|
||||||
redis.getBucket(baseRequest.getFingerprint()).delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取个股每日指标
|
|
||||||
* <blockquote>
|
|
||||||
* 接口:daily_basic<br>
|
|
||||||
* 更新时间:交易日每日15点~17点之间<br>
|
|
||||||
* 描述:获取全部股票每日重要的基本面指标,可用于选股分析、报表展示等。<br>
|
|
||||||
* 积分:用户需要至少600积分才可以调取,具体请参阅
|
|
||||||
* <a href="https://tushare.pro/document/1?doc_id=13">积分获取办法</a><br>
|
|
||||||
* </blockquote>
|
|
||||||
*/
|
|
||||||
public void updateDailyBasic(QueryWay queryWay) {
|
|
||||||
TushareRequestBody baseRequest = new TushareRequestBody("daily_basic");
|
|
||||||
baseRequest.addFields("ts_code,trade_date,close,turnover_rate,turnover_rate_f,volume_ratio,pe,pe_ttm,pb,ps,ps_ttm,dv_ratio,dv_ttm,total_share,float_share,free_share,total_mv,circ_mv");
|
|
||||||
Function<List<JSONObject>, Boolean> function = (t) -> {
|
|
||||||
stockDailyBasicDao.insertOrUpdateList(t);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
List<Future<TushareCrawlerResult>> executeResult;
|
|
||||||
if (queryWay.compareTo(QueryWay.ByStock) >= 0) {
|
|
||||||
executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), stockDailyBasicDao, function, 5000L, queryWay, null);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
executeResult = rollingQueryByDate(baseRequest, stockDailyBasicDao, function, queryWay, null);
|
|
||||||
}
|
|
||||||
log.info("每日指标数据更新完成");
|
|
||||||
for (Future<TushareCrawlerResult> f : executeResult) {
|
|
||||||
try {
|
|
||||||
TushareCrawlerResult result = f.get();
|
|
||||||
JSONObject request = f.get().getRequest();
|
|
||||||
if (result.isFatal()) {
|
|
||||||
log.error("每日指标数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
|
||||||
}
|
|
||||||
else if (!result.isSuccess()) {
|
|
||||||
log.warn("每日指标数据未获取:{}", request.toJSONString());
|
|
||||||
}
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
|
||||||
log.error("检查每日指标数据执行结果时发生错误", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (baseRequest.hasFingerprint()) {
|
|
||||||
redis.getBucket(baseRequest.getFingerprint()).delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <b><a href="https://waditu.com/document/2?doc_id=28">Tushare 复权因子</a></b><br>
|
|
||||||
* <blockquote>
|
|
||||||
* 接口:adj_factor<br>
|
|
||||||
* 更新时间:早上9点30分<br>
|
|
||||||
* 描述:获取股票复权因子,可提取单只股票全部历史复权因子,也可以提取单日全部股票的复权因子。<br>
|
|
||||||
* </blockquote>
|
|
||||||
* 虽然文档没说,但当这个接口请求单支股的所有数据时,也是 5000 条限制<br>
|
|
||||||
* <font color="red">请先更新交易日历和股票列表后再调用该方法,否则可能造成股票复权因子缺失</font>
|
|
||||||
*/
|
|
||||||
public void updateStockAdjustTushare(QueryWay queryWay) {
|
|
||||||
|
|
||||||
TushareRequestBody baseRequest = new TushareRequestBody("adj_factor");
|
|
||||||
Function<List<JSONObject>, Boolean> function = (t) -> {
|
|
||||||
stockAdjustDao.insertOrUpdateListTushare(t);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
List<Future<TushareCrawlerResult>> executeResult;
|
|
||||||
if (queryWay.compareTo(QueryWay.ByStock) >= 0) {
|
|
||||||
executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), stockAdjustDao, function, 5000L, queryWay, null);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
executeResult = rollingQueryByDate(baseRequest, stockAdjustDao, function, queryWay, null);
|
|
||||||
}
|
|
||||||
log.info("复权数据更新完成");
|
|
||||||
for (Future<TushareCrawlerResult> f : executeResult) {
|
|
||||||
try {
|
|
||||||
TushareCrawlerResult result = f.get();
|
|
||||||
JSONObject request = f.get().getRequest();
|
|
||||||
if (result.isFatal()) {
|
|
||||||
log.error("复权数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
|
||||||
}
|
|
||||||
else if (!result.isSuccess()) {
|
|
||||||
log.info("复权数据未获取:{}", request.toJSONString());
|
|
||||||
}
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
|
||||||
log.error("检查复权数据执行结果时发生错误", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (baseRequest.hasFingerprint()) {
|
|
||||||
redis.getBucket(baseRequest.getFingerprint()).delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <b><a href="https://waditu.com/document/2?doc_id=298">Tushare 涨跌停列表</a></b><br>
|
|
||||||
* <ul>
|
|
||||||
* <li>接口:limit_list_d<br>
|
|
||||||
* <li>描述:获取沪深A股每日涨跌停、炸板数据情况,数据从2020年开始<br>
|
|
||||||
* <li>限量:单次最大可以获取500条数据,可通过日期或者股票循环提取<br>
|
|
||||||
* <li>积分:120积分可查看数据,5000积分每分钟可以请求200次,8000积分以上每分钟500次,具体请参阅积分获取办法<br>
|
|
||||||
* </ul>
|
|
||||||
*/
|
|
||||||
public void updateStockLimit(QueryWay queryWay) {
|
|
||||||
|
|
||||||
TushareRequestBody baseRequest = new TushareRequestBody("limit_list_d");
|
|
||||||
Function<List<JSONObject>, Boolean> function = (t) -> {
|
|
||||||
stockLimitDao.insertOrUpdateListTushare(t);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
List<Future<TushareCrawlerResult>> executeResult;
|
|
||||||
if (queryWay.compareTo(QueryWay.ByStock) >= 0) {
|
|
||||||
executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), stockLimitDao, function, 500L, queryWay, null);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
executeResult = rollingQueryByDate(baseRequest, stockLimitDao, function, queryWay, LocalDateTime.of(2019, 11, 28, 0, 0, 0));
|
|
||||||
}
|
|
||||||
log.info("涨跌停数据更新完成");
|
|
||||||
for (Future<TushareCrawlerResult> f : executeResult) {
|
|
||||||
try {
|
|
||||||
TushareCrawlerResult result = f.get();
|
|
||||||
JSONObject request = f.get().getRequest();
|
|
||||||
if (result.isFatal()) {
|
|
||||||
log.error("涨跌停数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
|
||||||
}
|
|
||||||
else if (!result.isSuccess()) {
|
|
||||||
log.info("涨跌停数据未获取:{}", request.toJSONString());
|
|
||||||
}
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
|
||||||
log.error("检查涨跌停数据执行结果时发生错误", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (baseRequest.hasFingerprint()) {
|
|
||||||
redis.getBucket(baseRequest.getFingerprint()).delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* <b><a href="https://tushare.pro/document/2?doc_id=61">前十大股东</a>/<a href="https://tushare.pro/document/2?doc_id=62">前十大流通股东</a></b><br>
|
|
||||||
* <blockquote>
|
|
||||||
* 接口:top10_holders/top10_floatholders<br>
|
|
||||||
* 更新时间:不定时,报告日<br>
|
|
||||||
* 描述:获取上市公司前十大股东数据,包括持有数量和比例等信息/获取上市公司前十大流通股东数据,不包括比例信息<br>
|
|
||||||
* </blockquote>
|
|
||||||
* 文档说单次 100 条限制,但实际测试有单次 5000 条<br>
|
|
||||||
* <font color="red">请先更新交易日历和股票列表后再调用该方法</font><br>
|
|
||||||
* <font color="red">如果是 A+H 或者 A+B 股,可能存在同股票存在多个同名股东,且持股数据不一致。需要仔细研究如何清洗和使用</font>
|
|
||||||
*/
|
|
||||||
public void updateStockHolder(StockHolderType holderType) {
|
|
||||||
|
|
||||||
boolean isFloat = holderType.getIsFloat() == 1;
|
|
||||||
TushareRequestBody baseRequest = new TushareRequestBody(isFloat ? "top10_floatholders" : "top10_holders");
|
|
||||||
Function<List<JSONObject>, Boolean> function = (t) -> {
|
|
||||||
Map<String, JSONObject> map = new HashMap<>();
|
|
||||||
for (JSONObject jo : t) {
|
|
||||||
String holderName = jo.getString("holder_name");
|
|
||||||
String[] temp = {jo.getString("ts_code"), jo.getString("end_date"), holderName, holderType.name()};
|
|
||||||
String key = String.join("_", temp);
|
|
||||||
Integer holderOffset = 1;
|
|
||||||
while (map.containsKey(key)) {
|
|
||||||
log.warn("存在重复的 key {},开始重命名", key);
|
|
||||||
temp[2] = holderName + '^' + holderOffset++;
|
|
||||||
jo.put("holder_name", temp[2]);
|
|
||||||
key = String.join("_", temp);
|
|
||||||
}
|
|
||||||
map.put(key, jo);
|
|
||||||
}
|
|
||||||
stockHolderDao.insertOrUpdateListTushare(t, holderType);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
List<Future<TushareCrawlerResult>> executeResult;
|
|
||||||
executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), stockHolderDao, function, 5000L, QueryWay.ByStock, null);
|
|
||||||
String banner = "十大" + (isFloat ? "流通股东" : "股东");
|
|
||||||
log.info("{}数据更新完成", banner);
|
|
||||||
for (Future<TushareCrawlerResult> f : executeResult) {
|
|
||||||
try {
|
|
||||||
TushareCrawlerResult result = f.get();
|
|
||||||
JSONObject request = f.get().getRequest();
|
|
||||||
if (result.isFatal()) {
|
|
||||||
log.error("{}数据未获取,发生致命错误,将不会重试:{}", banner, request.toJSONString());
|
|
||||||
}
|
|
||||||
else if (!result.isSuccess()) {
|
|
||||||
log.info("{}数据未获取:{}", banner, request.toJSONString());
|
|
||||||
}
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
|
||||||
log.error("检查" + banner + "数据执行结果时发生错误", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (baseRequest.hasFingerprint()) {
|
|
||||||
redis.getBucket(baseRequest.getFingerprint()).delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <b>更新交易日历</b>
|
|
||||||
* <p>
|
|
||||||
* 更新当前交易日历到本年度的最后一天
|
|
||||||
* </p>
|
|
||||||
* @param stockMarkets 市场类型,若未提供则默认更新上交所和深交所
|
|
||||||
*/
|
|
||||||
public boolean updateStockCalendar(StockMarket...stockMarkets) {
|
|
||||||
LocalDate lastDayOfThisYear = LocalDateTimeUtils.getLastDayOfThisYear();
|
|
||||||
if (stockMarkets == null || stockMarkets.length == 0) {
|
|
||||||
stockMarkets = new StockMarket[] {StockMarket.SZ, StockMarket.SH};
|
|
||||||
}
|
|
||||||
for (StockMarket stockMarket : stockMarkets) {
|
|
||||||
|
|
||||||
log.debug("正在更新 {} 交易所的交易日历", stockMarket);
|
|
||||||
|
|
||||||
LocalDate latest = stockCalendarDao.getLatestLocalDate(stockMarket);
|
|
||||||
if (latest != null && latest.equals(lastDayOfThisYear)){
|
|
||||||
log.debug("{} 交易所的日历已更新到本年度最后一日,跳过", stockMarket);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
TushareRequestBody rq = new TushareRequestBody("trade_cal")
|
|
||||||
.addParam("exchange", stockMarket.getExchangeCode())
|
|
||||||
.addDateParam("end_date", lastDayOfThisYear)
|
|
||||||
.addFields("exchange,cal_date,is_open,pretrade_date");
|
|
||||||
if (latest != null) {
|
|
||||||
rq.addDateParam("start_date", latest);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
List<JSONObject> list = TushareClient.queryList(rq);
|
|
||||||
stockCalendarDao.insertOrUpdateList(list);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
log.error("JSONException", e);
|
|
||||||
return false;
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("IOException", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import java.util.regex.Pattern;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo;
|
import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo;
|
||||||
import link.at17.mid.tushare.enums.ListStatus;
|
import link.at17.mid.tushare.enums.ListStatus;
|
||||||
@@ -16,6 +17,7 @@ import lombok.experimental.Accessors;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Accessors(chain=true)
|
@Accessors(chain=true)
|
||||||
|
@TableName("stock_info")
|
||||||
public class StockInfo implements ITsStockInfo {
|
public class StockInfo implements ITsStockInfo {
|
||||||
|
|
||||||
private static final Pattern EM_CODE_PATTERN = Pattern.compile("^(0|1|90)\\.(.*?)$");
|
private static final Pattern EM_CODE_PATTERN = Pattern.compile("^(0|1|90)\\.(.*?)$");
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package link.at17.mid.tushare.data.models;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class UpdateLog {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package link.at17.mid.tushare.data.models;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonView;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.data.typehandler.JsonListTypeHandler;
|
||||||
|
import link.at17.mid.tushare.system.util.EncryptUtils;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class UpdateMethodInfo {
|
||||||
|
|
||||||
|
private String methodName;
|
||||||
|
|
||||||
|
private String declaringClassName;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@TableField(typeHandler = JsonListTypeHandler.class)
|
||||||
|
private List<UpdateParamInfo> params = new ArrayList<>();
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
if (StringUtils.isBlank(id) && !StringUtils.isAnyBlank(methodName, declaringClassName)) {
|
||||||
|
id = EncryptUtils.sha256(declaringClassName + '$' + methodName);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class UpdateParamInfo {
|
||||||
|
private String fullTypeName;
|
||||||
|
private String typeName;
|
||||||
|
private String name;
|
||||||
|
@JsonIgnore
|
||||||
|
private Class<?> typeClass;
|
||||||
|
private List<String> allowedEnumValues;
|
||||||
|
private Object value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package link.at17.mid.tushare.data.models;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import link.at17.mid.tushare.data.typehandler.UpdateMethodInfoListTypeHandler;
|
||||||
|
import link.at17.mid.tushare.data.validator.ValidCron;
|
||||||
|
import link.at17.mid.tushare.data.validator.ValidUpdateMethodInfo;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName(value="update_plan", autoResultMap = true) // 使用自定义类型转换器必须使 autoResultMap = true
|
||||||
|
@NotNull
|
||||||
|
@Validated
|
||||||
|
public class UpdatePlan {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* id
|
||||||
|
*/
|
||||||
|
@TableId(type=IdType.AUTO)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计划任务表达式
|
||||||
|
*/
|
||||||
|
@ValidCron
|
||||||
|
@NotBlank
|
||||||
|
private String cronExpr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计划名称
|
||||||
|
*/
|
||||||
|
@NotBlank
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计划内方法列表
|
||||||
|
*/
|
||||||
|
@TableField(typeHandler=UpdateMethodInfoListTypeHandler.class)
|
||||||
|
@NotEmpty
|
||||||
|
@Valid
|
||||||
|
private List<@ValidUpdateMethodInfo UpdateMethodInfo> methods = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否进行交易日检查
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
private Boolean openDayCheck = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
private Boolean enabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否有效
|
||||||
|
*/
|
||||||
|
@TableField(exist=false)
|
||||||
|
private Boolean valid = true;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package link.at17.mid.tushare.data.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.redisson.api.RedissonClient;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.annotation.UpdateMethod;
|
||||||
|
import link.at17.mid.tushare.dao.StockAdjustDao;
|
||||||
|
import link.at17.mid.tushare.data.crawler.QueryWay;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawlerResult;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody;
|
||||||
|
import link.at17.mid.tushare.data.validator.AllowedEnum;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class StockAdjustService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
StockInfoService stockInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
StockAdjustDao stockAdjustDao;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TushareCrawler tushareCrawler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
RedissonClient redis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于插入从 Tushare 获取的内容
|
||||||
|
* @param list
|
||||||
|
*/
|
||||||
|
public void insertOrUpdateList(List<JSONObject> list) {
|
||||||
|
stockAdjustDao.insertOrUpdateList(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <b><a href="https://waditu.com/document/2?doc_id=28">Tushare 复权因子</a></b><br>
|
||||||
|
* <blockquote>
|
||||||
|
* 接口:adj_factor<br>
|
||||||
|
* 更新时间:早上9点30分<br>
|
||||||
|
* 描述:获取股票复权因子,可提取单只股票全部历史复权因子,也可以提取单日全部股票的复权因子。<br>
|
||||||
|
* </blockquote>
|
||||||
|
* 虽然文档没说,但当这个接口请求单支股的所有数据时,也是 5000 条限制<br>
|
||||||
|
* <font color="red">请先更新交易日历和股票列表后再调用该方法,否则可能造成股票复权因子缺失</font>
|
||||||
|
*/
|
||||||
|
@UpdateMethod(name="股票复权因子", order=4)
|
||||||
|
public boolean updateData(
|
||||||
|
@AllowedEnum({"ByDateUpdate", "ByStockCrossCheck"})
|
||||||
|
QueryWay queryWay) {
|
||||||
|
|
||||||
|
TushareRequestBody baseRequest = new TushareRequestBody("adj_factor");
|
||||||
|
Function<List<JSONObject>, Boolean> function = (t) -> {
|
||||||
|
stockAdjustDao.insertOrUpdateList(t);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
List<Future<TushareCrawlerResult>> executeResult;
|
||||||
|
if (queryWay.compareTo(QueryWay.ByStock) >= 0) {
|
||||||
|
executeResult = tushareCrawler.rollingQueryByStock(baseRequest, stockInfoService.list(), stockAdjustDao, function, 5000L, queryWay, null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
executeResult = tushareCrawler.rollingQueryByDate(baseRequest, stockAdjustDao, function, queryWay, null);
|
||||||
|
}
|
||||||
|
log.info("复权数据更新完成");
|
||||||
|
for (Future<TushareCrawlerResult> f : executeResult) {
|
||||||
|
try {
|
||||||
|
TushareCrawlerResult result = f.get();
|
||||||
|
JSONObject request = f.get().getRequest();
|
||||||
|
if (result.isFatal()) {
|
||||||
|
log.error("复权数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
||||||
|
}
|
||||||
|
else if (!result.isSuccess()) {
|
||||||
|
log.info("复权数据未获取:{}", request.toJSONString());
|
||||||
|
}
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
log.error("检查复权数据执行结果时发生错误", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (baseRequest.hasFingerprint()) {
|
||||||
|
redis.getBucket(baseRequest.getFingerprint()).delete();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
package link.at17.mid.tushare.data.service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.temporal.Temporal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONException;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.annotation.UpdateMethod;
|
||||||
|
import link.at17.mid.tushare.dao.StockCalendarDao;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareClient;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody;
|
||||||
|
import link.at17.mid.tushare.data.models.StockCalendar;
|
||||||
|
import link.at17.mid.tushare.enums.StockMarket;
|
||||||
|
import link.at17.mid.tushare.service.BaseServiceImpl;
|
||||||
|
import link.at17.mid.tushare.system.util.LocalDateTimeUtils;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class StockCalendarService extends BaseServiceImpl<StockCalendarDao, StockCalendar> {
|
||||||
|
|
||||||
|
private static final StockMarket[] UPDATE_SUPPORTED_STOCK_MARKETS = new StockMarket[] {StockMarket.SZ, StockMarket.SH};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://tushare.pro/document/2?doc_id=26"><b>交易日历</b></a><br>
|
||||||
|
* <b>更新交易日历</b>
|
||||||
|
* <p>
|
||||||
|
* 描述:获取各大交易所交易日历数据,默认提取的是上交所<br>
|
||||||
|
* 积分:需2000积分
|
||||||
|
* </p>
|
||||||
|
* @param stockMarkets 市场类型,若未提供则默认更新上交所和深交所
|
||||||
|
*/
|
||||||
|
@UpdateMethod(name="交易日历", order=0)
|
||||||
|
public boolean updateData() {
|
||||||
|
|
||||||
|
LocalDate lastDayOfThisYear = LocalDateTimeUtils.getLastDayOfThisYear();
|
||||||
|
|
||||||
|
for (StockMarket stockMarket : UPDATE_SUPPORTED_STOCK_MARKETS) {
|
||||||
|
|
||||||
|
log.debug("正在更新 {} 交易所的交易日历", stockMarket);
|
||||||
|
|
||||||
|
LocalDate latest = getLatestLocalDate(stockMarket);
|
||||||
|
if (latest != null && latest.equals(lastDayOfThisYear)){
|
||||||
|
log.debug("{} 交易所的日历已更新到本年度最后一日,跳过", stockMarket);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TushareRequestBody rq = new TushareRequestBody("trade_cal")
|
||||||
|
.addParam("exchange", stockMarket.getExchangeCode())
|
||||||
|
.addDateParam("end_date", lastDayOfThisYear)
|
||||||
|
.addFields("exchange,cal_date,is_open,pretrade_date");
|
||||||
|
if (latest != null) {
|
||||||
|
rq.addDateParam("start_date", latest);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
List<JSONObject> list = TushareClient.queryList(rq);
|
||||||
|
baseMapper.insertOrUpdateList(list);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
log.error("JSONException", e);
|
||||||
|
return false;
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("IOException", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入从 Tushare 获取的数据
|
||||||
|
* @param list
|
||||||
|
* @return 插入是否成功
|
||||||
|
*/
|
||||||
|
public boolean insertOrUpdateList(List<JSONObject> list) {
|
||||||
|
return SqlHelper.retBool(baseMapper.insertOrUpdateList(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定证交所两个日期(含)之间的交易日个数<br>
|
||||||
|
* <p>
|
||||||
|
* 存储在数据库内的类型为无时分秒的 timestamp 类型,可视作 {@code java.time.LocalDate} <br>
|
||||||
|
* 若传入的是 java.time.LocalDateTime 类型,会忽略时分秒
|
||||||
|
* <p>例:
|
||||||
|
* <ul><li>
|
||||||
|
* <code>startDate : LocalDateTime 2021-08-16 01:20:30</code><br>
|
||||||
|
* <code>endDate : LocalDateTime 2021-08-20 19:20:30</code><br>
|
||||||
|
* 返回 5
|
||||||
|
* <li><code>countOpenDaysBetween(StockMarket.<b><i>SH</i></b>, startDate, endDate)</code> 其中<br>
|
||||||
|
* <code>startDate : LocalDateTime 2021-08-16 01:20:30</code><br>
|
||||||
|
* <code>endDate : LocalDateTime 2021-08-16 12:34:56</code><br>
|
||||||
|
* 返回 1
|
||||||
|
* <li><code>countOpenDaysBetween(StockMarket.<b><i>SH</i></b>, startDate, endDate)</code> 其中<br>
|
||||||
|
* <code>startDate : LocalDateTime 2021-08-15 01:20:30</code><br>
|
||||||
|
* <code>endDate : LocalDateTime 2021-08-16 12:34:56</code><br>
|
||||||
|
* 返回 1
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
* @param exchange 股市类型枚举,若提供的枚举不为 SZ 或 SH,则采用 SZ + SH 的所有交易日并去重计算
|
||||||
|
* @param startDate 开始日期
|
||||||
|
* @param endDate 结束日期
|
||||||
|
* @return
|
||||||
|
|
||||||
|
*/
|
||||||
|
public long countOpenDaysBetween(StockMarket exchange, Temporal startDate, Temporal endDate) {
|
||||||
|
return baseMapper.countOpenDaysBetween(exchange, startDate, endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定证交所两个日期(含)之间的所有交易日<br>
|
||||||
|
* 存储在数据库内的类型为无时分秒的 timestamp 类型,可视作 {@code java.time.LocalDate} <br>
|
||||||
|
* 若传入的是 java.time.LocalDateTime 类型,会忽略时分秒
|
||||||
|
* <p>例:
|
||||||
|
* <ul><li><code>countOpenDaysBetween(StockMarket.<b><i>SH</i></b>, startDate, endDate)</code> 其中<br>
|
||||||
|
* <code>startDate : LocalDateTime 2021-08-16 01:20:30</code><br>
|
||||||
|
* <code>endDate : LocalDateTime 2021-08-20 19:20:30</code><br>
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* 返回:
|
||||||
|
* <ul>
|
||||||
|
* <li>2021-08-16
|
||||||
|
* <li>2021-08-17
|
||||||
|
* <li>2021-08-18
|
||||||
|
* <li>2021-08-19
|
||||||
|
* <li>2021-08-20
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
* @param exchange 股市类型枚举,若提供的枚举不为 SZ 或 SH,则采用 SZ + SH 的所有交易日并去重获得结果
|
||||||
|
* @param startDate 开始日期(含)
|
||||||
|
* @param endDate 结束日期(含)
|
||||||
|
* @return
|
||||||
|
|
||||||
|
*/
|
||||||
|
public List<LocalDateTime> getAllOpenDatesBetween(StockMarket exchange, Temporal startDate, Temporal endDate){
|
||||||
|
return baseMapper.getAllOpenDatesBetween(exchange, startDate, endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定证交所指定日期偏移的指定交易日
|
||||||
|
* @param exchange
|
||||||
|
* @param date
|
||||||
|
* @param offset
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public LocalDateTime getOpenDateOffset(StockMarket exchange, Temporal date, long offset) {
|
||||||
|
return baseMapper.getOpenDateOffset(exchange, date, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定证交所指定日期(含)以后的所有交易日
|
||||||
|
* @param exchange
|
||||||
|
* @param sence 留空则查询所有
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<LocalDateTime> getAllOpenDateSence(StockMarket exchange, @Nullable Temporal sence) {
|
||||||
|
return baseMapper.getAllOpenDateSence(exchange, sence);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断指定日是否为指定交易所的交易日
|
||||||
|
* @param date 会截断时分秒,只取年月日
|
||||||
|
* @param exchange 目前只支持 SSE(深交所)和 SHSE(上交所)
|
||||||
|
* @return
|
||||||
|
* @see #isOpen(Temporal)
|
||||||
|
*/
|
||||||
|
public boolean isOpen(@NonNull Temporal date, @Nullable StockMarket exchange) {
|
||||||
|
return baseMapper.isOpen(date, exchange);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断指定日是否为任意交易所的交易日
|
||||||
|
* @param date 会截断时分秒,只取年月日
|
||||||
|
* @return
|
||||||
|
* @see #isOpen(Temporal, StockMarket)
|
||||||
|
*/
|
||||||
|
public boolean isOpen(@NonNull Temporal date) {
|
||||||
|
return baseMapper.isOpen(date, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库内指定证交所的最早一个交易日历
|
||||||
|
* <p>仅为日期,并未指定是否是开市日
|
||||||
|
* @param stockMarket 指定证交所,若为 {@code null} 则取最早一条(市场不确定)
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public StockCalendar getGreatest(@Nullable StockMarket stockMarket) {
|
||||||
|
return baseMapper.getGreatest(stockMarket);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库内指定证交所的最早一个交易日历
|
||||||
|
* <p>仅为日期,并未指定是否是开市日。取最早一条(市场不确定)
|
||||||
|
* @see #getGreatest(StockMarket)
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public StockCalendar getGreatest() {
|
||||||
|
return getGreatest(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库内指定证交所的最早一个交易日历的日期
|
||||||
|
* <p>仅为日期,并未指定是否是开市日。取最早一条(市场不确定)
|
||||||
|
* @see #getGreatest
|
||||||
|
* @return {@code LocalDate} 或 {@code null}
|
||||||
|
*/
|
||||||
|
public LocalDate getGreatestLocalDate() {
|
||||||
|
return getGreatestLocalDate(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库内指定证交所的最早一个交易日历的日期
|
||||||
|
* <p>仅为日期,并未指定是否是开市日
|
||||||
|
* @see #getGreatest
|
||||||
|
* @param stockMarket 指定证交所,若为 {@code null} 则取最早一条(市场不确定)
|
||||||
|
* @return {@code LocalDate} 或 {@code null}
|
||||||
|
*/
|
||||||
|
public LocalDate getGreatestLocalDate(@Nullable StockMarket stockMarket) {
|
||||||
|
StockCalendar stockCalendar = getGreatest(stockMarket);
|
||||||
|
if (stockCalendar != null) {
|
||||||
|
return stockCalendar.getDate().atStartOfDay().toLocalDate();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库内指定证交所的最早一个交易日历的日期时间
|
||||||
|
* <p>仅为日期,并未指定是否是开市日。取最早一条(市场不确定)
|
||||||
|
* @see #getGreatest
|
||||||
|
* @return {@code LocalDate} 或 {@code null}
|
||||||
|
*/
|
||||||
|
public LocalDateTime getGreatestLocalDateTime() {
|
||||||
|
return getGreatestLocalDateTime(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库内指定证交所的最早一个交易日历的日期时间
|
||||||
|
* <p>仅为日期,并未指定是否是开市日
|
||||||
|
* @see #getGreatest
|
||||||
|
* @param stockMarket 指定证交所,若为 {@code null} 则取最早一条(市场不确定)
|
||||||
|
* @return {@code LocalDateTime} 或 {@code null}
|
||||||
|
*/
|
||||||
|
public LocalDateTime getGreatestLocalDateTime(@Nullable StockMarket stockMarket) {
|
||||||
|
StockCalendar stockCalendar = getGreatest(stockMarket);
|
||||||
|
if (stockCalendar != null) {
|
||||||
|
return stockCalendar.getDate().atStartOfDay();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库内指定证交所的最新一个交易日历
|
||||||
|
* <p>仅为日期,并未指定是否是开市日
|
||||||
|
* @param stockMarket 指定证交所,若为 {@code null} 则取最新一条(市场不确定)
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public StockCalendar getLatest(@Nullable StockMarket stockMarket) {
|
||||||
|
return baseMapper.getLatest(stockMarket);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库内指定证交所的最近一个交易日历
|
||||||
|
* <p>仅为日期,并未指定是否是开市日。取最近一条(市场不确定)
|
||||||
|
* @see #getLatest(StockMarket)
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public StockCalendar getLatest() {
|
||||||
|
return getLatest(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库内指定证交所的最近一个交易日历的日期
|
||||||
|
* <p>仅为日期,并未指定是否是开市日
|
||||||
|
* @see #getGreatest
|
||||||
|
* @param stockMarket 指定证交所,若为 {@code null} 则取最近一条(市场不确定)
|
||||||
|
* @return {@code LocalDate} 或 {@code null}
|
||||||
|
*/
|
||||||
|
public LocalDate getLatestLocalDate(@Nullable StockMarket stockMarket) {
|
||||||
|
StockCalendar stockCalendar = getLatest(stockMarket);
|
||||||
|
if (stockCalendar != null) {
|
||||||
|
return stockCalendar.getDate().atStartOfDay().toLocalDate();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库内指定证交所的最近一个交易日历的日期
|
||||||
|
* <p>仅为日期,并未指定是否是开市日。取最近一条(市场不确定)
|
||||||
|
* @see #getGreatest
|
||||||
|
* @return {@code LocalDate} 或 {@code null}
|
||||||
|
*/
|
||||||
|
public LocalDate getLatestLocalDate() {
|
||||||
|
return getLatestLocalDate(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库内指定证交所的最近一个交易日历的日期时间
|
||||||
|
* <p>仅为日期时间,并未指定是否是开市日
|
||||||
|
* @see #getGreatest
|
||||||
|
* @param stockMarket 指定证交所,若为 {@code null} 则取最近一条(市场不确定)
|
||||||
|
* @return {@code LocalDateTime} 或 {@code null}
|
||||||
|
*/
|
||||||
|
public LocalDateTime getLatestLocalDateTime(@Nullable StockMarket stockMarket) {
|
||||||
|
StockCalendar stockCalendar = getLatest(stockMarket);
|
||||||
|
if (stockCalendar != null) {
|
||||||
|
return stockCalendar.getDate().atStartOfDay();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库内指定证交所的最近一个交易日历的日期时间
|
||||||
|
* <p>仅为日期时间,并未指定是否是开市日。取最近一条(市场不确定)
|
||||||
|
* @see #getGreatest
|
||||||
|
* @return {@code LocalDateTime} 或 {@code null}
|
||||||
|
*/
|
||||||
|
public LocalDateTime getLatestLocalDateTime() {
|
||||||
|
return getLatestLocalDateTime(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询今日是否开市,任意市场开市都返回 true
|
||||||
|
* @see #todayIsOpen(StockMarket)
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean todayIsOpen() {
|
||||||
|
return baseMapper.isOpen(LocalDateTime.now(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询今日是否是开市日
|
||||||
|
* @param stockMarket 指定市场,为 null 则任意市场开市都返回 true
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean todayIsOpen(StockMarket stockMarket) {
|
||||||
|
return baseMapper.isOpen(LocalDateTime.now(), stockMarket);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package link.at17.mid.tushare.data.service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.redisson.api.RedissonClient;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.annotation.UpdateMethod;
|
||||||
|
import link.at17.mid.tushare.dao.StockDailyBasicDao;
|
||||||
|
import link.at17.mid.tushare.data.crawler.QueryWay;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawlerResult;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody;
|
||||||
|
import link.at17.mid.tushare.data.models.StockValueEx;
|
||||||
|
import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo;
|
||||||
|
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
||||||
|
import link.at17.mid.tushare.data.validator.AllowedEnum;
|
||||||
|
import link.at17.mid.tushare.service.BaseServiceImpl;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class StockDailyBasicService extends BaseServiceImpl<StockDailyBasicDao, StockValueEx> implements ITsTradeDate {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
StockInfoService stockInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TushareCrawler tushareCrawler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
RedissonClient redis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新由 Tushare 获取的数据
|
||||||
|
* @param list
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean insertOrUpdateList(List<JSONObject> list) {
|
||||||
|
return SqlHelper.retBool(baseMapper.insertOrUpdateList(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalDateTime getLatestTradeDate(ITsStockInfo stockInfo) {
|
||||||
|
return baseMapper.getLatestTradeDate(stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LocalDateTime> getAllTradeDates(ITsStockInfo stockInfo) {
|
||||||
|
return baseMapper.getAllTradeDates(stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LocalDateTime> getAllMissingDates(ITsStockInfo stockInfo) {
|
||||||
|
return baseMapper.getAllMissingDates(stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取个股每日指标
|
||||||
|
* <blockquote>
|
||||||
|
* 接口:daily_basic<br>
|
||||||
|
* 更新时间:交易日每日15点~17点之间<br>
|
||||||
|
* 描述:获取全部股票每日重要的基本面指标,可用于选股分析、报表展示等。<br>
|
||||||
|
* 积分:用户需要至少600积分才可以调取,具体请参阅
|
||||||
|
* <a href="https://tushare.pro/document/1?doc_id=13">积分获取办法</a><br>
|
||||||
|
* </blockquote>
|
||||||
|
*/
|
||||||
|
@UpdateMethod(name="每日基本指标", order=3)
|
||||||
|
public boolean updateData(
|
||||||
|
@AllowedEnum({"ByDateUpdate", "ByStockCrossCheck"})
|
||||||
|
QueryWay queryWay) {
|
||||||
|
TushareRequestBody baseRequest = new TushareRequestBody("daily_basic");
|
||||||
|
baseRequest.addFields("ts_code,trade_date,close,turnover_rate,turnover_rate_f,volume_ratio,pe,pe_ttm,pb,ps,ps_ttm,dv_ratio,dv_ttm,total_share,float_share,free_share,total_mv,circ_mv");
|
||||||
|
Function<List<JSONObject>, Boolean> function = (t) -> {
|
||||||
|
insertOrUpdateList(t);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
List<Future<TushareCrawlerResult>> executeResult;
|
||||||
|
if (queryWay.compareTo(QueryWay.ByStock) >= 0) {
|
||||||
|
executeResult = tushareCrawler.rollingQueryByStock(baseRequest, stockInfoService.list(), this, function, 5000L, queryWay, null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
executeResult = tushareCrawler.rollingQueryByDate(baseRequest, this, function, queryWay, null);
|
||||||
|
}
|
||||||
|
log.info("每日指标数据更新完成");
|
||||||
|
for (Future<TushareCrawlerResult> f : executeResult) {
|
||||||
|
try {
|
||||||
|
TushareCrawlerResult result = f.get();
|
||||||
|
JSONObject request = f.get().getRequest();
|
||||||
|
if (result.isFatal()) {
|
||||||
|
log.error("每日指标数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
||||||
|
}
|
||||||
|
else if (!result.isSuccess()) {
|
||||||
|
log.warn("每日指标数据未获取:{}", request.toJSONString());
|
||||||
|
}
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
log.error("检查每日指标数据执行结果时发生错误", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (baseRequest.hasFingerprint()) {
|
||||||
|
redis.getBucket(baseRequest.getFingerprint()).delete();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package link.at17.mid.tushare.data.service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.temporal.Temporal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.redisson.api.RedissonClient;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.annotation.UpdateMethod;
|
||||||
|
import link.at17.mid.tushare.dao.StockDailyDao;
|
||||||
|
import link.at17.mid.tushare.data.crawler.QueryWay;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawlerResult;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody;
|
||||||
|
import link.at17.mid.tushare.data.models.StockValue;
|
||||||
|
import link.at17.mid.tushare.data.models.StockValueEx;
|
||||||
|
import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo;
|
||||||
|
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
||||||
|
import link.at17.mid.tushare.data.validator.AllowedEnum;
|
||||||
|
import link.at17.mid.tushare.service.BaseServiceImpl;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class StockDailyService extends BaseServiceImpl<StockDailyDao, StockValue> implements ITsTradeDate {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
StockInfoService stockInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TushareCrawler tushareCrawler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
RedissonClient redis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新由 Tushare 获取的数据
|
||||||
|
* @param list
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean insertOrUpdateList(List<JSONObject> list) {
|
||||||
|
return SqlHelper.retBool(baseMapper.insertOrUpdateList(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取除权日线数据 + 基本行情数据
|
||||||
|
* @param tsCode Tushare 股票代码,不允许为空
|
||||||
|
* @param endDate 结束日期(包含),留空则为最新一个交易日
|
||||||
|
* @param before 多少个交易日以前,留空则查询上市至 endDate 以来所有数据
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<StockValueEx> getExDailyBefore(String tsCode, Temporal endDate, Long before) {
|
||||||
|
return baseMapper.getExDailyBefore(tsCode, endDate, before);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取前复权日线数据
|
||||||
|
* @param tsCode Tushare 股票代码,不允许为空
|
||||||
|
* @param startDate 开始日期,留空则为股票上市日起
|
||||||
|
* @param endDate 结束日期,null 则为最新一个交易日
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<StockValue> getQfqDaily(String tsCode, Temporal startDate, Temporal endDate) {
|
||||||
|
return baseMapper.getQfqDaily(tsCode, startDate, endDate);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取前复权日线数据
|
||||||
|
* @param tsCode Tushare 股票代码,不允许为空
|
||||||
|
* @param endDate 结束日期,留空则为最新一个交易日
|
||||||
|
* @param before 多少个交易日以前,null 则查询上市以来所有数据
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<StockValue> getQfqDailyBefore(String tsCode, Temporal endDate, Long before) {
|
||||||
|
return baseMapper.getQfqDailyBefore(tsCode, endDate, before);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取前复权日线数据 + 基本行情数据
|
||||||
|
* @param tsCode Tushare 股票代码,不允许为空
|
||||||
|
* @param startDate 开始日期(包含),留空则为上市第一日
|
||||||
|
* @param after 多少个交易日以前,null 则查询 startDate 以来所有数据
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<StockValueEx> getExQfqDailyAfter(String tsCode, Temporal startDate, Long after) {
|
||||||
|
return baseMapper.getExQfqDailyAfter(tsCode, startDate, after);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取前复权日线数据 + 基本行情数据
|
||||||
|
* @param tsCode Tushare 股票代码,不允许为空
|
||||||
|
* @param endDate 结束日期,留空则为最新一个交易日
|
||||||
|
* @param before 多少个交易日以前,null 则查询上市以来所有数据
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<StockValueEx> getExQfqDailyBefore(String tsCode, Temporal endDate, Long before) {
|
||||||
|
return baseMapper.getExQfqDailyBefore(tsCode, endDate, before);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取前复权日线数据
|
||||||
|
* @param tsCode Tushare 股票代码,不允许为空
|
||||||
|
* @param startDate 开始日期(包含),留空则为上市第一日
|
||||||
|
* @param after 多少个交易日以前,null 则查询 startDate 以来所有数据
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<StockValue> getQfqDailyAfter(String tsCode, Temporal startDate, Long after) {
|
||||||
|
return baseMapper.getQfqDailyAfter(tsCode, startDate, after);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalDateTime getLatestTradeDate(ITsStockInfo stockInfo) {
|
||||||
|
return baseMapper.getLatestTradeDate(stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LocalDateTime> getAllTradeDates(ITsStockInfo stockInfo) {
|
||||||
|
return baseMapper.getAllTradeDates(stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LocalDateTime> getAllMissingDates(ITsStockInfo stockInfo) {
|
||||||
|
return baseMapper.getAllMissingDates(stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <b><a href="https://waditu.com/document/2?doc_id=27">更新日K数据</a></b><br/>
|
||||||
|
* 基础积分每分钟内最多调取500次,每次5000条数据,相当于23年历史,用户获得超过5000积分正常调取无频次限制。
|
||||||
|
* <br>
|
||||||
|
* <font color="red">请先更新交易日历和股票列表后再调用该方法,否则可能造成股票日 K 数据缺失</font>
|
||||||
|
* @param queryWay 滚动查询方式
|
||||||
|
* @see TushareCrawler#rollingQueryByDate
|
||||||
|
* @see TushareCrawler#rollingQueryByStock
|
||||||
|
*/
|
||||||
|
|
||||||
|
@UpdateMethod(name="日线数据", order=2)
|
||||||
|
public boolean updateData(
|
||||||
|
@AllowedEnum({"ByDateUpdate", "ByStockCrossCheck", "ByDateAll"})
|
||||||
|
QueryWay queryWay) {
|
||||||
|
TushareRequestBody baseRequest = new TushareRequestBody("daily")
|
||||||
|
.addFields("ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount");
|
||||||
|
Function<List<JSONObject>, Boolean> function = (t) -> {
|
||||||
|
insertOrUpdateList(t);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
List<Future<TushareCrawlerResult>> executeResult;
|
||||||
|
if (queryWay.compareTo(QueryWay.ByStock) >= 0) {
|
||||||
|
executeResult = tushareCrawler.rollingQueryByStock(baseRequest, stockInfoService.list(), this, function, 5000L, queryWay, null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
executeResult = tushareCrawler.rollingQueryByDate(baseRequest, this, function, queryWay, null);
|
||||||
|
}
|
||||||
|
log.info("日 K 数据更新完成");
|
||||||
|
for (Future<TushareCrawlerResult> f : executeResult) {
|
||||||
|
try {
|
||||||
|
TushareCrawlerResult result = f.get();
|
||||||
|
JSONObject request = f.get().getRequest();
|
||||||
|
if (result.isFatal()) {
|
||||||
|
log.error("日 K 数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
||||||
|
}
|
||||||
|
else if (!result.isSuccess()) {
|
||||||
|
log.warn("日 K 数据未获取:{}", request.toJSONString());
|
||||||
|
}
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
log.error("检查日 K 数据执行结果时发生错误", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (baseRequest.hasFingerprint()) {
|
||||||
|
redis.getBucket(baseRequest.getFingerprint()).delete();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package link.at17.mid.tushare.data.service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.redisson.api.RedissonClient;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.annotation.UpdateMethod;
|
||||||
|
import link.at17.mid.tushare.dao.StockHolderDao;
|
||||||
|
import link.at17.mid.tushare.data.crawler.QueryWay;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawlerResult;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody;
|
||||||
|
import link.at17.mid.tushare.data.models.StockHolder;
|
||||||
|
import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo;
|
||||||
|
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
||||||
|
import link.at17.mid.tushare.enums.StockHolderType;
|
||||||
|
import link.at17.mid.tushare.service.BaseServiceImpl;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class StockHolderService extends BaseServiceImpl<StockHolderDao, StockHolder> implements ITsTradeDate {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
RedissonClient redis;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TushareCrawler tushareCrawler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
StockInfoService stockInfoService;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <b><a href="https://tushare.pro/document/2?doc_id=61">前十大股东</a>/<a href="https://tushare.pro/document/2?doc_id=62">前十大流通股东</a></b><br>
|
||||||
|
* <blockquote>
|
||||||
|
* 接口:top10_holders/top10_floatholders<br>
|
||||||
|
* 更新时间:不定时,报告日<br>
|
||||||
|
* 描述:获取上市公司前十大股东数据,包括持有数量和比例等信息/获取上市公司前十大流通股东数据,不包括比例信息<br>
|
||||||
|
* </blockquote>
|
||||||
|
* 文档说单次 100 条限制,但实际测试有单次 5000 条<br>
|
||||||
|
* <font color="red">请先更新交易日历和股票列表后再调用该方法</font><br>
|
||||||
|
* <font color="red">如果是 A+H 或者 A+B 股,可能存在同股票存在多个同名股东,且持股数据不一致。需要仔细研究如何清洗和使用</font>
|
||||||
|
*/
|
||||||
|
// TODO: A+H 或者 A+B 股是否还需要重命名?
|
||||||
|
@UpdateMethod(name="十大股东数据", order=10)
|
||||||
|
public boolean updateData() {
|
||||||
|
|
||||||
|
StockHolderType[] holderTypes = new StockHolderType[] {
|
||||||
|
StockHolderType.TOP10,
|
||||||
|
StockHolderType.TOP10Float
|
||||||
|
};
|
||||||
|
|
||||||
|
for (StockHolderType holderType : holderTypes) {
|
||||||
|
|
||||||
|
boolean isFloat = holderType.getIsFloat() == 1;
|
||||||
|
TushareRequestBody baseRequest = new TushareRequestBody(isFloat ? "top10_floatholders" : "top10_holders");
|
||||||
|
Function<List<JSONObject>, Boolean> function = (t) -> {
|
||||||
|
Map<String, JSONObject> map = new HashMap<>();
|
||||||
|
for (JSONObject jo : t) {
|
||||||
|
String holderName = jo.getString("holder_name");
|
||||||
|
String[] temp = {jo.getString("ts_code"), jo.getString("end_date"), holderName, holderType.name()};
|
||||||
|
String key = String.join("_", temp);
|
||||||
|
Integer holderOffset = 1;
|
||||||
|
while (map.containsKey(key)) {
|
||||||
|
log.warn("存在重复的 key {},开始重命名", key);
|
||||||
|
temp[2] = holderName + '^' + holderOffset++;
|
||||||
|
jo.put("holder_name", temp[2]);
|
||||||
|
key = String.join("_", temp);
|
||||||
|
}
|
||||||
|
map.put(key, jo);
|
||||||
|
}
|
||||||
|
insertOrUpdateList(t, holderType);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
List<Future<TushareCrawlerResult>> executeResult;
|
||||||
|
executeResult = tushareCrawler.rollingQueryByStock(baseRequest, stockInfoService.list(), baseMapper, function, 5000L, QueryWay.ByStock, null);
|
||||||
|
String banner = "十大" + (isFloat ? "流通股东" : "股东");
|
||||||
|
log.info("{}数据更新完成", banner);
|
||||||
|
for (Future<TushareCrawlerResult> f : executeResult) {
|
||||||
|
try {
|
||||||
|
TushareCrawlerResult result = f.get();
|
||||||
|
JSONObject request = f.get().getRequest();
|
||||||
|
if (result.isFatal()) {
|
||||||
|
log.error("{}数据未获取,发生致命错误,将不会重试:{}", banner, request.toJSONString());
|
||||||
|
}
|
||||||
|
else if (!result.isSuccess()) {
|
||||||
|
log.info("{}数据未获取:{}", banner, request.toJSONString());
|
||||||
|
}
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
log.error("检查" + banner + "数据执行结果时发生错误", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (baseRequest.hasFingerprint()) {
|
||||||
|
redis.getBucket(baseRequest.getFingerprint()).delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入从 Tushare 抓取的内容
|
||||||
|
*
|
||||||
|
* @param list
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean insertOrUpdateList(List<JSONObject> list, StockHolderType holderType) {
|
||||||
|
return SqlHelper.retBool(baseMapper.insertOrUpdateList(list, holderType));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalDateTime getLatestTradeDate(ITsStockInfo stockInfo) {
|
||||||
|
return baseMapper.getLatestTradeDate(stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LocalDateTime> getAllTradeDates(ITsStockInfo stockInfo) {
|
||||||
|
return baseMapper.getAllTradeDates(stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LocalDateTime> getAllMissingDates(ITsStockInfo stockInfo) {
|
||||||
|
return baseMapper.getAllMissingDates(stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package link.at17.mid.tushare.data.service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONException;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||||
|
import com.github.yulichang.query.MPJLambdaQueryWrapper;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.annotation.UpdateMethod;
|
||||||
|
import link.at17.mid.tushare.dao.StockInfoDao;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareClient;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody;
|
||||||
|
import link.at17.mid.tushare.data.models.StockInfo;
|
||||||
|
import link.at17.mid.tushare.enums.ListStatus;
|
||||||
|
import link.at17.mid.tushare.service.BaseServiceImpl;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class StockInfoService extends BaseServiceImpl<StockInfoDao, StockInfo> {
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<StockInfo> list() {
|
||||||
|
return baseMapper.selectList(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用这个方法的话,传入的 Wrapper 需要设置别名 i
|
||||||
|
* <p>
|
||||||
|
* 如果用 QueryWrapper:
|
||||||
|
* <p>
|
||||||
|
* {@code ew.eq("i.ts_code", "000001.SZ")}
|
||||||
|
* <p>
|
||||||
|
* 如果用 MPJLambdaQueryWrapper:
|
||||||
|
* <p>
|
||||||
|
* {@code ew.setAlias("i").eq(StockInfo::getTsCode, "000001.SZ")}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<StockInfo> list(Wrapper<StockInfo> ew) {
|
||||||
|
return baseMapper.selectList(ew);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于插入从 Tushare 获取的内容
|
||||||
|
* @param list
|
||||||
|
*/
|
||||||
|
public void insertOrUpdateList(List<JSONObject> list) {
|
||||||
|
baseMapper.insertOrUpdateList(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据上市状态列举对应所有股票
|
||||||
|
* @param listStatus
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<StockInfo> listByListStatus(ListStatus listStatus) {
|
||||||
|
MPJLambdaQueryWrapper<StockInfo> ew = new MPJLambdaQueryWrapper<>();
|
||||||
|
return baseMapper.selectList(ew.setAlias("i").eq(StockInfo::getListStatus, listStatus));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://tushare.pro/document/2?doc_id=25"><b>股票列表</b></a><br>
|
||||||
|
* <b>更新股票列表</b><br/>
|
||||||
|
* 接口:stock_basic,可以通过数据工具调试和查看数据<br>
|
||||||
|
* 描述:获取基础信息数据,包括股票代码、名称、上市日期、退市日期等<br>
|
||||||
|
* 积分:2000积分起<br>
|
||||||
|
* 包括上市、退市和暂停上市,无权限(积分不足)状态下每小时最多访问该接口 1 次
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param queryWay 查询方法,本例中为 null,因为更新股票列表不需要指定 QueryWay
|
||||||
|
*/
|
||||||
|
|
||||||
|
@UpdateMethod(name="股票列表", order=1)
|
||||||
|
public boolean updateData() {
|
||||||
|
try {
|
||||||
|
List<JSONObject> stockInfos = new ArrayList<>();
|
||||||
|
// Tushare 经常改请求参数的规则,2024/11/13 更新:默认不提供 list_status 时,默认值是 L
|
||||||
|
TushareRequestBody requestBody = new TushareRequestBody("stock_basic")
|
||||||
|
.addFields("ts_code,name,area,industry,market,list_status,list_date,delist_date");
|
||||||
|
|
||||||
|
requestBody.addParam("list_status", "L");
|
||||||
|
stockInfos.addAll(TushareClient.queryList(requestBody));
|
||||||
|
|
||||||
|
requestBody.addParam("list_status", "D");
|
||||||
|
stockInfos.addAll(TushareClient.queryList(requestBody));
|
||||||
|
|
||||||
|
requestBody.addParam("list_status", "P");
|
||||||
|
stockInfos.addAll(TushareClient.queryList(requestBody));
|
||||||
|
|
||||||
|
insertOrUpdateList(stockInfos);
|
||||||
|
|
||||||
|
log.info("更新股票列表完成");
|
||||||
|
return true;
|
||||||
|
} catch (JSONException | IOException e) {
|
||||||
|
log.error("更新股票列表时发生错误", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package link.at17.mid.tushare.data.service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.redisson.api.RedissonClient;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.annotation.UpdateMethod;
|
||||||
|
import link.at17.mid.tushare.dao.StockLimitDao;
|
||||||
|
import link.at17.mid.tushare.data.crawler.QueryWay;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawlerResult;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody;
|
||||||
|
import link.at17.mid.tushare.data.models.StockLimit;
|
||||||
|
import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo;
|
||||||
|
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
||||||
|
import link.at17.mid.tushare.data.validator.AllowedEnum;
|
||||||
|
import link.at17.mid.tushare.service.BaseServiceImpl;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class StockLimitService extends BaseServiceImpl<StockLimitDao, StockLimit> implements ITsTradeDate {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
RedissonClient redis;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TushareCrawler tushareCrawler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
StockInfoService stockInfoService;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <b><a href="https://waditu.com/document/2?doc_id=298">Tushare 涨跌停列表</a></b><br>
|
||||||
|
* <ul>
|
||||||
|
* <li>接口:limit_list_d<br>
|
||||||
|
* <li>描述:获取沪深A股每日涨跌停、炸板数据情况,数据从2020年开始<br>
|
||||||
|
* <li>限量:单次最大可以获取500条数据,可通过日期或者股票循环提取<br>
|
||||||
|
* <li>积分:120积分可查看数据,5000积分每分钟可以请求200次,8000积分以上每分钟500次,具体请参阅积分获取办法<br>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@UpdateMethod(name="涨跌停数据", order=5)
|
||||||
|
public boolean updateData(
|
||||||
|
@AllowedEnum({"ByDateUpdate", "ByStockCrossCheck", "ByDateAll"})
|
||||||
|
QueryWay queryWay) {
|
||||||
|
|
||||||
|
TushareRequestBody baseRequest = new TushareRequestBody("limit_list_d");
|
||||||
|
Function<List<JSONObject>, Boolean> function = (t) -> {
|
||||||
|
insertOrUpdateList(t);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
List<Future<TushareCrawlerResult>> executeResult;
|
||||||
|
if (queryWay.compareTo(QueryWay.ByStock) >= 0) {
|
||||||
|
executeResult = tushareCrawler.rollingQueryByStock(baseRequest, stockInfoService.list(), this,
|
||||||
|
function, 500L, queryWay, null);
|
||||||
|
} else {
|
||||||
|
executeResult = tushareCrawler.rollingQueryByDate(baseRequest, this, function, queryWay,
|
||||||
|
LocalDateTime.of(2019, 11, 28, 0, 0, 0));
|
||||||
|
}
|
||||||
|
log.info("涨跌停数据更新完成");
|
||||||
|
for (Future<TushareCrawlerResult> f : executeResult) {
|
||||||
|
try {
|
||||||
|
TushareCrawlerResult result = f.get();
|
||||||
|
JSONObject request = f.get().getRequest();
|
||||||
|
if (result.isFatal()) {
|
||||||
|
log.error("涨跌停数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
||||||
|
} else if (!result.isSuccess()) {
|
||||||
|
log.info("涨跌停数据未获取:{}", request.toJSONString());
|
||||||
|
}
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
log.error("检查涨跌停数据执行结果时发生错误", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (baseRequest.hasFingerprint()) {
|
||||||
|
redis.getBucket(baseRequest.getFingerprint()).delete();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入从 Tushare 抓取的内容
|
||||||
|
*
|
||||||
|
* @param list
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean insertOrUpdateList(List<JSONObject> list) {
|
||||||
|
return SqlHelper.retBool(baseMapper.insertOrUpdateList(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalDateTime getLatestTradeDate(ITsStockInfo stockInfo) {
|
||||||
|
return baseMapper.getLatestTradeDate(stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LocalDateTime> getAllTradeDates(ITsStockInfo stockInfo) {
|
||||||
|
return baseMapper.getAllTradeDates(stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LocalDateTime> getAllMissingDates(ITsStockInfo stockInfo) {
|
||||||
|
return baseMapper.getAllMissingDates(stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package link.at17.mid.tushare.data.service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.temporal.Temporal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.redisson.api.RedissonClient;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.annotation.UpdateMethod;
|
||||||
|
import link.at17.mid.tushare.dao.StockMinuteDao;
|
||||||
|
import link.at17.mid.tushare.data.crawler.QueryWay;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawlerResult;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody;
|
||||||
|
import link.at17.mid.tushare.data.models.StockValue;
|
||||||
|
import link.at17.mid.tushare.data.models.StockValueEx;
|
||||||
|
import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo;
|
||||||
|
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
||||||
|
import link.at17.mid.tushare.data.validator.AllowedEnum;
|
||||||
|
import link.at17.mid.tushare.enums.StockSpan;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class StockMinuteService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
StockMinuteDao stockMinuteDao;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
StockInfoService stockInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TushareCrawler tushareCrawler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
RedissonClient redis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新由 Tushare 获取的数据
|
||||||
|
*
|
||||||
|
* @param list
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean insertOrUpdateList(List<JSONObject> list, StockSpan stockSpan) {
|
||||||
|
return SqlHelper.retBool(stockMinuteDao.insertOrUpdateList(list, stockSpan));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最新交易日<br/>
|
||||||
|
* 获取到股票的最新交易日
|
||||||
|
*
|
||||||
|
* @param stockSpan 分钟线频率
|
||||||
|
* @param stockInfo
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public LocalDateTime getLatestTradeDate(StockSpan stockSpan, ITsStockInfo stockInfo) {
|
||||||
|
return stockMinuteDao.getLatestTradeDate(stockSpan, stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定 freq 下的所有交易日
|
||||||
|
*
|
||||||
|
* @param stockSpan 分钟线频率
|
||||||
|
* @param stockInfo
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<LocalDateTime> getAllTradeDates(StockSpan stockSpan, ITsStockInfo stockInfo) {
|
||||||
|
return stockMinuteDao.getAllTradeDates(stockSpan, stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定 freq 下的数据缺失日,包括分钟数据不全日<br/>
|
||||||
|
* 如:60 分钟频率下,一日内 K 线数应为 240/60 + 1 = 5 条,则小于 5 条的日期都将被列为缺失日期
|
||||||
|
*
|
||||||
|
* @param stockSpan 分钟线频率
|
||||||
|
* @param stockInfo
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<LocalDateTime> getAllMissingDates(StockSpan stockSpan, ITsStockInfo stockInfo) {
|
||||||
|
return stockMinuteDao.getAllMissingDates(stockSpan, stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取前复权日线数据
|
||||||
|
*
|
||||||
|
* @param stockCode 股票代码,不允许为空
|
||||||
|
* @param stockSpan 分钟线频率
|
||||||
|
* @param startDate 开始日期,留空则为股票上市日起
|
||||||
|
* @param endDate 结束日期,留空则为最新一个交易日
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<StockValue> getQfqMinute(String stockCode, StockSpan stockSpan, Temporal startDate, Temporal endDate) {
|
||||||
|
return stockMinuteDao.getQfqMinute(stockCode, stockSpan, startDate, endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取前复权日线数据
|
||||||
|
*
|
||||||
|
* @param stockCode 股票代码,不允许为空
|
||||||
|
* @param endDate 结束日期,留空则为最新一个交易日
|
||||||
|
* @param before 多少个交易日以前,留空则查询上市以来所有数据
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<StockValue> getQfqMinuteBefore(String stockCode, StockSpan stockSpan, Temporal endDate, Long before) {
|
||||||
|
return stockMinuteDao.getQfqMinuteBefore(stockCode, stockSpan, endDate, before);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取前复权日线 Ex 数据
|
||||||
|
*
|
||||||
|
* @param stockCode 股票代码,不允许为空
|
||||||
|
* @param endDate 结束日期,留空则为最新一个交易日
|
||||||
|
* @param before 多少个交易日以前,留空则查询上市以来所有数据
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<StockValueEx> getExQfqMinuteBefore(String stockCode, StockSpan stockSpan, Temporal endDate,
|
||||||
|
Long before) {
|
||||||
|
return stockMinuteDao.getExQfqMinuteBefore(stockCode, stockSpan, endDate, before);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <b><a href="https://tushare.pro/document/1?doc_id=290">更新分钟K数据</a></b><br/>
|
||||||
|
* <p>
|
||||||
|
* 有权限时,每分钟500次,每次8000行数据,总量不限制
|
||||||
|
* </p>
|
||||||
|
* <font color="red">请先更新交易日历和股票列表后再调用该方法,否则可能造成股票分钟 K 数据缺失</font>
|
||||||
|
*
|
||||||
|
* @param queryWay 滚动查询方式,仅支持 StockSpan.ByStock... 系列
|
||||||
|
* @param stockSpan 股票粒度,仅支持 StockSpan.Minute - StockSpan.SixtyMinute
|
||||||
|
* @see TushareCrawler#rollingQueryByDate
|
||||||
|
* @see TushareCrawler#rollingQueryByStock
|
||||||
|
* @see link.at17.mid.tushare.enums.StockSpan
|
||||||
|
*/
|
||||||
|
@UpdateMethod(name="分钟 K 数据", order=9)
|
||||||
|
public boolean updateData(
|
||||||
|
@AllowedEnum({"ByDateUpdate", "ByStockCrossCheck", "ByDateAll" })
|
||||||
|
QueryWay queryWay,
|
||||||
|
@AllowedEnum({"Minute", "Minute5", "Minute15", "Minute30", "Minute60"})
|
||||||
|
StockSpan stockSpan) {
|
||||||
|
Assert.isTrue(Objects.nonNull(stockSpan), "stockSpan 不允许为空");
|
||||||
|
Assert.isTrue(Objects.nonNull(stockSpan.getMin()), "不支持的 StockSpan:" + stockSpan + ", 仅支持分钟数据类型");
|
||||||
|
Assert.isTrue(queryWay.compareTo(QueryWay.ByStock) >= 0, "不支持的 QueryWay!");
|
||||||
|
String freq = stockSpan.getMin() + "min";
|
||||||
|
TushareRequestBody baseRequest = new TushareRequestBody("stk_mins")
|
||||||
|
.addFields("ts_code,trade_time,open,close,high,low,vol,amount").addParam("freq", freq);
|
||||||
|
Function<List<JSONObject>, Boolean> function = (t) -> {
|
||||||
|
stockMinuteDao.insertOrUpdateList(t, stockSpan);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
// 特殊重写:获取最新日期和所有交易日,该重写仅对更新数据生效
|
||||||
|
ITsTradeDate minTradeDate = new ITsTradeDate() {
|
||||||
|
@Override
|
||||||
|
public LocalDateTime getLatestTradeDate(ITsStockInfo stockInfo) {
|
||||||
|
return stockMinuteDao.getLatestTradeDate(stockSpan, stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LocalDateTime> getAllTradeDates(ITsStockInfo stockInfo) {
|
||||||
|
return stockMinuteDao.getAllTradeDates(stockSpan, stockInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LocalDateTime> getAllMissingDates(ITsStockInfo stockInfo) {
|
||||||
|
return stockMinuteDao.getAllMissingDates(stockSpan, stockInfo);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
List<Future<TushareCrawlerResult>> executeResult = tushareCrawler.rollingQueryByStock(baseRequest,
|
||||||
|
stockInfoService.list(), minTradeDate, function, 8000L, queryWay, stockSpan);
|
||||||
|
log.info("{} 分钟 K 数据更新完成", stockSpan.getMin());
|
||||||
|
for (Future<TushareCrawlerResult> f : executeResult) {
|
||||||
|
try {
|
||||||
|
TushareCrawlerResult result = f.get();
|
||||||
|
JSONObject request = f.get().getRequest();
|
||||||
|
if (result.isFatal()) {
|
||||||
|
log.error("{} 分钟 K 数据未获取:{},发生致命错误,将不会重试:{}", stockSpan.getMin(), request.toJSONString());
|
||||||
|
} else if (!result.isSuccess()) {
|
||||||
|
log.warn("{} 分钟 K 数据未获取:{}", stockSpan.getMin(), request.toJSONString());
|
||||||
|
}
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
log.error("检查 {} 分钟 K 数据执行结果时发生错误\r\n{}", stockSpan.getMin(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (baseRequest.hasFingerprint()) {
|
||||||
|
redis.getBucket(baseRequest.getFingerprint()).delete();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package link.at17.mid.tushare.data.service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.redisson.api.RedissonClient;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.annotation.UpdateMethod;
|
||||||
|
import link.at17.mid.tushare.dao.ThsDailyDao;
|
||||||
|
import link.at17.mid.tushare.dao.ThsListDao;
|
||||||
|
import link.at17.mid.tushare.data.crawler.QueryWay;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawlerResult;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody;
|
||||||
|
import link.at17.mid.tushare.data.validator.AllowedEnum;
|
||||||
|
import link.at17.mid.tushare.enums.ThsStockMarket;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class ThsDailyService {
|
||||||
|
|
||||||
|
private static final LocalDateTime THS_DAILY_BEGIN_LOCALDATE = LocalDateTime.of(2007, 8, 1, 0, 0, 0);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
ThsDailyDao thsDailyDao;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
ThsListDao thsListDao;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TushareCrawler tushareCrawler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
RedissonClient redis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://tushare.pro/document/2?doc_id=260"><b>同花顺板块指数日线行情</b></a><br>
|
||||||
|
* 接口:ths_daily<br>
|
||||||
|
* 描述:获取同花顺板块指数行情。注:数据版权归属同花顺,如做商业用途,请主动联系同花顺,如需帮助请联系微信:waditu_a<br>
|
||||||
|
* 限量:单次最大3000行数据(需6000积分),可根据指数代码、日期参数循环提取。<br>
|
||||||
|
* @param queryWay
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@UpdateMethod(name="同花顺板块指数日线行情", order=8)
|
||||||
|
public boolean updateData(
|
||||||
|
@AllowedEnum({"ByDateUpdate", "ByStockCrossCheck"})
|
||||||
|
QueryWay queryWay) {
|
||||||
|
TushareRequestBody baseRequest = new TushareRequestBody("ths_daily").addFields("ts_code,trade_date,close,open,high,low,pre_close,avg_price,change,pct_change,vol,turnover_rate,total_mv,float_mv");
|
||||||
|
Function<List<JSONObject>, Boolean> function = (t) -> {
|
||||||
|
thsDailyDao.insertOrUpdateList(t);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
List<Future<TushareCrawlerResult>> executeResult;
|
||||||
|
if (queryWay.compareTo(QueryWay.ByStock) >= 0) {
|
||||||
|
executeResult = tushareCrawler.rollingQueryByStock(baseRequest, thsListDao.listByExchange(ThsStockMarket.A), thsDailyDao, function, 3000L, queryWay, null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
executeResult = tushareCrawler.rollingQueryByDate(baseRequest, thsDailyDao, function, queryWay, THS_DAILY_BEGIN_LOCALDATE);
|
||||||
|
}
|
||||||
|
log.info("同花顺板块指数行情更新完成");
|
||||||
|
for (Future<TushareCrawlerResult> f : executeResult) {
|
||||||
|
try {
|
||||||
|
TushareCrawlerResult result = f.get();
|
||||||
|
JSONObject request = result.getRequest();
|
||||||
|
if (result.isFatal()) {
|
||||||
|
log.error("同花顺板块指数行情数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
||||||
|
}
|
||||||
|
else if (!result.isSuccess()) {
|
||||||
|
log.warn("同花顺板块指数行情数据未获取:{}", request.toJSONString());
|
||||||
|
}
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
log.error("同花顺板块指数行情数据执行结果时发生错误", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (baseRequest.hasFingerprint()) {
|
||||||
|
redis.getBucket(baseRequest.getFingerprint()).delete();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入从 Tushare 获取的数据
|
||||||
|
* @param list
|
||||||
|
* @return 插入是否成功
|
||||||
|
*/
|
||||||
|
public boolean insertOrUpdateList(List<JSONObject> list) {
|
||||||
|
return SqlHelper.retBool(thsDailyDao.insertOrUpdateList(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package link.at17.mid.tushare.data.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.annotation.UpdateMethod;
|
||||||
|
import link.at17.mid.tushare.dao.ThsListDao;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareClient;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody;
|
||||||
|
import link.at17.mid.tushare.data.models.ThsStockInfo;
|
||||||
|
import link.at17.mid.tushare.service.BaseServiceImpl;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class ThsListService extends BaseServiceImpl<ThsListDao, ThsStockInfo> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://tushare.pro/document/2?doc_id=259"><b>同花顺概念和行业指数</b></a><br>
|
||||||
|
* 接口:ths_index<br>
|
||||||
|
* 描述:获取同花顺板块指数。注:数据版权归属同花顺,如做商业用途,请主动联系同花顺,如需帮助请联系微信migedata 。<br>
|
||||||
|
* 限量:本接口需获得600积分,单次最大5000,一次可提取全部数据,请勿循环提取。
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@UpdateMethod(name="同花顺板块指数列表", order=6)
|
||||||
|
public boolean updateData() {
|
||||||
|
try {
|
||||||
|
List<JSONObject> thsIndexes = TushareClient.queryList(new TushareRequestBody("ths_index"));
|
||||||
|
insertOrUpdateList(thsIndexes);
|
||||||
|
log.info("更新同花顺板块列表完成");
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新同花顺板块列表时发生错误", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入从 Tushare 获取的数据
|
||||||
|
* @param list
|
||||||
|
* @return 插入是否成功
|
||||||
|
*/
|
||||||
|
public boolean insertOrUpdateList(List<JSONObject> list) {
|
||||||
|
return SqlHelper.retBool(baseMapper.insertOrUpdateList(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package link.at17.mid.tushare.data.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.redisson.api.RedissonClient;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.annotation.UpdateMethod;
|
||||||
|
import link.at17.mid.tushare.dao.ThsListDao;
|
||||||
|
import link.at17.mid.tushare.dao.ThsMemberDao;
|
||||||
|
import link.at17.mid.tushare.data.crawler.QueryWay;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawlerResult;
|
||||||
|
import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody;
|
||||||
|
import link.at17.mid.tushare.data.models.ThsStockInfo;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class ThsMemberService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
ThsMemberDao thsMemberDao;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
ThsListDao thsListDao;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TushareCrawler tushareCrawler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
RedissonClient redis;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://tushare.pro/document/2?doc_id=261"><b>同花顺概念板块成分</b></a><br>
|
||||||
|
* 接口:ths_member<br>
|
||||||
|
* 描述:获取同花顺概念板块成分列表注:数据版权归属同花顺,如做商业用途,请主动联系同花顺。<br>
|
||||||
|
* 限量:用户积累5000积分可调取,每分钟可调取200次,可按概念板块代码循环提取所有成分
|
||||||
|
*/
|
||||||
|
@UpdateMethod(name="同花顺概念板块成分", order=7)
|
||||||
|
public boolean updateData() {
|
||||||
|
TushareRequestBody baseRequest = new TushareRequestBody("ths_member").addFields("ts_code,con_code,con_name,weight,in_date,out_date,is_new");
|
||||||
|
Function<List<JSONObject>, Boolean> function = (t) -> {
|
||||||
|
thsMemberDao.insertOrUpdateList(t);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
List<ThsStockInfo> stockInfoList = thsListDao.listByExchange(null);
|
||||||
|
while (true) {
|
||||||
|
List<Future<TushareCrawlerResult>> executeResult = tushareCrawler.rollingQueryByStock(baseRequest, stockInfoList, null, function, 5000L, QueryWay.ByStock, null);
|
||||||
|
|
||||||
|
stockInfoList.clear();
|
||||||
|
for (Future<TushareCrawlerResult> f : executeResult) {
|
||||||
|
try {
|
||||||
|
TushareCrawlerResult result = f.get();
|
||||||
|
JSONObject request = result.getRequest();
|
||||||
|
if (result.isFatal()) {
|
||||||
|
log.error("同花顺概念板块成分数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
||||||
|
}
|
||||||
|
else if (!result.isSuccess()) {
|
||||||
|
log.warn("同花顺概念板块成分数据未获取:{}", request.toJSONString());
|
||||||
|
stockInfoList.add(request.getJSONObject("params").to(ThsStockInfo.class));
|
||||||
|
}
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
log.error("同花顺概念板块成分数据执行结果时发生错误", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stockInfoList.size() == 0) {
|
||||||
|
log.info("同花顺概念板块成分更新完成");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
log.info("重新获取未获取成功的同花顺概念板块成分");
|
||||||
|
}
|
||||||
|
if (baseRequest.hasFingerprint()) {
|
||||||
|
redis.getBucket(baseRequest.getFingerprint()).delete();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入从 Tushare 获取的数据
|
||||||
|
* @param list
|
||||||
|
* @return 插入是否成功
|
||||||
|
*/
|
||||||
|
public boolean insertOrUpdateList(List<JSONObject> list) {
|
||||||
|
return SqlHelper.retBool(thsMemberDao.insertOrUpdateList(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package link.at17.mid.tushare.data.typehandler;
|
||||||
|
|
||||||
|
import java.sql.CallableStatement;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||||
|
import org.apache.ibatis.type.MappedTypes;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.apache.ibatis.type.BaseTypeHandler;
|
||||||
|
import org.apache.ibatis.type.JdbcType;
|
||||||
|
|
||||||
|
@MappedTypes(List.class)
|
||||||
|
@MappedJdbcTypes(JdbcType.VARCHAR)
|
||||||
|
@Slf4j
|
||||||
|
public class JsonListTypeHandler extends BaseTypeHandler<List<?>> {
|
||||||
|
private static final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setNonNullParameter(PreparedStatement ps, int i, List<?> parameter, JdbcType jdbcType) throws SQLException {
|
||||||
|
try {
|
||||||
|
ps.setString(i, mapper.writeValueAsString(parameter));
|
||||||
|
} catch (JsonProcessingException | SQLException e) {
|
||||||
|
log.error("将 {} 转换成 JSON String 并存储时错误", parameter, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<?> getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||||
|
String json = rs.getString(columnName);
|
||||||
|
try {
|
||||||
|
return mapper.readValue(json, new TypeReference<List<?>>() {});
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("将 JSON String {} 转换成 List<?> 时错误", json, e);
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<?> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||||
|
try {
|
||||||
|
return mapper.readValue(rs.getString(columnIndex), new TypeReference<List<?>>() {});
|
||||||
|
} catch (JsonProcessingException | SQLException e) {
|
||||||
|
log.error("将 ResultSet {} 转换成 List<?> 时错误", rs, e);
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<?> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||||
|
try {
|
||||||
|
return mapper.readValue(cs.getString(columnIndex), new TypeReference<List<?>>() {});
|
||||||
|
} catch (JsonProcessingException | SQLException e) {
|
||||||
|
log.error("将 CallableStatement {} 转换成 List<?> 时错误", cs, e);
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package link.at17.mid.tushare.data.typehandler;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||||
|
import org.apache.ibatis.type.MappedTypes;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.data.models.UpdateMethodInfo;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.apache.ibatis.type.JdbcType;
|
||||||
|
|
||||||
|
@MappedTypes(List.class)
|
||||||
|
@MappedJdbcTypes(JdbcType.VARCHAR)
|
||||||
|
@Slf4j
|
||||||
|
public class UpdateMethodInfoListTypeHandler extends AbstractJsonTypeHandler<List<UpdateMethodInfo>> {
|
||||||
|
private static final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<UpdateMethodInfo> parse(String json) {
|
||||||
|
try {
|
||||||
|
return mapper.readValue(json, new TypeReference<List<UpdateMethodInfo>>() {});
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String toJson(List<UpdateMethodInfo> obj) {
|
||||||
|
try {
|
||||||
|
return mapper.writeValueAsString(obj);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package link.at17.mid.tushare.data.util;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
|
|
||||||
import org.apache.commons.codec.binary.Hex;
|
|
||||||
|
|
||||||
public class CryptoUtil {
|
|
||||||
|
|
||||||
public static String getSHA256Str(String str) {
|
|
||||||
MessageDigest messageDigest;
|
|
||||||
String encdeStr = "";
|
|
||||||
try {
|
|
||||||
messageDigest = MessageDigest.getInstance("SHA-256");
|
|
||||||
byte[] hash = messageDigest.digest(str.getBytes(StandardCharsets.UTF_8));
|
|
||||||
encdeStr = Hex.encodeHexString(hash);
|
|
||||||
} catch (NoSuchAlgorithmException e) {}
|
|
||||||
return encdeStr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package link.at17.mid.tushare.data.validator;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import jakarta.validation.Constraint;
|
||||||
|
import jakarta.validation.Payload;
|
||||||
|
|
||||||
|
@Target(ElementType.PARAMETER)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Constraint(validatedBy = AllowedEnumValidator.class)
|
||||||
|
public @interface AllowedEnum {
|
||||||
|
String message() default "非法枚举值";
|
||||||
|
Class<?>[] groups() default {};
|
||||||
|
Class<? extends Payload>[] payload() default {};
|
||||||
|
String[] value(); // 允许的枚举名
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package link.at17.mid.tushare.data.validator;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import jakarta.validation.ConstraintValidator;
|
||||||
|
import jakarta.validation.ConstraintValidatorContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 允许枚举的验证器
|
||||||
|
*/
|
||||||
|
public class AllowedEnumValidator implements IValidator, ConstraintValidator<AllowedEnum, Enum<?>> {
|
||||||
|
private Set<String> allowed;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(AllowedEnum anno) {
|
||||||
|
allowed = Set.of(anno.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid(Enum<?> val, ConstraintValidatorContext ctx) {
|
||||||
|
return val == null || allowed.contains(val.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package link.at17.mid.tushare.data.validator;
|
||||||
|
import jakarta.validation.ConstraintValidator;
|
||||||
|
import jakarta.validation.ConstraintValidatorContext;
|
||||||
|
import org.springframework.scheduling.support.CronExpression;
|
||||||
|
|
||||||
|
public class CronValidator implements ConstraintValidator<ValidCron, String> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||||
|
if (value == null || value.isBlank()) return true; // 空值交由 @NotBlank 处理
|
||||||
|
try {
|
||||||
|
CronExpression.parse(value);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package link.at17.mid.tushare.data.validator;
|
||||||
|
|
||||||
|
import jakarta.validation.ConstraintValidatorContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速提供自定义错误信息返回
|
||||||
|
*/
|
||||||
|
public interface IValidator {
|
||||||
|
|
||||||
|
public default boolean invalid(ConstraintValidatorContext context, String message) {
|
||||||
|
context.disableDefaultConstraintViolation();
|
||||||
|
context.buildConstraintViolationWithTemplate(message)
|
||||||
|
.addConstraintViolation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package link.at17.mid.tushare.data.validator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.validation.ConstraintValidator;
|
||||||
|
import jakarta.validation.ConstraintValidatorContext;
|
||||||
|
import link.at17.mid.tushare.data.models.UpdateMethodInfo;
|
||||||
|
import link.at17.mid.tushare.data.models.UpdateMethodInfo.UpdateParamInfo;
|
||||||
|
import link.at17.mid.tushare.service.UpdateMethodService;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class UpdateMethodInfoValidator implements IValidator, ConstraintValidator<ValidUpdateMethodInfo, UpdateMethodInfo> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UpdateMethodService updateMethodService; // 内存维护的合法方法表
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid(UpdateMethodInfo methodInfo, ConstraintValidatorContext context) {
|
||||||
|
if (methodInfo == null) return true;
|
||||||
|
|
||||||
|
// 1. 校验 id 是否存在
|
||||||
|
if (methodInfo.getId() == null) {
|
||||||
|
return invalid(context, "方法 id 为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 校验 id 在内存中是否存储
|
||||||
|
UpdateMethodInfo validMethod = updateMethodService.findById(methodInfo.getId());
|
||||||
|
if (validMethod == null) {
|
||||||
|
return invalid(context, "方法 id 不存在: " + methodInfo.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = validMethod.getName();
|
||||||
|
|
||||||
|
// 3. 校验参数数量与名称
|
||||||
|
List<UpdateParamInfo> incoming = methodInfo.getParams();
|
||||||
|
List<UpdateParamInfo> reference = validMethod.getParams();
|
||||||
|
|
||||||
|
if (incoming == null || incoming.size() != reference.size()) {
|
||||||
|
return invalid(context, name + " 参数数量不一致: " + methodInfo.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < incoming.size(); i++) {
|
||||||
|
UpdateParamInfo in = incoming.get(i);
|
||||||
|
UpdateParamInfo ref = reference.get(i);
|
||||||
|
|
||||||
|
if (!Objects.equals(in.getName(), ref.getName())) {
|
||||||
|
return invalid(context, name + " 参数名不匹配: " + in.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ref.getAllowedEnumValues() != null &&
|
||||||
|
in.getValue() != null &&
|
||||||
|
!ref.getAllowedEnumValues().contains(String.valueOf(in.getValue()))) {
|
||||||
|
return invalid(context, name + " 参数值非法: " + in.getName() + " : " + in.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package link.at17.mid.tushare.data.validator;
|
||||||
|
import jakarta.validation.Constraint;
|
||||||
|
import jakarta.validation.Payload;
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
@Documented
|
||||||
|
@Constraint(validatedBy = CronValidator.class)
|
||||||
|
@Target({ ElementType.FIELD, ElementType.PARAMETER })
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface ValidCron {
|
||||||
|
|
||||||
|
String message() default "无效的 Cron 表达式";
|
||||||
|
|
||||||
|
Class<?>[] groups() default {};
|
||||||
|
|
||||||
|
Class<? extends Payload>[] payload() default {};
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package link.at17.mid.tushare.data.validator;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import jakarta.validation.Constraint;
|
||||||
|
import jakarta.validation.Payload;
|
||||||
|
|
||||||
|
@Target({ElementType.TYPE, ElementType.TYPE_USE})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Constraint(validatedBy = UpdateMethodInfoValidator.class)
|
||||||
|
@Documented
|
||||||
|
public @interface ValidUpdateMethodInfo {
|
||||||
|
String message() default "非法的 UpdatePlan";
|
||||||
|
Class<?>[] groups() default {};
|
||||||
|
Class<? extends Payload>[] payload() default {};
|
||||||
|
}
|
||||||
6
src/main/java/link/at17/mid/tushare/dto/JsonViews.java
Normal file
6
src/main/java/link/at17/mid/tushare/dto/JsonViews.java
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package link.at17.mid.tushare.dto;
|
||||||
|
|
||||||
|
public class JsonViews {
|
||||||
|
public static class Public {} // 给前端用
|
||||||
|
public static class Internal {} // 存库用
|
||||||
|
}
|
||||||
43
src/main/java/link/at17/mid/tushare/dto/LayPageReq.java
Normal file
43
src/main/java/link/at17/mid/tushare/dto/LayPageReq.java
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package link.at17.mid.tushare.dto;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台管理分页查询,适配 layui
|
||||||
|
* @author Doghole
|
||||||
|
*
|
||||||
|
* @param <T>
|
||||||
|
*/
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper=false)
|
||||||
|
public class LayPageReq<T> extends Page<T> {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 3471995935637905622L;
|
||||||
|
|
||||||
|
public LayPageReq<T> setPage(Long page) {
|
||||||
|
current = page;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getPage() {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LayPageReq<T> setLimit(Long limit) {
|
||||||
|
size = limit;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getLimit() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer start;
|
||||||
|
|
||||||
|
T condition;
|
||||||
|
}
|
||||||
@@ -5,8 +5,17 @@ import com.baomidou.mybatisplus.annotation.EnumValue;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
public enum ListStatus {
|
public enum ListStatus {
|
||||||
|
/**
|
||||||
|
* 上市
|
||||||
|
*/
|
||||||
LIST("L"),
|
LIST("L"),
|
||||||
|
/**
|
||||||
|
* 退市
|
||||||
|
*/
|
||||||
DELIST("D"),
|
DELIST("D"),
|
||||||
|
/**
|
||||||
|
* 停牌
|
||||||
|
*/
|
||||||
PAUSE("P");
|
PAUSE("P");
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package link.at17.mid.tushare.enums;
|
||||||
|
|
||||||
|
public enum UpdateLogType {
|
||||||
|
|
||||||
|
INFO,
|
||||||
|
ERROR,
|
||||||
|
SUCCESS
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package link.at17.mid.tushare.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.github.yulichang.base.MPJBaseMapper;
|
||||||
|
import com.github.yulichang.base.MPJBaseServiceImpl;
|
||||||
|
import com.github.yulichang.interfaces.MPJBaseJoin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <b>BaseServiceImpl</b>
|
||||||
|
* <br>基本业务实现
|
||||||
|
* <br>支持联查 Wrapper: {@code MPJLambdaWrapper}
|
||||||
|
* @author Doghole
|
||||||
|
*/
|
||||||
|
public abstract class BaseServiceImpl<M extends MPJBaseMapper<T>, T> extends MPJBaseServiceImpl<M, T> {
|
||||||
|
|
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
|
@Override
|
||||||
|
public T getOne(Wrapper<T> wrapper) {
|
||||||
|
return (wrapper instanceof MPJBaseJoin) ?
|
||||||
|
(T) selectJoinOne(currentModelClass(), (MPJBaseJoin)wrapper) : super.getOne(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
|
@Override
|
||||||
|
public long count(Wrapper<T> wrapper) {
|
||||||
|
return (wrapper instanceof MPJBaseJoin) ?
|
||||||
|
baseMapper.selectJoinCount((MPJBaseJoin) wrapper): super.count(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
|
@Override
|
||||||
|
public List<T> list(Wrapper<T> wrapper) {
|
||||||
|
return (wrapper instanceof MPJBaseJoin) ?
|
||||||
|
baseMapper.selectJoinList(currentModelClass(), (MPJBaseJoin)wrapper): super.list(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给子类用的方法
|
||||||
|
* @param <E>
|
||||||
|
* @param clazz
|
||||||
|
* @param wrapper
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
|
public <E extends T, X> E getOneJoin(Class<E> clazz, MPJBaseJoin<X> wrapper) {
|
||||||
|
return (E) baseMapper.selectJoinOne(clazz, (MPJBaseJoin)wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给子类用的方法
|
||||||
|
* @param <E>
|
||||||
|
* @param clazz
|
||||||
|
* @param wrapper
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
|
public <E extends T, X> List<E> listJoin(Class<E> clazz, MPJBaseJoin<X> wrapper) {
|
||||||
|
return baseMapper.selectJoinList(clazz, (MPJBaseJoin)wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于 Mybatis-Plus 的分页,返回对应 Page<T> 和 Wrapper<T> 的 Page
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
|
@Override
|
||||||
|
public <E extends IPage<T>> E page(E page, Wrapper<T> wrapper) {
|
||||||
|
return (wrapper instanceof MPJBaseJoin) ?
|
||||||
|
(E) baseMapper.selectJoinPage(page, this.currentModelClass(), (MPJBaseJoin)wrapper):
|
||||||
|
super.page(page, wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于 Mybatis-Plus 的分页,返回对应 Page<T> 和 Wrapper<T> 的 Page<br>
|
||||||
|
* 给子类用的方法
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
|
public <E extends IPage<S>, S extends T> E joinPage(E page, Class<S> clazz, MPJBaseJoin<S> wrapper) {
|
||||||
|
return (E) baseMapper.selectJoinPage(page, clazz, (MPJBaseJoin)wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
11
src/main/java/link/at17/mid/tushare/service/StatService.java
Normal file
11
src/main/java/link/at17/mid/tushare/service/StatService.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package link.at17.mid.tushare.service;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计和状态服务
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class StatService {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
package link.at17.mid.tushare.service;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Parameter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.reflections.Reflections;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import link.at17.mid.tushare.annotation.UpdateMethod;
|
||||||
|
import link.at17.mid.tushare.data.models.UpdateMethodInfo;
|
||||||
|
import link.at17.mid.tushare.data.models.UpdateMethodInfo.UpdateParamInfo;
|
||||||
|
import link.at17.mid.tushare.data.validator.AllowedEnum;
|
||||||
|
import link.at17.mid.tushare.system.util.SpringBeanDetector;
|
||||||
|
import link.at17.mid.tushare.system.util.SpringContextHolder;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
@Lazy(false)
|
||||||
|
@Validated
|
||||||
|
public class UpdateMethodService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
Reflections reflections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存放初始化 UpdateMethodInfo 的缓存,主要用于通过 id 来找到原始记录
|
||||||
|
*/
|
||||||
|
Map<String, UpdateMethodInfo> updateMethodInfoCaches = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
void postConstruct() {
|
||||||
|
|
||||||
|
// 从 reflections 拿信息
|
||||||
|
List<Method> updateMethods =
|
||||||
|
new ArrayList<>(reflections.getMethodsAnnotatedWith(UpdateMethod.class));
|
||||||
|
updateMethods.sort(Comparator.comparingInt(m -> m.getAnnotation(UpdateMethod.class).order()));
|
||||||
|
|
||||||
|
methodLabel:
|
||||||
|
for (Method method : updateMethods) {
|
||||||
|
|
||||||
|
UpdateMethodInfo info = new UpdateMethodInfo();
|
||||||
|
|
||||||
|
// 获取 UpdateMethod 注解基本信息
|
||||||
|
UpdateMethod um = method.getAnnotation(UpdateMethod.class);
|
||||||
|
String name = um.name();
|
||||||
|
info.setName(name);
|
||||||
|
info.setMethodName(method.getName());
|
||||||
|
Class<?> declaringClass = method.getDeclaringClass();
|
||||||
|
info.setDeclaringClassName(declaringClass.getName());
|
||||||
|
|
||||||
|
// 判断该 Class 是否交由 Spring 管理
|
||||||
|
boolean managedBySpring = SpringBeanDetector.isSpringManagedClass(declaringClass);
|
||||||
|
if (!managedBySpring) {
|
||||||
|
// TODO: 非 Spring 管理类的成员方法 / 静态方法的数据更新?
|
||||||
|
log.warn("方法 {} 所属类 {} 不归属于 Spring 管理,目前暂不支持作为更新服务候选项",
|
||||||
|
method.getName(), declaringClass.getSimpleName());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取该方法的所有参数并读取
|
||||||
|
List<UpdateMethodInfo.UpdateParamInfo> paramInfos = new ArrayList<>();
|
||||||
|
for (Parameter p : method.getParameters()) {
|
||||||
|
|
||||||
|
UpdateMethodInfo.UpdateParamInfo updateParamInfo = new UpdateMethodInfo.UpdateParamInfo();
|
||||||
|
updateParamInfo.setName(p.getName());
|
||||||
|
Class<?> parameterType = p.getType();
|
||||||
|
updateParamInfo.setFullTypeName(parameterType.getName());
|
||||||
|
updateParamInfo.setTypeName(parameterType.getSimpleName());
|
||||||
|
updateParamInfo.setTypeClass(parameterType);
|
||||||
|
|
||||||
|
if (parameterType.isEnum()) {
|
||||||
|
|
||||||
|
// 当前枚举下的所有枚举值
|
||||||
|
List<String> allEnums = Arrays.stream(parameterType.getEnumConstants())
|
||||||
|
.map(Object::toString)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
AllowedEnum allowedEnum = p.getAnnotation(AllowedEnum.class);
|
||||||
|
if (allowedEnum == null) {
|
||||||
|
// 未指定 AllowedEnum,将所有枚举放到 allowedEnumValues 里
|
||||||
|
updateParamInfo.setAllowedEnumValues(allEnums);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 指定了 AllowedEnum,将允许的枚举放到 allowedEnumValues 里
|
||||||
|
String[] allowedEnums = allowedEnum.value();
|
||||||
|
List<String> allowedEnumValues = new ArrayList<>();
|
||||||
|
for (String specificEnum : allowedEnums) {
|
||||||
|
if (allEnums.contains(specificEnum)) {
|
||||||
|
allowedEnumValues.add(specificEnum);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn("枚举类 {} 不存在指定的枚举值 {},将忽略", parameterType.getSimpleName(), specificEnum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateParamInfo.setAllowedEnumValues(allowedEnumValues);
|
||||||
|
}
|
||||||
|
paramInfos.add(updateParamInfo);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn("方法 {} 参数 {} 非枚举类,目前暂不支持");
|
||||||
|
continue methodLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.setParams(paramInfos);
|
||||||
|
updateMethodInfoCaches.put(info.getId(), info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取潜在的数据更新方法。这些方法通过反射扫描得来
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
|
||||||
|
public List<UpdateMethodInfo> getPotentialUpdateMethodInfos() {
|
||||||
|
return new ArrayList<>(this.updateMethodInfoCaches.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 UpdateMethodInfo 的 id 查找原始方法模板
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public UpdateMethodInfo findById(String id) {
|
||||||
|
return this.updateMethodInfoCaches.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填充从外部来的 UpdateMethodInfo
|
||||||
|
* <p>
|
||||||
|
* 注意这里的 incoming 必须是通过 @Valid 校验的
|
||||||
|
* @param incoming
|
||||||
|
*/
|
||||||
|
public void fillUpdateMethodInfo(@Validated UpdateMethodInfo incoming) {
|
||||||
|
// 填充 method 信息
|
||||||
|
UpdateMethodInfo valid = findById(incoming.getId());
|
||||||
|
BeanUtils.copyProperties(valid, incoming, "params");
|
||||||
|
if (valid.getParams() == null || valid.getParams().size() == 0) return;
|
||||||
|
for (int i = 0; i < valid.getParams().size(); i++) {
|
||||||
|
UpdateParamInfo in = incoming.getParams().get(i);
|
||||||
|
UpdateParamInfo ref = valid.getParams().get(i);
|
||||||
|
BeanUtils.copyProperties(ref, in, "value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行方法
|
||||||
|
* @param updateMethodInfo
|
||||||
|
* @throws InvocationTargetException
|
||||||
|
* @throws IllegalAccessException
|
||||||
|
* @throws SecurityException
|
||||||
|
* @throws NoSuchMethodException
|
||||||
|
* @throws ClassNotFoundException
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void execute(@Validated UpdateMethodInfo updateMethodInfo) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException {
|
||||||
|
Class<?> declaringClass = Class.forName(updateMethodInfo.getDeclaringClassName());
|
||||||
|
Object declaringClassInstance = SpringContextHolder.getBean(declaringClass);
|
||||||
|
boolean hasParams = !updateMethodInfo.getParams().isEmpty();
|
||||||
|
Method method;
|
||||||
|
|
||||||
|
if (hasParams) {
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
Class[] paramTypes = new Class<?>[updateMethodInfo.getParams().size()];
|
||||||
|
Object[] paramValues = new Object[updateMethodInfo.getParams().size()];
|
||||||
|
for (int i = 0; i < updateMethodInfo.getParams().size(); i++) {
|
||||||
|
UpdateParamInfo param = updateMethodInfo.getParams().get(i);
|
||||||
|
paramTypes[i] = Class.forName(param.getFullTypeName());
|
||||||
|
// 转换 parameterValue
|
||||||
|
paramValues[i] = Enum.valueOf(paramTypes[i], param.getValue().toString());
|
||||||
|
}
|
||||||
|
method = declaringClass.getDeclaredMethod(updateMethodInfo.getMethodName(), paramTypes);
|
||||||
|
method.invoke(declaringClassInstance, paramValues);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
method = declaringClass.getDeclaredMethod(updateMethodInfo.getMethodName());
|
||||||
|
method.invoke(declaringClassInstance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package link.at17.mid.tushare.service;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.quartz.CronScheduleBuilder;
|
||||||
|
import org.quartz.CronTrigger;
|
||||||
|
import org.quartz.JobBuilder;
|
||||||
|
import org.quartz.JobDataMap;
|
||||||
|
import org.quartz.JobDetail;
|
||||||
|
import org.quartz.JobKey;
|
||||||
|
import org.quartz.Scheduler;
|
||||||
|
import org.quartz.SchedulerException;
|
||||||
|
import org.quartz.TriggerBuilder;
|
||||||
|
import org.quartz.TriggerKey;
|
||||||
|
import org.quartz.impl.matchers.GroupMatcher;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.DependsOn;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.validation.ConstraintViolation;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.Validator;
|
||||||
|
import link.at17.mid.tushare.dao.UpdatePlanDao;
|
||||||
|
import link.at17.mid.tushare.data.models.UpdateMethodInfo;
|
||||||
|
import link.at17.mid.tushare.data.models.UpdatePlan;
|
||||||
|
import link.at17.mid.tushare.task.job.UpdatePlanJob;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Lazy(false)
|
||||||
|
@DependsOn({"updateMethodService", "updateMethodInfoValidator"})
|
||||||
|
@Slf4j
|
||||||
|
@Validated
|
||||||
|
public class UpdatePlanService extends BaseServiceImpl<UpdatePlanDao, UpdatePlan> {
|
||||||
|
|
||||||
|
public static final String JOB_GROUP_NAME = "UpdatePlanGroup";
|
||||||
|
|
||||||
|
@Resource(name="scheduler")
|
||||||
|
Scheduler scheduler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
UpdateMethodService updateMethodService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
Validator validator;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
void postConstruct() throws SchedulerException {
|
||||||
|
// 从数据库加载所有已保存 UpdatePlan 并逐一判断其有效性
|
||||||
|
List<UpdatePlan> updatePlans = list();
|
||||||
|
for (UpdatePlan updatePlan : updatePlans) {
|
||||||
|
Set<ConstraintViolation<Object>> v = validator.validate(updatePlan);
|
||||||
|
if (!v.isEmpty()) {
|
||||||
|
v.forEach(violation -> {
|
||||||
|
log.error("UpdatePlan {} 发生错误: {}", updatePlan.getName(), violation.getMessage());
|
||||||
|
});
|
||||||
|
// 设置 UpdatePlan 为无效
|
||||||
|
updatePlan.setValid(false);
|
||||||
|
saveOrUpdate(updatePlan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatePlans = list();
|
||||||
|
updatePlans.forEach(updatePlan -> {
|
||||||
|
try {
|
||||||
|
rescheduleTask(updatePlan);
|
||||||
|
} catch (SchedulerException e) {
|
||||||
|
log.error("从 UpdatePlan 数据表编排任务失败,id = {}, name = {}", updatePlan.getId(), updatePlan.getName(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定的任务
|
||||||
|
* @param updatePlan
|
||||||
|
* @throws SchedulerException
|
||||||
|
*/
|
||||||
|
public void deleteTask(UpdatePlan updatePlan) throws SchedulerException {
|
||||||
|
deleteTask(updatePlan.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定的任务
|
||||||
|
* @param id UpdatePlan::getId
|
||||||
|
* @throws SchedulerException
|
||||||
|
*/
|
||||||
|
public void deleteTask(Integer id) throws SchedulerException {
|
||||||
|
JobKey jobKey = JobKey.jobKey(id.toString(), JOB_GROUP_NAME);
|
||||||
|
if (scheduler.checkExists(jobKey)) {
|
||||||
|
scheduler.deleteJob(jobKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 UpdatePlan 实例新建或更新任务
|
||||||
|
* @param updatePlan
|
||||||
|
* @throws SchedulerException
|
||||||
|
*/
|
||||||
|
public void rescheduleTask(@Validated UpdatePlan updatePlan) throws SchedulerException {
|
||||||
|
JobKey jobKey = JobKey.jobKey(updatePlan.getId().toString(), JOB_GROUP_NAME);
|
||||||
|
TriggerKey triggerKey = TriggerKey.triggerKey(updatePlan.getId().toString(), JOB_GROUP_NAME);
|
||||||
|
|
||||||
|
deleteTask(updatePlan.getId());
|
||||||
|
|
||||||
|
if (!updatePlan.getEnabled() || !updatePlan.getValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JobDataMap jobData = new JobDataMap();
|
||||||
|
jobData.put("updatePlan", updatePlan);
|
||||||
|
|
||||||
|
JobDetail job = JobBuilder.newJob(UpdatePlanJob.class)
|
||||||
|
.withIdentity(jobKey)
|
||||||
|
.usingJobData(jobData)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
CronTrigger trigger = TriggerBuilder.newTrigger()
|
||||||
|
.withIdentity(triggerKey)
|
||||||
|
.withSchedule(CronScheduleBuilder.cronSchedule(updatePlan.getCronExpr()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
scheduler.scheduleJob(job, trigger);
|
||||||
|
log.debug("已(重新)编排任务 [{}]{}, cronExpr = \"{}\"",
|
||||||
|
updatePlan.getId(), updatePlan.getName(), updatePlan.getCronExpr());
|
||||||
|
|
||||||
|
log.debug("当前所有任务:");
|
||||||
|
Set<JobKey> jobKeys = scheduler.getJobKeys(GroupMatcher.groupEquals(JOB_GROUP_NAME));
|
||||||
|
jobKeys.forEach(jk -> {
|
||||||
|
UpdatePlan relative = getById(Integer.valueOf(jk.getName()));
|
||||||
|
log.debug("UpdatePlan [{}]{}, cronExpr = \"{}\"",
|
||||||
|
relative.getId(), relative.getName(), relative.getCronExpr());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行方法
|
||||||
|
* @param updatePlan
|
||||||
|
* @throws ClassNotFoundException
|
||||||
|
* @throws NoSuchMethodException
|
||||||
|
* @throws SecurityException
|
||||||
|
* @throws IllegalAccessException
|
||||||
|
* @throws InvocationTargetException
|
||||||
|
*/
|
||||||
|
public void execute(@Valid UpdatePlan updatePlan) throws ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException, InvocationTargetException {
|
||||||
|
for (UpdateMethodInfo info : updatePlan.getMethods()) {
|
||||||
|
updateMethodService.execute(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<UpdatePlan> list() {
|
||||||
|
return list(new LambdaQueryWrapper<UpdatePlan>().orderByAsc(UpdatePlan::getId));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package link.at17.mid.tushare.system.util;
|
||||||
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
|
import org.springframework.stereotype.*;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SpringBeanDetector {
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
private static final List<Class> SPRING_COMPONENT_ANNOTATIONS = List.of(
|
||||||
|
Component.class,
|
||||||
|
Service.class,
|
||||||
|
Repository.class,
|
||||||
|
Controller.class,
|
||||||
|
RestController.class,
|
||||||
|
Configuration.class,
|
||||||
|
ControllerAdvice.class,
|
||||||
|
RestControllerAdvice.class,
|
||||||
|
Aspect.class
|
||||||
|
);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static boolean isSpringManagedClass(Class<?> clazz) {
|
||||||
|
for (@SuppressWarnings("rawtypes") Class ann : SPRING_COMPONENT_ANNOTATIONS) {
|
||||||
|
if (AnnotationUtils.findAnnotation(clazz, ann) != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static String findMatchedAnnotation(Class<?> clazz) {
|
||||||
|
for (@SuppressWarnings("rawtypes") Class ann : SPRING_COMPONENT_ANNOTATIONS) {
|
||||||
|
if (AnnotationUtils.findAnnotation(clazz, ann) != null) {
|
||||||
|
return ann.getSimpleName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import java.lang.reflect.Modifier;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
@@ -34,6 +35,14 @@ public class SpringContextHolder implements ApplicationContextAware, DisposableB
|
|||||||
|
|
||||||
private static ApplicationContext applicationContext = null;
|
private static ApplicationContext applicationContext = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实现ApplicationContextAware接口, 注入Context到静态变量中.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||||
|
SpringContextHolder.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取得存储在静态变量中的ApplicationContext.
|
* 取得存储在静态变量中的ApplicationContext.
|
||||||
*/
|
*/
|
||||||
@@ -59,6 +68,14 @@ public class SpringContextHolder implements ApplicationContextAware, DisposableB
|
|||||||
return applicationContext.getBean(requiredType);
|
return applicationContext.getBean(requiredType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从静态变量applicationContext中取得Beans, 自动转型为所赋值对象的类型的 Map.
|
||||||
|
*/
|
||||||
|
public static <T> Map<String, T> getBeansOfType(Class<T> requiredType) {
|
||||||
|
assertContextInjected();
|
||||||
|
return applicationContext.getBeansOfType(requiredType);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除SpringContextHolder中的ApplicationContext为Null.
|
* 清除SpringContextHolder中的ApplicationContext为Null.
|
||||||
*/
|
*/
|
||||||
@@ -69,13 +86,6 @@ public class SpringContextHolder implements ApplicationContextAware, DisposableB
|
|||||||
applicationContext = null;
|
applicationContext = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 实现ApplicationContextAware接口, 注入Context到静态变量中.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
|
||||||
SpringContextHolder.applicationContext = applicationContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 实现DisposableBean接口, 在Context关闭时清理静态变量.
|
* 实现DisposableBean接口, 在Context关闭时清理静态变量.
|
||||||
|
|||||||
@@ -5,18 +5,20 @@ import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
|||||||
import org.springframework.scheduling.quartz.AdaptableJobFactory;
|
import org.springframework.scheduling.quartz.AdaptableJobFactory;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 工厂,使得 Job 实例能获取 Spring 管理的 Bean
|
||||||
|
* <p>
|
||||||
|
* 如某 UpdatePlan implements Job, 在其 execute 方法中需要调用 updatePlanService, 则必须要实例能获取 updatePlanService
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class TaskSchedulerFactory extends AdaptableJobFactory {
|
public class AutowireCapableJobFactory extends AdaptableJobFactory {
|
||||||
|
|
||||||
// 需要使用这个BeanFactory对Qurartz创建好Job实例进行后续处理,属于Spring的技术范畴.
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private AutowireCapableBeanFactory capableBeanFactory;
|
private AutowireCapableBeanFactory capableBeanFactory;
|
||||||
|
|
||||||
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
|
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
|
||||||
// 首先,调用父类的方法创建好Quartz所需的Job实例
|
|
||||||
Object jobInstance = super.createJobInstance(bundle);
|
Object jobInstance = super.createJobInstance(bundle);
|
||||||
// 然后,使用BeanFactory为创建好的Job实例进行属性自动装配并将其纳入到Spring容器的管理之中,属于Spring的技术范畴.
|
capableBeanFactory.autowireBean(jobInstance); // 自动注入 Spring Bean
|
||||||
capableBeanFactory.autowireBean(jobInstance);
|
|
||||||
return jobInstance;
|
return jobInstance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package link.at17.mid.tushare.task.job;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.ArrayUtils;
|
|
||||||
import java.util.Calendar;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.time.DateUtils;
|
|
||||||
import org.quartz.Job;
|
|
||||||
import org.quartz.JobExecutionContext;
|
|
||||||
import org.quartz.JobExecutionException;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
|
|
||||||
import link.at17.mid.tushare.dao.StockCalendarDao;
|
|
||||||
import link.at17.mid.tushare.data.crawler.QueryWay;
|
|
||||||
import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler;
|
|
||||||
import link.at17.mid.tushare.data.models.StockCalendar;
|
|
||||||
import link.at17.mid.tushare.enums.StockHolderType;
|
|
||||||
import link.at17.mid.tushare.enums.StockMarket;
|
|
||||||
import link.at17.mid.tushare.system.util.SpringContextHolder;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
public class DailyUpdateDataJob implements Job {
|
|
||||||
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
TushareCrawler tushareCrawler;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
StockCalendarDao stockCalendarDao;
|
|
||||||
|
|
||||||
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
|
|
||||||
|
|
||||||
|
|
||||||
log.info("每日定时数据更新开始");
|
|
||||||
|
|
||||||
try {
|
|
||||||
String[] profile = SpringContextHolder.getApplicationContext().getEnvironment().getActiveProfiles();
|
|
||||||
if (ArrayUtils.contains(profile, "remote")) {
|
|
||||||
log.info("当前环境为远程调试环境,不参与每日数据更新");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
log.error("获取当前 active profile 失败", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
StockCalendar szLatestCal = stockCalendarDao.getLatest(StockMarket.SZ);
|
|
||||||
StockCalendar shLatestCal = stockCalendarDao.getLatest(StockMarket.SH);
|
|
||||||
if (szLatestCal == null || shLatestCal == null) {
|
|
||||||
tushareCrawler.updateStockCalendar(StockMarket.SH, StockMarket.SZ);
|
|
||||||
}
|
|
||||||
|
|
||||||
tushareCrawler.updateStockCalendar(StockMarket.SH, StockMarket.SZ);
|
|
||||||
|
|
||||||
// 查询当日是否是交易日
|
|
||||||
Boolean todayIsOpen = stockCalendarDao.exists(new LambdaQueryWrapper<StockCalendar>()
|
|
||||||
.eq(StockCalendar::getDate, DateUtils.truncate(Calendar.getInstance().getTime(), Calendar.DATE))
|
|
||||||
.eq(StockCalendar::getIsOpen, true));
|
|
||||||
|
|
||||||
if (!todayIsOpen) {
|
|
||||||
log.info("当日非交易日,忽略更新");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int updateStockListRetry = 5;
|
|
||||||
while (--updateStockListRetry > 0) {
|
|
||||||
if (tushareCrawler.updateStockList()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (updateStockListRetry == 0) {
|
|
||||||
// updateStockList failed
|
|
||||||
log.warn("updateStockList 尝试更新失败,将在下一结算日后更新");
|
|
||||||
}
|
|
||||||
tushareCrawler.updateStockDaily(QueryWay.ByDateUpdate);
|
|
||||||
tushareCrawler.updateDailyBasic(QueryWay.ByDateUpdate);
|
|
||||||
tushareCrawler.updateStockAdjustTushare(QueryWay.ByDateUpdate);
|
|
||||||
tushareCrawler.updateStockLimit(QueryWay.ByDateUpdate);
|
|
||||||
|
|
||||||
tushareCrawler.updateThsDaily(QueryWay.ByDateUpdate);
|
|
||||||
tushareCrawler.updateThsMember();
|
|
||||||
tushareCrawler.updateStockHolder(StockHolderType.TOP10Float);
|
|
||||||
|
|
||||||
log.info("每日定时更新数据完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package link.at17.mid.tushare.task.job;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
|
||||||
|
import org.quartz.Job;
|
||||||
|
import org.quartz.JobExecutionContext;
|
||||||
|
import org.quartz.JobExecutionException;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.data.models.UpdatePlan;
|
||||||
|
import link.at17.mid.tushare.data.service.StockCalendarService;
|
||||||
|
import link.at17.mid.tushare.service.UpdatePlanService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
@Validated
|
||||||
|
public class UpdatePlanJob implements Job {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UpdatePlanService updatePlanService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private StockCalendarService stockCalendarService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(JobExecutionContext context) throws JobExecutionException {
|
||||||
|
UpdatePlan plan = (UpdatePlan) context.getMergedJobDataMap().get("updatePlan");
|
||||||
|
|
||||||
|
if (!plan.getEnabled()) {
|
||||||
|
log.info("任务 [{}]{} 未启用,忽略任务", plan.getId(), plan.getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!plan.getValid()) {
|
||||||
|
log.info("任务 [{}]{} 不合法,忽略任务", plan.getId(), plan.getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 交易日检查
|
||||||
|
if (plan.getOpenDayCheck()) {
|
||||||
|
Boolean todayIsOpen = stockCalendarService.todayIsOpen();
|
||||||
|
if (!todayIsOpen) {
|
||||||
|
log.info("任务 [{}]{} 开启了交易日检查,当日非交易日,忽略任务", plan.getId(), plan.getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("任务 [{}]{} 开始执行...", plan.getId(), plan.getName());
|
||||||
|
updatePlanService.execute(plan);
|
||||||
|
log.info("任务 [{}]{} 执行完毕", plan.getId(), plan.getName());
|
||||||
|
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException
|
||||||
|
| InvocationTargetException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,10 +7,10 @@ import jakarta.annotation.PostConstruct;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import link.at17.mid.tushare.cache.CacheEvictionJob;
|
import link.at17.mid.tushare.cache.CacheEvictionJob;
|
||||||
import link.at17.mid.tushare.task.TaskConstants;
|
import link.at17.mid.tushare.task.TaskConstants;
|
||||||
import link.at17.mid.tushare.task.TaskSchedulerFactory;
|
import link.at17.mid.tushare.task.AutowireCapableJobFactory;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class CacheDailyEvictionScheduler extends TaskSchedulerFactory {
|
public class CacheDailyEvictionScheduler extends AutowireCapableJobFactory {
|
||||||
|
|
||||||
@Resource(name="scheduler")
|
@Resource(name="scheduler")
|
||||||
Scheduler scheduler;
|
Scheduler scheduler;
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
package link.at17.mid.tushare.task.scheduler;
|
|
||||||
|
|
||||||
import org.quartz.*;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import link.at17.mid.tushare.task.TaskSchedulerFactory;
|
|
||||||
import link.at17.mid.tushare.task.job.DailyUpdateDataJob;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class DailyUpdateDataScheduler extends TaskSchedulerFactory {
|
|
||||||
|
|
||||||
@Resource(name="scheduler")
|
|
||||||
private Scheduler scheduler;
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void startScheduler() throws SchedulerException {
|
|
||||||
//创建调度器Schedule
|
|
||||||
//创建JobDetail实例,并与HelloWordlJob类绑定
|
|
||||||
|
|
||||||
JobDetail jobDetail = JobBuilder.newJob(DailyUpdateDataJob.class).withIdentity("cronJob").build();
|
|
||||||
//创建触发器Trigger实例(每天3点执行)
|
|
||||||
CronTrigger cronTrigger =
|
|
||||||
TriggerBuilder.newTrigger().withIdentity("cronTrigger")
|
|
||||||
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 18 * * ? ")).build();
|
|
||||||
//开始执行
|
|
||||||
scheduler.scheduleJob(jobDetail, cronTrigger);
|
|
||||||
scheduler.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -40,8 +40,16 @@ public class ManageController {
|
|||||||
return "admin/manage/views/index.html";
|
return "admin/manage/views/index.html";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/manage/demo-index")
|
||||||
|
private String demoIndex() {
|
||||||
|
return "admin/manage/views/demo-index.html";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/manage/{*routine}")
|
@GetMapping("/manage/{*routine}")
|
||||||
private String routine(@PathVariable String routine) {
|
private String routine(@PathVariable String routine) {
|
||||||
|
routine = routine.replaceAll("\\/+", "/");
|
||||||
|
routine = routine.replaceAll("\\\\+", "/");
|
||||||
|
routine = routine.replaceFirst("\\/", "");
|
||||||
return "admin/manage/views/" + routine;
|
return "admin/manage/views/" + routine;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package link.at17.mid.tushare.web.controller;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.validation.Validator;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.data.models.UpdateMethodInfo;
|
||||||
|
import link.at17.mid.tushare.data.models.UpdatePlan;
|
||||||
|
import link.at17.mid.tushare.dto.LayPageReq;
|
||||||
|
import link.at17.mid.tushare.dto.LayPageResp;
|
||||||
|
import link.at17.mid.tushare.service.UpdateMethodService;
|
||||||
|
import link.at17.mid.tushare.service.UpdatePlanService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/admin/manage/reviews/update-methods")
|
||||||
|
@Slf4j
|
||||||
|
public class UpdateMethodController extends BaseController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
Validator validator;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
UpdateMethodService updateMethodService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
UpdatePlanService updatePlanService;
|
||||||
|
|
||||||
|
@GetMapping("/plan-list")
|
||||||
|
public String planList() {
|
||||||
|
return "admin/manage/reviews/plans/plan-list";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/method-list-resp")
|
||||||
|
@ResponseBody
|
||||||
|
public Collection<UpdateMethodInfo> updateMethodInfoList() {
|
||||||
|
return updateMethodService.getPotentialUpdateMethodInfos();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
@ResponseBody
|
||||||
|
public LayPageResp<?> list(LayPageReq<UpdatePlan> req) {
|
||||||
|
return new LayPageResp<>(updatePlanService.page(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package link.at17.mid.tushare.web.controller;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.validation.Validator;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
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.ResponseBody;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||||
|
|
||||||
|
import link.at17.mid.tushare.data.models.UpdatePlan;
|
||||||
|
import link.at17.mid.tushare.dto.LayPageReq;
|
||||||
|
import link.at17.mid.tushare.dto.LayPageResp;
|
||||||
|
import link.at17.mid.tushare.dto.R;
|
||||||
|
import link.at17.mid.tushare.service.UpdateMethodService;
|
||||||
|
import link.at17.mid.tushare.service.UpdatePlanService;
|
||||||
|
import link.at17.mid.tushare.web.exception.RException;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/admin/manage/reviews/plans")
|
||||||
|
@Slf4j
|
||||||
|
@Validated
|
||||||
|
public class UpdatePlanController extends BaseController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
Validator validator;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
UpdateMethodService updateMethodService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
UpdatePlanService updatePlanService;
|
||||||
|
|
||||||
|
@GetMapping("/plan-list")
|
||||||
|
public String planList() {
|
||||||
|
return "admin/manage/reviews/plans/plan-list";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
@ResponseBody
|
||||||
|
public LayPageResp<?> list(LayPageReq<UpdatePlan> req) {
|
||||||
|
return new LayPageResp<>(updatePlanService.page(req, new LambdaQueryWrapper<UpdatePlan>().orderByAsc(UpdatePlan::getId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/get")
|
||||||
|
@ResponseBody
|
||||||
|
public R<?> get(Integer id) {
|
||||||
|
UpdatePlan plan;
|
||||||
|
if (id == null) {
|
||||||
|
plan = new UpdatePlan();
|
||||||
|
return R.ok(plan);
|
||||||
|
}
|
||||||
|
plan = updatePlanService.getById(id);
|
||||||
|
return R.judge(plan != null, plan, "无法找到对应 ID 的 Plan");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/save")
|
||||||
|
@ResponseBody
|
||||||
|
public R<?> save(@Validated @RequestBody UpdatePlan updatePlan) {
|
||||||
|
updatePlan.getMethods().forEach(method -> updateMethodService.fillUpdateMethodInfo(method));
|
||||||
|
updatePlan.setValid(true);
|
||||||
|
return R.judge(updatePlanService.saveOrUpdate(updatePlan));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/updateBool")
|
||||||
|
@ResponseBody
|
||||||
|
public R<?> updateBool(Integer id, String name, Boolean value) {
|
||||||
|
if (!List.of("enabled", "openDayCheck").contains(name)) {
|
||||||
|
throw RException.badRequest("非法字段名" + name);
|
||||||
|
}
|
||||||
|
if (value == null) {
|
||||||
|
throw RException.badRequest("不允许空值");
|
||||||
|
}
|
||||||
|
|
||||||
|
TableInfo tableInfo = TableInfoHelper.getTableInfo(UpdatePlan.class);
|
||||||
|
String idField = tableInfo.getKeyColumn();
|
||||||
|
String dbField = tableInfo.getFieldList().stream()
|
||||||
|
.filter(f -> f.getProperty().equals(name))
|
||||||
|
.findFirst()
|
||||||
|
.map(TableFieldInfo::getColumn)
|
||||||
|
.orElse(null);
|
||||||
|
return R.judge(updatePlanService.update(new UpdateWrapper<UpdatePlan>().eq(idField, id).set(dbField, value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/execute")
|
||||||
|
@ResponseBody
|
||||||
|
public void execute(Integer id) throws ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException, InvocationTargetException {
|
||||||
|
UpdatePlan updatePlan = updatePlanService.getById(id);
|
||||||
|
updatePlanService.execute(updatePlan);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -27,10 +27,10 @@ mybatis-plus:
|
|||||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||||
|
|
||||||
logging.level:
|
logging.level:
|
||||||
link.at17.mid.tushare: info
|
link.at17.mid.tushare: debug
|
||||||
link.at17.mid.tushare.test: debug
|
link.at17.mid.tushare.test: debug
|
||||||
org.springframework.security: debug
|
# org.springframework.security: debug
|
||||||
org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager: trace
|
# org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager: trace
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
devtools:
|
devtools:
|
||||||
@@ -50,7 +50,7 @@ spring:
|
|||||||
profiles.active: local
|
profiles.active: local
|
||||||
session.timeout: 86400
|
session.timeout: 86400
|
||||||
thymeleaf:
|
thymeleaf:
|
||||||
prefix: classpath:/webpage/
|
prefix: classpath:/templates/
|
||||||
suffix: .html
|
suffix: .html
|
||||||
mode: HTML
|
mode: HTML
|
||||||
encoding: UTF-8
|
encoding: UTF-8
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"tushareToken" : "123",
|
"tushareToken" : "6f284d9246bad80c3eff946f3ecae8442072b1e60652785f66007509",
|
||||||
"proxyType" : "SOCKS",
|
"proxyType" : "DIRECT",
|
||||||
"proxyHost" : "",
|
"proxyHost" : "",
|
||||||
"proxyPort" : 1,
|
"proxyPort" : 1,
|
||||||
"ignoreHttpsVerification" : true,
|
"ignoreHttpsVerification" : false,
|
||||||
"proxyUrl" : "socks5://:1"
|
"proxyUrl" : null
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
"https://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||||
<mapper namespace="link.at17.mid.tushare.dao.StockAdjustDao">
|
<mapper namespace="link.at17.mid.tushare.dao.StockAdjustDao">
|
||||||
|
|
||||||
<insert id="insertOrUpdateListTushare" parameterType="list">
|
<insert id="insertOrUpdateList" parameterType="list">
|
||||||
INSERT INTO stock_adjust_factor_tushare
|
INSERT INTO stock_adjust_factor_tushare
|
||||||
(ts_code, trade_date, adj_factor)
|
(ts_code, trade_date, adj_factor)
|
||||||
VALUES
|
VALUES
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
</if>
|
</if>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getAllOpenDate" resultType="java.time.LocalDateTime">
|
<select id="getAllOpenDateSence" resultType="java.time.LocalDateTime">
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
"date"
|
"date"
|
||||||
FROM
|
FROM
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
d.vol, d.amount, d.pct_chg, d."change"
|
d.vol, d.amount, d.pct_chg, d."change"
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<select id="getQfqDailyTushare" resultType="link.at17.mid.tushare.data.models.StockValue">
|
<select id="getQfqDaily" resultType="link.at17.mid.tushare.data.models.StockValue">
|
||||||
SELECT
|
SELECT
|
||||||
<include refid="qfqFields"/>
|
<include refid="qfqFields"/>
|
||||||
FROM
|
FROM
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
s.trade_date ASC
|
s.trade_date ASC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getQfqDailyBeforeTushare" resultType="link.at17.mid.tushare.data.models.StockValue">
|
<select id="getQfqDailyBefore" resultType="link.at17.mid.tushare.data.models.StockValue">
|
||||||
SELECT * FROM (
|
SELECT * FROM (
|
||||||
SELECT
|
SELECT
|
||||||
<include refid="qfqFields"/>
|
<include refid="qfqFields"/>
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
|
||||||
<select id="getQfqDailyAfterTushare" resultType="link.at17.mid.tushare.data.models.StockValue">
|
<select id="getQfqDailyAfter" resultType="link.at17.mid.tushare.data.models.StockValue">
|
||||||
SELECT
|
SELECT
|
||||||
<include refid="qfqFields"/>
|
<include refid="qfqFields"/>
|
||||||
FROM
|
FROM
|
||||||
@@ -254,7 +254,7 @@
|
|||||||
</if>
|
</if>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getExQfqDailyAfterTushare" resultType="link.at17.mid.tushare.data.models.StockValueEx">
|
<select id="getExQfqDailyAfter" resultType="link.at17.mid.tushare.data.models.StockValueEx">
|
||||||
SELECT
|
SELECT
|
||||||
<include refid="qfqFields"/>,
|
<include refid="qfqFields"/>,
|
||||||
b.turnover_rate AS turnoverRate,
|
b.turnover_rate AS turnoverRate,
|
||||||
@@ -325,7 +325,7 @@
|
|||||||
</if>
|
</if>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getExQfqDailyBeforeTushare" resultType="link.at17.mid.tushare.data.models.StockValueEx">
|
<select id="getExQfqDailyBefore" resultType="link.at17.mid.tushare.data.models.StockValueEx">
|
||||||
SELECT * FROM (
|
SELECT * FROM (
|
||||||
SELECT
|
SELECT
|
||||||
<include refid="qfqFields"/>,
|
<include refid="qfqFields"/>,
|
||||||
@@ -398,7 +398,7 @@
|
|||||||
) f ORDER BY f.trade_date ASC
|
) f ORDER BY f.trade_date ASC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getExDailyBeforeTushare" resultType="link.at17.mid.tushare.data.models.StockValueEx">
|
<select id="getExDailyBefore" resultType="link.at17.mid.tushare.data.models.StockValueEx">
|
||||||
SELECT * FROM (
|
SELECT * FROM (
|
||||||
SELECT
|
SELECT
|
||||||
d.*, d.trade_date as "date",
|
d.*, d.trade_date as "date",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
"https://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||||
<mapper namespace="link.at17.mid.tushare.dao.StockHolderDao">
|
<mapper namespace="link.at17.mid.tushare.dao.StockHolderDao">
|
||||||
|
|
||||||
<insert id="insertOrUpdateListTushare" parameterType="list">
|
<insert id="insertOrUpdateList" parameterType="list">
|
||||||
INSERT INTO stock_holder
|
INSERT INTO stock_holder
|
||||||
(ts_code, ann_date, end_date, holder_name, hold_amount, hold_ratio, holder_type)
|
(ts_code, ann_date, end_date, holder_name, hold_amount, hold_ratio, holder_type)
|
||||||
VALUES
|
VALUES
|
||||||
|
|||||||
@@ -36,12 +36,12 @@
|
|||||||
stock_info i
|
stock_info i
|
||||||
|
|
||||||
|
|
||||||
INNER JOIN stock_ths_member m ON (m.con_code = i.ts_code
|
LEFT JOIN stock_ths_member m ON (m.con_code = i.ts_code
|
||||||
<if test="stockCode != null">
|
<if test="stockCode != null">
|
||||||
AND m.con_code = #{stockCode}
|
AND m.con_code = #{stockCode}
|
||||||
</if>
|
</if>
|
||||||
)
|
)
|
||||||
INNER JOIN stock_ths_list l ON l.ts_code = m.ts_code
|
LEFT JOIN stock_ths_list l ON l.ts_code = m.ts_code
|
||||||
WHERE
|
WHERE
|
||||||
1 = 1
|
1 = 1
|
||||||
<if test="stockCode != null">
|
<if test="stockCode != null">
|
||||||
@@ -57,8 +57,8 @@
|
|||||||
STRING_AGG(l."name", ', ' ORDER BY l.list_date DESC) AS ths_belongings
|
STRING_AGG(l."name", ', ' ORDER BY l.list_date DESC) AS ths_belongings
|
||||||
FROM
|
FROM
|
||||||
stock_info i
|
stock_info i
|
||||||
INNER JOIN stock_ths_member m ON m.con_code = i.ts_code
|
LEFT JOIN stock_ths_member m ON m.con_code = i.ts_code
|
||||||
INNER JOIN stock_ths_list l ON l.ts_code = m.ts_code
|
LEFT JOIN stock_ths_list l ON l.ts_code = m.ts_code
|
||||||
WHERE
|
WHERE
|
||||||
1 = 1
|
1 = 1
|
||||||
<if test="listStatus != null">
|
<if test="listStatus != null">
|
||||||
@@ -67,4 +67,34 @@
|
|||||||
GROUP BY i.ts_code
|
GROUP BY i.ts_code
|
||||||
ORDER BY i.ts_code ASC
|
ORDER BY i.ts_code ASC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="list" resultType="link.at17.mid.tushare.data.models.StockInfo">
|
||||||
|
SELECT
|
||||||
|
i.*,
|
||||||
|
STRING_AGG(l."name", ', ' ORDER BY l.list_date DESC) AS ths_belongings
|
||||||
|
FROM
|
||||||
|
stock_info i
|
||||||
|
LEFT JOIN stock_ths_member m ON m.con_code = i.ts_code
|
||||||
|
LEFT JOIN stock_ths_list l ON l.ts_code = m.ts_code
|
||||||
|
<if test="ew != null">
|
||||||
|
${ew.customSqlSegment}
|
||||||
|
</if>
|
||||||
|
GROUP BY i.ts_code
|
||||||
|
ORDER BY i.ts_code ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectList" resultType="link.at17.mid.tushare.data.models.StockInfo">
|
||||||
|
SELECT
|
||||||
|
i.*,
|
||||||
|
STRING_AGG(l."name", ', ' ORDER BY l.list_date DESC) AS ths_belongings
|
||||||
|
FROM
|
||||||
|
stock_info i
|
||||||
|
LEFT JOIN stock_ths_member m ON m.con_code = i.ts_code
|
||||||
|
LEFT JOIN stock_ths_list l ON l.ts_code = m.ts_code
|
||||||
|
<if test="ew != null">
|
||||||
|
${ew.customSqlSegment}
|
||||||
|
</if>
|
||||||
|
GROUP BY i.ts_code
|
||||||
|
ORDER BY i.ts_code ASC
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
"https://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||||
<mapper namespace="link.at17.mid.tushare.dao.StockLimitDao">
|
<mapper namespace="link.at17.mid.tushare.dao.StockLimitDao">
|
||||||
|
|
||||||
<insert id="insertOrUpdateListTushare" parameterType="list">
|
<insert id="insertOrUpdateList" parameterType="list">
|
||||||
INSERT INTO stock_limit
|
INSERT INTO stock_limit
|
||||||
(trade_date, ts_code, industry, name, close, pct_chg, amount, limit_amount, float_mv, total_mv, turnover_ratio, fd_amount, first_time, last_time, open_times, up_stat, limit_times, "limit")
|
(trade_date, ts_code, industry, name, close, pct_chg, amount, limit_amount, float_mv, total_mv, turnover_ratio, fd_amount, first_time, last_time, open_times, up_stat, limit_times, "limit")
|
||||||
VALUES
|
VALUES
|
||||||
|
|||||||
@@ -108,7 +108,7 @@
|
|||||||
m.vol, m.amount, m.freq
|
m.vol, m.amount, m.freq
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<select id="getQfqMinuteTushare" resultType="link.at17.mid.tushare.data.models.StockValue">
|
<select id="getQfqMinute" resultType="link.at17.mid.tushare.data.models.StockValue">
|
||||||
SELECT
|
SELECT
|
||||||
<include refid="qfqFields"/>
|
<include refid="qfqFields"/>
|
||||||
FROM
|
FROM
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
m.trade_time ASC
|
m.trade_time ASC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getQfqMinuteBeforeTushare" resultType="link.at17.mid.tushare.data.models.StockValue">
|
<select id="getQfqMinuteBefore" resultType="link.at17.mid.tushare.data.models.StockValue">
|
||||||
SELECT * FROM (
|
SELECT * FROM (
|
||||||
SELECT
|
SELECT
|
||||||
<include refid="qfqFields"/>
|
<include refid="qfqFields"/>
|
||||||
@@ -216,7 +216,7 @@
|
|||||||
) f ORDER BY f.date ASC
|
) f ORDER BY f.date ASC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getExQfqMinuteBeforeTushare" resultType="link.at17.mid.tushare.data.models.StockValueEx">
|
<select id="getExQfqMinuteBefore" resultType="link.at17.mid.tushare.data.models.StockValueEx">
|
||||||
SELECT * FROM (
|
SELECT * FROM (
|
||||||
SELECT
|
SELECT
|
||||||
<include refid="qfqFields"/>,
|
<include refid="qfqFields"/>,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
"https://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||||
<mapper namespace="link.at17.mid.tushare.dao.StockThsDailyDao">
|
<mapper namespace="link.at17.mid.tushare.dao.ThsDailyDao">
|
||||||
|
|
||||||
<insert id="insertOrUpdateList" parameterType="list">
|
<insert id="insertOrUpdateList" parameterType="list">
|
||||||
INSERT INTO stock_ths_daily
|
INSERT INTO stock_ths_daily
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
"https://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||||
<mapper namespace="link.at17.mid.tushare.dao.StockThsListDao">
|
<mapper namespace="link.at17.mid.tushare.dao.ThsListDao">
|
||||||
|
|
||||||
<insert id="insertOrUpdateList" parameterType="list">
|
<insert id="insertOrUpdateList" parameterType="list">
|
||||||
INSERT INTO stock_ths_list
|
INSERT INTO stock_ths_list
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
"https://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||||
<mapper namespace="link.at17.mid.tushare.dao.StockThsMemberDao">
|
<mapper namespace="link.at17.mid.tushare.dao.ThsMemberDao">
|
||||||
|
|
||||||
<insert id="insertOrUpdateList" parameterType="list">
|
<insert id="insertOrUpdateList" parameterType="list">
|
||||||
INSERT INTO stock_ths_member
|
INSERT INTO stock_ths_member
|
||||||
@@ -12,10 +12,21 @@
|
|||||||
Target Server Version : 170000 (170000)
|
Target Server Version : 170000 (170000)
|
||||||
File Encoding : 65001
|
File Encoding : 65001
|
||||||
|
|
||||||
Date: 14/10/2025 15:08:02
|
Date: 27/10/2025 23:25:57
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Sequence structure for update_plan_id_seq
|
||||||
|
-- ----------------------------
|
||||||
|
DROP SEQUENCE IF EXISTS "public"."update_plan_id_seq";
|
||||||
|
CREATE SEQUENCE "public"."update_plan_id_seq"
|
||||||
|
INCREMENT 1
|
||||||
|
MINVALUE 1
|
||||||
|
MAXVALUE 2147483647
|
||||||
|
START 1
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for stock_adjust_factor_tushare
|
-- Table structure for stock_adjust_factor_tushare
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
@@ -106,7 +117,7 @@ DROP TABLE IF EXISTS "public"."stock_holder";
|
|||||||
CREATE TABLE "public"."stock_holder" (
|
CREATE TABLE "public"."stock_holder" (
|
||||||
"ts_code" varchar(11) COLLATE "pg_catalog"."default" NOT NULL,
|
"ts_code" varchar(11) COLLATE "pg_catalog"."default" NOT NULL,
|
||||||
"ann_date" date NOT NULL,
|
"ann_date" date NOT NULL,
|
||||||
"end_date" date NOT NULL,
|
"end_date" timestamp(0) NOT NULL,
|
||||||
"holder_name" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
|
"holder_name" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
|
||||||
"hold_amount" numeric(23,3) NOT NULL,
|
"hold_amount" numeric(23,3) NOT NULL,
|
||||||
"hold_ratio" numeric(23,3) DEFAULT NULL::numeric,
|
"hold_ratio" numeric(23,3) DEFAULT NULL::numeric,
|
||||||
@@ -199,7 +210,7 @@ CREATE TABLE "public"."stock_minute" (
|
|||||||
DROP TABLE IF EXISTS "public"."stock_ths_daily";
|
DROP TABLE IF EXISTS "public"."stock_ths_daily";
|
||||||
CREATE TABLE "public"."stock_ths_daily" (
|
CREATE TABLE "public"."stock_ths_daily" (
|
||||||
"ts_code" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
|
"ts_code" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
|
||||||
"trade_date" date NOT NULL,
|
"trade_date" timestamp(0) NOT NULL,
|
||||||
"close" numeric(23,3) DEFAULT NULL::numeric,
|
"close" numeric(23,3) DEFAULT NULL::numeric,
|
||||||
"open" numeric(23,3) DEFAULT NULL::numeric,
|
"open" numeric(23,3) DEFAULT NULL::numeric,
|
||||||
"high" numeric(23,3) DEFAULT NULL::numeric,
|
"high" numeric(23,3) DEFAULT NULL::numeric,
|
||||||
@@ -224,7 +235,7 @@ CREATE TABLE "public"."stock_ths_list" (
|
|||||||
"name" varchar(255) COLLATE "pg_catalog"."default",
|
"name" varchar(255) COLLATE "pg_catalog"."default",
|
||||||
"count" int4,
|
"count" int4,
|
||||||
"exchange" varchar(255) COLLATE "pg_catalog"."default",
|
"exchange" varchar(255) COLLATE "pg_catalog"."default",
|
||||||
"list_date" date,
|
"list_date" timestamp(0),
|
||||||
"type" varchar(5) COLLATE "pg_catalog"."default"
|
"type" varchar(5) COLLATE "pg_catalog"."default"
|
||||||
)
|
)
|
||||||
;
|
;
|
||||||
@@ -244,6 +255,26 @@ CREATE TABLE "public"."stock_ths_member" (
|
|||||||
)
|
)
|
||||||
;
|
;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for update_plan
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS "public"."update_plan";
|
||||||
|
CREATE TABLE "public"."update_plan" (
|
||||||
|
"id" int4 NOT NULL GENERATED BY DEFAULT AS IDENTITY (
|
||||||
|
INCREMENT 1
|
||||||
|
MINVALUE 1
|
||||||
|
MAXVALUE 2147483647
|
||||||
|
START 1
|
||||||
|
CACHE 1
|
||||||
|
),
|
||||||
|
"cron_expr" varchar(255) COLLATE "pg_catalog"."default",
|
||||||
|
"name" varchar(255) COLLATE "pg_catalog"."default",
|
||||||
|
"methods" text COLLATE "pg_catalog"."default",
|
||||||
|
"open_day_check" bool,
|
||||||
|
"enabled" bool
|
||||||
|
)
|
||||||
|
;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for web_user
|
-- Table structure for web_user
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
@@ -258,6 +289,13 @@ CREATE TABLE "public"."web_user" (
|
|||||||
)
|
)
|
||||||
;
|
;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Alter sequences owned by
|
||||||
|
-- ----------------------------
|
||||||
|
ALTER SEQUENCE "public"."update_plan_id_seq"
|
||||||
|
OWNED BY "public"."update_plan"."id";
|
||||||
|
SELECT setval('"public"."update_plan_id_seq"', 2, true);
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Primary Key structure for table stock_adjust_factor_tushare
|
-- Primary Key structure for table stock_adjust_factor_tushare
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
@@ -341,6 +379,11 @@ ALTER TABLE "public"."stock_ths_list" ADD CONSTRAINT "stock_ths_list_pkey" PRIMA
|
|||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
ALTER TABLE "public"."stock_ths_member" ADD CONSTRAINT "stock_ths_member_pkey" PRIMARY KEY ("ts_code", "con_code", "con_name");
|
ALTER TABLE "public"."stock_ths_member" ADD CONSTRAINT "stock_ths_member_pkey" PRIMARY KEY ("ts_code", "con_code", "con_name");
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Primary Key structure for table update_plan
|
||||||
|
-- ----------------------------
|
||||||
|
ALTER TABLE "public"."update_plan" ADD CONSTRAINT "update_plan_pkey" PRIMARY KEY ("id");
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Primary Key structure for table web_user
|
-- Primary Key structure for table web_user
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
|
|||||||
BIN
src/main/resources/static/admin/res/adminui/dist/css/res/logo.png
vendored
Normal file
BIN
src/main/resources/static/admin/res/adminui/dist/css/res/logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
1198
src/main/resources/static/admin/res/modules/cron.js
Normal file
1198
src/main/resources/static/admin/res/modules/cron.js
Normal file
File diff suppressed because it is too large
Load Diff
195
src/main/resources/static/admin/res/modules/cron/cron.css
Normal file
195
src/main/resources/static/admin/res/modules/cron/cron.css
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
@ Name:layui.cron Cron表达式解析器
|
||||||
|
@ Author:贝哥哥
|
||||||
|
@ License:MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/* 样式加载完毕的标识 */
|
||||||
|
html #layuicss-cron {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
width: 1989px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 初始化 */
|
||||||
|
.layui-cron * {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主体结构 */
|
||||||
|
.layui-cron,
|
||||||
|
.layui-cron * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-cron {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 66666666;
|
||||||
|
margin: 5px 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 14px;
|
||||||
|
-webkit-animation-duration: 0.2s;
|
||||||
|
animation-duration: 0.2s;
|
||||||
|
-webkit-animation-fill-mode: both;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .layui-cron-main{width: 272px;} */
|
||||||
|
.layui-cron-header *,
|
||||||
|
.layui-cron-content .btn {
|
||||||
|
transition-duration: .3s;
|
||||||
|
-webkit-transition-duration: .3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 微微往下滑入 */
|
||||||
|
@keyframes cron-downbit {
|
||||||
|
0% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: translate3d(0, -5px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.layui-cron{animation-name: cron-downbit;}
|
||||||
|
.layui-cron-static{ position: relative; z-index: 0; display: inline-block; margin: 0; -webkit-animation: none; animation: none;}
|
||||||
|
|
||||||
|
|
||||||
|
/* 主体结构 */
|
||||||
|
.layui-cron-content{position: relative; padding: 10px; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none;}
|
||||||
|
|
||||||
|
|
||||||
|
/* 底部结构 */
|
||||||
|
.layui-cron-footer{position: relative; height: 46px; line-height: 26px; padding: 10px 20px;border-top: 1px solid whitesmoke;}
|
||||||
|
.layui-cron-footer span{margin-right: 15px; display: inline-block; cursor: pointer; font-size: 12px;}
|
||||||
|
.layui-cron-footer span:hover{color: #5FB878;}
|
||||||
|
.cron-footer-btns{position: absolute; right: 10px; top: 10px;}
|
||||||
|
.cron-footer-btns span{height: 26px; line-height: 26px; margin: 0 0 0 -1px; padding: 0 10px; border: 1px solid #C9C9C9; background-color: #fff; white-space: nowrap; vertical-align: top; border-radius: 2px;}
|
||||||
|
|
||||||
|
|
||||||
|
/* 提示 */
|
||||||
|
.layui-cron-hint{position: absolute; top: 115px; left: 50%; width: 250px; margin-left: -125px; line-height: 20px; padding: 15px; text-align: center; font-size: 12px; color: #FF5722;}
|
||||||
|
|
||||||
|
|
||||||
|
/* 默认简约主题 */
|
||||||
|
.layui-cron, .layui-cron-hint{border: 1px solid #d2d2d2; box-shadow: 0 2px 4px rgba(0,0,0,.12); background-color: #fff; color: #666;}
|
||||||
|
.layui-cron-content{border-top: none 0; border-bottom: none 0;}
|
||||||
|
|
||||||
|
/* tab */
|
||||||
|
.layui-cron .layui-tab-card{
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-cron .layui-tab-card > .layui-tab-title li{
|
||||||
|
min-width: 70px;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
.layui-cron .layui-tab-content{
|
||||||
|
padding: 10px;
|
||||||
|
height: 230px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* form */
|
||||||
|
.layui-cron .cron-input-mid {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 0 12px;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.layui-cron .cron-input {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 0 8px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 48px;
|
||||||
|
text-align: right;
|
||||||
|
-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
|
||||||
|
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||||
|
transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
|
||||||
|
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||||
|
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 谷歌 */
|
||||||
|
.layui-cron input::-webkit-outer-spin-button,
|
||||||
|
.layui-cron input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 火狐 */
|
||||||
|
.layui-cron input[type="number"]{
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-cron .cron-input:focus {
|
||||||
|
outline: 0;
|
||||||
|
border: 1px solid #01AAED;
|
||||||
|
box-shadow: inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 4px 0px #01aaed;
|
||||||
|
translate: 1s;
|
||||||
|
}
|
||||||
|
.layui-cron .cron-tips {
|
||||||
|
color: grey;
|
||||||
|
line-height: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-cron .layui-form-radio{
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-cron .cron-row{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.layui-cron .cron-row+.cron-row{
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-cron .cron-grid{
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
width: 480px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-cron .cron-grid .layui-form-checkbox{
|
||||||
|
padding-left: 22px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.layui-cron .cron-grid .layui-form-checkbox[lay-skin="primary"] span{
|
||||||
|
padding-right: 13px;
|
||||||
|
min-width: 29px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提示 */
|
||||||
|
.layui-cron-hint{position: absolute; top: 115px; left: 50%; width: 250px; margin-left: -125px; line-height: 20px; padding: 15px; text-align: center; font-size: 12px; color: #FF5722;}
|
||||||
|
|
||||||
|
.layui-cron-run-hint{
|
||||||
|
max-height: 104px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding: 10px;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
.cron-run-list+.cron-run-list{
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
BIN
src/main/resources/static/favicon.ico
Normal file
BIN
src/main/resources/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
@@ -169,7 +169,7 @@ layui.use(['table', 'slider', 'element', 'form'], function() {
|
|||||||
toolbar: '#toolbar',
|
toolbar: '#toolbar',
|
||||||
text: {none: '请输入股票代码查询数据'},
|
text: {none: '请输入股票代码查询数据'},
|
||||||
cols: [ (function() {
|
cols: [ (function() {
|
||||||
let a = [{
|
let a = [ {
|
||||||
field: 'dateStr',
|
field: 'dateStr',
|
||||||
title: '锚点变更日',
|
title: '锚点变更日',
|
||||||
templet: (d) => {
|
templet: (d) => {
|
||||||
@@ -378,7 +378,7 @@ layui.use(['table', 'slider', 'element', 'form'], function() {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'clearPeriodBlock':
|
case 'clearPeriodBlock':
|
||||||
myChart.setOption({series: [{
|
myChart.setOption({series: [ {
|
||||||
markArea: {data: undefined}}]});
|
markArea: {data: undefined}}]});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -902,7 +902,7 @@ let toGraphicsIndex = (i, period) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
myChart.setOption({series: [{
|
myChart.setOption({series: [ {
|
||||||
data: window._values,
|
data: window._values,
|
||||||
markArea: {data: data}}]});
|
markArea: {data: data}}]});
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,444 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>layui table 组件综合演示</title>
|
||||||
|
<meta name="renderer" content="webkit">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link href="../../../res/layui/css/layui.css" rel="stylesheet">
|
||||||
|
<link href="../../../res/adminui/dist/css/admin.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script id="addPlan" type="text/html">
|
||||||
|
<style>
|
||||||
|
.layui-form-select dl{max-height: 160px}
|
||||||
|
.top-0 {top: 0!important}
|
||||||
|
td[handler] {cursor: move}
|
||||||
|
</style>
|
||||||
|
<div class="layui-form" style="margin:10px 15px" id="editPlanForm" lay-filter="editPlanForm">
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<input type="hidden" name="id"/>
|
||||||
|
<label class="layui-form-label">计划名称<span>*</span></label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" lay-verify="required" name="name" 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">
|
||||||
|
<table id="table-methods" lay-filter="table-methods"></table>
|
||||||
|
</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>
|
||||||
|
<div class="layui-card layadmin-header">
|
||||||
|
<div class="layui-breadcrumb" lay-filter="breadcrumb">
|
||||||
|
<a lay-href="">主页</a>
|
||||||
|
<a><cite>应用</cite></a>
|
||||||
|
<a><cite>任务</cite></a>
|
||||||
|
<a><cite>计划列表</cite></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="layui-fluid">
|
||||||
|
<div class="layui-row layui-col-space15">
|
||||||
|
<div class="layui-col-md12">
|
||||||
|
<div class="layui-card">
|
||||||
|
<div class="layui-card-header">综合演示</div>
|
||||||
|
<div class="layui-card-body">
|
||||||
|
<table class="layui-hide" id="plan-table" lay-filter="plan-table"></table>
|
||||||
|
<script type="text/html" id="toolbarDemo">
|
||||||
|
<div class="layui-btn-container">
|
||||||
|
<button class="layui-btn layui-btn-sm" lay-event="new">新增</button>
|
||||||
|
<button class="layui-btn layui-btn-sm layui-bg-blue" id="reloadTest">
|
||||||
|
重载测试
|
||||||
|
<i class="layui-icon layui-icon-down layui-font-12"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
<script type="text/html" id="enabled-switchTpl">
|
||||||
|
<input type="checkbox" name="enabled" title="" lay-text="启用|停用" lay-skin="switch" lay-filter="table-switch"
|
||||||
|
value="{{d.id}}" {{ d.enabled ? 'checked' : ''}}>
|
||||||
|
</script>
|
||||||
|
<script type="text/html" id="openDayCheck-switchTpl">
|
||||||
|
<input type="checkbox" name="openDayCheck" title="" lay-text="开启|关闭" lay-skin="switch" lay-filter="table-switch"
|
||||||
|
value="{{d.id}}" {{ d.openDayCheck ? 'checked' : ''}}>
|
||||||
|
</script>
|
||||||
|
<script type="text/html" id="barDemo">
|
||||||
|
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
|
||||||
|
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../../../res/layui/layui.js"></script>
|
||||||
|
<script>
|
||||||
|
layui
|
||||||
|
.config({
|
||||||
|
base: '../../../res/' // 静态资源所在路径
|
||||||
|
}).use(['index', 'table', 'dropdown', 'cron'], function(){
|
||||||
|
var table = layui.table, dropdown = layui.dropdown, form = layui.form, $ = layui.$;
|
||||||
|
var methods;
|
||||||
|
var sortableInstance = null;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/admin/manage/reviews/update-methods/method-list-resp',
|
||||||
|
success: res => methods = res
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建渲染实例
|
||||||
|
table.render({
|
||||||
|
elem: '#plan-table'
|
||||||
|
,url: '/admin/manage/reviews/plans/list' // 此处为静态模拟数据,实际使用时需换成真实接口
|
||||||
|
,toolbar: '#toolbarDemo'
|
||||||
|
,defaultToolbar: ['filter', 'exports', 'print', {
|
||||||
|
title: '帮助'
|
||||||
|
,layEvent: 'LAYTABLE_TIPS'
|
||||||
|
,icon: 'layui-icon-tips'
|
||||||
|
}]
|
||||||
|
,height: 'full-100' // 最大高度减去其他容器已占有的高度差
|
||||||
|
,cellMinWidth: 80
|
||||||
|
,totalRow: true // 开启合计行
|
||||||
|
,page: true
|
||||||
|
,cols: [ [
|
||||||
|
{type: 'checkbox', fixed: 'left'}
|
||||||
|
,{field:'id', fixed: 'left', width:80, title: 'ID', sort: true, totalRowText: '合计:'}
|
||||||
|
,{field:'name', width:200, title: '名称'}
|
||||||
|
,{field:'cronExpr', title:'计划任务表达式', hide: 0, width:150}
|
||||||
|
,{field:'enabled', width:90, title: '启用', templet: '#enabled-switchTpl'}
|
||||||
|
,{field:'openDayCheck', width:90, title: '交易日验证', templet: '#openDayCheck-switchTpl'}
|
||||||
|
,{field:'valid', title: '有效', width: 80, templet: d => {
|
||||||
|
return '<span title="当值为无效时,请检查方法参数等是否有效">' +
|
||||||
|
d.valid ? '<font color="green">有效</font>' : '<font color="red">无效</font>' +
|
||||||
|
'</span>';
|
||||||
|
}}
|
||||||
|
,{fixed: 'right', title:'操作', width: 125, minWidth: 125, toolbar: '#barDemo'}
|
||||||
|
]]
|
||||||
|
,done: function(){
|
||||||
|
var id = this.id;
|
||||||
|
}
|
||||||
|
,error: function(res, msg){
|
||||||
|
console.log(res, msg)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单提交事件
|
||||||
|
form.on('submit(submitPlan)', function (obj) {
|
||||||
|
console.log(obj);
|
||||||
|
|
||||||
|
let form = obj.elem.closest('.layui-form');
|
||||||
|
// 查找其下所有表单
|
||||||
|
form.querySelectorAll('input').forEach(input => {
|
||||||
|
if (input.name)
|
||||||
|
obj.field[input.name] = input.value;
|
||||||
|
})
|
||||||
|
|
||||||
|
// 主要是清洗 methods
|
||||||
|
const tbodies = document.querySelectorAll('#table-methods tbody');
|
||||||
|
const selectedMethods = [];
|
||||||
|
for (let i = 0; i < tbodies.length; i++) {
|
||||||
|
const tbody = tbodies[i];
|
||||||
|
const checkbox = tbody.querySelector('input[type="checkbox"]');
|
||||||
|
if (!checkbox?.checked) continue;
|
||||||
|
const methodId = tbody.dataset.id;
|
||||||
|
// 查找选择的参数
|
||||||
|
const selects = tbody.querySelectorAll('select');
|
||||||
|
const params = [];
|
||||||
|
selects.forEach(select => {
|
||||||
|
params.push({name: select.getAttribute('name'), value: select.value})
|
||||||
|
})
|
||||||
|
selectedMethods.push({
|
||||||
|
id: methodId, params: params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
obj.field.methods = selectedMethods;
|
||||||
|
|
||||||
|
|
||||||
|
console.log(obj.field);
|
||||||
|
|
||||||
|
const load = layui.layer.load(4);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/admin/manage/reviews/plans/save',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify(obj.field),
|
||||||
|
success: res => {
|
||||||
|
// POST
|
||||||
|
layer.msg('操作成功', {icon: 1, time: 1000}, function() {
|
||||||
|
// TABLE RELOAD
|
||||||
|
layui.table.reload('plan-table')
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: res => {
|
||||||
|
layer.alert(res.responseJSON.message || '发生错误', {
|
||||||
|
icon: 2,
|
||||||
|
shadeClose: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
complete: () => layui.layer.close(load)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
async function openNewForm(id) {
|
||||||
|
const load = layui.layer.load(4);
|
||||||
|
const json = await (await fetch((() => {
|
||||||
|
const url = '/admin/manage/reviews/plans/get';
|
||||||
|
if (id) return url + '?id=' + id;
|
||||||
|
return url
|
||||||
|
})())).json();
|
||||||
|
if (json.ok) {
|
||||||
|
layui.layer.close(load);
|
||||||
|
openPlanForm(json.data)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
layui.layer.close(load);
|
||||||
|
layer.alert(json.message || '发生错误', {
|
||||||
|
icon: 2,
|
||||||
|
shadeClose: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPlanForm(plan) {
|
||||||
|
layer.open({
|
||||||
|
type: 1,
|
||||||
|
title: `${plan.id ? '编辑':'新建'}任务`,
|
||||||
|
yes: (index, layero) => {
|
||||||
|
layero.find('[lay-filter="submitPlan"]').click()
|
||||||
|
},
|
||||||
|
skin: 'top-0 layui-anim layui-anim-rl layui-layer-adminRight',
|
||||||
|
area: '50%', anim: -1, shadeClose: !0, clostBtn: !1, move: !1,
|
||||||
|
offset: 'r', content: $('#addPlan').html(),
|
||||||
|
btn: ['提交', '关闭'],
|
||||||
|
success: async (layero, layerIndex) => {
|
||||||
|
const el = $(layero);
|
||||||
|
console.log(plan);
|
||||||
|
['id', 'name', 'cronExpr'].forEach(x => {
|
||||||
|
const fieldEl = el[0].querySelector(`[name="${x}"]`);
|
||||||
|
if (!fieldEl) return;
|
||||||
|
fieldEl.value = plan[x];
|
||||||
|
});
|
||||||
|
// 开关控件值绑定
|
||||||
|
['enabled', 'openDayCheck'].forEach(x => {
|
||||||
|
const fieldEl = el[0].querySelector(`[name="${x}"]`);
|
||||||
|
if (!fieldEl) return;
|
||||||
|
fieldEl.checked = !!plan[x];
|
||||||
|
fieldEl.value = !!plan[x];
|
||||||
|
layui.form.on('switch(' + x + ')', obj => {
|
||||||
|
obj.elem.value = obj.elem.checked;
|
||||||
|
console.log('name', x, 'checked', obj.elem.checked, 'value', obj.elem.value)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// 渲染方法表格
|
||||||
|
const table = $('#table-methods');
|
||||||
|
table.addClass('layui-table');
|
||||||
|
table.html('');
|
||||||
|
table.append(`
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>选择</th>
|
||||||
|
<th>方法名</th>
|
||||||
|
<th>参数</th>
|
||||||
|
<th>值</th>
|
||||||
|
</tr>
|
||||||
|
</thead>`);
|
||||||
|
// 每一层都是一样的 field, name...只是 rowspan 根据 params 个数而变
|
||||||
|
// 每个方法 rowspan = params.length
|
||||||
|
|
||||||
|
// 去重合并:
|
||||||
|
// 去除传回 plan 中不合法的 method, 并打上 checked: true
|
||||||
|
// 保留 potentialMethods 中未在 plan 中启用的 method, 并打上 checked: false
|
||||||
|
// 合并两个 array
|
||||||
|
const potentialIds = new Set(methods.map(x => x.id));
|
||||||
|
const currentMethodsValid = plan.methods.filter(x => potentialIds.has(x.id)).map(x => ({ ...x, checked: true }));
|
||||||
|
|
||||||
|
const currentMethodsIds = new Set(currentMethodsValid.map(x => x.id));
|
||||||
|
const potentialUncheckedMethods = methods.filter(x => !currentMethodsIds.has(x.id)).map(x => ({ ...x, checked: false }));
|
||||||
|
|
||||||
|
const result = [...currentMethodsValid, ...potentialUncheckedMethods];
|
||||||
|
|
||||||
|
result.forEach(m => {
|
||||||
|
const tbody = $(`<tbody data-id="${m.id}"></tbody>`);
|
||||||
|
let paramLength = m.params && Array.isArray(m.params) && m.params.length || 0;
|
||||||
|
if (paramLength) {
|
||||||
|
m.params.forEach((p, pIdx) => {
|
||||||
|
const tr = [];
|
||||||
|
if (pIdx === 0) {
|
||||||
|
tr.push(`<td rowspan="${paramLength}">
|
||||||
|
<input ${m.checked ? 'checked':''} type="checkbox" name="layTableCheckbox" lay-skin="primary" lay-type="layTableCheckbox" title="">
|
||||||
|
</td>`);
|
||||||
|
tr.push(`<td handler rowspan="${paramLength}">${m.name}</td>`);
|
||||||
|
}
|
||||||
|
tr.push(`<td>${p.name}</td>`);
|
||||||
|
// 渲染参数选择
|
||||||
|
const select = $(`<select name="${p.name}" class="layui-border" lay-ignore></select>`);
|
||||||
|
if (Array.isArray(p.allowedEnumValues) && p.allowedEnumValues.length) {
|
||||||
|
const value = p.value || p.allowedEnumValues[0] || null;
|
||||||
|
p.allowedEnumValues.forEach(e => {
|
||||||
|
let option = `<option value="${e}" ${e == value ? 'selected' : ''}>${e}</option>`;
|
||||||
|
select.append(option)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
tr.push(`<td>${select[0].outerHTML}</td>`);
|
||||||
|
tbody.append(`<tr>${tr.join('')}</tr>`);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const tr = [];
|
||||||
|
tr.push(`<td>
|
||||||
|
<input ${m.checked ? 'checked':''} type="checkbox" name="layTableCheckbox" lay-skin="primary" lay-type="layTableCheckbox" title="">
|
||||||
|
</td>`);
|
||||||
|
tr.push(`<td colspan="3" handler>${m.name}</td>`);
|
||||||
|
tbody.append(`<tr>${tr.join('')}</tr>`);
|
||||||
|
}
|
||||||
|
table.append(tbody);
|
||||||
|
})
|
||||||
|
$.each(table.find('td'), (_, td) => {
|
||||||
|
$(td).addClass("layui-table-col-special");
|
||||||
|
});
|
||||||
|
if (sortableInstance) sortableInstance.destroy();
|
||||||
|
|
||||||
|
let currentGroup = null;
|
||||||
|
let groupRows = [];
|
||||||
|
|
||||||
|
sortableInstance = new Sortable(
|
||||||
|
document.querySelector('#table-methods'), {
|
||||||
|
animation: 150,
|
||||||
|
handle: 'td[handler]'
|
||||||
|
});
|
||||||
|
layui.cron.render({
|
||||||
|
elem: '[name="cronExpr"]',
|
||||||
|
btns: ['confirm']
|
||||||
|
});
|
||||||
|
layui.form.render();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具栏事件
|
||||||
|
table.on('toolbar(plan-table)', async function(obj){
|
||||||
|
var id = obj.config.id;
|
||||||
|
var checkStatus = table.checkStatus(id);
|
||||||
|
var othis = lay(this);
|
||||||
|
switch(obj.event){
|
||||||
|
case 'new':
|
||||||
|
openNewForm();
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
//触发单元格工具事件
|
||||||
|
table.on('tool(plan-table)', function(obj){ // 双击 toolDouble
|
||||||
|
var data = obj.data;
|
||||||
|
//console.log(obj)
|
||||||
|
if(obj.event === 'del'){
|
||||||
|
layer.confirm('真的删除行么', function(index){
|
||||||
|
obj.del();
|
||||||
|
layer.close(index);
|
||||||
|
});
|
||||||
|
} else if(obj.event === 'edit'){
|
||||||
|
openNewForm(obj.data.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateSwitch(elem, load) {
|
||||||
|
var id = elem.value;
|
||||||
|
var name = elem.name;
|
||||||
|
$.ajax({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/admin/manage/reviews/plans/updateBool',
|
||||||
|
data: {
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
value: elem.checked
|
||||||
|
},
|
||||||
|
success: res => {
|
||||||
|
layer.msg('操作成功', {time: 1000});
|
||||||
|
},
|
||||||
|
error: res => {
|
||||||
|
// 恢复开关状态
|
||||||
|
elem.checked = !elem.checked;
|
||||||
|
layui.form.render();
|
||||||
|
layer.msg(res.responseJSON.message || '操作失败', {time: 2000});
|
||||||
|
},
|
||||||
|
complete: () => {if (load) layui.layer.close(load)}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
form.on('switch(table-switch)', function (obj) {
|
||||||
|
updateSwitch(obj.elem, layui.layer.load(4));
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
//触发表格复选框选择
|
||||||
|
table.on('checkbox(plan-table)', function(obj){
|
||||||
|
console.log(obj)
|
||||||
|
});
|
||||||
|
|
||||||
|
//触发表格单选框选择
|
||||||
|
table.on('radio(plan-table)', function(obj){
|
||||||
|
console.log(obj)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 行单击事件
|
||||||
|
table.on('row(plan-table)', function(obj){
|
||||||
|
//console.log(obj);
|
||||||
|
//layer.closeAll('tips');
|
||||||
|
});
|
||||||
|
// 行双击事件
|
||||||
|
table.on('rowDouble(plan-table)', function(obj){
|
||||||
|
console.log(obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 单元格编辑事件
|
||||||
|
table.on('edit(plan-table)', function(obj){
|
||||||
|
var field = obj.field //得到字段
|
||||||
|
,value = obj.value //得到修改后的值
|
||||||
|
,data = obj.data; //得到所在行所有键值
|
||||||
|
|
||||||
|
var update = {};
|
||||||
|
update[field] = value;
|
||||||
|
obj.update(update);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user