This commit is contained in:
2025-10-27 23:30:05 +08:00
parent 4bf21639c1
commit 6327874166
197 changed files with 5866 additions and 1580 deletions

View File

@@ -90,6 +90,7 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
@@ -255,6 +256,11 @@
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join-boot-starter</artifactId>
<version>1.4.11</version>
</dependency>
</dependencies>

View File

@@ -1,15 +1,12 @@
package link.at17.mid.tushare;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import link.at17.mid.tushare.task.scheduler.DailyUpdateDataScheduler;
@EnableAsync
@EnableScheduling
@SpringBootApplication
@@ -17,9 +14,6 @@ import link.at17.mid.tushare.task.scheduler.DailyUpdateDataScheduler;
@EnableCaching(proxyTargetClass=true)
public class TushareDataServiceApplication {
@Autowired
DailyUpdateDataScheduler dailyUpdateDataScheduler;
public static void main(String[] args) {
SpringApplication.run(TushareDataServiceApplication.class, args);
}

View File

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

View File

@@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
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.system.util.LocalDateTimeUtils;
import lombok.extern.slf4j.Slf4j;
@@ -18,18 +18,18 @@ import lombok.extern.slf4j.Slf4j;
public class StockCalendarController {
@Autowired
StockCalendarDao stockCalendarDao;
StockCalendarService stockCalendarService;
@GetMapping("todayIsOpen")
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")
private String isOpen(@Param("stockMarket") StockMarket stockMarket, @Param("date") String date) {
LocalDateTime dateTime = LocalDateTimeUtils.parseDate(date);
return String.valueOf(stockCalendarDao.isOpen(dateTime, stockMarket));
return String.valueOf(stockCalendarService.isOpen(dateTime, stockMarket));
}
}

View File

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

View File

@@ -3,7 +3,6 @@ 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;
@@ -13,15 +12,6 @@ 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;
/**
@@ -34,17 +24,6 @@ public class PlatformInterceptor 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 {
@@ -75,85 +54,6 @@ public class PlatformInterceptor implements HandlerInterceptor {
}
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());
}
}
}

View File

@@ -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) {
// 只在 ControllerHandlerMethod里才做注入
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());
}
}
}

View File

@@ -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) {
// 只在 ControllerHandlerMethod里才做注入
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());
}
}
}

View File

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

View File

@@ -8,17 +8,17 @@ import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import link.at17.mid.tushare.task.TaskSchedulerFactory;
import link.at17.mid.tushare.task.AutowireCapableJobFactory;
@Configuration
public class QuartzConfig {
@Autowired
private TaskSchedulerFactory taskSchedulerFactory;
private AutowireCapableJobFactory autowireCapableJobFactory;
@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setJobFactory(taskSchedulerFactory);
schedulerFactoryBean.setJobFactory(autowireCapableJobFactory);
return schedulerFactoryBean;
}

View File

@@ -7,18 +7,28 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
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
@Import(ConfigAutoRegistrar.class)
public class VerichConfig implements WebMvcConfigurer {
@Autowired
PlatformInterceptor platformInterceptor;
StaticAttributeInterceptor platformInterceptor;
@Autowired
UpdateMethodInterceptor updateMethodInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(platformInterceptor)
registry
.addInterceptor(platformInterceptor)
.addPathPatterns("/**");
registry
.addInterceptor(updateMethodInterceptor)
.addPathPatterns("/**");
}
}

View File

@@ -15,5 +15,5 @@ import java.util.List;
@Mapper
public interface StockAdjustDao extends ITsTradeDate {
@BatchInsert
int insertOrUpdateListTushare(@BatchList List<JSONObject> list);
int insertOrUpdateList(@BatchList List<JSONObject> list);
}

View File

@@ -1,7 +1,7 @@
package link.at17.mid.tushare.dao;
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.BatchList;
@@ -22,13 +22,13 @@ import java.util.List;
@Component
@Mapper
public interface StockCalendarDao extends BaseMapper<StockCalendar> {
public interface StockCalendarDao extends MPJBaseMapper<StockCalendar> {
/**
* 更新股票日历
* @param list
*/
@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);
/**
* 获取数据库内指定证交所的最早一个交易日历的日期
* <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>仅为日期,并未指定是否是开市日
@@ -75,52 +45,24 @@ public interface StockCalendarDao extends BaseMapper<StockCalendar> {
*/
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;
}
/**
* 获取数据库内指定证交所的最早一个交易日历的日期时间
* <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>>
* 获取指定证交所两个日期(含)之间的交易日个数<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>
* <code>startDate&nbsp;: LocalDateTime 2021-08-16 01:20:30</code><br>
* <code>endDate&nbsp;&nbsp;&nbsp;: 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>
* <code>startDate&nbsp;: LocalDateTime 2021-08-16 01:20:30</code><br>
* <code>endDate&nbsp;&nbsp;&nbsp;: 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>
* <code>startDate&nbsp;: LocalDateTime 2021-08-15 01:20:30</code><br>
* <code>endDate&nbsp;&nbsp;&nbsp;: LocalDateTime 2021-08-16 12:34:56</code><br>
* 返回 1
* </ul>
* </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);
/**
* 获取指定证交所两个日期(含)之间的所有交易日<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>
* 返回2021-08-16 2021-08-17 2021-08-18 2021-08-19 2021-08-20
* <code>startDate&nbsp;: LocalDateTime 2021-08-16 01:20:30</code><br>
* <code>endDate&nbsp;&nbsp;: 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 的所有交易日并去重获得结果
@@ -164,10 +115,10 @@ public interface StockCalendarDao extends BaseMapper<StockCalendar> {
/**
* 获取指定证交所指定日期(含)以后的所有交易日
* @param exchange
* @param after 留空则查询所有
* @param sence 留空则查询所有
* @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);
/**
* 判断指定日是否为指定交易所的交易日

View File

@@ -1,9 +1,11 @@
package link.at17.mid.tushare.dao;
import com.alibaba.fastjson2.JSONObject;
import com.github.yulichang.base.MPJBaseMapper;
import link.at17.mid.tushare.annotation.BatchInsert;
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 org.apache.ibatis.annotations.Mapper;
@@ -13,11 +15,11 @@ import java.util.List;
@Component
@Mapper
public interface StockDailyBasicDao extends ITsTradeDate {
public interface StockDailyBasicDao extends MPJBaseMapper<StockValueEx>, ITsTradeDate {
/**
* 批量插入/更新日基本数据
* @param list
*/
@BatchInsert
void insertOrUpdateList(@BatchList List<JSONObject> list);
int insertOrUpdateList(@BatchList List<JSONObject> list);
}

View File

@@ -1,11 +1,11 @@
package link.at17.mid.tushare.dao;
import com.alibaba.fastjson2.JSONObject;
import com.github.yulichang.base.MPJBaseMapper;
import link.at17.mid.tushare.annotation.BatchInsert;
import link.at17.mid.tushare.annotation.BatchList;
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 org.apache.ibatis.annotations.Mapper;
@@ -13,65 +13,65 @@ import org.apache.ibatis.annotations.Param;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.temporal.Temporal;
import java.util.List;
@Component
@Mapper
public interface StockDailyDao extends ITsTradeDate {
public interface StockDailyDao extends MPJBaseMapper<StockValue>, ITsTradeDate {
/**
* 批量插入/更新日线数据
* @param list
*/
@BatchInsert
void insertOrUpdateList(@BatchList List<JSONObject> list);
int insertOrUpdateList(@BatchList List<JSONObject> list);
/**
* 获取除权日线数据 + 基本行情数据
* @param stockCode 股票代码,不允许为空
* @param tsCode Tushare 股票代码,不允许为空
* @param endDate 结束日期(包含),留空则为最新一个交易日
* @param before 多少个交易日以前,留空则查询上市以来所有数据
* @param before 多少个交易日以前,留空则查询上市至 endDate 以来所有数据
* @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 endDate 结束日期null 则为最新一个交易日
* @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 before 多少个交易日以前null 则查询上市以来所有数据
* @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 after 多少个交易日以前null 则查询 startDate 以来所有数据
* @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 before 多少个交易日以前null 则查询上市以来所有数据
* @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 after 多少个交易日以前null 则查询 startDate 以来所有数据
* @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);
}

View File

@@ -1,6 +1,7 @@
package link.at17.mid.tushare.dao;
import com.alibaba.fastjson2.JSONObject;
import com.github.yulichang.base.MPJBaseMapper;
import link.at17.mid.tushare.annotation.BatchInsert;
import link.at17.mid.tushare.annotation.BatchList;
@@ -18,9 +19,9 @@ import java.util.List;
@Component
@Mapper
public interface StockHolderDao extends ITsTradeDate {
public interface StockHolderDao extends MPJBaseMapper<StockHolder>, ITsTradeDate {
@BatchInsert
int insertOrUpdateListTushare(@BatchList List<JSONObject> list, @NonNull StockHolderType holderType);
int insertOrUpdateList(@BatchList List<JSONObject> list, @NonNull StockHolderType holderType);
@EvictAfterUpdate("tushare")
@Cacheable("stockHolderDao.getAllByStockCode")

View File

@@ -1,7 +1,7 @@
package link.at17.mid.tushare.dao;
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.BatchList;
@@ -16,7 +16,7 @@ import java.util.List;
@Component
@Mapper
public interface StockInfoDao extends BaseMapper<StockInfo> {
public interface StockInfoDao extends MPJBaseMapper<StockInfo> {
@BatchInsert
void insertOrUpdateList(@BatchList List<JSONObject> list);
StockInfo getStockInfoByStockCode(@Param("stockCode") String stockCode);

View File

@@ -1,7 +1,7 @@
package link.at17.mid.tushare.dao;
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.BatchList;
@@ -15,7 +15,7 @@ import java.util.List;
@Component
@Mapper
public interface StockLimitDao extends BaseMapper<StockLimit>, ITsTradeDate {
public interface StockLimitDao extends MPJBaseMapper<StockLimit>, ITsTradeDate {
@BatchInsert
int insertOrUpdateListTushare(@BatchList List<JSONObject> list);
int insertOrUpdateList(@BatchList List<JSONObject> list);
}

View File

@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.temporal.Temporal;
import java.util.List;
@Component
@@ -24,7 +25,7 @@ public interface StockMinuteDao {
* @param list
*/
@BatchInsert
void insertOrUpdateList(@NotNull StockSpan stockSpan, @BatchList List<JSONObject> list);
int insertOrUpdateList(@BatchList List<JSONObject> list, @NotNull StockSpan stockSpan);
/**
* 获取最新交易日<br/>
@@ -57,7 +58,7 @@ public interface StockMinuteDao {
* @param endDate 结束日期,留空则为最新一个交易日
* @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 股票代码,不允许为空
@@ -65,7 +66,7 @@ public interface StockMinuteDao {
* @param before 多少个交易日以前,留空则查询上市以来所有数据
* @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 数据
* @param stockCode 股票代码,不允许为空
@@ -73,5 +74,5 @@ public interface StockMinuteDao {
* @param before 多少个交易日以前,留空则查询上市以来所有数据
* @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);
}

View File

@@ -17,13 +17,13 @@ import java.util.List;
@Component
@Mapper
public interface StockThsDailyDao extends ITsTradeDate {
public interface ThsDailyDao extends ITsTradeDate {
/**
* 批量插入/更新日线数据
* @param list
*/
@BatchInsert
void insertOrUpdateList(@BatchList List<JSONObject> list);
int insertOrUpdateList(@BatchList List<JSONObject> list);
/**
* 获取板块日线数据
* @param stockCode 板块代码不允许为空

View File

@@ -2,7 +2,7 @@ package link.at17.mid.tushare.dao;
import com.alibaba.fastjson2.JSONObject;
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.BatchList;
@@ -16,9 +16,9 @@ import java.util.List;
@Component
@Mapper
public interface StockThsListDao extends BaseMapper<ThsStockInfo> {
public interface ThsListDao extends MPJBaseMapper<ThsStockInfo> {
@BatchInsert
void insertOrUpdateList(@BatchList List<JSONObject> list);
int insertOrUpdateList(@BatchList List<JSONObject> list);
default List<ThsStockInfo> listByExchange(ThsStockMarket exchange){
return selectList(new LambdaQueryWrapper<ThsStockInfo>().eq(exchange != null, ThsStockInfo::getExchange, exchange));
}

View File

@@ -1,11 +1,9 @@
package link.at17.mid.tushare.dao;
import com.alibaba.fastjson2.JSONObject;
import link.at17.mid.tushare.annotation.BatchInsert;
import link.at17.mid.tushare.annotation.BatchList;
import link.at17.mid.tushare.data.models.StockInfo;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Component;
@@ -13,13 +11,13 @@ import java.util.List;
@Component
@Mapper
public interface StockThsMemberDao {
public interface ThsMemberDao {
@BatchInsert
void insertOrUpdateList(@BatchList List<JSONObject> list);
int insertOrUpdateList(@BatchList List<JSONObject> list);
List<StockInfo> getThsMembers(String tsCode);
/**
* 获取同花顺所属概念
* @param tsCode
* 获取个股的同花顺所属概念以逗号分隔
* @param tsCode 个股的 Tushare 代码
* @return
*/
String getBelongings(String tsCode);

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

View File

@@ -1,11 +1,13 @@
package link.at17.mid.tushare.data.crawler;
import link.at17.mid.tushare.annotation.StaticAttribute;
/**
* 查询方式
* @author Barry
*
*/
@StaticAttribute
public enum QueryWay {
/**
* 以日期更新,适用于已有从上市开始的部分数据的更新

View File

@@ -6,11 +6,18 @@ import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
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.JSONException;
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 okhttp3.Call;
import okhttp3.MediaType;
@@ -20,13 +27,21 @@ import okhttp3.RequestBody;
import okhttp3.Response;
@Slf4j
@Component
public class TushareClient {
private static final String TUSHARE_URL = "http://api.tushare.pro";
// private static String token = "83a82fadb0bbb803f008b31ce09479e5107f4aba3f28d5df2174c642";
// private static String token = "c473f86ae2f5703f58eecf9864fa9ec91d67edbc01e3294f6a4f9c32";
// private static String token = "ec3d7415bf3dfebf0f27b1e5a9805f809b083fb1f8590c8c2bdc633f";
private static String token = "6f284d9246bad80c3eff946f3ecae8442072b1e60652785f66007509";
private static String getToken() {
SystemConfig systemConfig = SpringContextHolder.getBean(SystemConfig.class);
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
*/
public static JSONObject query(TushareRequestBody tr) throws IOException, JSONException {
OkHttpClient okHttpClient = new OkHttpClient();
tr.setToken(token);
OkHttpClient okHttpClient = OkHttpClientProvider.getInstance();
tr.setToken(getToken());
Request request = new Request.Builder()
.url(TUSHARE_URL)
.post(

View File

@@ -1,53 +1,32 @@
package link.at17.mid.tushare.data.crawler.tushare;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Function;
import java.util.stream.Stream;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import com.alibaba.fastjson2.JSONException;
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.RetryAndDelay;
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.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.StockSpan;
import link.at17.mid.tushare.enums.ThsStockMarket;
import link.at17.mid.tushare.system.util.LocalDateTimeUtils;
import lombok.extern.slf4j.Slf4j;
/**
@@ -61,32 +40,9 @@ public class TushareCrawler {
@Autowired
private StockInfoDao stockInfoDao;
private StockInfoService stockInfoService;
@Autowired
private StockDailyDao stockDailyDao;
@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 StockCalendarService stockCalendarService;
private static final int MAX_THREADS = 5;
/**
@@ -103,7 +59,7 @@ public class TushareCrawler {
* @return
* @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){
ExecutorService es = Executors.newFixedThreadPool(MAX_THREADS);
RetryAndDelay retryAndDelay = new RetryAndDelay().setThreadNum(MAX_THREADS);
@@ -122,7 +78,7 @@ public class TushareCrawler {
queryWay = QueryWay.ByDateCrossCheck;
return rollingQueryByDate(baseRequest, iTradeDate, processDataFunc, queryWay, start);
}
start = stockCalendarDao.getGreatestLocalDateTime(null);
start = stockCalendarService.getGreatestLocalDateTime();
}
if (queryWay == QueryWay.ByDateUpdate && iTradeDate != null) {
@@ -137,7 +93,7 @@ public class TushareCrawler {
LocalDateTime end = LocalDateTime.now().toLocalDate().atStartOfDay();
while(!start.isAfter(end)) {
if (all || stockCalendarDao.isOpen(start, (StockMarket)null)) {
if (all || stockCalendarService.isOpen(start, (StockMarket)null)) {
TushareRequestBody rq = baseRequest.clone()
.addDateParam("trade_date", start);
executeResult.add(es.submit(new TushareResponseCallable(rq, processDataFunc)
@@ -152,10 +108,10 @@ public class TushareCrawler {
}
// 交叉检查
final LocalDateTime currentDate = LocalDateTime.now();
final List<LocalDateTime> szStockCalendars = stockCalendarDao.getAllOpenDatesBetween(StockMarket.SZ, null, currentDate);
final List<LocalDateTime> shStockCalendars = stockCalendarDao.getAllOpenDatesBetween(StockMarket.SH, null, currentDate);
final List<LocalDateTime> allStockCalendars = stockCalendarDao.getAllOpenDatesBetween((StockMarket)null, null, currentDate);
final List<StockInfo> stockInfos = stockInfoDao.getStockListByListStatus(null);
final List<LocalDateTime> szStockCalendars = stockCalendarService.getAllOpenDatesBetween(StockMarket.SZ, null, currentDate);
final List<LocalDateTime> shStockCalendars = stockCalendarService.getAllOpenDatesBetween(StockMarket.SH, null, currentDate);
final List<LocalDateTime> allStockCalendars = stockCalendarService.getAllOpenDatesBetween((StockMarket)null, null, currentDate);
final List<StockInfo> stockInfos = stockInfoService.list();
TreeSet<LocalDateTime> needUpdates = new TreeSet<>();
stockInfos.forEach(stockInfo -> {
List<LocalDateTime> stockTradeDates = iTradeDate.getAllTradeDates(stockInfo);
@@ -204,7 +160,7 @@ public class TushareCrawler {
* @return 每条请求的执行结果
* @see TushareCrawler#rollingQueryByDate
*/
private List<Future<TushareCrawlerResult>> rollingQueryByStock(
public List<Future<TushareCrawlerResult>> rollingQueryByStock(
TushareRequestBody baseRequest,
List<? extends ITsStockInfo> stockList,
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;
// 计算 start 和 end 之间一共有多少个交易日
long dataBetween = stockCalendarDao.countOpenDaysBetween(exchange, start, end);
long dataBetween = stockCalendarService.countOpenDaysBetween(exchange, start, end);
long dataPerDay = 1;
if (stockSpan.compareTo(StockSpan.Daily) == -1 && stockSpan.compareTo(StockSpan.Minute) >= 0) {
// 分钟数,重新算下 dataBetween
@@ -277,14 +233,14 @@ public class TushareCrawler {
}
dataBetween *= dataPerDay;
long daySpan = singleMax / dataPerDay;
LocalDateTime tmpEndDate = stockCalendarDao.getOpenDateOffset(exchange, start, daySpan);
LocalDateTime tmpEndDate = stockCalendarService.getOpenDateOffset(exchange, start, daySpan);
while (dataBetween > singleMax) {
rq.addDateParam("end_date",tmpEndDate);
executeResult.add(es.submit(new TushareResponseCallable(rq.clone(), afterRespFunc)
.setRetryAndDelay(retryAndDelay)));
start = tmpEndDate;
tmpEndDate = stockCalendarDao.getOpenDateOffset(exchange, start, daySpan);
tmpEndDate = stockCalendarService.getOpenDateOffset(exchange, start, daySpan);
rq.addDateParam("start_date", start);
dataBetween -= daySpan * dataPerDay;
}
@@ -302,7 +258,7 @@ public class TushareCrawler {
}
else {
// 交叉检查
final List<StockInfo> stockInfos = stockInfoDao.getStockListByListStatus(null);
final List<StockInfo> stockInfos = stockInfoService.list();
for(StockInfo stockInfo : stockInfos) {
StockMarket exchange = stockInfo.getExchange();
List<LocalDateTime> missingDates = iTradeDate.getAllMissingDates(stockInfo);
@@ -320,7 +276,7 @@ public class TushareCrawler {
// = 2022-12-06 两天,成为范围以后将会变成更新 start 和 end 之间
// 的所有数据,这样将会大大增加不必要的更新请求。
long dataBetween = stockCalendarDao.countOpenDaysBetween(exchange, start, end);
long dataBetween = stockCalendarService.countOpenDaysBetween(exchange, start, end);
long dataPerDay = 1;
if (stockSpan.compareTo(StockSpan.Daily) == -1 && stockSpan.compareTo(StockSpan.Minute) >= 0) {
// 分钟数,重新算下 dataBetween
@@ -329,7 +285,7 @@ public class TushareCrawler {
dataBetween *= dataPerDay;
long daySpan = singleMax / dataPerDay;
LocalDateTime tmpEndDate = stockCalendarDao.getOpenDateOffset(exchange, start, daySpan);
LocalDateTime tmpEndDate = stockCalendarService.getOpenDateOffset(exchange, start, daySpan);
while (dataBetween > singleMax) {
rq.addDateParam("end_date", tmpEndDate);
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))) {
start = missingDates.get(++i);
}
tmpEndDate = stockCalendarDao.getOpenDateOffset(exchange, start, daySpan);
tmpEndDate = stockCalendarService.getOpenDateOffset(exchange, start, daySpan);
rq.addDateParam("start_date", start);
dataBetween -= daySpan * dataPerDay;
}
@@ -357,471 +313,4 @@ public class TushareCrawler {
}
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;
}
}

View File

@@ -7,6 +7,7 @@ import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
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.enums.ListStatus;
@@ -16,6 +17,7 @@ import lombok.experimental.Accessors;
@Data
@Accessors(chain=true)
@TableName("stock_info")
public class StockInfo implements ITsStockInfo {
private static final Pattern EM_CODE_PATTERN = Pattern.compile("^(0|1|90)\\.(.*?)$");

View File

@@ -0,0 +1,8 @@
package link.at17.mid.tushare.data.models;
import lombok.Data;
@Data
public class UpdateLog {
}

View File

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

View File

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

View File

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

View File

@@ -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&nbsp;: LocalDateTime 2021-08-16 01:20:30</code><br>
* <code>endDate&nbsp;&nbsp;&nbsp;: 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&nbsp;: LocalDateTime 2021-08-16 01:20:30</code><br>
* <code>endDate&nbsp;&nbsp;&nbsp;: 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&nbsp;: LocalDateTime 2021-08-15 01:20:30</code><br>
* <code>endDate&nbsp;&nbsp;&nbsp;: 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&nbsp;: LocalDateTime 2021-08-16 01:20:30</code><br>
* <code>endDate&nbsp;&nbsp;: 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(); // 允许的枚举名
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
package link.at17.mid.tushare.dto;
public class JsonViews {
public static class Public {} // 给前端用
public static class Internal {} // 存库用
}

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

View File

@@ -5,8 +5,17 @@ import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;
public enum ListStatus {
/**
* 上市
*/
LIST("L"),
/**
* 退市
*/
DELIST("D"),
/**
* 停牌
*/
PAUSE("P");
@Getter

View File

@@ -0,0 +1,9 @@
package link.at17.mid.tushare.enums;
public enum UpdateLogType {
INFO,
ERROR,
SUCCESS
}

View File

@@ -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&lt;T> 和 Wrapper&lt;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);
}
}

View File

@@ -0,0 +1,11 @@
package link.at17.mid.tushare.service;
import org.springframework.stereotype.Service;
/**
* 统计和状态服务
*/
@Service
public class StatService {
}

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.Validate;
@@ -34,6 +35,14 @@ public class SpringContextHolder implements ApplicationContextAware, DisposableB
private static ApplicationContext applicationContext = null;
/**
* 实现ApplicationContextAware接口, 注入Context到静态变量中.
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
SpringContextHolder.applicationContext = applicationContext;
}
/**
* 取得存储在静态变量中的ApplicationContext.
*/
@@ -59,6 +68,14 @@ public class SpringContextHolder implements ApplicationContextAware, DisposableB
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.
*/
@@ -69,13 +86,6 @@ public class SpringContextHolder implements ApplicationContextAware, DisposableB
applicationContext = null;
}
/**
* 实现ApplicationContextAware接口, 注入Context到静态变量中.
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
SpringContextHolder.applicationContext = applicationContext;
}
/**
* 实现DisposableBean接口, 在Context关闭时清理静态变量.

View File

@@ -5,18 +5,20 @@ import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;
/**
* Job 工厂使得 Job 实例能获取 Spring 管理的 Bean
* <p>
* 如某 UpdatePlan implements Job, 在其 execute 方法中需要调用 updatePlanService, 则必须要实例能获取 updatePlanService
*/
@Component
public class TaskSchedulerFactory extends AdaptableJobFactory {
public class AutowireCapableJobFactory extends AdaptableJobFactory {
// 需要使用这个BeanFactory对Qurartz创建好Job实例进行后续处理属于Spring的技术范畴.
@Autowired
private AutowireCapableBeanFactory capableBeanFactory;
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
// 首先调用父类的方法创建好Quartz所需的Job实例
Object jobInstance = super.createJobInstance(bundle);
// 然后使用BeanFactory为创建好的Job实例进行属性自动装配并将其纳入到Spring容器的管理之中属于Spring的技术范畴.
capableBeanFactory.autowireBean(jobInstance);
capableBeanFactory.autowireBean(jobInstance); // 自动注入 Spring Bean
return jobInstance;
}
}

View File

@@ -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("每日定时更新数据完成");
}
}

View File

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

View File

@@ -7,10 +7,10 @@ import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import link.at17.mid.tushare.cache.CacheEvictionJob;
import link.at17.mid.tushare.task.TaskConstants;
import link.at17.mid.tushare.task.TaskSchedulerFactory;
import link.at17.mid.tushare.task.AutowireCapableJobFactory;
@Component
public class CacheDailyEvictionScheduler extends TaskSchedulerFactory {
public class CacheDailyEvictionScheduler extends AutowireCapableJobFactory {
@Resource(name="scheduler")
Scheduler scheduler;

View File

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

View File

@@ -40,8 +40,16 @@ public class ManageController {
return "admin/manage/views/index.html";
}
@GetMapping("/manage/demo-index")
private String demoIndex() {
return "admin/manage/views/demo-index.html";
}
@GetMapping("/manage/{*routine}")
private String routine(@PathVariable String routine) {
routine = routine.replaceAll("\\/+", "/");
routine = routine.replaceAll("\\\\+", "/");
routine = routine.replaceFirst("\\/", "");
return "admin/manage/views/" + routine;
}

View File

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

View File

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

View File

@@ -27,10 +27,10 @@ mybatis-plus:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging.level:
link.at17.mid.tushare: info
link.at17.mid.tushare: debug
link.at17.mid.tushare.test: debug
org.springframework.security: debug
org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager: trace
# org.springframework.security: debug
# org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager: trace
spring:
devtools:
@@ -50,7 +50,7 @@ spring:
profiles.active: local
session.timeout: 86400
thymeleaf:
prefix: classpath:/webpage/
prefix: classpath:/templates/
suffix: .html
mode: HTML
encoding: UTF-8

View File

@@ -1,8 +1,8 @@
{
"tushareToken" : "123",
"proxyType" : "SOCKS",
"tushareToken" : "6f284d9246bad80c3eff946f3ecae8442072b1e60652785f66007509",
"proxyType" : "DIRECT",
"proxyHost" : "",
"proxyPort" : 1,
"ignoreHttpsVerification" : true,
"proxyUrl" : "socks5://:1"
"ignoreHttpsVerification" : false,
"proxyUrl" : null
}

View File

@@ -3,7 +3,7 @@
"https://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="link.at17.mid.tushare.dao.StockAdjustDao">
<insert id="insertOrUpdateListTushare" parameterType="list">
<insert id="insertOrUpdateList" parameterType="list">
INSERT INTO stock_adjust_factor_tushare
(ts_code, trade_date, adj_factor)
VALUES

View File

@@ -123,7 +123,7 @@
</if>
</select>
<select id="getAllOpenDate" resultType="java.time.LocalDateTime">
<select id="getAllOpenDateSence" resultType="java.time.LocalDateTime">
SELECT DISTINCT
"date"
FROM

View File

@@ -101,7 +101,7 @@
d.vol, d.amount, d.pct_chg, d."change"
</sql>
<select id="getQfqDailyTushare" resultType="link.at17.mid.tushare.data.models.StockValue">
<select id="getQfqDaily" resultType="link.at17.mid.tushare.data.models.StockValue">
SELECT
<include refid="qfqFields"/>
FROM
@@ -157,7 +157,7 @@
s.trade_date ASC
</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
<include refid="qfqFields"/>
@@ -207,7 +207,7 @@
</select>
<select id="getQfqDailyAfterTushare" resultType="link.at17.mid.tushare.data.models.StockValue">
<select id="getQfqDailyAfter" resultType="link.at17.mid.tushare.data.models.StockValue">
SELECT
<include refid="qfqFields"/>
FROM
@@ -254,7 +254,7 @@
</if>
</select>
<select id="getExQfqDailyAfterTushare" resultType="link.at17.mid.tushare.data.models.StockValueEx">
<select id="getExQfqDailyAfter" resultType="link.at17.mid.tushare.data.models.StockValueEx">
SELECT
<include refid="qfqFields"/>,
b.turnover_rate AS turnoverRate,
@@ -325,7 +325,7 @@
</if>
</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
<include refid="qfqFields"/>,
@@ -398,7 +398,7 @@
) f ORDER BY f.trade_date ASC
</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
d.*, d.trade_date as "date",

View File

@@ -3,7 +3,7 @@
"https://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="link.at17.mid.tushare.dao.StockHolderDao">
<insert id="insertOrUpdateListTushare" parameterType="list">
<insert id="insertOrUpdateList" parameterType="list">
INSERT INTO stock_holder
(ts_code, ann_date, end_date, holder_name, hold_amount, hold_ratio, holder_type)
VALUES

View File

@@ -36,12 +36,12 @@
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">
AND m.con_code = #{stockCode}
</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
1 = 1
<if test="stockCode != null">
@@ -57,8 +57,8 @@
STRING_AGG(l."name", ', ' ORDER BY l.list_date DESC) AS ths_belongings
FROM
stock_info i
INNER 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_member m ON m.con_code = i.ts_code
LEFT JOIN stock_ths_list l ON l.ts_code = m.ts_code
WHERE
1 = 1
<if test="listStatus != null">
@@ -67,4 +67,34 @@
GROUP BY i.ts_code
ORDER BY i.ts_code ASC
</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>

View File

@@ -3,7 +3,7 @@
"https://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="link.at17.mid.tushare.dao.StockLimitDao">
<insert id="insertOrUpdateListTushare" parameterType="list">
<insert id="insertOrUpdateList" parameterType="list">
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")
VALUES

View File

@@ -108,7 +108,7 @@
m.vol, m.amount, m.freq
</sql>
<select id="getQfqMinuteTushare" resultType="link.at17.mid.tushare.data.models.StockValue">
<select id="getQfqMinute" resultType="link.at17.mid.tushare.data.models.StockValue">
SELECT
<include refid="qfqFields"/>
FROM
@@ -165,7 +165,7 @@
m.trade_time ASC
</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
<include refid="qfqFields"/>
@@ -216,7 +216,7 @@
) f ORDER BY f.date ASC
</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
<include refid="qfqFields"/>,

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"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 INTO stock_ths_daily

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"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 INTO stock_ths_list

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"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 INTO stock_ths_member

View File

@@ -12,10 +12,21 @@
Target Server Version : 170000 (170000)
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
-- ----------------------------
@@ -106,7 +117,7 @@ DROP TABLE IF EXISTS "public"."stock_holder";
CREATE TABLE "public"."stock_holder" (
"ts_code" varchar(11) COLLATE "pg_catalog"."default" 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,
"hold_amount" numeric(23,3) NOT NULL,
"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";
CREATE TABLE "public"."stock_ths_daily" (
"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,
"open" 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",
"count" int4,
"exchange" varchar(255) COLLATE "pg_catalog"."default",
"list_date" date,
"list_date" timestamp(0),
"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
-- ----------------------------
@@ -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
-- ----------------------------
@@ -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");
-- ----------------------------
-- 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
-- ----------------------------

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,195 @@
/**
@ Namelayui.cron Cron表达式解析器
@ Author贝哥哥
@ LicenseMIT
*/
/* 样式加载完毕的标识 */
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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -169,7 +169,7 @@ layui.use(['table', 'slider', 'element', 'form'], function() {
toolbar: '#toolbar',
text: {none: '请输入股票代码查询数据'},
cols: [ (function() {
let a = [{
let a = [ {
field: 'dateStr',
title: '锚点变更日',
templet: (d) => {
@@ -378,7 +378,7 @@ layui.use(['table', 'slider', 'element', 'form'], function() {
});
break;
case 'clearPeriodBlock':
myChart.setOption({series: [{
myChart.setOption({series: [ {
markArea: {data: undefined}}]});
}
});
@@ -902,7 +902,7 @@ let toGraphicsIndex = (i, period) => {
}
}
myChart.setOption({series: [{
myChart.setOption({series: [ {
data: window._values,
markArea: {data: data}}]});
}

View File

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