Initial commit: Java股票数据获取项目

- 基于Tushare Pro Java SDK 2.0.5-RELEASE版本
- 实现股票基本信息和日线行情数据获取
- 使用MyBatis进行数据库操作
- 支持定时任务调度
- Spring Boot框架集成

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ycg 2025-12-05 11:31:25 +08:00
commit ef5cbc22dc
29 changed files with 1964 additions and 0 deletions

View File

@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(mvn clean compile:*)"
],
"deny": [],
"ask": []
}
}

38
.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.codeverse.userSettings.MarscodeWorkspaceAppSettingsState">
<option name="progress" value="1.0" />
</component>
</project>

18
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="stock_data@fnv4.skdbj.email" uuid="434a0422-4a51-4522-9765-fecbb4acba4a">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://fnv4.skdbj.email:15340/stock_data</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.resource.type" value="Deployment" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

7
.idea/encodings.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

View File

@ -0,0 +1,5 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
</profile>
</component>

14
.idea/misc.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="ms-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

6
.idea/sqldialects.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/src/main/resources/sql/init.sql" dialect="MySQL" />
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

73
CLAUDE.md Normal file
View File

@ -0,0 +1,73 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
这是一个定时抓取股票数据的Java项目通过调用Tushare Pro接口获取股票数据并存储到数据库中。
## 技术栈
- **Java**: 主要编程语言
- **Maven**: 项目构建和依赖管理
- **Tushare Pro Java SDK**: 股票数据获取接口
- **MyBatis**: ORM框架用于数据库操作
## 常用命令
### 构建和运行
```bash
# 编译项目
mvn compile
# 打包项目
mvn package
# 运行主程序
mvn exec:java -Dexec.mainClass="com.sjz.App"
# 运行测试
mvn test
# 清理构建文件
mvn clean
```
### 开发相关
```bash
# 生成项目依赖树
mvn dependency:tree
# 查看项目信息
mvn help:effective-pom
# 更新依赖
mvn dependency:resolve
```
## 项目结构
```
src/main/java/com/sjz/
├── App.java # 主入口类
├── config/ # 配置类
├── service/ # 业务逻辑层
├── mapper/ # MyBatis数据访问层
├── model/ # 数据模型类
├── task/ # 定时任务
└── util/ # 工具类
src/test/java/com/sjz/ # 测试类
src/main/resources/ # 配置文件和MyBatis映射文件
```
## 开发注意事项
- Tushare Pro接口需要配置有效的API token
- 数据库连接信息需要在配置文件中正确设置
- 注意Tushare接口的调用频率限制避免超出限制
- 股票数据的更新通常是交易日进行,需要考虑非交易日的处理
## 数据库
项目使用MyBatis进行数据库操作相关的SQL映射文件通常位于`src/main/resources/mapper/`目录下。

165
README.md Normal file
View File

@ -0,0 +1,165 @@
# 股票数据抓取项目
基于Tushare Pro接口的股票数据定时抓取系统使用Spring Boot + MyBatis + MySQL技术栈。
## 功能特性
- 🔄 **定时抓取**: 支持定时获取股票基本信息和日线行情数据
- 📊 **数据存储**: 使用MySQL存储股票数据支持批量插入和去重
- ⚡ **高性能**: 使用HikariCP连接池和MyBatis优化数据库操作
- 🔧 **灵活配置**: 支持通过配置文件自定义抓取策略和数据库连接
- 📈 **多时间周期**: 支持日线、周线、月线数据抓取
- 🗃️ **数据清理**: 自动清理历史数据,控制存储空间
## 技术栈
- **Java 8+**: 主要编程语言
- **Spring Boot 2.7**: 应用框架
- **MyBatis**: ORM框架
- **MySQL**: 数据存储
- **Tushare Pro SDK**: 股票数据接口
- **Quartz**: 定时任务调度
- **HikariCP**: 数据库连接池
## 快速开始
### 1. 环境准备
- JDK 8+
- Maven 3.6+
- MySQL 5.7+
### 2. 数据库初始化
执行SQL脚本创建数据库和表
```sql
source src/main/resources/sql/init.sql
```
### 3. 配置文件修改
编辑 `src/main/resources/application.properties`
```properties
# Tushare配置
tushare.token=your_actual_tushare_token
# 数据库配置
db.url=jdbc:mysql://localhost:3306/stock_data?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
db.username=your_username
db.password=your_password
```
### 4. 编译和运行
```bash
# 编译项目
mvn clean compile
# 打包项目
mvn clean package
# 运行应用程序
java -jar target/go-stock-1.0-SNAPSHOT.jar
```
## 使用说明
### 命令行参数
```bash
# 启动应用程序并运行定时任务
java -jar go-stock.jar
# 手动获取股票基本信息
java -jar go-stock.jar fetch-basic
# 手动获取股票日线行情数据
java -jar go-stock.jar fetch-daily
# 清理历史数据保留60天
java -jar go-stock.jar clean-data 60
# 显示帮助信息
java -jar go-stock.jar help
```
### 定时任务配置
`application.properties` 中配置定时任务:
```properties
# 启用定时任务
task.enabled=true
# 股票基本信息获取时间每天凌晨1点
task.cron.stock.basic=0 0 1 * * ?
# 股票日线数据获取时间每个交易日下午3点30分
task.cron.stock.daily=0 30 15 * * ?
```
## 项目结构
```
src/main/java/com/sjz/
├── App.java # 主程序入口
├── config/ # 配置类
│ ├── DatabaseConfig.java # 数据库配置
│ └── TushareConfig.java # Tushare配置
├── mapper/ # 数据访问层
│ ├── StockBasicMapper.java
│ └── StockDailyMapper.java
├── model/ # 数据模型
│ ├── StockBasic.java
│ └── StockDaily.java
├── service/ # 业务逻辑层
│ ├── StockBasicService.java
│ └── StockDailyService.java
├── task/ # 定时任务
│ └── StockDataScheduler.java
└── util/ # 工具类
├── ConfigUtil.java
└── DateUtil.java
src/main/resources/
├── application.properties # 配置文件
├── mybatis-config.xml # MyBatis配置
└── sql/init.sql # 数据库初始化脚本
```
## API限制说明
Tushare Pro接口有以下限制
- 每分钟请求次数限制:根据用户等级不同
- 每次返回记录数限制通常5000条
- 建议在请求间添加适当延时,避免触发限制
## 监控和日志
- 使用SLF4J + Logback记录运行日志
- 数据库操作记录详细日志
- 定时任务执行状态监控
- 错误信息记录和报警
## 注意事项
1. **Tushare Token**: 需要在Tushare官网注册并获取有效的API Token
2. **交易日**: 系统会自动判断交易日,非交易日不会执行数据抓取
3. **数据去重**: 使用唯一键约束确保数据不重复
4. **数据备份**: 建议定期备份数据库
5. **监控告警**: 建议配置监控告警,及时发现问题
## 扩展功能
- 添加更多数据源
- 实现数据分析和统计功能
- 添加Web管理界面
- 支持更多时间周期数据
- 添加邮件通知功能
## 许可证
本项目仅供学习和研究使用请遵守相关API使用条款。

134
pom.xml Normal file
View File

@ -0,0 +1,134 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sjz</groupId>
<artifactId>go-stock</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>go-stock</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>3.1.5</version>
</dependency>
<!-- JUnit 测试框架 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- Tushare Pro Java SDK -->
<dependency>
<groupId>com.github.qhh6eq</groupId>
<artifactId>tushare-pro-java-sdk</artifactId>
<version>2.0.5-RELEASE</version>
</dependency>
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- MySQL连接器 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- HikariCP 连接池 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>
<!-- 日志框架 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.7</version>
</dependency>
<!-- JSON处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- 定时任务 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Spring Boot Maven Plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.14</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>com.sjz.App</mainClass>
</configuration>
</plugin>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>8</source>
<target>8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- Maven Surefire Plugin for running tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<includes>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,102 @@
package com.sjz;
import com.sjz.task.StockDataScheduler;
import com.sjz.util.ConfigUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* 股票数据抓取应用程序启动类
*
* @author sjz
*/
@SpringBootApplication
@ComponentScan(basePackages = "com.sjz")
@EnableTransactionManagement
public class App implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(App.class);
@Autowired
private StockDataScheduler stockDataScheduler;
public static void main(String[] args) {
try {
// 验证必要的配置
ConfigUtil.validateRequiredProperties(
"tushare.token",
"db.url",
"db.username",
"db.password"
);
logger.info("正在启动股票数据抓取应用程序...");
SpringApplication.run(App.class, args);
logger.info("股票数据抓取应用程序启动完成");
} catch (Exception e) {
logger.error("应用程序启动失败", e);
System.exit(1);
}
}
@Override
public void run(String... args) throws Exception {
logger.info("股票数据抓取应用程序正在运行...");
if (args.length > 0) {
String command = args[0];
switch (command.toLowerCase()) {
case "fetch-basic":
logger.info("手动触发股票基本信息获取...");
stockDataScheduler.triggerFetchStockBasic();
break;
case "fetch-daily":
logger.info("手动触发股票日线行情数据获取...");
stockDataScheduler.triggerFetchStockDaily();
break;
case "clean-data":
int daysToKeep = args.length > 1 ? Integer.parseInt(args[1]) : 90;
logger.info("手动触发历史数据清理,保留 {} 天数据...", daysToKeep);
stockDataScheduler.triggerCleanHistoryData(daysToKeep);
break;
case "help":
printUsage();
break;
default:
logger.warn("未知命令: {}", command);
printUsage();
}
} else {
// 没有命令行参数时显示帮助信息
printUsage();
// 启动定时任务
logger.info("定时任务已启动,将按照配置的时间执行数据抓取任务");
}
}
private void printUsage() {
System.out.println("\n=== 股票数据抓取应用程序使用说明 ===");
System.out.println("启动命令: java -jar go-stock.jar [命令]");
System.out.println();
System.out.println("可用命令:");
System.out.println(" 无参数 - 启动应用程序并运行定时任务");
System.out.println(" fetch-basic - 手动获取股票基本信息");
System.out.println(" fetch-daily - 手动获取股票日线行情数据");
System.out.println(" clean-data [days] - 清理历史数据默认保留90天");
System.out.println(" help - 显示此帮助信息");
System.out.println();
System.out.println("示例:");
System.out.println(" java -jar go-stock.jar fetch-basic");
System.out.println(" java -jar go-stock.jar fetch-daily");
System.out.println(" java -jar go-stock.jar clean-data 60");
System.out.println("====================================\n");
}
}

View File

@ -0,0 +1,81 @@
package com.sjz.config;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* 数据库配置类
*
* @author sjz
*/
@Configuration
@EnableTransactionManagement
@MapperScan("com.sjz.mapper")
public class DatabaseConfig {
/**
* 创建数据源
*/
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setDriverClassName("com.mysql.cj.jdbc.Driver");
config.setJdbcUrl("jdbc:mysql://localhost:3306/stock_data?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false");
config.setUsername("root");
config.setPassword("your_password_here");
// 连接池配置
config.setMaximumPoolSize(10);
config.setMinimumIdle(5);
config.setIdleTimeout(300000);
config.setConnectionTimeout(20000);
config.setMaxLifetime(1200000);
return new HikariDataSource(config);
}
/**
* 创建SqlSessionFactory
*/
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// 设置MyBatis配置文件位置
sessionFactory.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
// 设置Mapper XML文件位置
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
// 设置类型别名包
sessionFactory.setTypeAliasesPackage("com.sjz.model");
return sessionFactory.getObject();
}
/**
* 事务管理器
*/
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}

View File

@ -0,0 +1,37 @@
package com.sjz.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.github.tusharepro.core.TusharePro;
import com.github.tusharepro.core.TushareProService;
/**
* Tushare配置类
*
* @author sjz
*/
@Configuration
public class TushareConfig {
@Value("${tushare.token}")
private String tushareToken;
@Value("${tushare.api.url}")
private String apiUrl;
/**
* 初始化Tushare Pro全局配置
*/
@Bean
public TusharePro initTusharePro() {
TusharePro tusharePro = new TusharePro.Builder()
.setToken(tushareToken)
.build();
// 设置全局配置
TusharePro.setGlobal(tusharePro);
return tusharePro;
}
}

View File

@ -0,0 +1,57 @@
package com.sjz.mapper;
import com.sjz.model.StockBasic;
import org.apache.ibatis.annotations.*;
import java.util.List;
/**
* 股票基本信息Mapper接口
*
* @author sjz
*/
@Mapper
public interface StockBasicMapper {
/**
* 批量插入股票基本信息
*/
@Insert("<script>" +
"INSERT INTO stock_basic (ts_code, symbol, name, area, industry, market, list_date, create_time, update_time) " +
"VALUES " +
"<foreach collection='list' item='item' separator=','>" +
"(#{item.tsCode}, #{item.symbol}, #{item.name}, #{item.area}, #{item.industry}, #{item.market}, #{item.listDate}, NOW(), NOW())" +
"</foreach>" +
" ON DUPLICATE KEY UPDATE " +
"name = VALUES(name), area = VALUES(area), industry = VALUES(industry), " +
"market = VALUES(market), list_date = VALUES(list_date), update_time = NOW()" +
"</script>")
int batchInsert(@Param("list") List<StockBasic> stockBasics);
/**
* 根据TS代码查询股票基本信息
*/
@Select("SELECT ts_code, symbol, name, area, industry, market, list_date, create_time, update_time " +
"FROM stock_basic WHERE ts_code = #{tsCode}")
StockBasic selectByTsCode(@Param("tsCode") String tsCode);
/**
* 查询所有股票基本信息
*/
@Select("SELECT ts_code, symbol, name, area, industry, market, list_date, create_time, update_time " +
"FROM stock_basic ORDER BY ts_code")
List<StockBasic> selectAll();
/**
* 根据市场类型查询股票列表
*/
@Select("SELECT ts_code, symbol, name, area, industry, market, list_date, create_time, update_time " +
"FROM stock_basic WHERE market = #{market} ORDER BY ts_code")
List<StockBasic> selectByMarket(@Param("market") String market);
/**
* 统计股票数量
*/
@Select("SELECT COUNT(*) FROM stock_basic")
int count();
}

View File

@ -0,0 +1,68 @@
package com.sjz.mapper;
import com.sjz.model.StockDaily;
import org.apache.ibatis.annotations.*;
import java.util.List;
/**
* 股票日线行情Mapper接口
*
* @author sjz
*/
@Mapper
public interface StockDailyMapper {
/**
* 批量插入股票日线行情数据
*/
@Insert("<script>" +
"INSERT INTO stock_daily (ts_code, trade_date, open, high, low, close, pre_close, change, pct_chg, vol, amount, create_time) " +
"VALUES " +
"<foreach collection='list' item='item' separator=','>" +
"(#{item.tsCode}, #{item.tradeDate}, #{item.open}, #{item.high}, #{item.low}, #{item.close}, " +
"#{item.preClose}, #{item.change}, #{item.pctChg}, #{item.vol}, #{item.amount}, NOW())" +
"</foreach>" +
" ON DUPLICATE KEY UPDATE " +
"open = VALUES(open), high = VALUES(high), low = VALUES(low), close = VALUES(close), " +
"pre_close = VALUES(pre_close), change = VALUES(change), pct_chg = VALUES(pct_chg), " +
"vol = VALUES(vol), amount = VALUES(amount)" +
"</script>")
int batchInsert(@Param("list") List<StockDaily> stockDailies);
/**
* 根据TS代码和交易日期查询
*/
@Select("SELECT ts_code, trade_date, open, high, low, close, pre_close, change, pct_chg, vol, amount, create_time " +
"FROM stock_daily WHERE ts_code = #{tsCode} AND trade_date = #{tradeDate}")
StockDaily selectByTsCodeAndDate(@Param("tsCode") String tsCode, @Param("tradeDate") String tradeDate);
/**
* 根据TS代码查询指定日期范围内的数据
*/
@Select("SELECT ts_code, trade_date, open, high, low, close, pre_close, change, pct_chg, vol, amount, create_time " +
"FROM stock_daily WHERE ts_code = #{tsCode} AND trade_date >= #{startDate} AND trade_date <= #{endDate} " +
"ORDER BY trade_date")
List<StockDaily> selectByTsCodeAndDateRange(@Param("tsCode") String tsCode,
@Param("startDate") String startDate,
@Param("endDate") String endDate);
/**
* 根据交易日期查询所有股票数据
*/
@Select("SELECT ts_code, trade_date, open, high, low, close, pre_close, change, pct_chg, vol, amount, create_time " +
"FROM stock_daily WHERE trade_date = #{tradeDate} ORDER BY ts_code")
List<StockDaily> selectByTradeDate(@Param("tradeDate") String tradeDate);
/**
* 获取指定股票的最新交易日期
*/
@Select("SELECT MAX(trade_date) FROM stock_daily WHERE ts_code = #{tsCode}")
String selectLatestTradeDateByTsCode(@Param("tsCode") String tsCode);
/**
* 删除指定日期之前的数据数据清理
*/
@Delete("DELETE FROM stock_daily WHERE trade_date < #{beforeDate}")
int deleteBeforeDate(@Param("beforeDate") String beforeDate);
}

View File

@ -0,0 +1,109 @@
package com.sjz.model;
import java.util.Date;
/**
* 股票基本信息模型
*
* @author sjz
*/
public class StockBasic {
private String tsCode; // TS代码
private String symbol; // 股票代码
private String name; // 股票名称
private String area; // 所在地域
private String industry; // 所属行业
private String market; // 市场类型
private String listDate; // 上市日期
private Date createTime; // 创建时间
private Date updateTime; // 更新时间
// 构造函数
public StockBasic() {}
// Getter和Setter方法
public String getTsCode() {
return tsCode;
}
public void setTsCode(String tsCode) {
this.tsCode = tsCode;
}
public String getSymbol() {
return symbol;
}
public void setSymbol(String symbol) {
this.symbol = symbol;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getArea() {
return area;
}
public void setArea(String area) {
this.area = area;
}
public String getIndustry() {
return industry;
}
public void setIndustry(String industry) {
this.industry = industry;
}
public String getMarket() {
return market;
}
public void setMarket(String market) {
this.market = market;
}
public String getListDate() {
return listDate;
}
public void setListDate(String listDate) {
this.listDate = listDate;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
@Override
public String toString() {
return "StockBasic{" +
"tsCode='" + tsCode + '\'' +
", symbol='" + symbol + '\'' +
", name='" + name + '\'' +
", area='" + area + '\'' +
", industry='" + industry + '\'' +
", market='" + market + '\'' +
", listDate='" + listDate + '\'' +
'}';
}
}

View File

@ -0,0 +1,137 @@
package com.sjz.model;
import java.math.BigDecimal;
import java.util.Date;
/**
* 股票日线行情数据模型
*
* @author sjz
*/
public class StockDaily {
private String tsCode; // TS代码
private String tradeDate; // 交易日期
private BigDecimal open; // 开盘价
private BigDecimal high; // 最高价
private BigDecimal low; // 最低价
private BigDecimal close; // 收盘价
private BigDecimal preClose;// 前收盘价
private BigDecimal change; // 涨跌额
private BigDecimal pctChg; // 涨跌幅
private BigDecimal vol; // 成交量()
private BigDecimal amount; // 成交额(千元)
private Date createTime; // 创建时间
// 构造函数
public StockDaily() {}
// Getter和Setter方法
public String getTsCode() {
return tsCode;
}
public void setTsCode(String tsCode) {
this.tsCode = tsCode;
}
public String getTradeDate() {
return tradeDate;
}
public void setTradeDate(String tradeDate) {
this.tradeDate = tradeDate;
}
public BigDecimal getOpen() {
return open;
}
public void setOpen(BigDecimal open) {
this.open = open;
}
public BigDecimal getHigh() {
return high;
}
public void setHigh(BigDecimal high) {
this.high = high;
}
public BigDecimal getLow() {
return low;
}
public void setLow(BigDecimal low) {
this.low = low;
}
public BigDecimal getClose() {
return close;
}
public void setClose(BigDecimal close) {
this.close = close;
}
public BigDecimal getPreClose() {
return preClose;
}
public void setPreClose(BigDecimal preClose) {
this.preClose = preClose;
}
public BigDecimal getChange() {
return change;
}
public void setChange(BigDecimal change) {
this.change = change;
}
public BigDecimal getPctChg() {
return pctChg;
}
public void setPctChg(BigDecimal pctChg) {
this.pctChg = pctChg;
}
public BigDecimal getVol() {
return vol;
}
public void setVol(BigDecimal vol) {
this.vol = vol;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
@Override
public String toString() {
return "StockDaily{" +
"tsCode='" + tsCode + '\'' +
", tradeDate='" + tradeDate + '\'' +
", open=" + open +
", high=" + high +
", low=" + low +
", close=" + close +
", pctChg=" + pctChg +
'}';
}
}

View File

@ -0,0 +1,106 @@
package com.sjz.service;
import com.github.tusharepro.core.TushareProService;
import com.github.tusharepro.core.entity.StockBasicEntity;
import com.github.tusharepro.core.http.Request;
import com.sjz.mapper.StockBasicMapper;
import com.sjz.model.StockBasic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 股票基本信息服务
*
* @author sjz
*/
@Service
@Transactional
public class StockBasicService {
private static final Logger logger = LoggerFactory.getLogger(StockBasicService.class);
@Autowired
private StockBasicMapper stockBasicMapper;
// 注意TushareProService现在是静态类不需要注入
/**
* 获取并保存股票基本信息
*/
public void fetchAndSaveStockBasic() {
try {
logger.info("开始获取股票基本信息...");
// 使用新的API调用方式获取股票基本信息
List<StockBasicEntity> stockBasicEntities = TushareProService.stockBasic(
new Request<StockBasicEntity>() {}
.allFields()
.param(com.github.tusharepro.core.bean.StockBasic.Params.list_status.value("L"))); // 只获取上市状态的股票
if (stockBasicEntities == null || stockBasicEntities.isEmpty()) {
logger.warn("未获取到股票基本信息数据");
return;
}
logger.info("成功获取到 {} 条股票基本信息", stockBasicEntities.size());
// 转换为本地实体类
List<StockBasic> stockBasics = stockBasicEntities.stream()
.map(entity -> {
StockBasic stockBasic = new StockBasic();
stockBasic.setTsCode(entity.getTsCode());
stockBasic.setSymbol(entity.getSymbol());
stockBasic.setName(entity.getName());
stockBasic.setArea(entity.getArea());
stockBasic.setIndustry(entity.getIndustry());
stockBasic.setMarket(entity.getMarket());
stockBasic.setListDate(entity.getListDate() != null ? entity.getListDate().toString() : null);
return stockBasic;
})
.collect(Collectors.toList());
// 批量保存到数据库
if (!stockBasics.isEmpty()) {
int count = stockBasicMapper.batchInsert(stockBasics);
logger.info("成功保存 {} 条股票基本信息", count);
}
} catch (IOException e) {
logger.error("获取股票基本信息失败", e);
throw new RuntimeException("获取股票基本信息失败", e);
}
}
/**
* 获取所有股票基本信息
*/
@Transactional(readOnly = true)
public List<StockBasic> getAllStockBasic() {
return stockBasicMapper.selectAll();
}
/**
* 根据市场类型获取股票列表
*/
@Transactional(readOnly = true)
public List<StockBasic> getStockByMarket(String market) {
return stockBasicMapper.selectByMarket(market);
}
/**
* 根据TS代码获取股票信息
*/
@Transactional(readOnly = true)
public StockBasic getStockByTsCode(String tsCode) {
return stockBasicMapper.selectByTsCode(tsCode);
}
}

View File

@ -0,0 +1,192 @@
package com.sjz.service;
import com.sjz.mapper.StockBasicMapper;
import com.sjz.mapper.StockDailyMapper;
import com.sjz.model.StockBasic;
import com.sjz.model.StockDaily;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.github.tusharepro.core.TushareProService;
import com.github.tusharepro.core.entity.DailyEntity;
import com.github.tusharepro.core.http.Request;
import com.github.tusharepro.core.bean.Daily;
import java.io.IOException;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
/**
* 股票日线行情服务
*
* @author sjz
*/
@Service
@Transactional
public class StockDailyService {
private static final Logger logger = LoggerFactory.getLogger(StockDailyService.class);
@Autowired
private StockDailyMapper stockDailyMapper;
@Autowired
private StockBasicMapper stockBasicMapper;
// 注意TushareProService现在是静态类不需要注入
/**
* 获取并保存股票日线行情数据
*/
public void fetchAndSaveStockDaily() {
try {
logger.info("开始获取股票日线行情数据...");
// 获取所有股票基本信息
List<StockBasic> stockBasics = stockBasicMapper.selectAll();
if (stockBasics.isEmpty()) {
logger.warn("没有找到股票基本信息,请先执行股票基本信息获取任务");
return;
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String tradeDate = sdf.format(new Date()); // 获取当前日期作为交易日期
int totalCount = 0;
int batchSize = 100; // 每批处理100只股票
// 分批处理股票
for (int i = 0; i < stockBasics.size(); i += batchSize) {
int endIndex = Math.min(i + batchSize, stockBasics.size());
List<StockBasic> batchStocks = stockBasics.subList(i, endIndex);
List<StockDaily> stockDailies = fetchStockDailyData(batchStocks, tradeDate);
if (!stockDailies.isEmpty()) {
int count = stockDailyMapper.batchInsert(stockDailies);
totalCount += count;
logger.info("批次 {}-{}: 保存了 {} 条日线数据", i, endIndex, count);
}
// 避免请求过于频繁
Thread.sleep(1000);
}
logger.info("总共获取并保存了 {} 条股票日线行情数据", totalCount);
} catch (Exception e) {
logger.error("获取股票日线行情数据失败", e);
throw new RuntimeException("获取股票日线行情数据失败", e);
}
}
/**
* 获取指定股票列表的日线数据
*/
private List<StockDaily> fetchStockDailyData(List<StockBasic> stockBasics, String tradeDate) throws IOException {
List<StockDaily> stockDailies = new ArrayList<>();
// 构建TS代码字符串
StringBuilder tsCodes = new StringBuilder();
for (int i = 0; i < stockBasics.size(); i++) {
if (i > 0) {
tsCodes.append(",");
}
tsCodes.append(stockBasics.get(i).getTsCode());
}
// 使用新的API调用方式获取日线数据
List<DailyEntity> dailyEntities = TushareProService.daily(
new Request<DailyEntity>() {}
.allFields()
.param(Daily.Params.ts_code.value(tsCodes.toString()))
.param(Daily.Params.trade_date.value(tradeDate)));
if (dailyEntities != null && !dailyEntities.isEmpty()) {
stockDailies = dailyEntities.stream()
.map(entity -> {
StockDaily stockDaily = new StockDaily();
stockDaily.setTsCode(entity.getTsCode());
stockDaily.setTradeDate(entity.getTradeDate() != null ? entity.getTradeDate().toString() : null);
// 处理数值字段
if (entity.getOpen() != null) {
stockDaily.setOpen(BigDecimal.valueOf(entity.getOpen()));
}
if (entity.getHigh() != null) {
stockDaily.setHigh(BigDecimal.valueOf(entity.getHigh()));
}
if (entity.getLow() != null) {
stockDaily.setLow(BigDecimal.valueOf(entity.getLow()));
}
if (entity.getClose() != null) {
stockDaily.setClose(BigDecimal.valueOf(entity.getClose()));
}
if (entity.getPreClose() != null) {
stockDaily.setPreClose(BigDecimal.valueOf(entity.getPreClose()));
}
if (entity.getChange() != null) {
stockDaily.setChange(BigDecimal.valueOf(entity.getChange()));
}
if (entity.getPctChg() != null) {
stockDaily.setPctChg(BigDecimal.valueOf(entity.getPctChg()));
}
if (entity.getVol() != null) {
stockDaily.setVol(BigDecimal.valueOf(entity.getVol()));
}
if (entity.getAmount() != null) {
stockDaily.setAmount(BigDecimal.valueOf(entity.getAmount()));
}
return stockDaily;
})
.collect(Collectors.toList());
}
return stockDailies;
}
/**
* 获取指定股票的日线数据
*/
@Transactional(readOnly = true)
public List<StockDaily> getStockDailyByTsCodeAndDateRange(String tsCode, String startDate, String endDate) {
return stockDailyMapper.selectByTsCodeAndDateRange(tsCode, startDate, endDate);
}
/**
* 获取指定日期的所有股票日线数据
*/
@Transactional(readOnly = true)
public List<StockDaily> getStockDailyByTradeDate(String tradeDate) {
return stockDailyMapper.selectByTradeDate(tradeDate);
}
/**
* 获取指定股票的最新交易日期
*/
@Transactional(readOnly = true)
public String getLatestTradeDateByTsCode(String tsCode) {
return stockDailyMapper.selectLatestTradeDateByTsCode(tsCode);
}
/**
* 清理历史数据保留指定天数的数据
*/
@Transactional
public int cleanHistoryData(int daysToKeep) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, -daysToKeep);
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String beforeDate = sdf.format(calendar.getTime());
int count = stockDailyMapper.deleteBeforeDate(beforeDate);
logger.info("清理了 {} 天前的 {} 条历史数据", daysToKeep, count);
return count;
}
}

View File

@ -0,0 +1,118 @@
package com.sjz.task;
import com.sjz.service.StockBasicService;
import com.sjz.service.StockDailyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 股票数据定时任务调度器
*
* @author sjz
*/
@Component
@EnableScheduling
public class StockDataScheduler {
private static final Logger logger = LoggerFactory.getLogger(StockDataScheduler.class);
@Autowired
private StockBasicService stockBasicService;
@Autowired
private StockDailyService stockDailyService;
@Value("${task.enabled:true}")
private boolean taskEnabled;
/**
* 每天凌晨1点获取股票基本信息每周执行一次即可
*/
@Scheduled(cron = "${task.cron.stock.basic:0 0 1 * * ?}")
public void fetchStockBasic() {
if (!taskEnabled) {
logger.info("定时任务已禁用,跳过股票基本信息获取");
return;
}
logger.info("开始执行股票基本信息获取定时任务...");
try {
stockBasicService.fetchAndSaveStockBasic();
logger.info("股票基本信息获取定时任务执行完成");
} catch (Exception e) {
logger.error("股票基本信息获取定时任务执行失败", e);
}
}
/**
* 每个交易日下午3点30分获取股票日线行情数据
*/
@Scheduled(cron = "${task.cron.stock.daily:0 30 15 * * ?}")
public void fetchStockDaily() {
if (!taskEnabled) {
logger.info("定时任务已禁用,跳过股票日线行情数据获取");
return;
}
logger.info("开始执行股票日线行情数据获取定时任务...");
try {
stockDailyService.fetchAndSaveStockDaily();
logger.info("股票日线行情数据获取定时任务执行完成");
} catch (Exception e) {
logger.error("股票日线行情数据获取定时任务执行失败", e);
}
}
/**
* 每周日凌晨2点清理历史数据保留90天
*/
@Scheduled(cron = "0 0 2 * * SUN")
public void cleanHistoryData() {
if (!taskEnabled) {
logger.info("定时任务已禁用,跳过历史数据清理");
return;
}
logger.info("开始执行历史数据清理定时任务...");
try {
stockDailyService.cleanHistoryData(90); // 保留90天的数据
logger.info("历史数据清理定时任务执行完成");
} catch (Exception e) {
logger.error("历史数据清理定时任务执行失败", e);
}
}
/**
* 手动触发股票基本信息获取用于测试
*/
public void triggerFetchStockBasic() {
logger.info("手动触发股票基本信息获取任务...");
fetchStockBasic();
}
/**
* 手动触发股票日线行情数据获取用于测试
*/
public void triggerFetchStockDaily() {
logger.info("手动触发股票日线行情数据获取任务...");
fetchStockDaily();
}
/**
* 手动触发历史数据清理用于测试
*/
public void triggerCleanHistoryData(int daysToKeep) {
logger.info("手动触发历史数据清理任务,保留 {} 天数据...", daysToKeep);
try {
stockDailyService.cleanHistoryData(daysToKeep);
logger.info("手动历史数据清理任务执行完成");
} catch (Exception e) {
logger.error("手动历史数据清理任务执行失败", e);
}
}
}

View File

@ -0,0 +1,128 @@
package com.sjz.util;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* 配置工具类
*
* @author sjz
*/
public class ConfigUtil {
private static final Properties properties = new Properties();
static {
try (InputStream is = ConfigUtil.class.getClassLoader().getResourceAsStream("application.properties")) {
if (is != null) {
properties.load(is);
}
} catch (IOException e) {
throw new RuntimeException("Failed to load application.properties", e);
}
}
/**
* 获取配置值
*/
public static String getProperty(String key) {
return properties.getProperty(key);
}
/**
* 获取配置值如果不存在则返回默认值
*/
public static String getProperty(String key, String defaultValue) {
return properties.getProperty(key, defaultValue);
}
/**
* 获取整数配置值
*/
public static int getIntProperty(String key, int defaultValue) {
String value = getProperty(key);
if (value == null) {
return defaultValue;
}
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
/**
* 获取布尔配置值
*/
public static boolean getBooleanProperty(String key, boolean defaultValue) {
String value = getProperty(key);
if (value == null) {
return defaultValue;
}
return Boolean.parseBoolean(value);
}
/**
* 获取长整数配置值
*/
public static long getLongProperty(String key, long defaultValue) {
String value = getProperty(key);
if (value == null) {
return defaultValue;
}
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
/**
* 检查必要的配置是否存在
*/
public static void validateRequiredProperties(String... requiredKeys) {
for (String key : requiredKeys) {
String value = getProperty(key);
if (value == null || value.trim().isEmpty()) {
throw new RuntimeException("Required property '" + key + "' is missing or empty");
}
}
}
/**
* 获取Tushare Token
*/
public static String getTushareToken() {
String token = getProperty("tushare.token");
if (token == null || token.equals("your_tushare_token_here")) {
throw new RuntimeException("Tushare token is not configured properly");
}
return token;
}
/**
* 获取数据库URL
*/
public static String getDatabaseUrl() {
return getProperty("db.url");
}
/**
* 获取数据库用户名
*/
public static String getDatabaseUsername() {
return getProperty("db.username");
}
/**
* 获取数据库密码
*/
public static String getDatabasePassword() {
String password = getProperty("db.password");
if (password == null || password.equals("your_password_here")) {
throw new RuntimeException("Database password is not configured properly");
}
return password;
}
}

View File

@ -0,0 +1,124 @@
package com.sjz.util;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
/**
* 日期工具类
*
* @author sjz
*/
public class DateUtil {
public static final String DATE_FORMAT_YYYYMMDD = "yyyyMMdd";
public static final String DATE_FORMAT_YYYY_MM_DD = "yyyy-MM-dd";
public static final String DATETIME_FORMAT_YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
/**
* 获取当前日期字符串
*/
public static String getCurrentDateString(String format) {
SimpleDateFormat sdf = new SimpleDateFormat(format);
return sdf.format(new Date());
}
/**
* 获取当前日期字符串yyyyMMdd格式
*/
public static String getCurrentDateString() {
return getCurrentDateString(DATE_FORMAT_YYYYMMDD);
}
/**
* 获取指定天数前的日期字符串
*/
public static String getDateBefore(int days, String format) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, -days);
SimpleDateFormat sdf = new SimpleDateFormat(format);
return sdf.format(calendar.getTime());
}
/**
* 获取指定天数前的日期字符串yyyyMMdd格式
*/
public static String getDateBefore(int days) {
return getDateBefore(days, DATE_FORMAT_YYYYMMDD);
}
/**
* 获取指定天数后的日期字符串
*/
public static String getDateAfter(int days, String format) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, days);
SimpleDateFormat sdf = new SimpleDateFormat(format);
return sdf.format(calendar.getTime());
}
/**
* 获取指定天数后的日期字符串yyyyMMdd格式
*/
public static String getDateAfter(int days) {
return getDateAfter(days, DATE_FORMAT_YYYYMMDD);
}
/**
* 日期格式转换
*/
public static String convertDateFormat(String date, String fromFormat, String toFormat) throws ParseException {
SimpleDateFormat fromSdf = new SimpleDateFormat(fromFormat);
SimpleDateFormat toSdf = new SimpleDateFormat(toFormat);
Date parsedDate = fromSdf.parse(date);
return toSdf.format(parsedDate);
}
/**
* 检查是否为交易日简单的周一到周五检查
* 注意这里只是简单的判断实际的交易日需要排除节假日
*/
public static boolean isTradeDay(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
return dayOfWeek != Calendar.SATURDAY && dayOfWeek != Calendar.SUNDAY;
}
/**
* 检查是否为交易日简单的周一到周五检查
*/
public static boolean isTradeDay(String dateString) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_YYYYMMDD);
Date date = sdf.parse(dateString);
return isTradeDay(date);
}
/**
* 获取最近的交易日向前查找
*/
public static String getRecentTradeDate() {
Calendar calendar = Calendar.getInstance();
int daysBack = 0;
do {
calendar.add(Calendar.DAY_OF_MONTH, -1);
daysBack++;
} while (daysBack <= 10 && !isTradeDay(calendar.getTime())); // 最多向前查找10天
SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_YYYYMMDD);
return sdf.format(calendar.getTime());
}
/**
* 计算两个日期之间的天数差
*/
public static int daysBetween(String startDate, String endDate, String format) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat(format);
Date start = sdf.parse(startDate);
Date end = sdf.parse(endDate);
long diff = end.getTime() - start.getTime();
return (int) (diff / (24 * 60 * 60 * 1000));
}
}

View File

@ -0,0 +1,31 @@
# Tushare配置
tushare.token=2876ea85cb005fb5fa17c809a98174f2d5aae8b1f830110a5ead6211
tushare.api.url=https://api.tushare.pro
# 数据库配置
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://fnv4.skdbj.email:15340/stock_data?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
db.username=stock
db.password=stock
# 连接池配置
db.pool.maximumPoolSize=10
db.pool.minimumIdle=5
db.pool.idleTimeout=300000
db.pool.connectionTimeout=20000
db.pool.maxLifetime=1200000
# MyBatis配置
mybatis.configLocation=mybatis-config.xml
mybatis.mapperLocations=mapper/*.xml
mybatis.typeAliasesPackage=com.sjz.model
# 定时任务配置
task.enabled=true
task.cron.stock.basic=0 0 1 * * ?
task.cron.stock.daily=0 30 15 * * ?
# 日志配置
logging.level.root=INFO
logging.level.com.sjz=DEBUG
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 开启驼峰命名自动映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
<!-- 设置超时时间 -->
<setting name="defaultStatementTimeout" value="30"/>
<!-- 设置获取数据的策略 -->
<setting name="defaultFetchSize" value="100"/>
<!-- 设置日志实现 -->
<setting name="logImpl" value="SLF4J"/>
</settings>
<typeAliases>
<!-- 实体类别名包扫描 -->
<package name="com.sjz.model"/>
</typeAliases>
<typeHandlers>
<!-- 类型处理器 -->
</typeHandlers>
<plugins>
<!-- 分页插件等 -->
</plugins>
</configuration>

View File

@ -0,0 +1,118 @@
-- 股票数据抓取项目数据库初始化脚本
-- 创建数据库
CREATE DATABASE IF NOT EXISTS stock_data DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE stock_data;
-- 股票基本信息表
CREATE TABLE IF NOT EXISTS stock_basic (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
ts_code VARCHAR(20) NOT NULL UNIQUE COMMENT 'TS代码',
symbol VARCHAR(20) NOT NULL COMMENT '股票代码',
name VARCHAR(100) NOT NULL COMMENT '股票名称',
area VARCHAR(50) COMMENT '所在地域',
industry VARCHAR(100) COMMENT '所属行业',
market VARCHAR(20) COMMENT '市场类型',
list_date VARCHAR(20) COMMENT '上市日期',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_ts_code (ts_code),
INDEX idx_symbol (symbol),
INDEX idx_market (market),
INDEX idx_industry (industry)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='股票基本信息表';
-- 股票日线行情表
CREATE TABLE IF NOT EXISTS stock_daily (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
ts_code VARCHAR(20) NOT NULL COMMENT 'TS代码',
trade_date VARCHAR(20) NOT NULL COMMENT '交易日期',
open DECIMAL(10,3) COMMENT '开盘价',
high DECIMAL(10,3) COMMENT '最高价',
low DECIMAL(10,3) COMMENT '最低价',
close DECIMAL(10,3) COMMENT '收盘价',
pre_close DECIMAL(10,3) COMMENT '前收盘价',
change DECIMAL(10,3) COMMENT '涨跌额',
pct_chg DECIMAL(8,3) COMMENT '涨跌幅(%)',
vol DECIMAL(15,2) COMMENT '成交量(手)',
amount DECIMAL(18,2) COMMENT '成交额(千元)',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
UNIQUE KEY uk_ts_trade_date (ts_code, trade_date),
INDEX idx_ts_code (ts_code),
INDEX idx_trade_date (trade_date),
INDEX idx_ts_trade_date (ts_code, trade_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='股票日线行情表';
-- 股票周线行情表
CREATE TABLE IF NOT EXISTS stock_weekly (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
ts_code VARCHAR(20) NOT NULL COMMENT 'TS代码',
trade_date VARCHAR(20) NOT NULL COMMENT '交易日期',
open DECIMAL(10,3) COMMENT '开盘价',
high DECIMAL(10,3) COMMENT '最高价',
low DECIMAL(10,3) COMMENT '最低价',
close DECIMAL(10,3) COMMENT '收盘价',
pre_close DECIMAL(10,3) COMMENT '前收盘价',
change DECIMAL(10,3) COMMENT '涨跌额',
pct_chg DECIMAL(8,3) COMMENT '涨跌幅(%)',
vol DECIMAL(15,2) COMMENT '成交量(手)',
amount DECIMAL(18,2) COMMENT '成交额(千元)',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
UNIQUE KEY uk_ts_trade_date (ts_code, trade_date),
INDEX idx_ts_code (ts_code),
INDEX idx_trade_date (trade_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='股票周线行情表';
-- 股票月线行情表
CREATE TABLE IF NOT EXISTS stock_monthly (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
ts_code VARCHAR(20) NOT NULL COMMENT 'TS代码',
trade_date VARCHAR(20) NOT NULL COMMENT '交易日期',
open DECIMAL(10,3) COMMENT '开盘价',
high DECIMAL(10,3) COMMENT '最高价',
low DECIMAL(10,3) COMMENT '最低价',
close DECIMAL(10,3) COMMENT '收盘价',
pre_close DECIMAL(10,3) COMMENT '前收盘价',
change DECIMAL(10,3) COMMENT '涨跌额',
pct_chg DECIMAL(8,3) COMMENT '涨跌幅(%)',
vol DECIMAL(15,2) COMMENT '成交量(手)',
amount DECIMAL(18,2) COMMENT '成交额(千元)',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
UNIQUE KEY uk_ts_trade_date (ts_code, trade_date),
INDEX idx_ts_code (ts_code),
INDEX idx_trade_date (trade_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='股票月线行情表';
-- 数据获取日志表
CREATE TABLE IF NOT EXISTS fetch_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
fetch_type VARCHAR(50) NOT NULL COMMENT '获取类型(basic/daily/weekly/monthly)',
fetch_date VARCHAR(20) NOT NULL COMMENT '获取日期',
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '开始时间',
end_time TIMESTAMP NULL COMMENT '结束时间',
status VARCHAR(20) NOT NULL COMMENT '状态(running/success/failed)',
total_count INT DEFAULT 0 COMMENT '总记录数',
success_count INT DEFAULT 0 COMMENT '成功记录数',
error_count INT DEFAULT 0 COMMENT '失败记录数',
error_message TEXT COMMENT '错误信息',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX idx_fetch_type (fetch_type),
INDEX idx_fetch_date (fetch_date),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据获取日志表';
-- 插入示例数据(可选)
-- INSERT INTO stock_basic (ts_code, symbol, name, area, industry, market, list_date) VALUES
-- ('000001.SZ', '000001', '平安银行', '深圳', '银行', '主板', '19910403'),
-- ('000002.SZ', '000002', '万科A', '深圳', '房地产', '主板', '19910129');
-- 创建索引以优化查询性能
-- 为日线表添加复合索引
CREATE INDEX idx_daily_ts_trade_date ON stock_daily(ts_code, trade_date);
CREATE INDEX idx_daily_trade_date_ts ON stock_daily(trade_date, ts_code);
-- 为基本信息表添加全文索引(如果需要搜索股票名称)
-- ALTER TABLE stock_basic ADD FULLTEXT INDEX ft_name (name);
-- 显示表结构
SHOW TABLES;

View File

@ -0,0 +1,38 @@
package com.sjz;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
/**
* Unit test for simple App.
*/
public class AppTest
extends TestCase
{
/**
* Create the test case
*
* @param testName name of the test case
*/
public AppTest( String testName )
{
super( testName );
}
/**
* @return the suite of tests being tested
*/
public static Test suite()
{
return new TestSuite( AppTest.class );
}
/**
* Rigourous Test :-)
*/
public void testApp()
{
assertTrue( true );
}
}