From ef5cbc22dc1c6514f83cd546aafaee0dcd925ecf Mon Sep 17 00:00:00 2001 From: ycg <3208975282@qq.com> Date: Fri, 5 Dec 2025 11:31:25 +0800 Subject: [PATCH] =?UTF-8?q?Initial=20commit:=20Java=E8=82=A1=E7=A5=A8?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=8E=B7=E5=8F=96=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 基于Tushare Pro Java SDK 2.0.5-RELEASE版本 - 实现股票基本信息和日线行情数据获取 - 使用MyBatis进行数据库操作 - 支持定时任务调度 - Spring Boot框架集成 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .claude/settings.local.json | 9 + .gitignore | 38 ++++ .idea/.gitignore | 8 + .idea/MarsCodeWorkspaceAppSettings.xml | 6 + .idea/dataSources.xml | 18 ++ .idea/encodings.xml | 7 + .idea/inspectionProfiles/Project_Default.xml | 5 + .idea/misc.xml | 14 ++ .idea/sqldialects.xml | 6 + .idea/vcs.xml | 6 + CLAUDE.md | 73 +++++++ README.md | 165 +++++++++++++++ pom.xml | 134 ++++++++++++ src/main/java/com/sjz/App.java | 102 ++++++++++ .../java/com/sjz/config/DatabaseConfig.java | 81 ++++++++ .../java/com/sjz/config/TushareConfig.java | 37 ++++ .../java/com/sjz/mapper/StockBasicMapper.java | 57 ++++++ .../java/com/sjz/mapper/StockDailyMapper.java | 68 +++++++ src/main/java/com/sjz/model/StockBasic.java | 109 ++++++++++ src/main/java/com/sjz/model/StockDaily.java | 137 +++++++++++++ .../com/sjz/service/StockBasicService.java | 106 ++++++++++ .../com/sjz/service/StockDailyService.java | 192 ++++++++++++++++++ .../java/com/sjz/task/StockDataScheduler.java | 118 +++++++++++ src/main/java/com/sjz/util/ConfigUtil.java | 128 ++++++++++++ src/main/java/com/sjz/util/DateUtil.java | 124 +++++++++++ src/main/resources/application.properties | 31 +++ src/main/resources/mybatis-config.xml | 29 +++ src/main/resources/sql/init.sql | 118 +++++++++++ src/test/java/com/sjz/AppTest.java | 38 ++++ 29 files changed, 1964 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/MarsCodeWorkspaceAppSettings.xml create mode 100644 .idea/dataSources.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/sqldialects.xml create mode 100644 .idea/vcs.xml create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/com/sjz/App.java create mode 100644 src/main/java/com/sjz/config/DatabaseConfig.java create mode 100644 src/main/java/com/sjz/config/TushareConfig.java create mode 100644 src/main/java/com/sjz/mapper/StockBasicMapper.java create mode 100644 src/main/java/com/sjz/mapper/StockDailyMapper.java create mode 100644 src/main/java/com/sjz/model/StockBasic.java create mode 100644 src/main/java/com/sjz/model/StockDaily.java create mode 100644 src/main/java/com/sjz/service/StockBasicService.java create mode 100644 src/main/java/com/sjz/service/StockDailyService.java create mode 100644 src/main/java/com/sjz/task/StockDataScheduler.java create mode 100644 src/main/java/com/sjz/util/ConfigUtil.java create mode 100644 src/main/java/com/sjz/util/DateUtil.java create mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/mybatis-config.xml create mode 100644 src/main/resources/sql/init.sql create mode 100644 src/test/java/com/sjz/AppTest.java diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..e39076f --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(mvn clean compile:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/MarsCodeWorkspaceAppSettings.xml b/.idea/MarsCodeWorkspaceAppSettings.xml new file mode 100644 index 0000000..e2a065b --- /dev/null +++ b/.idea/MarsCodeWorkspaceAppSettings.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..fb57750 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,18 @@ + + + + + mysql.8 + true + com.mysql.cj.jdbc.Driver + jdbc:mysql://fnv4.skdbj.email:15340/stock_data + + + + + + + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..8d66637 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d61b968 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..b8306ef --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a2e8560 --- /dev/null +++ b/CLAUDE.md @@ -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/`目录下。 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a18a81a --- /dev/null +++ b/README.md @@ -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使用条款。 \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d863713 --- /dev/null +++ b/pom.xml @@ -0,0 +1,134 @@ + + 4.0.0 + + com.sjz + go-stock + 1.0-SNAPSHOT + jar + + go-stock + http://maven.apache.org + + + UTF-8 + 21 + 21 + + + + + + org.springframework.boot + spring-boot-starter + 3.1.5 + + + + + junit + junit + 4.13.2 + test + + + + + com.github.qhh6eq + tushare-pro-java-sdk + 2.0.5-RELEASE + + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 3.0.3 + + + + + mysql + mysql-connector-java + 8.0.33 + + + + + com.zaxxer + HikariCP + 5.0.1 + + + + + org.slf4j + slf4j-api + 2.0.7 + + + ch.qos.logback + logback-classic + 1.4.7 + + + + + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + + + + + org.quartz-scheduler + quartz + 2.3.2 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 2.7.14 + + + + repackage + + + + + com.sjz.App + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 8 + 8 + UTF-8 + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + **/*Test.java + + + + + + diff --git a/src/main/java/com/sjz/App.java b/src/main/java/com/sjz/App.java new file mode 100644 index 0000000..aff42a6 --- /dev/null +++ b/src/main/java/com/sjz/App.java @@ -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"); + } +} \ No newline at end of file diff --git a/src/main/java/com/sjz/config/DatabaseConfig.java b/src/main/java/com/sjz/config/DatabaseConfig.java new file mode 100644 index 0000000..97d1abb --- /dev/null +++ b/src/main/java/com/sjz/config/DatabaseConfig.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/sjz/config/TushareConfig.java b/src/main/java/com/sjz/config/TushareConfig.java new file mode 100644 index 0000000..2648ddf --- /dev/null +++ b/src/main/java/com/sjz/config/TushareConfig.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/sjz/mapper/StockBasicMapper.java b/src/main/java/com/sjz/mapper/StockBasicMapper.java new file mode 100644 index 0000000..ab81290 --- /dev/null +++ b/src/main/java/com/sjz/mapper/StockBasicMapper.java @@ -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("") + int batchInsert(@Param("list") List 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 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 selectByMarket(@Param("market") String market); + + /** + * 统计股票数量 + */ + @Select("SELECT COUNT(*) FROM stock_basic") + int count(); +} \ No newline at end of file diff --git a/src/main/java/com/sjz/mapper/StockDailyMapper.java b/src/main/java/com/sjz/mapper/StockDailyMapper.java new file mode 100644 index 0000000..0ce82b4 --- /dev/null +++ b/src/main/java/com/sjz/mapper/StockDailyMapper.java @@ -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("") + int batchInsert(@Param("list") List 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 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 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); +} \ No newline at end of file diff --git a/src/main/java/com/sjz/model/StockBasic.java b/src/main/java/com/sjz/model/StockBasic.java new file mode 100644 index 0000000..6861306 --- /dev/null +++ b/src/main/java/com/sjz/model/StockBasic.java @@ -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 + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/sjz/model/StockDaily.java b/src/main/java/com/sjz/model/StockDaily.java new file mode 100644 index 0000000..c007f94 --- /dev/null +++ b/src/main/java/com/sjz/model/StockDaily.java @@ -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 + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/sjz/service/StockBasicService.java b/src/main/java/com/sjz/service/StockBasicService.java new file mode 100644 index 0000000..e0600b3 --- /dev/null +++ b/src/main/java/com/sjz/service/StockBasicService.java @@ -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 stockBasicEntities = TushareProService.stockBasic( + new Request() {} + .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 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 getAllStockBasic() { + return stockBasicMapper.selectAll(); + } + + /** + * 根据市场类型获取股票列表 + */ + @Transactional(readOnly = true) + public List getStockByMarket(String market) { + return stockBasicMapper.selectByMarket(market); + } + + /** + * 根据TS代码获取股票信息 + */ + @Transactional(readOnly = true) + public StockBasic getStockByTsCode(String tsCode) { + return stockBasicMapper.selectByTsCode(tsCode); + } +} \ No newline at end of file diff --git a/src/main/java/com/sjz/service/StockDailyService.java b/src/main/java/com/sjz/service/StockDailyService.java new file mode 100644 index 0000000..14b6c3a --- /dev/null +++ b/src/main/java/com/sjz/service/StockDailyService.java @@ -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 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 batchStocks = stockBasics.subList(i, endIndex); + + List 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 fetchStockDailyData(List stockBasics, String tradeDate) throws IOException { + List 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 dailyEntities = TushareProService.daily( + new Request() {} + .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 getStockDailyByTsCodeAndDateRange(String tsCode, String startDate, String endDate) { + return stockDailyMapper.selectByTsCodeAndDateRange(tsCode, startDate, endDate); + } + + /** + * 获取指定日期的所有股票日线数据 + */ + @Transactional(readOnly = true) + public List 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; + } +} \ No newline at end of file diff --git a/src/main/java/com/sjz/task/StockDataScheduler.java b/src/main/java/com/sjz/task/StockDataScheduler.java new file mode 100644 index 0000000..63815f0 --- /dev/null +++ b/src/main/java/com/sjz/task/StockDataScheduler.java @@ -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); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/sjz/util/ConfigUtil.java b/src/main/java/com/sjz/util/ConfigUtil.java new file mode 100644 index 0000000..a8546fe --- /dev/null +++ b/src/main/java/com/sjz/util/ConfigUtil.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/sjz/util/DateUtil.java b/src/main/java/com/sjz/util/DateUtil.java new file mode 100644 index 0000000..37e6244 --- /dev/null +++ b/src/main/java/com/sjz/util/DateUtil.java @@ -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)); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..f7a347c --- /dev/null +++ b/src/main/resources/application.properties @@ -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 \ No newline at end of file diff --git a/src/main/resources/mybatis-config.xml b/src/main/resources/mybatis-config.xml new file mode 100644 index 0000000..6227564 --- /dev/null +++ b/src/main/resources/mybatis-config.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/sql/init.sql b/src/main/resources/sql/init.sql new file mode 100644 index 0000000..536fb0d --- /dev/null +++ b/src/main/resources/sql/init.sql @@ -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; \ No newline at end of file diff --git a/src/test/java/com/sjz/AppTest.java b/src/test/java/com/sjz/AppTest.java new file mode 100644 index 0000000..6933f7f --- /dev/null +++ b/src/test/java/com/sjz/AppTest.java @@ -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 ); + } +}